From 4bf44b16562335b3d09b6df0150521bb5b5f776f Mon Sep 17 00:00:00 2001 From: Feuerfuchs Date: Mon, 18 May 2020 12:12:43 +0200 Subject: WIP: Refactoring --- .gitignore | 2 +- Dockerfile | 10 +- Makefile | 14 +- README.md | 8 +- assets/iosevka-aile-regular.woff | Bin 13588 -> 187604 bytes assets/iosevka-aile-regular.woff2 | Bin 11036 -> 130736 bytes assets/iosevka-term-ss03-regular.woff | Bin 13864 -> 155916 bytes assets/iosevka-term-ss03-regular.woff2 | Bin 11384 -> 111644 bytes assets/startpage.txt | 6 +- cmd/port/main.go | 25 ++ fonts/glyphs.txt | 2 +- go.mod | 13 +- go.sum | 217 ++++++++++ gopherproxy.bin | Bin 0 -> 14744336 bytes gopherproxy/gopherproxy.go | 705 --------------------------------- gopherproxy/libgemini/libgemini.go | 145 ------- gopherproxy/libgopher/libgopher.go | 312 --------------- gopherproxy/template.go | 122 ------ internal/port/gemini.go | 205 ++++++++++ internal/port/gopher.go | 217 ++++++++++ internal/port/main.go | 301 ++++++++++++++ internal/port/tpl/gemini.html | 0 internal/port/tpl/gopher.html | 0 internal/port/tpl/startpage.html | 120 ++++++ main.go | 25 -- pkg/libgemini/libgemini.go | 145 +++++++ pkg/libgopher/libgopher.go | 312 +++++++++++++++ 27 files changed, 1569 insertions(+), 1337 deletions(-) create mode 100644 cmd/port/main.go create mode 100755 gopherproxy.bin delete mode 100644 gopherproxy/gopherproxy.go delete mode 100644 gopherproxy/libgemini/libgemini.go delete mode 100644 gopherproxy/libgopher/libgopher.go delete mode 100644 gopherproxy/template.go create mode 100644 internal/port/gemini.go create mode 100644 internal/port/gopher.go create mode 100644 internal/port/main.go create mode 100644 internal/port/tpl/gemini.html create mode 100644 internal/port/tpl/gopher.html create mode 100644 internal/port/tpl/startpage.html delete mode 100644 main.go create mode 100644 pkg/libgemini/libgemini.go create mode 100644 pkg/libgopher/libgopher.go diff --git a/.gitignore b/.gitignore index fe92af3..f02946e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,6 @@ dist *.bak coverage.txt -gopherproxy.bin +port.bin .vscode diff --git a/Dockerfile b/Dockerfile index 8072728..5db4ab0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,16 +2,16 @@ FROM golang:alpine EXPOSE 80/tcp -ENTRYPOINT ["gopherproxy"] +ENTRYPOINT ["port"] RUN \ apk add --update git && \ rm -rf /var/cache/apk/* -RUN mkdir -p /go/src/git.feuerfuchs.dev/Feuerfuchs/gopherproxy -WORKDIR /go/src/git.feuerfuchs.dev/Feuerfuchs/gopherproxy +RUN mkdir -p /go/src/git.vulpes.one/Feuerfuchs/port +WORKDIR /go/src/git.vulpes.one/Feuerfuchs/port -COPY . /go/src/git.feuerfuchs.dev/Feuerfuchs/gopherproxy +COPY . /go/src/git.vulpes.one/Feuerfuchs/port RUN go get -v -d -RUN go install -v git.feuerfuchs.dev/Feuerfuchs/gopherproxy/... +RUN go install -v git.vulpes.one/Feuerfuchs/port/... diff --git a/Makefile b/Makefile index 75a8f2e..8513d69 100644 --- a/Makefile +++ b/Makefile @@ -3,16 +3,16 @@ all: dev dev: build - ./gopherproxy.bin -bind 127.0.0.1:8000 + ./port.bin -bind 127.0.0.1:8000 build: clean sassc -t compressed css/main.scss assets/style.css tsc --strict --module none --outFile /dev/stdout js/* | terser --compress --mangle -o assets/main.js -- - pyftsubset fonts/iosevka-term-ss03-regular.ttf --name-IDs+=0,4,6 --text-file=fonts/glyphs.txt --flavor='woff' --with-zopfli --output-file='assets/iosevka-term-ss03-regular.woff' - pyftsubset fonts/iosevka-term-ss03-regular.ttf --name-IDs+=0,4,6 --text-file=fonts/glyphs.txt --flavor='woff2' --output-file='assets/iosevka-term-ss03-regular.woff2' - pyftsubset fonts/iosevka-aile-regular.ttf --name-IDs+=0,4,6 --text-file=fonts/glyphs.txt --flavor='woff' --with-zopfli --output-file='assets/iosevka-aile-regular.woff' - pyftsubset fonts/iosevka-aile-regular.ttf --name-IDs+=0,4,6 --text-file=fonts/glyphs.txt --flavor='woff2' --output-file='assets/iosevka-aile-regular.woff2' - go build -o ./gopherproxy.bin ./main.go + pyftsubset fonts/iosevka-term-ss03-regular.ttf "*" --name-IDs+=0,4,6 --flavor='woff' --with-zopfli --output-file='assets/iosevka-term-ss03-regular.woff' + pyftsubset fonts/iosevka-term-ss03-regular.ttf "*" --name-IDs+=0,4,6 --flavor='woff2' --output-file='assets/iosevka-term-ss03-regular.woff2' + pyftsubset fonts/iosevka-aile-regular.ttf "*" --name-IDs+=0,4,6 --flavor='woff' --with-zopfli --output-file='assets/iosevka-aile-regular.woff' + pyftsubset fonts/iosevka-aile-regular.ttf "*" --name-IDs+=0,4,6 --flavor='woff2' --output-file='assets/iosevka-aile-regular.woff2' + go build -o ./port.bin ./cmd/port profile: @go test -cpuprofile cpu.prof -memprofile mem.prof -v -bench . @@ -24,4 +24,4 @@ test: @go test -v -race -cover -coverprofile=coverage.txt -covermode=atomic . clean: - @git clean -f -d -X + #@git clean -f -d -X diff --git a/README.md b/README.md index e4f5d97..40ed18a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Gopher (RFC 1436) Web Proxy -gopherproxy is a Gopher (RFC 1436) and Gemini (gopher://zaibatsu.circumlunar.space/1/~solderpunk/gemini/docs/) Web Proxy that acts as a gateway into Gopherspace/Geminispace by proxying standard Web HTTP requests to Gopher/Gemini requests of the target server. +port is a Gopher (RFC 1436) and Gemini (gopher://zaibatsu.circumlunar.space/1/~solderpunk/gemini/docs/) Web Proxy that acts as a gateway into Gopherspace/Geminispace by proxying standard Web HTTP requests to Gopher/Gemini requests of the target server. -gopherproxy is a fork of [https://github.com/prologic/gopherproxy](https://github.com/prologic/gopherproxy). +port is a fork of [https://github.com/prologic/port](https://github.com/prologic/port). Demo: https://proxy.vulpes.one/ @@ -15,14 +15,14 @@ Demo: https://proxy.vulpes.one/ ## Installation ```#!bash -$ go install git.feuerfuchs.dev/Feuerfuchs/gopherproxy/... +$ go install git.vulpes.one/Feuerfuchs/port/... ``` ## Usage ```#!bash -$ gopherproxy +$ port ``` Arguments: diff --git a/assets/iosevka-aile-regular.woff b/assets/iosevka-aile-regular.woff index a23b09c..97b42ea 100644 Binary files a/assets/iosevka-aile-regular.woff and b/assets/iosevka-aile-regular.woff differ diff --git a/assets/iosevka-aile-regular.woff2 b/assets/iosevka-aile-regular.woff2 index 2a4375e..fea3967 100644 Binary files a/assets/iosevka-aile-regular.woff2 and b/assets/iosevka-aile-regular.woff2 differ diff --git a/assets/iosevka-term-ss03-regular.woff b/assets/iosevka-term-ss03-regular.woff index 18d4b27..eb2568a 100644 Binary files a/assets/iosevka-term-ss03-regular.woff and b/assets/iosevka-term-ss03-regular.woff differ diff --git a/assets/iosevka-term-ss03-regular.woff2 b/assets/iosevka-term-ss03-regular.woff2 index af3e978..957b020 100644 Binary files a/assets/iosevka-term-ss03-regular.woff2 and b/assets/iosevka-term-ss03-regular.woff2 differ diff --git a/assets/startpage.txt b/assets/startpage.txt index 53c12ab..46b22c1 100644 --- a/assets/startpage.txt +++ b/assets/startpage.txt @@ -1,6 +1,6 @@ - P R O X Y - - - - - - - for - - - - - - - GOPHER + GEMINI + P R O X Y +- - - - - - for - - - - - - + G O P H E R + G E M I N I GETTING STARTED -- diff --git a/cmd/port/main.go b/cmd/port/main.go new file mode 100644 index 0000000..6cdccc6 --- /dev/null +++ b/cmd/port/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "flag" + "log" + + port "git.vulpes.one/Feuerfuchs/port/internal/port" +) + +var ( + // TODO: Allow config file and environment vars + // (opt -> env -> config -> default) + bind = flag.String("bind", "0.0.0.0:8000", "[int]:port to bind to") + startpagefile = flag.String("startpage-file", "startpage.txt", "Default page to display if no URL is specified") + robotsfile = flag.String("robots-file", "robots.txt", "robots.txt file") + robotsdebug = flag.Bool("robots-debug", false, "print output about ignored robots.txt") + vipsconcurrency = flag.Int("vips-concurrency", 1, "Concurrency level of libvips") +) + +func main() { + flag.Parse() + + // Use a config struct + log.Fatal(port.ListenAndServe(*bind, *startpagefile, *robotsfile, *robotsdebug, *vipsconcurrency)) +} diff --git a/fonts/glyphs.txt b/fonts/glyphs.txt index 5acadec..ac0461a 100644 --- a/fonts/glyphs.txt +++ b/fonts/glyphs.txt @@ -1 +1 @@ - !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`´abcdefghijklmnopqrstuvwxyz{|}~äöüÄÖÜßẞ↓↙←↖↑↗→↘€»«„“”·…°’‾█▓▒░ ▀▄‐╭╮─│╰╯╱╲╳ʻ‘ + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`´abcdefghijklmnopqrstuvwxyz{|}~äöüÄÖÜßẞ↓↙←↖↑↗→↘€»«„“”·…°’‾█▓▒░ ▀▄‐╭╮─│╰╯┌┐└┘├╱╲╳ʻ‘ diff --git a/go.mod b/go.mod index 6cde9a1..f6759d3 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,12 @@ -module git.feuerfuchs.dev/Feuerfuchs/gopherproxy +module git.vulpes.one/Feuerfuchs/port require ( github.com/NYTimes/gziphandler v1.1.1 - github.com/davidbyttow/govips v0.0.0-20190304175058-d272f04c0fea - github.com/gobuffalo/packr/v2 v2.1.0 - github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 - github.com/temoto/robotstxt v0.0.0-20180810133444-97ee4a9ee6ea - golang.org/x/net v0.0.0-20190311183353-d8887717615a - golang.org/x/text v0.3.0 + github.com/davidbyttow/govips v0.0.0-20200412130214-cbefdd8c639a + github.com/gobuffalo/packr/v2 v2.8.0 + github.com/temoto/robotstxt v1.1.1 + golang.org/x/net v0.0.0-20200513185701-a91f0712d120 + golang.org/x/text v0.3.2 ) go 1.13 diff --git a/go.sum b/go.sum index 7f255e0..171b051 100644 --- a/go.sum +++ b/go.sum @@ -1,92 +1,309 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davidbyttow/govips v0.0.0-20190304175058-d272f04c0fea h1:ZtETbJTO1R3qVLdVbpjrDhD5fR8bYVhhq2RMi7rOlH4= github.com/davidbyttow/govips v0.0.0-20190304175058-d272f04c0fea/go.mod h1:a3qO525EPfJNYa0NXBcNtXzJvyQsJAxphEDa7OOHPBk= +github.com/davidbyttow/govips v0.0.0-20200412130214-cbefdd8c639a h1:g1X1c43wACmPtvFo+szBy/0coLkRwVJ0+i2f6YJITto= +github.com/davidbyttow/govips v0.0.0-20200412130214-cbefdd8c639a/go.mod h1:a3qO525EPfJNYa0NXBcNtXzJvyQsJAxphEDa7OOHPBk= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= +github.com/gobuffalo/attrs v1.0.0 h1:aAK8D55L0GNVbVRlaCwvAGE2BcGdEoVoK86vgQpuUZ8= +github.com/gobuffalo/attrs v1.0.0/go.mod h1:YWU+sjOr7V05rNzvCoVc5qQTC2IgEz9434TFW5GhNaw= github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= +github.com/gobuffalo/depgen v0.2.0 h1:CYuqsR8sq+L9G9+A6uUcTEuaK8AGenAjtYOm238fN3M= +github.com/gobuffalo/depgen v0.2.0/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= github.com/gobuffalo/envy v1.6.15 h1:OsV5vOpHYUpP7ZLS6sem1y40/lNX1BZj+ynMiRi21lQ= github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU= github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= +github.com/gobuffalo/envy v1.8.1 h1:RUr68liRvs0TS1D5qdW3mQv2SjAsu1QWMCx1tG4kDjs= +github.com/gobuffalo/envy v1.8.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/flect v0.2.0 h1:EWCvMGGxOjsgwlWaP+f4+Hh6yrrte7JeFL2S6b+0hdM= +github.com/gobuffalo/flect v0.2.0/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e h1:JbHBQOMhE0wmpSuejnSkdnL2rULqQTwEGgVe85o7+No= github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= +github.com/gobuffalo/genny v0.6.0 h1:d7c6d66ZrTHHty01hDX1/TcTWvAJQxRZl885KWX5kHY= +github.com/gobuffalo/genny v0.6.0/go.mod h1:Vigx9VDiNscYpa/LwrURqGXLSIbzTfapt9+K6gF1kTA= github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= +github.com/gobuffalo/github_flavored_markdown v1.1.0/go.mod h1:TSpTKWcRTI0+v7W3x8dkSKMLJSUpuVitlptCkpeY8ic= github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5 h1:f3Fpd5AqsFuTHUEhUeEMIFJkX8FpVnzdW+GpYxIyXkA= github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= +github.com/gobuffalo/gogen v0.2.0 h1:Xx7NCe+/y++eII2aWAFZ09/81MhDCsZwvMzIFJoQRnU= +github.com/gobuffalo/gogen v0.2.0/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= +github.com/gobuffalo/helpers v0.5.0/go.mod h1:stpgxJ2C7T99NLyAxGUnYMM2zAtBk5NKQR0SIbd05j4= +github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2 h1:8thhT+kUJMTMy3HlX4+y9Da+BNJck+p109tqqKp7WDs= github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= +github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= +github.com/gobuffalo/logger v1.0.3 h1:YaXOTHNPCvkqqA7w05A4v0k2tCdpr+sgFlgINbQ6gqc= +github.com/gobuffalo/logger v1.0.3/go.mod h1:SoeejUwldiS7ZsyCBphOGURmWdwUFXs0J7TCjEhjKxM= github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= github.com/gobuffalo/mapi v1.0.2 h1:fq9WcL1BYrm36SzK6+aAnZ8hcp+SrmnDyAxhNx8dvJk= github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/mapi v1.2.1 h1:TyfbtQaW7GvS4DXdF1KQOSGrW6L0uiFmGDz+JgEIMbM= +github.com/gobuffalo/mapi v1.2.1/go.mod h1:giGJ2AUESRepOFYAzWpq8Gf/s/QDryxoEHisQtFN3cY= github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0 h1:P6naWPiHm/7R3eYx/ub3VhaW9G+1xAMJ6vzACePaGPI= github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= +github.com/gobuffalo/packd v1.0.0 h1:6ERZvJHfe24rfFmA9OaoKBdC7+c9sydrytMg8SdFGBM= +github.com/gobuffalo/packd v1.0.0/go.mod h1:6VTc4htmJRFB7u1m/4LeMTWjFoYrUiBkU9Fdec9hrhI= github.com/gobuffalo/packr v1.25.0 h1:NtPK45yOKFdTKHTvRGKL+UIKAKmJVWIVJOZBDI/qEdY= github.com/gobuffalo/packr v1.25.0/go.mod h1:NqsGg8CSB2ZD+6RBIRs18G7aZqdYDlYNNvsSqP6T4/U= github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= github.com/gobuffalo/packr/v2 v2.1.0 h1:nWGTgGtZrR4yBQvmAKF4AthraObjRMzx6lJa9e+JbLQ= github.com/gobuffalo/packr/v2 v2.1.0/go.mod h1:n90ZuXIc2KN2vFAOQascnPItp9A2g9QYSvYvS3AjQEM= +github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc= +github.com/gobuffalo/packr/v2 v2.8.0 h1:IULGd15bQL59ijXLxEvA5wlMxsmx/ZkQv9T282zNVIY= +github.com/gobuffalo/packr/v2 v2.8.0/go.mod h1:PDk2k3vGevNE3SwVyVRgQCCXETC9SaONCNSXT1Q8M1g= +github.com/gobuffalo/plush v3.8.3+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI= github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754 h1:tpom+2CJmpzAWj5/VEHync2rJGi+epHNIeRSWjzGA+4= github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= +github.com/gobuffalo/syncx v0.1.0 h1://CNTQ/+VFQizkW24DrBtTBvj8c2+chz5Y7kbboQ2qk= +github.com/gobuffalo/syncx v0.1.0/go.mod h1:Mg/s+5pv7IgxEp6sA+NFpqS4o2x+R9dQNwbwT0iuOGQ= +github.com/gobuffalo/tags v2.1.7+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67q8/xRMVJEaakauVQYeY= +github.com/gobuffalo/uuid v2.0.5+incompatible/go.mod h1:ErhIzkRhm0FtRuiE/PeORqcw4cVi1RtSpnwYrxuvkfE= +github.com/gobuffalo/validate v2.0.3+incompatible/go.mod h1:N+EtDe0J8252BgfzQUChBgfd6L93m9weay53EWFVsMM= +github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/karrick/godirwalk v1.8.0 h1:ycpSqVon/QJJoaT1t8sae0tp1Stg21j+dyuS7OoagcA= github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= +github.com/karrick/godirwalk v1.15.3 h1:0a2pXOgtB16CqIqXTiT7+K9L73f74n/aNQUnH6Ortew= +github.com/karrick/godirwalk v1.15.3/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI= +github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2 h1:JgVTCPf0uBVcUSWpyXmGpgOc62nK5HWUBKAGc3Qqa5k= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= +github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY= +github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI= github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prologic/go-gopher v0.0.0-20181230133552-0c68ed5f58b0 h1:10LO/S8HVjIuEHsHea//Cena1Ztgy23f/e8HFC0w5ow= github.com/prologic/go-gopher v0.0.0-20181230133552-0c68ed5f58b0/go.mod h1:LiuwIXz4es4YIUOD6yRv8mES9n9dFbe4z0+TcrLkhXg= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.5.2 h1:qLvObTrvO/XRCqmkKxUlOBc48bI3efyDuAZe25QiF0w= +github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/temoto/robotstxt v0.0.0-20180810133444-97ee4a9ee6ea h1:hH8P1IiDpzRU6ZDbDh/RDnVuezi2oOXJpApa06M0zyI= github.com/temoto/robotstxt v0.0.0-20180810133444-97ee4a9ee6ea/go.mod h1:aOux3gHPCftJ3KHq6Pz/AlDjYJ7Y+yKfm1gU/3B0u04= +github.com/temoto/robotstxt v1.1.1 h1:Gh8RCs8ouX3hRSxxK7B1mO5RFByQ4CmJZDwgom++JaA= +github.com/temoto/robotstxt v1.1.1/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c h1:/nJuwDLoL/zrqY6gf57vxC+Pi+pZ8bfhpPkicO5H7W4= +golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120 h1:EZ3cVSzKOlJxAd8e8YAJ7no8nNypTxexh/YE/xW3ZEY= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190404132500-923d25813098 h1:MtqjsZmyGRgMmLUgxnmMJ6RYdvd2ib8ipiayHhqSxs4= golang.org/x/tools v0.0.0-20190404132500-923d25813098/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191224055732-dd894d0a8a40/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200308013534-11ec41452d41 h1:9Di9iYgOt9ThCipBxChBVhgNipDoE5mxO84rQV7D0FE= +golang.org/x/tools v0.0.0-20200308013534-11ec41452d41/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/gopherproxy.bin b/gopherproxy.bin new file mode 100755 index 0000000..e8f94c1 Binary files /dev/null and b/gopherproxy.bin differ diff --git a/gopherproxy/gopherproxy.go b/gopherproxy/gopherproxy.go deleted file mode 100644 index 453ee9c..0000000 --- a/gopherproxy/gopherproxy.go +++ /dev/null @@ -1,705 +0,0 @@ -package gopherproxy - -import ( - "bufio" - "bytes" - "crypto/md5" - "fmt" - "html" - "html/template" - "io" - "io/ioutil" - "log" - "mime" - "net" - "net/http" - "net/url" - "regexp" - "strings" - - "golang.org/x/net/html/charset" - "golang.org/x/text/transform" - - "git.feuerfuchs.dev/Feuerfuchs/gopherproxy/gopherproxy/libgemini" - "git.feuerfuchs.dev/Feuerfuchs/gopherproxy/gopherproxy/libgopher" - - "github.com/NYTimes/gziphandler" - "github.com/davidbyttow/govips/pkg/vips" - "github.com/gobuffalo/packr/v2" - "github.com/temoto/robotstxt" -) - -var ( - TermEscapeSGRPattern = regexp.MustCompile("\\[\\d+(;\\d+)*m") -) - -const ( - ITEM_TYPE_GEMINI_LINE = "" - ITEM_TYPE_GEMINI_LINK = " =>" -) - -type Item struct { - Link template.URL - Type string - Text string -} - -type AssetList struct { - Style string - JS string - FontW string - FontW2 string - PropFontW string - PropFontW2 string -} - -type TemplateVariables struct { - Title string - URI string - Assets AssetList - RawText string - Lines []Item - Error bool - Protocol string -} - -func resolveURI(uri string, baseURL *url.URL) (resolvedURI string) { - if strings.HasPrefix(uri, "//") { - resolvedURI = "/gemini/" + strings.TrimPrefix(uri, "//") - } else if strings.HasPrefix(uri, "gemini://") { - resolvedURI = "/gemini/" + strings.TrimPrefix(uri, "gemini://") - } else if strings.HasPrefix(uri, "gopher://") { - resolvedURI = "/gopher/" + strings.TrimPrefix(uri, "gopher://") - } else { - url, err := url.Parse(uri) - if err != nil { - return "" - } - adjustedURI := baseURL.ResolveReference(url) - path := adjustedURI.Path - if !strings.HasPrefix(path, "/") { - path = "/" + path - } - if adjustedURI.Scheme == "gemini" { - resolvedURI = "/gemini/" + adjustedURI.Host + path - } else if adjustedURI.Scheme == "gopher" { - resolvedURI = "/gopher/" + adjustedURI.Host + path - } else { - resolvedURI = adjustedURI.String() - } - } - - return -} - -func renderGopherDirectory(w http.ResponseWriter, tpl *template.Template, assetList AssetList, uri string, hostport string, d libgopher.Directory) error { - var title string - - out := make([]Item, len(d.Items)) - - for i, x := range d.Items { - if x.Type == libgopher.INFO && x.Selector == "TITLE" { - title = x.Description - continue - } - - tr := Item{ - Text: x.Description, - Type: x.Type.String(), - } - - if x.Type == libgopher.INFO { - out[i] = tr - continue - } - - if strings.HasPrefix(x.Selector, "URL:") || strings.HasPrefix(x.Selector, "/URL:") { - link := strings.TrimPrefix(strings.TrimPrefix(x.Selector, "/"), "URL:") - if strings.HasPrefix(link, "gemini://") { - link = fmt.Sprintf( - "/gemini/%s", - strings.TrimPrefix(link, "gemini://"), - ) - } else if strings.HasPrefix(link, "gopher://") { - link = fmt.Sprintf( - "/gopher/%s", - strings.TrimPrefix(link, "gopher://"), - ) - } - tr.Link = template.URL(link) - } else { - var linkHostport string - if x.Port != "70" { - linkHostport = net.JoinHostPort(x.Host, x.Port) - } else { - linkHostport = x.Host - } - - path := url.PathEscape(x.Selector) - path = strings.Replace(path, "%2F", "/", -1) - tr.Link = template.URL( - fmt.Sprintf( - "/gopher/%s/%s%s", - linkHostport, - string(byte(x.Type)), - path, - ), - ) - } - - out[i] = tr - } - - if title == "" { - if uri != "" { - title = fmt.Sprintf("%s/%s", hostport, uri) - } else { - title = hostport - } - } - - return tpl.Execute(w, TemplateVariables{ - Title: title, - URI: fmt.Sprintf("%s/%s", hostport, uri), - Assets: assetList, - Lines: out, - Protocol: "gopher", - }) -} - -func parseGeminiDocument(body *bytes.Buffer, uri string, hostport string) (items []Item) { - baseURL, err := url.Parse(fmt.Sprintf( - "gemini://%s/%s", - hostport, - uri, - )) - if err != nil { - return []Item{} - } - - scanner := bufio.NewScanner(body) - - for scanner.Scan() { - line := strings.Trim(scanner.Text(), "\r\n") - line = TermEscapeSGRPattern.ReplaceAllString(line, "") - - item := Item{ - Type: ITEM_TYPE_GEMINI_LINE, - Text: line, - } - - linkMatch := libgemini.LinkPattern.FindStringSubmatch(line) - if len(linkMatch) != 0 && linkMatch[0] != "" { - item.Type = ITEM_TYPE_GEMINI_LINK - item.Link = template.URL(resolveURI(linkMatch[1], baseURL)) - item.Text = linkMatch[2] - if item.Text == "" { - item.Text = linkMatch[1] - } - } - - items = append(items, item) - } - - return -} - -func DefaultHandler(tpl *template.Template, startpagetext string, assetList AssetList) http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - if err := tpl.Execute(w, TemplateVariables{ - Title: "Gopher/Gemini proxy", - Assets: assetList, - RawText: startpagetext, - Protocol: "startpage", - }); err != nil { - log.Println("Template error: " + err.Error()) - } - } -} - -// GopherHandler returns a Handler that proxies requests -// to the specified Gopher server as denoated by the first argument -// to the request path and renders the content using the provided template. -// The optional robots parameters points to a robotstxt.RobotsData struct -// to test user agents against a configurable robotst.txt file. -func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool) http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - agent := req.UserAgent() - path := strings.TrimPrefix(req.URL.Path, "/gopher/") - - if robotsdata != nil && robotsdebug && !robotsdata.TestAgent(path, agent) { - log.Printf("UserAgent %s ignored robots.txt", agent) - } - - parts := strings.Split(path, "/") - hostport := parts[0] - - if len(hostport) == 0 { - http.Redirect(w, req, "/", http.StatusFound) - return - } - - title := hostport - - var qs string - - if req.URL.RawQuery != "" { - qs = fmt.Sprintf("?%s", url.QueryEscape(req.URL.RawQuery)) - } - - uri, err := url.QueryUnescape(strings.Join(parts[1:], "/")) - if err != nil { - if e := tpl.Execute(w, TemplateVariables{ - Title: title, - URI: hostport, - Assets: assetList, - RawText: fmt.Sprintf("Error: %s", err), - Error: true, - Protocol: "gopher", - }); e != nil { - log.Println("Template error: " + e.Error()) - log.Println(err.Error()) - } - return - } - - if uri != "" { - title = fmt.Sprintf("%s/%s", hostport, uri) - } - - res, err := libgopher.Get( - fmt.Sprintf( - "gopher://%s/%s%s", - hostport, - uri, - qs, - ), - ) - - if err != nil { - if e := tpl.Execute(w, TemplateVariables{ - Title: title, - URI: fmt.Sprintf("%s/%s", hostport, uri), - Assets: assetList, - RawText: fmt.Sprintf("Error: %s", err), - Error: true, - Protocol: "gopher", - }); e != nil { - log.Println("Template error: " + e.Error()) - } - return - } - - if res.Body != nil { - if len(parts) < 2 { - io.Copy(w, res.Body) - } else if strings.HasPrefix(parts[1], "0") && !strings.HasSuffix(uri, ".xml") && !strings.HasSuffix(uri, ".asc") { - buf := new(bytes.Buffer) - buf.ReadFrom(res.Body) - - if err := tpl.Execute(w, TemplateVariables{ - Title: title, - URI: fmt.Sprintf("%s/%s", hostport, uri), - Assets: assetList, - RawText: buf.String(), - Protocol: "gopher", - }); err != nil { - log.Println("Template error: " + err.Error()) - } - } else if strings.HasPrefix(parts[1], "T") { - _, _, err = vips.NewTransform(). - Load(res.Body). - ResizeStrategy(vips.ResizeStrategyAuto). - ResizeWidth(160). - Quality(75). - Output(w). - Apply() - } else { - io.Copy(w, res.Body) - } - } else { - if err := renderGopherDirectory(w, tpl, assetList, uri, hostport, res.Dir); err != nil { - if e := tpl.Execute(w, TemplateVariables{ - Title: title, - URI: fmt.Sprintf("%s/%s", hostport, uri), - Assets: assetList, - RawText: fmt.Sprintf("Error: %s", err), - Error: true, - Protocol: "gopher", - }); e != nil { - log.Println("Template error: " + e.Error()) - log.Println(e.Error()) - } - } - } - } -} - -func GeminiHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool) http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - agent := req.UserAgent() - path := strings.TrimPrefix(req.URL.Path, "/gemini/") - - if robotsdata != nil && robotsdebug && !robotsdata.TestAgent(path, agent) { - log.Printf("UserAgent %s ignored robots.txt", agent) - } - - parts := strings.Split(path, "/") - hostport := parts[0] - - if len(hostport) == 0 { - http.Redirect(w, req, "/", http.StatusFound) - return - } - - title := hostport - - var qs string - - if req.URL.RawQuery != "" { - qs = fmt.Sprintf("?%s", url.QueryEscape(req.URL.RawQuery)) - } - - uri, err := url.QueryUnescape(strings.Join(parts[1:], "/")) - if err != nil { - if e := tpl.Execute(w, TemplateVariables{ - Title: title, - URI: hostport, - Assets: assetList, - RawText: fmt.Sprintf("Error: %s", err), - Error: true, - Protocol: "gemini", - }); e != nil { - log.Println("Template error: " + e.Error()) - log.Println(err.Error()) - } - return - } - - if uri != "" { - title = fmt.Sprintf("%s/%s", hostport, uri) - } - - res, err := libgemini.Get( - fmt.Sprintf( - "gemini://%s/%s%s", - hostport, - uri, - qs, - ), - ) - - if err != nil { - if e := tpl.Execute(w, TemplateVariables{ - Title: title, - URI: fmt.Sprintf("%s/%s", hostport, uri), - Assets: assetList, - RawText: fmt.Sprintf("Error: %s", err), - Error: true, - Protocol: "gemini", - }); e != nil { - log.Println("Template error: " + e.Error()) - log.Println(err.Error()) - } - return - } - - if int(res.Header.Status/10) == 3 { - baseURL, err := url.Parse(fmt.Sprintf( - "gemini://%s/%s", - hostport, - uri, - )) - if err != nil { - if e := tpl.Execute(w, TemplateVariables{ - Title: title, - URI: fmt.Sprintf("%s/%s", hostport, uri), - Assets: assetList, - RawText: fmt.Sprintf("Error: %s", err), - Error: true, - Protocol: "gemini", - }); e != nil { - log.Println("Template error: " + e.Error()) - log.Println(err.Error()) - } - return - } - - http.Redirect(w, req, resolveURI(res.Header.Meta, baseURL), http.StatusFound) - return - } - - if int(res.Header.Status/10) != 2 { - if err := tpl.Execute(w, TemplateVariables{ - Title: title, - URI: fmt.Sprintf("%s/%s", hostport, uri), - Assets: assetList, - RawText: fmt.Sprintf("Error %d: %s", res.Header.Status, res.Header.Meta), - Error: true, - Protocol: "gemini", - }); err != nil { - log.Println("Template error: " + err.Error()) - } - return - } - - if strings.HasPrefix(res.Header.Meta, "text/") { - buf := new(bytes.Buffer) - - _, params, err := mime.ParseMediaType(res.Header.Meta) - if err != nil { - buf.ReadFrom(res.Body) - } else { - encoding, _ := charset.Lookup(params["charset"]) - readbuf := new(bytes.Buffer) - readbuf.ReadFrom(res.Body) - - writer := transform.NewWriter(buf, encoding.NewDecoder()) - writer.Write(readbuf.Bytes()) - writer.Close() - } - - var ( - rawText string - items []Item - ) - - if strings.HasPrefix(res.Header.Meta, libgemini.MIME_GEMINI) { - items = parseGeminiDocument(buf, uri, hostport) - } else { - rawText = buf.String() - } - - if err := tpl.Execute(w, TemplateVariables{ - Title: title, - URI: fmt.Sprintf("%s/%s", hostport, uri), - Assets: assetList, - Lines: items, - RawText: rawText, - Protocol: "gemini", - }); err != nil { - log.Println("Template error: " + err.Error()) - } - } else { - io.Copy(w, res.Body) - } - } -} - -// RobotsTxtHandler returns the contents of the robots.txt file -// if configured and valid. -func RobotsTxtHandler(robotstxtdata []byte) http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - if robotstxtdata == nil { - http.Error(w, "Not Found", http.StatusNotFound) - return - } - - w.Header().Set("Content-Type", "text/plain") - w.Write(robotstxtdata) - } -} - -func FaviconHandler(favicondata []byte) http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - if favicondata == nil { - http.Error(w, "Not Found", http.StatusNotFound) - return - } - - w.Header().Set("Content-Type", "image/vnd.microsoft.icon") - w.Header().Set("Cache-Control", "max-age=2592000") - w.Write(favicondata) - } -} - -func StyleHandler(styledata []byte) http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", "text/css") - w.Header().Set("Cache-Control", "max-age=2592000") - w.Write(styledata) - } -} - -func JavaScriptHandler(jsdata []byte) http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", "text/javascript") - w.Header().Set("Cache-Control", "max-age=2592000") - w.Write(jsdata) - } -} - -func FontHandler(woff2 bool, fontdata []byte) http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - if fontdata == nil { - http.Error(w, "Not Found", http.StatusNotFound) - return - } - - if woff2 { - w.Header().Set("Content-Type", "font/woff2") - } else { - w.Header().Set("Content-Type", "font/woff") - } - w.Header().Set("Cache-Control", "max-age=2592000") - - w.Write(fontdata) - } -} - -// ListenAndServe creates a listening HTTP server bound to -// the interface specified by bind and sets up a Gopher to HTTP -// proxy proxying requests as requested and by default will prozy -// to a Gopher server address specified by uri if no servers is -// specified by the request. The robots argument is a pointer to -// a robotstxt.RobotsData struct for testing user agents against -// a configurable robots.txt file. -func ListenAndServe(bind, startpagefile string, robotsfile string, robotsdebug bool, vipsconcurrency int) error { - var ( - tpl *template.Template - robotsdata *robotstxt.RobotsData - ) - - robotstxtdata, err := ioutil.ReadFile(robotsfile) - if err != nil { - log.Printf("error reading robots.txt: %s", err) - robotstxtdata = nil - } else { - robotsdata, err = robotstxt.FromBytes(robotstxtdata) - if err != nil { - log.Printf("error reading robots.txt: %s", err) - robotstxtdata = nil - } - } - - box := packr.New("assets", "../assets") - - fontdataw, err := box.Find("iosevka-term-ss03-regular.woff") - if err != nil { - fontdataw = []byte{} - } - fontwAsset := fmt.Sprintf("/iosevka-term-ss03-regular-%x.woff", md5.Sum(fontdataw)) - - fontdataw2, err := box.Find("iosevka-term-ss03-regular.woff2") - if err != nil { - fontdataw2 = []byte{} - } - fontw2Asset := fmt.Sprintf("/iosevka-term-ss03-regular-%x.woff2", md5.Sum(fontdataw2)) - - propfontdataw, err := box.Find("iosevka-aile-regular.woff") - if err != nil { - propfontdataw = []byte{} - } - propfontwAsset := fmt.Sprintf("/iosevka-aile-regular-%x.woff", md5.Sum(propfontdataw)) - - propfontdataw2, err := box.Find("iosevka-aile-regular.woff2") - if err != nil { - propfontdataw2 = []byte{} - } - propfontw2Asset := fmt.Sprintf("/iosevka-aile-regular-%x.woff2", md5.Sum(propfontdataw2)) - - styledata, err := box.Find("style.css") - if err != nil { - styledata = []byte{} - } - styleAsset := fmt.Sprintf("/style-%x.css", md5.Sum(styledata)) - - jsdata, err := box.Find("main.js") - if err != nil { - jsdata = []byte{} - } - jsAsset := fmt.Sprintf("/main-%x.js", md5.Sum(jsdata)) - - favicondata, err := box.Find("favicon.ico") - if err != nil { - favicondata = []byte{} - } - - startpagedata, err := ioutil.ReadFile(startpagefile) - if err != nil { - startpagedata, err = box.Find("startpage.txt") - if err != nil { - startpagedata = []byte{} - } - } - startpagetext := string(startpagedata) - - tpldata, err := ioutil.ReadFile(".template") - if err == nil { - tpltext = string(tpldata) - } - - funcMap := template.FuncMap{ - "safeHtml": func(s string) template.HTML { - return template.HTML(s) - }, - "safeCss": func(s string) template.CSS { - return template.CSS(s) - }, - "safeJs": func(s string) template.JS { - return template.JS(s) - }, - "HTMLEscape": func(s string) string { - return html.EscapeString(s) - }, - "split": strings.Split, - "last": func(s []string) string { - return s[len(s)-1] - }, - "pop": func(s []string) []string { - return s[:len(s)-1] - }, - "replace": func(pattern, output string, input interface{}) string { - var re = regexp.MustCompile(pattern) - var inputStr = fmt.Sprintf("%v", input) - return re.ReplaceAllString(inputStr, output) - }, - "trimLeftChar": func(s string) string { - for i := range s { - if i > 0 { - return s[i:] - } - } - return s[:0] - }, - "hasPrefix": func(s string, prefix string) bool { - return strings.HasPrefix(s, prefix) - }, - "title": func(s string) string { - return strings.Title(s) - }, - } - - tpl, err = template.New("gophermenu").Funcs(funcMap).Parse(tpltext) - if err != nil { - log.Fatal(err) - } - - vips.Startup(&vips.Config{ - ConcurrencyLevel: vipsconcurrency, - }) - - assets := AssetList{ - Style: styleAsset, - JS: jsAsset, - FontW: fontwAsset, - FontW2: fontw2Asset, - PropFontW: propfontwAsset, - PropFontW2: propfontw2Asset, - } - - http.Handle("/", gziphandler.GzipHandler(DefaultHandler(tpl, startpagetext, assets))) - http.Handle("/gopher/", gziphandler.GzipHandler(GopherHandler(tpl, robotsdata, assets, robotsdebug))) - http.Handle("/gemini/", gziphandler.GzipHandler(GeminiHandler(tpl, robotsdata, assets, robotsdebug))) - http.Handle("/robots.txt", gziphandler.GzipHandler(RobotsTxtHandler(robotstxtdata))) - http.Handle("/favicon.ico", gziphandler.GzipHandler(FaviconHandler(favicondata))) - http.Handle(styleAsset, gziphandler.GzipHandler(StyleHandler(styledata))) - http.Handle(jsAsset, gziphandler.GzipHandler(JavaScriptHandler(jsdata))) - http.HandleFunc(fontwAsset, FontHandler(false, fontdataw)) - http.HandleFunc(fontw2Asset, FontHandler(true, fontdataw2)) - http.HandleFunc(propfontwAsset, FontHandler(false, propfontdataw)) - http.HandleFunc(propfontw2Asset, FontHandler(true, propfontdataw2)) - //http.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir("assets/")))) - - return http.ListenAndServe(bind, nil) -} diff --git a/gopherproxy/libgemini/libgemini.go b/gopherproxy/libgemini/libgemini.go deleted file mode 100644 index 303490c..0000000 --- a/gopherproxy/libgemini/libgemini.go +++ /dev/null @@ -1,145 +0,0 @@ -package libgemini - -import ( - "bufio" - "crypto/tls" - "errors" - "fmt" - "io" - "mime" - "net" - "net/url" - "regexp" - "strconv" - "strings" -) - -const ( - CRLF = "\r\n" -) - -const ( - STATUS_INPUT = 10 - STATUS_SUCCESS = 20 - STATUS_SUCCESS_CERT = 21 - STATUS_REDIRECT_TEMP = 30 - STATUS_REDIRECT_PERM = 31 - STATUS_TEMP_FAILURE = 40 - STATUS_SERVER_UNAVAILABLE = 41 - STATUS_CGI_ERROR = 42 - STATUS_PROXY_ERROR = 43 - STATUS_SLOW_DOWN = 44 - STATUS_PERM_FAILURE = 50 - STATUS_NOT_FOUND = 51 - STATUS_GONE = 52 - STATUS_PROXY_REFUSED = 53 - STATUS_BAD_REQUEST = 59 - STATUS_CLIENT_CERT_EXPIRED = 60 - STATUS_TRANSIENT_CERT_REQUEST = 61 - STATUS_AUTH_CERT_REQUIRED = 62 - STATUS_CERT_REJECTED = 63 - STATUS_FUTURE_CERT_REJECTED = 64 - STATUS_EXPIRED_CERT_REJECTED = 65 -) - -const ( - MIME_GEMINI = "text/gemini" - DEFAULT_MIME = MIME_GEMINI - DEFAULT_CHARSET = "utf-8" -) - -var ( - HeaderPattern = regexp.MustCompile("^(\\d\\d)[ \\t]+(.*)$") - LinkPattern = regexp.MustCompile("^=>[ \\t]*([^ \\t]+)(?:[ \\t]+(.*))?$") -) - -type Header struct { - Status int - Meta string -} - -type Response struct { - Header *Header - Body io.Reader -} - -func Get(uri string) (*Response, error) { - u, err := url.Parse(uri) - if err != nil { - return nil, err - } - - if u.Scheme != "gemini" { - return nil, errors.New("invalid scheme for uri") - } - - host := u.Hostname() - port := u.Port() - - if port == "" { - port = "1965" - } - - conn, err := tls.Dial("tcp", net.JoinHostPort(host, port), &tls.Config{ - MinVersion: tls.VersionTLS12, - InsecureSkipVerify: true, - }) - if err != nil { - return nil, err - } - - _, err = conn.Write([]byte(u.String() + CRLF)) - if err != nil { - conn.Close() - return nil, err - } - - reader := bufio.NewReader(conn) - - line, _, err := reader.ReadLine() - if err != nil { - conn.Close() - return nil, err - } - - header, err := ParseHeader(string(line)) - if err != nil { - conn.Close() - return nil, err - } - - return &Response{ - Header: header, - Body: reader, - }, nil -} - -func ParseHeader(line string) (header *Header, err error) { - matches := HeaderPattern.FindStringSubmatch(line) - - status, err := strconv.Atoi(matches[1]) - if err != nil { - return nil, err - } - - meta := matches[2] - - if int(status/10) == 2 { - mediaType, params, err := mime.ParseMediaType(meta) - - if err != nil { - meta = fmt.Sprintf("%s;charset=%s", DEFAULT_MIME, DEFAULT_CHARSET) - } else if strings.HasPrefix(mediaType, "text/") { - if _, ok := params["charset"]; !ok { - meta += ";charset=" + DEFAULT_CHARSET - } - } - } - - header = &Header{ - Status: status, - Meta: meta, - } - - return -} diff --git a/gopherproxy/libgopher/libgopher.go b/gopherproxy/libgopher/libgopher.go deleted file mode 100644 index 86d58ff..0000000 --- a/gopherproxy/libgopher/libgopher.go +++ /dev/null @@ -1,312 +0,0 @@ -package libgopher - -import ( - "bufio" - "errors" - "io" - "log" - "net" - "net/url" - "strings" -) - -// Item Types -const ( - FILE = ItemType('0') // Item is a file - DIRECTORY = ItemType('1') // Item is a directory - PHONEBOOK = ItemType('2') // Item is a CSO phone-book server - ERROR = ItemType('3') // Error - BINHEX = ItemType('4') // Item is a BinHexed Macintosh file. - DOSARCHIVE = ItemType('5') // Item is DOS binary archive of some sort. (*) - UUENCODED = ItemType('6') // Item is a UNIX uuencoded file. - INDEXSEARCH = ItemType('7') // Item is an Index-Search server. - TELNET = ItemType('8') // Item points to a text-based telnet session. - BINARY = ItemType('9') // Item is a binary file! (*) - - // (*) Client must read until the TCP connection is closed. - - REDUNDANT = ItemType('+') // Item is a redundant server - TN3270 = ItemType('T') // Item points to a text-based tn3270 session. - GIF = ItemType('g') // Item is a GIF format graphics file. - IMAGE = ItemType('I') // Item is some kind of image file. - - // non-standard - INFO = ItemType('i') // Item is an informational message - HTML = ItemType('h') // Item is a HTML document - AUDIO = ItemType('s') // Item is an Audio file - PNG = ItemType('p') // Item is a PNG Image - DOC = ItemType('d') // Item is a Document -) - -const ( - // END represents the terminator used in directory responses - END = byte('.') - - // TAB is the delimiter used to separate item response parts - TAB = byte('\t') - - // CRLF is the delimiter used per line of response item - CRLF = "\r\n" - - // DEFAULT is the default item type - DEFAULT = BINARY -) - -// ItemType represents the type of an item -type ItemType byte - -// Return a human friendly represation of an ItemType -func (it ItemType) String() string { - switch it { - case FILE: - return "TXT" - case DIRECTORY: - return "DIR" - case PHONEBOOK: - return "PHO" - case ERROR: - return "ERR" - case BINHEX: - return "HEX" - case DOSARCHIVE: - return "ARC" - case UUENCODED: - return "UUE" - case INDEXSEARCH: - return "QRY" - case TELNET: - return "TEL" - case BINARY: - return "BIN" - case REDUNDANT: - return "DUP" - case TN3270: - return "TN3" - case GIF: - return "GIF" - case IMAGE: - return "IMG" - case INFO: - return "NFO" - case HTML: - return "HTM" - case AUDIO: - return "SND" - case PNG: - return "PNG" - case DOC: - return "DOC" - default: - return "???" - } -} - -// Item describes an entry in a directory listing. -type Item struct { - Type ItemType `json:"type"` - Description string `json:"description"` - Selector string `json:"selector"` - Host string `json:"host"` - Port string `json:"port"` - - // non-standard extensions (ignored by standard clients) - Extras []string `json:"extras"` -} - -// ParseItem parses a line of text into an item -func ParseItem(line string) (item *Item, err error) { - parts := strings.Split(strings.Trim(line, "\r\n"), "\t") - - if len(parts[0]) < 1 { - return nil, errors.New("no item type: " + string(line)) - } - - item = &Item{ - Type: ItemType(parts[0][0]), - Description: string(parts[0][1:]), - Extras: make([]string, 0), - } - - // Selector - if len(parts) > 1 { - item.Selector = string(parts[1]) - } else { - item.Selector = "" - } - - // Host - if len(parts) > 2 { - item.Host = string(parts[2]) - } else { - item.Host = "null.host" - } - - // Port - if len(parts) > 3 { - item.Port = string(parts[3]) - } else { - item.Port = "0" - } - - // Extras - if len(parts) >= 4 { - for _, v := range parts[4:] { - item.Extras = append(item.Extras, string(v)) - } - } - - return -} - -func (i *Item) isDirectoryLike() bool { - switch i.Type { - case DIRECTORY: - return true - case INDEXSEARCH: - return true - default: - return false - } -} - -// Directory representes a Gopher Menu of Items -type Directory struct { - Items []*Item `json:"items"` -} - -// Response represents a Gopher resource that -// Items contains a non-empty array of Item(s) -// for directory types, otherwise the Body -// contains the fetched resource (file, image, etc). -type Response struct { - Type ItemType - Dir Directory - Body io.Reader -} - -// Get fetches a Gopher resource by URI -func Get(uri string) (*Response, error) { - u, err := url.Parse(uri) - if err != nil { - return nil, err - } - - if u.Scheme != "gopher" { - return nil, errors.New("invalid scheme for uri") - } - - host := u.Hostname() - port := u.Port() - - if port == "" { - port = "70" - } - - var ( - Type ItemType - Selector string - ) - - path := strings.TrimPrefix(u.Path, "/") - if len(path) > 2 { - Type = ItemType(path[0]) - Selector = path[1:] - if u.RawQuery != "" { - Selector += "\t" + u.RawQuery - } - } else if len(path) == 1 { - Type = ItemType(path[0]) - Selector = "" - } else { - Type = ItemType(DIRECTORY) - Selector = "" - } - - i := Item{Type: Type, Selector: Selector, Host: host, Port: port} - res := Response{Type: i.Type} - - if i.isDirectoryLike() { - d, err := i.FetchDirectory() - if err != nil { - return nil, err - } - - res.Dir = d - } else { - reader, err := i.FetchFile() - if err != nil { - return nil, err - } - - res.Body = reader - } - - return &res, nil -} - -// FetchFile fetches data, not directory information. -// Calling this on a DIRECTORY Item type -// or unsupported type will return an error. -func (i *Item) FetchFile() (io.Reader, error) { - if i.Type == DIRECTORY { - return nil, errors.New("cannot fetch a directory as a file") - } - - conn, err := net.Dial("tcp", net.JoinHostPort(i.Host, i.Port)) - if err != nil { - return nil, err - } - - _, err = conn.Write([]byte(i.Selector + CRLF)) - if err != nil { - conn.Close() - return nil, err - } - - return conn, nil -} - -// FetchDirectory fetches directory information, not data. -// Calling this on an Item whose type is not DIRECTORY will return an error. -func (i *Item) FetchDirectory() (Directory, error) { - if !i.isDirectoryLike() { - return Directory{}, errors.New("cannot fetch a file as a directory") - } - - conn, err := net.Dial("tcp", net.JoinHostPort(i.Host, i.Port)) - if err != nil { - return Directory{}, err - } - - _, err = conn.Write([]byte(i.Selector + CRLF)) - if err != nil { - return Directory{}, err - } - - reader := bufio.NewReader(conn) - scanner := bufio.NewScanner(reader) - scanner.Split(bufio.ScanLines) - - var items []*Item - - for scanner.Scan() { - line := strings.Trim(scanner.Text(), "\r\n") - - if len(line) == 0 { - continue - } - - if len(line) == 1 && line[0] == END { - break - } - - item, err := ParseItem(line) - if err != nil { - log.Printf("Error parsing %q: %q", line, err) - continue - } - items = append(items, item) - } - - return Directory{items}, nil -} diff --git a/gopherproxy/template.go b/gopherproxy/template.go deleted file mode 100644 index 6b90cc0..0000000 --- a/gopherproxy/template.go +++ /dev/null @@ -1,122 +0,0 @@ -package gopherproxy - -var tpltext = ` - - - - - {{ .Title }}{{ if ne .Protocol "startpage" }} - {{ .Protocol | title }} proxy{{ end }} - - - - -
-
- {{ .Protocol }}://:// - - {{- if .URI -}} - {{- $page := . -}} - {{- $href := printf "/%s" .Protocol -}} - {{- $uriParts := split .URI "/" -}} - - {{- $uriLast := $uriParts | last -}} - {{- $uriParts = $uriParts | pop -}} - {{- if eq $uriLast "" -}} - {{- $uriLast = $uriParts | last -}} - {{- $uriParts = $uriParts | pop -}} - {{- end -}} - - {{- range $i, $part := $uriParts -}} - {{- if and (eq $page.Protocol "gopher") (eq $i 1) -}} - {{- $href = printf "%s/1" $href -}} - {{- $part = $part | trimLeftChar -}} - {{- if not (eq $part "") -}} - {{- $href = printf "%s/%s" $href $part -}} - /{{ $part }} - {{- end -}} - {{- else -}} - {{- $href = printf "%s/%s" $href . -}} - {{- if ne $i 0 -}} - / - {{- end -}} - {{ . }} - {{- end -}} - {{- end -}} - {{- if ne (len $uriParts) 0 -}} - / - {{- end -}} - {{- if and (eq $page.Protocol "gopher") (eq (len $uriParts) 1) -}} - {{- $uriLast = $uriLast | trimLeftChar -}} - {{- end -}} - {{ $uriLast }} - {{- end -}} -
-
- {{- if and (not .Lines) (not .Error) (eq .Protocol "gopher") -}} - - {{- end -}} -
-
-
-
-
-				{{- if .Lines -}}
-					{{- $content := "" -}}
-					{{- range .Lines -}}
-						{{- if ne $content "" -}}
-							{{- $content = printf "%s\n" $content -}}
-						{{- end -}}
-						{{- if .Link -}}
-							{{- $content = printf "%s%s" $content (printf "%s  %s" .Type .Type .Link (.Text | HTMLEscape)) -}}
-						{{- else -}}
-							{{- $content = printf "%s%s" $content (printf "     %s" (.Text | HTMLEscape)) -}}
-						{{- end -}}
-					{{- end -}}
-					{{- $content | safeHtml -}}
-				{{- else -}}
-					{{- .RawText -}}
-				{{- end -}}
-			
-
- - - -` diff --git a/internal/port/gemini.go b/internal/port/gemini.go new file mode 100644 index 0000000..f9b0b97 --- /dev/null +++ b/internal/port/gemini.go @@ -0,0 +1,205 @@ +package port + +import ( + "bytes" + "fmt" + "html/template" + "io" + "log" + "mime" + "net/http" + "net/url" + "regexp" + "strings" + + "golang.org/x/net/html/charset" + "golang.org/x/text/transform" + + "git.vulpes.one/Feuerfuchs/port/port/libgemini" + + "github.com/temoto/robotstxt" +) + +var ( + TermEscapeSGRPattern = regexp.MustCompile("\\[\\d+(;\\d+)*m") +) + +func resolveURI(uri string, baseURL *url.URL) (resolvedURI string) { + if strings.HasPrefix(uri, "//") { + resolvedURI = "/gemini/" + strings.TrimPrefix(uri, "//") + } else if strings.HasPrefix(uri, "gemini://") { + resolvedURI = "/gemini/" + strings.TrimPrefix(uri, "gemini://") + } else if strings.HasPrefix(uri, "gopher://") { + resolvedURI = "/gopher/" + strings.TrimPrefix(uri, "gopher://") + } else { + url, err := url.Parse(uri) + if err != nil { + return "" + } + adjustedURI := baseURL.ResolveReference(url) + path := adjustedURI.Path + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + if adjustedURI.Scheme == "gemini" { + resolvedURI = "/gemini/" + adjustedURI.Host + path + } else if adjustedURI.Scheme == "gopher" { + resolvedURI = "/gopher/" + adjustedURI.Host + path + } else { + resolvedURI = adjustedURI.String() + } + } + + return +} + +func GeminiHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + agent := req.UserAgent() + path := strings.TrimPrefix(req.URL.Path, "/gemini/") + + if robotsdata != nil && robotsdebug && !robotsdata.TestAgent(path, agent) { + log.Printf("UserAgent %s ignored robots.txt", agent) + } + + parts := strings.Split(path, "/") + hostport := parts[0] + + if len(hostport) == 0 { + http.Redirect(w, req, "/", http.StatusFound) + return + } + + title := hostport + + var qs string + + if req.URL.RawQuery != "" { + qs = fmt.Sprintf("?%s", url.QueryEscape(req.URL.RawQuery)) + } + + uri, err := url.QueryUnescape(strings.Join(parts[1:], "/")) + if err != nil { + if e := tpl.Execute(w, TemplateVariables{ + Title: title, + URI: hostport, + Assets: assetList, + RawText: fmt.Sprintf("Error: %s", err), + Error: true, + Protocol: "gemini", + }); e != nil { + log.Println("Template error: " + e.Error()) + log.Println(err.Error()) + } + return + } + + if uri != "" { + title = fmt.Sprintf("%s/%s", hostport, uri) + } + + res, err := libgemini.Get( + fmt.Sprintf( + "gemini://%s/%s%s", + hostport, + uri, + qs, + ), + ) + + if err != nil { + if e := tpl.Execute(w, TemplateVariables{ + Title: title, + URI: fmt.Sprintf("%s/%s", hostport, uri), + Assets: assetList, + RawText: fmt.Sprintf("Error: %s", err), + Error: true, + Protocol: "gemini", + }); e != nil { + log.Println("Template error: " + e.Error()) + log.Println(err.Error()) + } + return + } + + if int(res.Header.Status/10) == 3 { + baseURL, err := url.Parse(fmt.Sprintf( + "gemini://%s/%s", + hostport, + uri, + )) + if err != nil { + if e := tpl.Execute(w, TemplateVariables{ + Title: title, + URI: fmt.Sprintf("%s/%s", hostport, uri), + Assets: assetList, + RawText: fmt.Sprintf("Error: %s", err), + Error: true, + Protocol: "gemini", + }); e != nil { + log.Println("Template error: " + e.Error()) + log.Println(err.Error()) + } + return + } + + http.Redirect(w, req, resolveURI(res.Header.Meta, baseURL), http.StatusFound) + return + } + + if int(res.Header.Status/10) != 2 { + if err := tpl.Execute(w, TemplateVariables{ + Title: title, + URI: fmt.Sprintf("%s/%s", hostport, uri), + Assets: assetList, + RawText: fmt.Sprintf("Error %d: %s", res.Header.Status, res.Header.Meta), + Error: true, + Protocol: "gemini", + }); err != nil { + log.Println("Template error: " + err.Error()) + } + return + } + + if strings.HasPrefix(res.Header.Meta, "text/") { + buf := new(bytes.Buffer) + + _, params, err := mime.ParseMediaType(res.Header.Meta) + if err != nil { + buf.ReadFrom(res.Body) + } else { + encoding, _ := charset.Lookup(params["charset"]) + readbuf := new(bytes.Buffer) + readbuf.ReadFrom(res.Body) + + writer := transform.NewWriter(buf, encoding.NewDecoder()) + writer.Write(readbuf.Bytes()) + writer.Close() + } + + var ( + rawText string + items []Item + ) + + if strings.HasPrefix(res.Header.Meta, libgemini.MIME_GEMINI) { + items = parseGeminiDocument(buf, uri, hostport) + } else { + rawText = buf.String() + } + + if err := tpl.Execute(w, TemplateVariables{ + Title: title, + URI: fmt.Sprintf("%s/%s", hostport, uri), + Assets: assetList, + Lines: items, + RawText: rawText, + Protocol: "gemini", + }); err != nil { + log.Println("Template error: " + err.Error()) + } + } else { + io.Copy(w, res.Body) + } + } +} diff --git a/internal/port/gopher.go b/internal/port/gopher.go new file mode 100644 index 0000000..ebeb213 --- /dev/null +++ b/internal/port/gopher.go @@ -0,0 +1,217 @@ +package port + +import ( + "bytes" + "fmt" + "html/template" + "io" + "log" + "net" + "net/http" + "net/url" + "strings" + + "git.vulpes.one/Feuerfuchs/port/port/libgopher" + + "github.com/davidbyttow/govips/pkg/vips" + "github.com/temoto/robotstxt" +) + +type Item struct { + Link template.URL + Type string + Text string +} + +func renderGopherDirectory(w http.ResponseWriter, tpl *template.Template, assetList AssetList, uri string, hostport string, d libgopher.Directory) error { + var title string + + out := make([]Item, len(d.Items)) + + for i, x := range d.Items { + if x.Type == libgopher.INFO && x.Selector == "TITLE" { + title = x.Description + continue + } + + tr := Item{ + Text: x.Description, + Type: x.Type.String(), + } + + if x.Type == libgopher.INFO { + out[i] = tr + continue + } + + if strings.HasPrefix(x.Selector, "URL:") || strings.HasPrefix(x.Selector, "/URL:") { + link := strings.TrimPrefix(strings.TrimPrefix(x.Selector, "/"), "URL:") + if strings.HasPrefix(link, "gemini://") { + link = fmt.Sprintf( + "/gemini/%s", + strings.TrimPrefix(link, "gemini://"), + ) + } else if strings.HasPrefix(link, "gopher://") { + link = fmt.Sprintf( + "/gopher/%s", + strings.TrimPrefix(link, "gopher://"), + ) + } + tr.Link = template.URL(link) + } else { + var linkHostport string + if x.Port != "70" { + linkHostport = net.JoinHostPort(x.Host, x.Port) + } else { + linkHostport = x.Host + } + + path := url.PathEscape(x.Selector) + path = strings.Replace(path, "%2F", "/", -1) + tr.Link = template.URL( + fmt.Sprintf( + "/gopher/%s/%s%s", + linkHostport, + string(byte(x.Type)), + path, + ), + ) + } + + out[i] = tr + } + + if title == "" { + if uri != "" { + title = fmt.Sprintf("%s/%s", hostport, uri) + } else { + title = hostport + } + } + + return tpl.Execute(w, TemplateVariables{ + Title: title, + URI: fmt.Sprintf("%s/%s", hostport, uri), + Assets: assetList, + Lines: out, + Protocol: "gopher", + }) +} + +// GopherHandler returns a Handler that proxies requests +// to the specified Gopher server as denoated by the first argument +// to the request path and renders the content using the provided template. +// The optional robots parameters points to a robotstxt.RobotsData struct +// to test user agents against a configurable robotst.txt file. +func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + agent := req.UserAgent() + path := strings.TrimPrefix(req.URL.Path, "/gopher/") + + if robotsdata != nil && robotsdebug && !robotsdata.TestAgent(path, agent) { + log.Printf("UserAgent %s ignored robots.txt", agent) + } + + parts := strings.Split(path, "/") + hostport := parts[0] + + if len(hostport) == 0 { + http.Redirect(w, req, "/", http.StatusFound) + return + } + + title := hostport + + var qs string + + if req.URL.RawQuery != "" { + qs = fmt.Sprintf("?%s", url.QueryEscape(req.URL.RawQuery)) + } + + uri, err := url.QueryUnescape(strings.Join(parts[1:], "/")) + if err != nil { + if e := tpl.Execute(w, TemplateVariables{ + Title: title, + URI: hostport, + Assets: assetList, + RawText: fmt.Sprintf("Error: %s", err), + Error: true, + Protocol: "gopher", + }); e != nil { + log.Println("Template error: " + e.Error()) + log.Println(err.Error()) + } + return + } + + if uri != "" { + title = fmt.Sprintf("%s/%s", hostport, uri) + } + + res, err := libgopher.Get( + fmt.Sprintf( + "gopher://%s/%s%s", + hostport, + uri, + qs, + ), + ) + + if err != nil { + if e := tpl.Execute(w, TemplateVariables{ + Title: title, + URI: fmt.Sprintf("%s/%s", hostport, uri), + Assets: assetList, + RawText: fmt.Sprintf("Error: %s", err), + Error: true, + Protocol: "gopher", + }); e != nil { + log.Println("Template error: " + e.Error()) + } + return + } + + if res.Body != nil { + if len(parts) < 2 { + io.Copy(w, res.Body) + } else if strings.HasPrefix(parts[1], "0") && !strings.HasSuffix(uri, ".xml") && !strings.HasSuffix(uri, ".asc") { + buf := new(bytes.Buffer) + buf.ReadFrom(res.Body) + + if err := tpl.Execute(w, TemplateVariables{ + Title: title, + URI: fmt.Sprintf("%s/%s", hostport, uri), + Assets: assetList, + RawText: buf.String(), + Protocol: "gopher", + }); err != nil { + log.Println("Template error: " + err.Error()) + } + } else if strings.HasPrefix(parts[1], "T") { + _, _, err = vips.NewTransform(). + Load(res.Body). + ResizeStrategy(vips.ResizeStrategyAuto). + ResizeWidth(160). + Quality(75). + Output(w). + Apply() + } else { + io.Copy(w, res.Body) + } + } else { + if err := renderGopherDirectory(w, tpl, assetList, uri, hostport, res.Dir); err != nil { + if e := tpl.Execute(w, TemplateVariables{ + Title: title, + URI: fmt.Sprintf("%s/%s", hostport, uri), + Assets: assetList, + RawText: fmt.Sprintf("Error: %s", err), + Error: true, + Protocol: "gopher", + }); e != nil { + log.Println("Template error: " + e.Error()) + log.Println(e.Error()) + } + } + } + } +} diff --git a/internal/port/main.go b/internal/port/main.go new file mode 100644 index 0000000..5cdd794 --- /dev/null +++ b/internal/port/main.go @@ -0,0 +1,301 @@ +package port + +import ( + "crypto/md5" + "fmt" + "html" + "html/template" + "io/ioutil" + "log" + "net/http" + "regexp" + "strings" + + "github.com/NYTimes/gziphandler" + "github.com/davidbyttow/govips/pkg/vips" + "github.com/gobuffalo/packr/v2" + "github.com/temoto/robotstxt" +) + +type AssetList struct { + Style string + JS string + FontW string + FontW2 string + PropFontW string + PropFontW2 string +} + +type TemplateVariables struct { + Title string + URI string + Assets AssetList + RawText string + Lines []Item + Error bool + Protocol string +} + +func DefaultHandler(tpl *template.Template, startpagetext string, assetList AssetList) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + if err := tpl.Execute(w, TemplateVariables{ + Title: "Gopher/Gemini proxy", + Assets: assetList, + RawText: startpagetext, + Protocol: "startpage", + }); err != nil { + log.Println("Template error: " + err.Error()) + } + } +} + +// RobotsTxtHandler returns the contents of the robots.txt file +// if configured and valid. +func RobotsTxtHandler(robotstxtdata []byte) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + if robotstxtdata == nil { + http.Error(w, "Not Found", http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", "text/plain") + w.Write(robotstxtdata) + } +} + +func FaviconHandler(favicondata []byte) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + if favicondata == nil { + http.Error(w, "Not Found", http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", "image/vnd.microsoft.icon") + w.Header().Set("Cache-Control", "max-age=2592000") + w.Write(favicondata) + } +} + +func StyleHandler(styledata []byte) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "text/css") + w.Header().Set("Cache-Control", "max-age=2592000") + w.Write(styledata) + } +} + +func JavaScriptHandler(jsdata []byte) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "text/javascript") + w.Header().Set("Cache-Control", "max-age=2592000") + w.Write(jsdata) + } +} + +func FontHandler(woff2 bool, fontdata []byte) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + if fontdata == nil { + http.Error(w, "Not Found", http.StatusNotFound) + return + } + + if woff2 { + w.Header().Set("Content-Type", "font/woff2") + } else { + w.Header().Set("Content-Type", "font/woff") + } + w.Header().Set("Cache-Control", "max-age=2592000") + + w.Write(fontdata) + } +} + +// ListenAndServe creates a listening HTTP server bound to +// the interface specified by bind and sets up a Gopher to HTTP +// proxy proxying requests as requested and by default will prozy +// to a Gopher server address specified by uri if no servers is +// specified by the request. The robots argument is a pointer to +// a robotstxt.RobotsData struct for testing user agents against +// a configurable robots.txt file. +func ListenAndServe(bind, startpagefile string, robotsfile string, robotsdebug bool, vipsconcurrency int) error { + box := packr.New("assets", "../assets") + + // + // Robots + + var robotsdata *robotstxt.RobotsData + + robotstxtdata, err := ioutil.ReadFile(robotsfile) + if err != nil { + log.Printf("error reading robots.txt: %s", err) + robotstxtdata = nil + } else { + robotsdata, err = robotstxt.FromBytes(robotstxtdata) + if err != nil { + log.Printf("error reading robots.txt: %s", err) + robotstxtdata = nil + } + } + + // + // Fonts + + fontdataw, err := box.Find("iosevka-term-ss03-regular.woff") + if err != nil { + fontdataw = []byte{} + } + fontwAsset := fmt.Sprintf("/iosevka-term-ss03-regular-%x.woff", md5.Sum(fontdataw)) + + fontdataw2, err := box.Find("iosevka-term-ss03-regular.woff2") + if err != nil { + fontdataw2 = []byte{} + } + fontw2Asset := fmt.Sprintf("/iosevka-term-ss03-regular-%x.woff2", md5.Sum(fontdataw2)) + + propfontdataw, err := box.Find("iosevka-aile-regular.woff") + if err != nil { + propfontdataw = []byte{} + } + propfontwAsset := fmt.Sprintf("/iosevka-aile-regular-%x.woff", md5.Sum(propfontdataw)) + + propfontdataw2, err := box.Find("iosevka-aile-regular.woff2") + if err != nil { + propfontdataw2 = []byte{} + } + propfontw2Asset := fmt.Sprintf("/iosevka-aile-regular-%x.woff2", md5.Sum(propfontdataw2)) + + // + // Stylesheet + + styledata, err := box.Find("style.css") + if err != nil { + styledata = []byte{} + } + styleAsset := fmt.Sprintf("/style-%x.css", md5.Sum(styledata)) + + // + // JavaScript + + jsdata, err := box.Find("main.js") + if err != nil { + jsdata = []byte{} + } + jsAsset := fmt.Sprintf("/main-%x.js", md5.Sum(jsdata)) + + // + // Favicon + + favicondata, err := box.Find("favicon.ico") + if err != nil { + favicondata = []byte{} + } + + // + // Start page text + + startpagedata, err := ioutil.ReadFile(startpagefile) + if err != nil { + startpagedata, err = box.Find("startpage.txt") + if err != nil { + startpagedata = []byte{} + } + } + startpagetext := string(startpagedata) + + // + // + + var allFiles []string + files, err := ioutil.ReadDir("./tpl") + if err != nil { + fmt.Println(err) + } + for _, file := range files { + filename := file.Name() + if strings.HasSuffix(filename, ".html") { + allFiles = append(allFiles, "./tpl/"+filename) + } + } + + templates, err = template.ParseFiles(allFiles...) + + // + + funcMap := template.FuncMap{ + "safeHtml": func(s string) template.HTML { + return template.HTML(s) + }, + "safeCss": func(s string) template.CSS { + return template.CSS(s) + }, + "safeJs": func(s string) template.JS { + return template.JS(s) + }, + "HTMLEscape": func(s string) string { + return html.EscapeString(s) + }, + "split": strings.Split, + "last": func(s []string) string { + return s[len(s)-1] + }, + "pop": func(s []string) []string { + return s[:len(s)-1] + }, + "replace": func(pattern, output string, input interface{}) string { + var re = regexp.MustCompile(pattern) + var inputStr = fmt.Sprintf("%v", input) + return re.ReplaceAllString(inputStr, output) + }, + "trimLeftChar": func(s string) string { + for i := range s { + if i > 0 { + return s[i:] + } + } + return s[:0] + }, + "hasPrefix": func(s string, prefix string) bool { + return strings.HasPrefix(s, prefix) + }, + "title": func(s string) string { + return strings.Title(s) + }, + } + + // + + startpageTpl := templates.Lookup("startpage.html").Funcs(funcMap) + geminiTpl := templates.Lookup("gemini.html").Funcs(funcMap) + gopherTpl := templates.Lookup("gopher.html").Funcs(funcMap) + + // + // + + vips.Startup(&vips.Config{ + ConcurrencyLevel: vipsconcurrency, + }) + + assets := AssetList{ + Style: styleAsset, + JS: jsAsset, + FontW: fontwAsset, + FontW2: fontw2Asset, + PropFontW: propfontwAsset, + PropFontW2: propfontw2Asset, + } + + http.Handle("/", gziphandler.GzipHandler(DefaultHandler(startpageTpl, startpagetext, assets))) + http.Handle("/gopher/", gziphandler.GzipHandler(GopherHandler(gopherTpl, robotsdata, assets, robotsdebug))) + http.Handle("/gemini/", gziphandler.GzipHandler(GeminiHandler(geminiTpl, robotsdata, assets, robotsdebug))) + http.Handle("/robots.txt", gziphandler.GzipHandler(RobotsTxtHandler(robotstxtdata))) + http.Handle("/favicon.ico", gziphandler.GzipHandler(FaviconHandler(favicondata))) + http.Handle(styleAsset, gziphandler.GzipHandler(StyleHandler(styledata))) + http.Handle(jsAsset, gziphandler.GzipHandler(JavaScriptHandler(jsdata))) + http.HandleFunc(fontwAsset, FontHandler(false, fontdataw)) + http.HandleFunc(fontw2Asset, FontHandler(true, fontdataw2)) + http.HandleFunc(propfontwAsset, FontHandler(false, propfontdataw)) + http.HandleFunc(propfontw2Asset, FontHandler(true, propfontdataw2)) + //http.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir("assets/")))) + + return http.ListenAndServe(bind, nil) +} diff --git a/internal/port/tpl/gemini.html b/internal/port/tpl/gemini.html new file mode 100644 index 0000000..e69de29 diff --git a/internal/port/tpl/gopher.html b/internal/port/tpl/gopher.html new file mode 100644 index 0000000..e69de29 diff --git a/internal/port/tpl/startpage.html b/internal/port/tpl/startpage.html new file mode 100644 index 0000000..8482a6f --- /dev/null +++ b/internal/port/tpl/startpage.html @@ -0,0 +1,120 @@ + + + + + + {{ .Title }}{{ if ne .Protocol "startpage" }} - {{ .Protocol | title }} proxy{{ end }} + + + + +
+
+ {{ .Protocol }}://:// + + {{- if .URI -}} + {{- $page := . -}} + {{- $href := printf "/%s" .Protocol -}} + {{- $uriParts := split .URI "/" -}} + + {{- $uriLast := $uriParts | last -}} + {{- $uriParts = $uriParts | pop -}} + {{- if eq $uriLast "" -}} + {{- $uriLast = $uriParts | last -}} + {{- $uriParts = $uriParts | pop -}} + {{- end -}} + + {{- range $i, $part := $uriParts -}} + {{- if and (eq $page.Protocol "gopher") (eq $i 1) -}} + {{- $href = printf "%s/1" $href -}} + {{- $part = $part | trimLeftChar -}} + {{- if not (eq $part "") -}} + {{- $href = printf "%s/%s" $href $part -}} + /{{ $part }} + {{- end -}} + {{- else -}} + {{- $href = printf "%s/%s" $href . -}} + {{- if ne $i 0 -}} + / + {{- end -}} + {{ . }} + {{- end -}} + {{- end -}} + {{- if ne (len $uriParts) 0 -}} + / + {{- end -}} + {{- if and (eq $page.Protocol "gopher") (eq (len $uriParts) 1) -}} + {{- $uriLast = $uriLast | trimLeftChar -}} + {{- end -}} + {{ $uriLast }} + {{- end -}} +
+
+ {{- if and (not .Lines) (not .Error) (eq .Protocol "gopher") -}} + + {{- end -}} +
+
+
+
+
+				{{- if .Lines -}}
+					{{- $content := "" -}}
+					{{- range .Lines -}}
+						{{- if ne $content "" -}}
+							{{- $content = printf "%s\n" $content -}}
+						{{- end -}}
+						{{- if .Link -}}
+							{{- $content = printf "%s%s" $content (printf "%s  %s" .Type .Type .Link (.Text | HTMLEscape)) -}}
+						{{- else -}}
+							{{- $content = printf "%s%s" $content (printf "     %s" (.Text | HTMLEscape)) -}}
+						{{- end -}}
+					{{- end -}}
+					{{- $content | safeHtml -}}
+				{{- else -}}
+					{{- .RawText -}}
+				{{- end -}}
+			
+
+ + + + diff --git a/main.go b/main.go deleted file mode 100644 index f5a82fe..0000000 --- a/main.go +++ /dev/null @@ -1,25 +0,0 @@ -package main - -import ( - "flag" - "log" - - "git.feuerfuchs.dev/Feuerfuchs/gopherproxy/gopherproxy" -) - -var ( - // TODO: Allow config file and environment vars - // (opt -> env -> config -> default) - bind = flag.String("bind", "0.0.0.0:8000", "[int]:port to bind to") - startpagefile = flag.String("startpage-file", "startpage.txt", "Default page to display if no URL is specified") - robotsfile = flag.String("robots-file", "robots.txt", "robots.txt file") - robotsdebug = flag.Bool("robots-debug", false, "print output about ignored robots.txt") - vipsconcurrency = flag.Int("vips-concurrency", 1, "Concurrency level of libvips") -) - -func main() { - flag.Parse() - - // Use a config struct - log.Fatal(gopherproxy.ListenAndServe(*bind, *startpagefile, *robotsfile, *robotsdebug, *vipsconcurrency)) -} diff --git a/pkg/libgemini/libgemini.go b/pkg/libgemini/libgemini.go new file mode 100644 index 0000000..303490c --- /dev/null +++ b/pkg/libgemini/libgemini.go @@ -0,0 +1,145 @@ +package libgemini + +import ( + "bufio" + "crypto/tls" + "errors" + "fmt" + "io" + "mime" + "net" + "net/url" + "regexp" + "strconv" + "strings" +) + +const ( + CRLF = "\r\n" +) + +const ( + STATUS_INPUT = 10 + STATUS_SUCCESS = 20 + STATUS_SUCCESS_CERT = 21 + STATUS_REDIRECT_TEMP = 30 + STATUS_REDIRECT_PERM = 31 + STATUS_TEMP_FAILURE = 40 + STATUS_SERVER_UNAVAILABLE = 41 + STATUS_CGI_ERROR = 42 + STATUS_PROXY_ERROR = 43 + STATUS_SLOW_DOWN = 44 + STATUS_PERM_FAILURE = 50 + STATUS_NOT_FOUND = 51 + STATUS_GONE = 52 + STATUS_PROXY_REFUSED = 53 + STATUS_BAD_REQUEST = 59 + STATUS_CLIENT_CERT_EXPIRED = 60 + STATUS_TRANSIENT_CERT_REQUEST = 61 + STATUS_AUTH_CERT_REQUIRED = 62 + STATUS_CERT_REJECTED = 63 + STATUS_FUTURE_CERT_REJECTED = 64 + STATUS_EXPIRED_CERT_REJECTED = 65 +) + +const ( + MIME_GEMINI = "text/gemini" + DEFAULT_MIME = MIME_GEMINI + DEFAULT_CHARSET = "utf-8" +) + +var ( + HeaderPattern = regexp.MustCompile("^(\\d\\d)[ \\t]+(.*)$") + LinkPattern = regexp.MustCompile("^=>[ \\t]*([^ \\t]+)(?:[ \\t]+(.*))?$") +) + +type Header struct { + Status int + Meta string +} + +type Response struct { + Header *Header + Body io.Reader +} + +func Get(uri string) (*Response, error) { + u, err := url.Parse(uri) + if err != nil { + return nil, err + } + + if u.Scheme != "gemini" { + return nil, errors.New("invalid scheme for uri") + } + + host := u.Hostname() + port := u.Port() + + if port == "" { + port = "1965" + } + + conn, err := tls.Dial("tcp", net.JoinHostPort(host, port), &tls.Config{ + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: true, + }) + if err != nil { + return nil, err + } + + _, err = conn.Write([]byte(u.String() + CRLF)) + if err != nil { + conn.Close() + return nil, err + } + + reader := bufio.NewReader(conn) + + line, _, err := reader.ReadLine() + if err != nil { + conn.Close() + return nil, err + } + + header, err := ParseHeader(string(line)) + if err != nil { + conn.Close() + return nil, err + } + + return &Response{ + Header: header, + Body: reader, + }, nil +} + +func ParseHeader(line string) (header *Header, err error) { + matches := HeaderPattern.FindStringSubmatch(line) + + status, err := strconv.Atoi(matches[1]) + if err != nil { + return nil, err + } + + meta := matches[2] + + if int(status/10) == 2 { + mediaType, params, err := mime.ParseMediaType(meta) + + if err != nil { + meta = fmt.Sprintf("%s;charset=%s", DEFAULT_MIME, DEFAULT_CHARSET) + } else if strings.HasPrefix(mediaType, "text/") { + if _, ok := params["charset"]; !ok { + meta += ";charset=" + DEFAULT_CHARSET + } + } + } + + header = &Header{ + Status: status, + Meta: meta, + } + + return +} diff --git a/pkg/libgopher/libgopher.go b/pkg/libgopher/libgopher.go new file mode 100644 index 0000000..86d58ff --- /dev/null +++ b/pkg/libgopher/libgopher.go @@ -0,0 +1,312 @@ +package libgopher + +import ( + "bufio" + "errors" + "io" + "log" + "net" + "net/url" + "strings" +) + +// Item Types +const ( + FILE = ItemType('0') // Item is a file + DIRECTORY = ItemType('1') // Item is a directory + PHONEBOOK = ItemType('2') // Item is a CSO phone-book server + ERROR = ItemType('3') // Error + BINHEX = ItemType('4') // Item is a BinHexed Macintosh file. + DOSARCHIVE = ItemType('5') // Item is DOS binary archive of some sort. (*) + UUENCODED = ItemType('6') // Item is a UNIX uuencoded file. + INDEXSEARCH = ItemType('7') // Item is an Index-Search server. + TELNET = ItemType('8') // Item points to a text-based telnet session. + BINARY = ItemType('9') // Item is a binary file! (*) + + // (*) Client must read until the TCP connection is closed. + + REDUNDANT = ItemType('+') // Item is a redundant server + TN3270 = ItemType('T') // Item points to a text-based tn3270 session. + GIF = ItemType('g') // Item is a GIF format graphics file. + IMAGE = ItemType('I') // Item is some kind of image file. + + // non-standard + INFO = ItemType('i') // Item is an informational message + HTML = ItemType('h') // Item is a HTML document + AUDIO = ItemType('s') // Item is an Audio file + PNG = ItemType('p') // Item is a PNG Image + DOC = ItemType('d') // Item is a Document +) + +const ( + // END represents the terminator used in directory responses + END = byte('.') + + // TAB is the delimiter used to separate item response parts + TAB = byte('\t') + + // CRLF is the delimiter used per line of response item + CRLF = "\r\n" + + // DEFAULT is the default item type + DEFAULT = BINARY +) + +// ItemType represents the type of an item +type ItemType byte + +// Return a human friendly represation of an ItemType +func (it ItemType) String() string { + switch it { + case FILE: + return "TXT" + case DIRECTORY: + return "DIR" + case PHONEBOOK: + return "PHO" + case ERROR: + return "ERR" + case BINHEX: + return "HEX" + case DOSARCHIVE: + return "ARC" + case UUENCODED: + return "UUE" + case INDEXSEARCH: + return "QRY" + case TELNET: + return "TEL" + case BINARY: + return "BIN" + case REDUNDANT: + return "DUP" + case TN3270: + return "TN3" + case GIF: + return "GIF" + case IMAGE: + return "IMG" + case INFO: + return "NFO" + case HTML: + return "HTM" + case AUDIO: + return "SND" + case PNG: + return "PNG" + case DOC: + return "DOC" + default: + return "???" + } +} + +// Item describes an entry in a directory listing. +type Item struct { + Type ItemType `json:"type"` + Description string `json:"description"` + Selector string `json:"selector"` + Host string `json:"host"` + Port string `json:"port"` + + // non-standard extensions (ignored by standard clients) + Extras []string `json:"extras"` +} + +// ParseItem parses a line of text into an item +func ParseItem(line string) (item *Item, err error) { + parts := strings.Split(strings.Trim(line, "\r\n"), "\t") + + if len(parts[0]) < 1 { + return nil, errors.New("no item type: " + string(line)) + } + + item = &Item{ + Type: ItemType(parts[0][0]), + Description: string(parts[0][1:]), + Extras: make([]string, 0), + } + + // Selector + if len(parts) > 1 { + item.Selector = string(parts[1]) + } else { + item.Selector = "" + } + + // Host + if len(parts) > 2 { + item.Host = string(parts[2]) + } else { + item.Host = "null.host" + } + + // Port + if len(parts) > 3 { + item.Port = string(parts[3]) + } else { + item.Port = "0" + } + + // Extras + if len(parts) >= 4 { + for _, v := range parts[4:] { + item.Extras = append(item.Extras, string(v)) + } + } + + return +} + +func (i *Item) isDirectoryLike() bool { + switch i.Type { + case DIRECTORY: + return true + case INDEXSEARCH: + return true + default: + return false + } +} + +// Directory representes a Gopher Menu of Items +type Directory struct { + Items []*Item `json:"items"` +} + +// Response represents a Gopher resource that +// Items contains a non-empty array of Item(s) +// for directory types, otherwise the Body +// contains the fetched resource (file, image, etc). +type Response struct { + Type ItemType + Dir Directory + Body io.Reader +} + +// Get fetches a Gopher resource by URI +func Get(uri string) (*Response, error) { + u, err := url.Parse(uri) + if err != nil { + return nil, err + } + + if u.Scheme != "gopher" { + return nil, errors.New("invalid scheme for uri") + } + + host := u.Hostname() + port := u.Port() + + if port == "" { + port = "70" + } + + var ( + Type ItemType + Selector string + ) + + path := strings.TrimPrefix(u.Path, "/") + if len(path) > 2 { + Type = ItemType(path[0]) + Selector = path[1:] + if u.RawQuery != "" { + Selector += "\t" + u.RawQuery + } + } else if len(path) == 1 { + Type = ItemType(path[0]) + Selector = "" + } else { + Type = ItemType(DIRECTORY) + Selector = "" + } + + i := Item{Type: Type, Selector: Selector, Host: host, Port: port} + res := Response{Type: i.Type} + + if i.isDirectoryLike() { + d, err := i.FetchDirectory() + if err != nil { + return nil, err + } + + res.Dir = d + } else { + reader, err := i.FetchFile() + if err != nil { + return nil, err + } + + res.Body = reader + } + + return &res, nil +} + +// FetchFile fetches data, not directory information. +// Calling this on a DIRECTORY Item type +// or unsupported type will return an error. +func (i *Item) FetchFile() (io.Reader, error) { + if i.Type == DIRECTORY { + return nil, errors.New("cannot fetch a directory as a file") + } + + conn, err := net.Dial("tcp", net.JoinHostPort(i.Host, i.Port)) + if err != nil { + return nil, err + } + + _, err = conn.Write([]byte(i.Selector + CRLF)) + if err != nil { + conn.Close() + return nil, err + } + + return conn, nil +} + +// FetchDirectory fetches directory information, not data. +// Calling this on an Item whose type is not DIRECTORY will return an error. +func (i *Item) FetchDirectory() (Directory, error) { + if !i.isDirectoryLike() { + return Directory{}, errors.New("cannot fetch a file as a directory") + } + + conn, err := net.Dial("tcp", net.JoinHostPort(i.Host, i.Port)) + if err != nil { + return Directory{}, err + } + + _, err = conn.Write([]byte(i.Selector + CRLF)) + if err != nil { + return Directory{}, err + } + + reader := bufio.NewReader(conn) + scanner := bufio.NewScanner(reader) + scanner.Split(bufio.ScanLines) + + var items []*Item + + for scanner.Scan() { + line := strings.Trim(scanner.Text(), "\r\n") + + if len(line) == 0 { + continue + } + + if len(line) == 1 && line[0] == END { + break + } + + item, err := ParseItem(line) + if err != nil { + log.Printf("Error parsing %q: %q", line, err) + continue + } + items = append(items, item) + } + + return Directory{items}, nil +} -- cgit v1.2.3-70-g09d2