diff options
| author | Feuerfuchs <git@feuerfuchs.dev> | 2019-11-26 13:13:02 +0100 | 
|---|---|---|
| committer | Feuerfuchs <git@feuerfuchs.dev> | 2019-11-26 13:13:02 +0100 | 
| commit | 93115f804220c31c2aa10f123560fb11135f06d8 (patch) | |
| tree | a3c12f68c4263f3e34b1e03f12e7962aab9cfbf5 | |
| parent | Fix title on startpage (diff) | |
| download | gopherproxy-93115f804220c31c2aa10f123560fb11135f06d8.tar.gz gopherproxy-93115f804220c31c2aa10f123560fb11135f06d8.tar.bz2 gopherproxy-93115f804220c31c2aa10f123560fb11135f06d8.zip | |
Add IPv6 support, general restructuring
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | Makefile | 4 | ||||
| -rw-r--r-- | go.mod | 1 | ||||
| -rw-r--r-- | gopherproxy/gopherproxy.go (renamed from gopherproxy.go) | 48 | ||||
| -rw-r--r-- | gopherproxy/libgemini/libgemini.go (renamed from libgemini.go) | 46 | ||||
| -rw-r--r-- | gopherproxy/libgopher/libgopher.go | 312 | ||||
| -rw-r--r-- | gopherproxy/template.go (renamed from template.go) | 0 | ||||
| -rw-r--r-- | main.go (renamed from cmd/gopherproxy/main.go) | 2 | 
8 files changed, 361 insertions, 54 deletions
| @@ -3,6 +3,6 @@ dist | |||
| 3 | *.bak | 3 | *.bak | 
| 4 | coverage.txt | 4 | coverage.txt | 
| 5 | 5 | ||
| 6 | gopherproxy | 6 | gopherproxy.bin | 
| 7 | 7 | ||
| 8 | .vscode | 8 | .vscode | 
| @@ -3,7 +3,7 @@ | |||
| 3 | all: dev | 3 | all: dev | 
| 4 | 4 | ||
| 5 | dev: build | 5 | dev: build | 
| 6 | ./gopherproxy -bind 127.0.0.1:8000 | 6 | ./gopherproxy.bin -bind 127.0.0.1:8000 | 
| 7 | 7 | ||
| 8 | build: clean | 8 | build: clean | 
| 9 | sassc -t compressed css/main.scss assets/style.css | 9 | sassc -t compressed css/main.scss assets/style.css | 
| @@ -12,7 +12,7 @@ build: clean | |||
| 12 | 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' | 12 | 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' | 
| 13 | 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' | 13 | 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' | 
| 14 | 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' | 14 | 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' | 
| 15 | go build -o ./gopherproxy ./cmd/gopherproxy/main.go | 15 | go build -o ./gopherproxy.bin ./main.go | 
| 16 | 16 | ||
| 17 | profile: | 17 | profile: | 
| 18 | @go test -cpuprofile cpu.prof -memprofile mem.prof -v -bench . | 18 | @go test -cpuprofile cpu.prof -memprofile mem.prof -v -bench . | 
| @@ -5,7 +5,6 @@ require ( | |||
| 5 | github.com/davidbyttow/govips v0.0.0-20190304175058-d272f04c0fea | 5 | github.com/davidbyttow/govips v0.0.0-20190304175058-d272f04c0fea | 
| 6 | github.com/gobuffalo/packr/v2 v2.1.0 | 6 | github.com/gobuffalo/packr/v2 v2.1.0 | 
| 7 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 | 7 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 | 
| 8 | github.com/prologic/go-gopher v0.0.0-20181230133552-0c68ed5f58b0 | ||
| 9 | github.com/temoto/robotstxt v0.0.0-20180810133444-97ee4a9ee6ea | 8 | github.com/temoto/robotstxt v0.0.0-20180810133444-97ee4a9ee6ea | 
| 10 | golang.org/x/net v0.0.0-20190311183353-d8887717615a | 9 | golang.org/x/net v0.0.0-20190311183353-d8887717615a | 
| 11 | golang.org/x/text v0.3.0 | 10 | golang.org/x/text v0.3.0 | 
| diff --git a/gopherproxy.go b/gopherproxy/gopherproxy.go index 8c0bb89..6556845 100644 --- a/gopherproxy.go +++ b/gopherproxy/gopherproxy.go | |||
| @@ -11,6 +11,7 @@ import ( | |||
| 11 | "io/ioutil" | 11 | "io/ioutil" | 
| 12 | "log" | 12 | "log" | 
| 13 | "mime" | 13 | "mime" | 
| 14 | "net" | ||
| 14 | "net/http" | 15 | "net/http" | 
| 15 | "net/url" | 16 | "net/url" | 
| 16 | "regexp" | 17 | "regexp" | 
| @@ -19,15 +20,13 @@ import ( | |||
| 19 | "golang.org/x/net/html/charset" | 20 | "golang.org/x/net/html/charset" | 
| 20 | "golang.org/x/text/transform" | 21 | "golang.org/x/text/transform" | 
| 21 | 22 | ||
| 22 | "github.com/temoto/robotstxt" | 23 | "git.feuerfuchs.dev/Feuerfuchs/gopherproxy/gopherproxy/libgemini" | 
| 23 | 24 | "git.feuerfuchs.dev/Feuerfuchs/gopherproxy/gopherproxy/libgopher" | |
| 24 | "github.com/prologic/go-gopher" | ||
| 25 | |||
| 26 | "github.com/gobuffalo/packr/v2" | ||
| 27 | |||
| 28 | "github.com/davidbyttow/govips/pkg/vips" | ||
| 29 | 25 | ||
| 30 | "github.com/NYTimes/gziphandler" | 26 | "github.com/NYTimes/gziphandler" | 
| 27 | "github.com/davidbyttow/govips/pkg/vips" | ||
| 28 | "github.com/gobuffalo/packr/v2" | ||
| 29 | "github.com/temoto/robotstxt" | ||
| 31 | ) | 30 | ) | 
| 32 | 31 | ||
| 33 | const ( | 32 | const ( | 
| @@ -63,10 +62,14 @@ func resolveURI(uri string, baseURL *url.URL) (resolvedURI string) { | |||
| 63 | return "" | 62 | return "" | 
| 64 | } | 63 | } | 
| 65 | adjustedURI := baseURL.ResolveReference(url) | 64 | adjustedURI := baseURL.ResolveReference(url) | 
| 65 | path := adjustedURI.Path | ||
| 66 | if !strings.HasPrefix(path, "/") { | ||
| 67 | path = "/" + path | ||
| 68 | } | ||
| 66 | if adjustedURI.Scheme == "gemini" { | 69 | if adjustedURI.Scheme == "gemini" { | 
| 67 | resolvedURI = "/gemini/" + adjustedURI.Host + adjustedURI.Path | 70 | resolvedURI = "/gemini/" + adjustedURI.Host + path | 
| 68 | } else if adjustedURI.Scheme == "gopher" { | 71 | } else if adjustedURI.Scheme == "gopher" { | 
| 69 | resolvedURI = "/gopher/" + adjustedURI.Host + adjustedURI.Path | 72 | resolvedURI = "/gopher/" + adjustedURI.Host + path | 
| 70 | } else { | 73 | } else { | 
| 71 | resolvedURI = adjustedURI.String() | 74 | resolvedURI = adjustedURI.String() | 
| 72 | } | 75 | } | 
| @@ -75,13 +78,13 @@ func resolveURI(uri string, baseURL *url.URL) (resolvedURI string) { | |||
| 75 | return | 78 | return | 
| 76 | } | 79 | } | 
| 77 | 80 | ||
| 78 | func renderGopherDirectory(w http.ResponseWriter, tpl *template.Template, assetList AssetList, uri string, hostport string, d gopher.Directory) error { | 81 | func renderGopherDirectory(w http.ResponseWriter, tpl *template.Template, assetList AssetList, uri string, hostport string, d libgopher.Directory) error { | 
| 79 | var title string | 82 | var title string | 
| 80 | 83 | ||
| 81 | out := make([]Item, len(d.Items)) | 84 | out := make([]Item, len(d.Items)) | 
| 82 | 85 | ||
| 83 | for i, x := range d.Items { | 86 | for i, x := range d.Items { | 
| 84 | if x.Type == gopher.INFO && x.Selector == "TITLE" { | 87 | if x.Type == libgopher.INFO && x.Selector == "TITLE" { | 
| 85 | title = x.Description | 88 | title = x.Description | 
| 86 | continue | 89 | continue | 
| 87 | } | 90 | } | 
| @@ -91,7 +94,7 @@ func renderGopherDirectory(w http.ResponseWriter, tpl *template.Template, assetL | |||
| 91 | Type: x.Type.String(), | 94 | Type: x.Type.String(), | 
| 92 | } | 95 | } | 
| 93 | 96 | ||
| 94 | if x.Type == gopher.INFO { | 97 | if x.Type == libgopher.INFO { | 
| 95 | out[i] = tr | 98 | out[i] = tr | 
| 96 | continue | 99 | continue | 
| 97 | } | 100 | } | 
| @@ -111,18 +114,19 @@ func renderGopherDirectory(w http.ResponseWriter, tpl *template.Template, assetL | |||
| 111 | } | 114 | } | 
| 112 | tr.Link = template.URL(link) | 115 | tr.Link = template.URL(link) | 
| 113 | } else { | 116 | } else { | 
| 114 | var hostport string | 117 | var linkHostport string | 
| 115 | if x.Port == 70 { | 118 | if x.Port != "70" { | 
| 116 | hostport = x.Host | 119 | linkHostport = net.JoinHostPort(x.Host, x.Port) | 
| 117 | } else { | 120 | } else { | 
| 118 | hostport = fmt.Sprintf("%s:%d", x.Host, x.Port) | 121 | linkHostport = x.Host | 
| 119 | } | 122 | } | 
| 123 | |||
| 120 | path := url.PathEscape(x.Selector) | 124 | path := url.PathEscape(x.Selector) | 
| 121 | path = strings.Replace(path, "%2F", "/", -1) | 125 | path = strings.Replace(path, "%2F", "/", -1) | 
| 122 | tr.Link = template.URL( | 126 | tr.Link = template.URL( | 
| 123 | fmt.Sprintf( | 127 | fmt.Sprintf( | 
| 124 | "/gopher/%s/%s%s", | 128 | "/gopher/%s/%s%s", | 
| 125 | hostport, | 129 | linkHostport, | 
| 126 | string(byte(x.Type)), | 130 | string(byte(x.Type)), | 
| 127 | path, | 131 | path, | 
| 128 | ), | 132 | ), | 
| @@ -171,7 +175,7 @@ func parseGeminiDocument(body *bytes.Buffer, uri string, hostport string) (items | |||
| 171 | Text: line, | 175 | Text: line, | 
| 172 | } | 176 | } | 
| 173 | 177 | ||
| 174 | linkMatch := GeminiLinkPattern.FindStringSubmatch(line) | 178 | linkMatch := libgemini.LinkPattern.FindStringSubmatch(line) | 
| 175 | if len(linkMatch) != 0 && linkMatch[0] != "" { | 179 | if len(linkMatch) != 0 && linkMatch[0] != "" { | 
| 176 | item.Type = ITEM_TYPE_GEMINI_LINK | 180 | item.Type = ITEM_TYPE_GEMINI_LINK | 
| 177 | item.Link = template.URL(resolveURI(linkMatch[1], baseURL)) | 181 | item.Link = template.URL(resolveURI(linkMatch[1], baseURL)) | 
| @@ -254,7 +258,7 @@ func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass | |||
| 254 | title = fmt.Sprintf("%s/%s", hostport, uri) | 258 | title = fmt.Sprintf("%s/%s", hostport, uri) | 
| 255 | } | 259 | } | 
| 256 | 260 | ||
| 257 | res, err := gopher.Get( | 261 | res, err := libgopher.Get( | 
| 258 | fmt.Sprintf( | 262 | fmt.Sprintf( | 
| 259 | "gopher://%s/%s%s", | 263 | "gopher://%s/%s%s", | 
| 260 | hostport, | 264 | hostport, | 
| @@ -371,7 +375,7 @@ func GeminiHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass | |||
| 371 | title = fmt.Sprintf("%s/%s", hostport, uri) | 375 | title = fmt.Sprintf("%s/%s", hostport, uri) | 
| 372 | } | 376 | } | 
| 373 | 377 | ||
| 374 | res, err := GeminiGet( | 378 | res, err := libgemini.Get( | 
| 375 | fmt.Sprintf( | 379 | fmt.Sprintf( | 
| 376 | "gemini://%s/%s%s", | 380 | "gemini://%s/%s%s", | 
| 377 | hostport, | 381 | hostport, | 
| @@ -458,7 +462,7 @@ func GeminiHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass | |||
| 458 | items []Item | 462 | items []Item | 
| 459 | ) | 463 | ) | 
| 460 | 464 | ||
| 461 | if strings.HasPrefix(res.Header.Meta, MIME_GEMINI) { | 465 | if strings.HasPrefix(res.Header.Meta, libgemini.MIME_GEMINI) { | 
| 462 | items = parseGeminiDocument(buf, uri, hostport) | 466 | items = parseGeminiDocument(buf, uri, hostport) | 
| 463 | } else { | 467 | } else { | 
| 464 | rawText = buf.String() | 468 | rawText = buf.String() | 
| @@ -567,7 +571,7 @@ func ListenAndServe(bind, startpagefile string, robotsfile string, robotsdebug b | |||
| 567 | } | 571 | } | 
| 568 | } | 572 | } | 
| 569 | 573 | ||
| 570 | box := packr.New("assets", "./assets") | 574 | box := packr.New("assets", "../assets") | 
| 571 | 575 | ||
| 572 | fontdataw, err := box.Find("iosevka-term-ss03-regular.woff") | 576 | fontdataw, err := box.Find("iosevka-term-ss03-regular.woff") | 
| 573 | if err != nil { | 577 | if err != nil { | 
| diff --git a/libgemini.go b/gopherproxy/libgemini/libgemini.go index 94744ba..303490c 100644 --- a/libgemini.go +++ b/gopherproxy/libgemini/libgemini.go | |||
| @@ -1,11 +1,13 @@ | |||
| 1 | package gopherproxy | 1 | package libgemini | 
| 2 | 2 | ||
| 3 | import ( | 3 | import ( | 
| 4 | "bufio" | 4 | "bufio" | 
| 5 | "crypto/tls" | 5 | "crypto/tls" | 
| 6 | "errors" | 6 | "errors" | 
| 7 | "fmt" | ||
| 7 | "io" | 8 | "io" | 
| 8 | "mime" | 9 | "mime" | 
| 10 | "net" | ||
| 9 | "net/url" | 11 | "net/url" | 
| 10 | "regexp" | 12 | "regexp" | 
| 11 | "strconv" | 13 | "strconv" | 
| @@ -47,21 +49,21 @@ const ( | |||
| 47 | ) | 49 | ) | 
| 48 | 50 | ||
| 49 | var ( | 51 | var ( | 
| 50 | HeaderPattern = regexp.MustCompile("^(\\d\\d)[ \\t]+(.*)$") | 52 | HeaderPattern = regexp.MustCompile("^(\\d\\d)[ \\t]+(.*)$") | 
| 51 | GeminiLinkPattern = regexp.MustCompile("^=>[ \\t]*([^ \\t]+)(?:[ \\t]+(.*))?$") | 53 | LinkPattern = regexp.MustCompile("^=>[ \\t]*([^ \\t]+)(?:[ \\t]+(.*))?$") | 
| 52 | ) | 54 | ) | 
| 53 | 55 | ||
| 54 | type GeminiHeader struct { | 56 | type Header struct { | 
| 55 | Status int | 57 | Status int | 
| 56 | Meta string | 58 | Meta string | 
| 57 | } | 59 | } | 
| 58 | 60 | ||
| 59 | type GeminiResponse struct { | 61 | type Response struct { | 
| 60 | Header *GeminiHeader | 62 | Header *Header | 
| 61 | Body io.Reader | 63 | Body io.Reader | 
| 62 | } | 64 | } | 
| 63 | 65 | ||
| 64 | func GeminiGet(uri string) (*GeminiResponse, error) { | 66 | func Get(uri string) (*Response, error) { | 
| 65 | u, err := url.Parse(uri) | 67 | u, err := url.Parse(uri) | 
| 66 | if err != nil { | 68 | if err != nil { | 
| 67 | return nil, err | 69 | return nil, err | 
| @@ -71,24 +73,14 @@ func GeminiGet(uri string) (*GeminiResponse, error) { | |||
| 71 | return nil, errors.New("invalid scheme for uri") | 73 | return nil, errors.New("invalid scheme for uri") | 
| 72 | } | 74 | } | 
| 73 | 75 | ||
| 74 | var ( | 76 | host := u.Hostname() | 
| 75 | host string | 77 | port := u.Port() | 
| 76 | port int | ||
| 77 | ) | ||
| 78 | 78 | ||
| 79 | hostport := strings.Split(u.Host, ":") | 79 | if port == "" { | 
| 80 | if len(hostport) == 2 { | 80 | port = "1965" | 
| 81 | host = hostport[0] | ||
| 82 | n, err := strconv.ParseInt(hostport[1], 10, 32) | ||
| 83 | if err != nil { | ||
| 84 | return nil, err | ||
| 85 | } | ||
| 86 | port = int(n) | ||
| 87 | } else { | ||
| 88 | host, port = hostport[0], 1965 | ||
| 89 | } | 81 | } | 
| 90 | 82 | ||
| 91 | conn, err := tls.Dial("tcp", host+":"+strconv.Itoa(port), &tls.Config{ | 83 | conn, err := tls.Dial("tcp", net.JoinHostPort(host, port), &tls.Config{ | 
| 92 | MinVersion: tls.VersionTLS12, | 84 | MinVersion: tls.VersionTLS12, | 
| 93 | InsecureSkipVerify: true, | 85 | InsecureSkipVerify: true, | 
| 94 | }) | 86 | }) | 
| @@ -110,19 +102,19 @@ func GeminiGet(uri string) (*GeminiResponse, error) { | |||
| 110 | return nil, err | 102 | return nil, err | 
| 111 | } | 103 | } | 
| 112 | 104 | ||
| 113 | header, err := ParseGeminiHeader(string(line)) | 105 | header, err := ParseHeader(string(line)) | 
| 114 | if err != nil { | 106 | if err != nil { | 
| 115 | conn.Close() | 107 | conn.Close() | 
| 116 | return nil, err | 108 | return nil, err | 
| 117 | } | 109 | } | 
| 118 | 110 | ||
| 119 | return &GeminiResponse{ | 111 | return &Response{ | 
| 120 | Header: header, | 112 | Header: header, | 
| 121 | Body: reader, | 113 | Body: reader, | 
| 122 | }, nil | 114 | }, nil | 
| 123 | } | 115 | } | 
| 124 | 116 | ||
| 125 | func ParseGeminiHeader(line string) (header *GeminiHeader, err error) { | 117 | func ParseHeader(line string) (header *Header, err error) { | 
| 126 | matches := HeaderPattern.FindStringSubmatch(line) | 118 | matches := HeaderPattern.FindStringSubmatch(line) | 
| 127 | 119 | ||
| 128 | status, err := strconv.Atoi(matches[1]) | 120 | status, err := strconv.Atoi(matches[1]) | 
| @@ -136,7 +128,7 @@ func ParseGeminiHeader(line string) (header *GeminiHeader, err error) { | |||
| 136 | mediaType, params, err := mime.ParseMediaType(meta) | 128 | mediaType, params, err := mime.ParseMediaType(meta) | 
| 137 | 129 | ||
| 138 | if err != nil { | 130 | if err != nil { | 
| 139 | meta = DEFAULT_MIME + ";charset=" + DEFAULT_CHARSET | 131 | meta = fmt.Sprintf("%s;charset=%s", DEFAULT_MIME, DEFAULT_CHARSET) | 
| 140 | } else if strings.HasPrefix(mediaType, "text/") { | 132 | } else if strings.HasPrefix(mediaType, "text/") { | 
| 141 | if _, ok := params["charset"]; !ok { | 133 | if _, ok := params["charset"]; !ok { | 
| 142 | meta += ";charset=" + DEFAULT_CHARSET | 134 | meta += ";charset=" + DEFAULT_CHARSET | 
| @@ -144,7 +136,7 @@ func ParseGeminiHeader(line string) (header *GeminiHeader, err error) { | |||
| 144 | } | 136 | } | 
| 145 | } | 137 | } | 
| 146 | 138 | ||
| 147 | header = &GeminiHeader{ | 139 | header = &Header{ | 
| 148 | Status: status, | 140 | Status: status, | 
| 149 | Meta: meta, | 141 | Meta: meta, | 
| 150 | } | 142 | } | 
| diff --git a/gopherproxy/libgopher/libgopher.go b/gopherproxy/libgopher/libgopher.go new file mode 100644 index 0000000..86d58ff --- /dev/null +++ b/gopherproxy/libgopher/libgopher.go | |||
| @@ -0,0 +1,312 @@ | |||
| 1 | package libgopher | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "bufio" | ||
| 5 | "errors" | ||
| 6 | "io" | ||
| 7 | "log" | ||
| 8 | "net" | ||
| 9 | "net/url" | ||
| 10 | "strings" | ||
| 11 | ) | ||
| 12 | |||
| 13 | // Item Types | ||
| 14 | const ( | ||
| 15 | FILE = ItemType('0') // Item is a file | ||
| 16 | DIRECTORY = ItemType('1') // Item is a directory | ||
| 17 | PHONEBOOK = ItemType('2') // Item is a CSO phone-book server | ||
| 18 | ERROR = ItemType('3') // Error | ||
| 19 | BINHEX = ItemType('4') // Item is a BinHexed Macintosh file. | ||
| 20 | DOSARCHIVE = ItemType('5') // Item is DOS binary archive of some sort. (*) | ||
| 21 | UUENCODED = ItemType('6') // Item is a UNIX uuencoded file. | ||
| 22 | INDEXSEARCH = ItemType('7') // Item is an Index-Search server. | ||
| 23 | TELNET = ItemType('8') // Item points to a text-based telnet session. | ||
| 24 | BINARY = ItemType('9') // Item is a binary file! (*) | ||
| 25 | |||
| 26 | // (*) Client must read until the TCP connection is closed. | ||
| 27 | |||
| 28 | REDUNDANT = ItemType('+') // Item is a redundant server | ||
| 29 | TN3270 = ItemType('T') // Item points to a text-based tn3270 session. | ||
| 30 | GIF = ItemType('g') // Item is a GIF format graphics file. | ||
| 31 | IMAGE = ItemType('I') // Item is some kind of image file. | ||
| 32 | |||
| 33 | // non-standard | ||
| 34 | INFO = ItemType('i') // Item is an informational message | ||
| 35 | HTML = ItemType('h') // Item is a HTML document | ||
| 36 | AUDIO = ItemType('s') // Item is an Audio file | ||
| 37 | PNG = ItemType('p') // Item is a PNG Image | ||
| 38 | DOC = ItemType('d') // Item is a Document | ||
| 39 | ) | ||
| 40 | |||
| 41 | const ( | ||
| 42 | // END represents the terminator used in directory responses | ||
| 43 | END = byte('.') | ||
| 44 | |||
| 45 | // TAB is the delimiter used to separate item response parts | ||
| 46 | TAB = byte('\t') | ||
| 47 | |||
| 48 | // CRLF is the delimiter used per line of response item | ||
| 49 | CRLF = "\r\n" | ||
| 50 | |||
| 51 | // DEFAULT is the default item type | ||
| 52 | DEFAULT = BINARY | ||
| 53 | ) | ||
| 54 | |||
| 55 | // ItemType represents the type of an item | ||
| 56 | type ItemType byte | ||
| 57 | |||
| 58 | // Return a human friendly represation of an ItemType | ||
| 59 | func (it ItemType) String() string { | ||
| 60 | switch it { | ||
| 61 | case FILE: | ||
| 62 | return "TXT" | ||
| 63 | case DIRECTORY: | ||
| 64 | return "DIR" | ||
| 65 | case PHONEBOOK: | ||
| 66 | return "PHO" | ||
| 67 | case ERROR: | ||
| 68 | return "ERR" | ||
| 69 | case BINHEX: | ||
| 70 | return "HEX" | ||
| 71 | case DOSARCHIVE: | ||
| 72 | return "ARC" | ||
| 73 | case UUENCODED: | ||
| 74 | return "UUE" | ||
| 75 | case INDEXSEARCH: | ||
| 76 | return "QRY" | ||
| 77 | case TELNET: | ||
| 78 | return "TEL" | ||
| 79 | case BINARY: | ||
| 80 | return "BIN" | ||
| 81 | case REDUNDANT: | ||
| 82 | return "DUP" | ||
| 83 | case TN3270: | ||
| 84 | return "TN3" | ||
| 85 | case GIF: | ||
| 86 | return "GIF" | ||
| 87 | case IMAGE: | ||
| 88 | return "IMG" | ||
| 89 | case INFO: | ||
| 90 | return "NFO" | ||
| 91 | case HTML: | ||
| 92 | return "HTM" | ||
| 93 | case AUDIO: | ||
| 94 | return "SND" | ||
| 95 | case PNG: | ||
| 96 | return "PNG" | ||
| 97 | case DOC: | ||
| 98 | return "DOC" | ||
| 99 | default: | ||
| 100 | return "???" | ||
| 101 | } | ||
| 102 | } | ||
| 103 | |||
| 104 | // Item describes an entry in a directory listing. | ||
| 105 | type Item struct { | ||
| 106 | Type ItemType `json:"type"` | ||
| 107 | Description string `json:"description"` | ||
| 108 | Selector string `json:"selector"` | ||
| 109 | Host string `json:"host"` | ||
| 110 | Port string `json:"port"` | ||
| 111 | |||
| 112 | // non-standard extensions (ignored by standard clients) | ||
| 113 | Extras []string `json:"extras"` | ||
| 114 | } | ||
| 115 | |||
| 116 | // ParseItem parses a line of text into an item | ||
| 117 | func ParseItem(line string) (item *Item, err error) { | ||
| 118 | parts := strings.Split(strings.Trim(line, "\r\n"), "\t") | ||
| 119 | |||
| 120 | if len(parts[0]) < 1 { | ||
| 121 | return nil, errors.New("no item type: " + string(line)) | ||
| 122 | } | ||
| 123 | |||
| 124 | item = &Item{ | ||
| 125 | Type: ItemType(parts[0][0]), | ||
| 126 | Description: string(parts[0][1:]), | ||
| 127 | Extras: make([]string, 0), | ||
| 128 | } | ||
| 129 | |||
| 130 | // Selector | ||
| 131 | if len(parts) > 1 { | ||
| 132 | item.Selector = string(parts[1]) | ||
| 133 | } else { | ||
| 134 | item.Selector = "" | ||
| 135 | } | ||
| 136 | |||
| 137 | // Host | ||
| 138 | if len(parts) > 2 { | ||
| 139 | item.Host = string(parts[2]) | ||
| 140 | } else { | ||
| 141 | item.Host = "null.host" | ||
| 142 | } | ||
| 143 | |||
| 144 | // Port | ||
| 145 | if len(parts) > 3 { | ||
| 146 | item.Port = string(parts[3]) | ||
| 147 | } else { | ||
| 148 | item.Port = "0" | ||
| 149 | } | ||
| 150 | |||
| 151 | // Extras | ||
| 152 | if len(parts) >= 4 { | ||
| 153 | for _, v := range parts[4:] { | ||
| 154 | item.Extras = append(item.Extras, string(v)) | ||
| 155 | } | ||
| 156 | } | ||
| 157 | |||
| 158 | return | ||
| 159 | } | ||
| 160 | |||
| 161 | func (i *Item) isDirectoryLike() bool { | ||
| 162 | switch i.Type { | ||
| 163 | case DIRECTORY: | ||
| 164 | return true | ||
| 165 | case INDEXSEARCH: | ||
| 166 | return true | ||
| 167 | default: | ||
| 168 | return false | ||
| 169 | } | ||
| 170 | } | ||
| 171 | |||
| 172 | // Directory representes a Gopher Menu of Items | ||
| 173 | type Directory struct { | ||
| 174 | Items []*Item `json:"items"` | ||
| 175 | } | ||
| 176 | |||
| 177 | // Response represents a Gopher resource that | ||
| 178 | // Items contains a non-empty array of Item(s) | ||
| 179 | // for directory types, otherwise the Body | ||
| 180 | // contains the fetched resource (file, image, etc). | ||
| 181 | type Response struct { | ||
| 182 | Type ItemType | ||
| 183 | Dir Directory | ||
| 184 | Body io.Reader | ||
| 185 | } | ||
| 186 | |||
| 187 | // Get fetches a Gopher resource by URI | ||
| 188 | func Get(uri string) (*Response, error) { | ||
| 189 | u, err := url.Parse(uri) | ||
| 190 | if err != nil { | ||
| 191 | return nil, err | ||
| 192 | } | ||
| 193 | |||
| 194 | if u.Scheme != "gopher" { | ||
| 195 | return nil, errors.New("invalid scheme for uri") | ||
| 196 | } | ||
| 197 | |||
| 198 | host := u.Hostname() | ||
| 199 | port := u.Port() | ||
| 200 | |||
| 201 | if port == "" { | ||
| 202 | port = "70" | ||
| 203 | } | ||
| 204 | |||
| 205 | var ( | ||
| 206 | Type ItemType | ||
| 207 | Selector string | ||
| 208 | ) | ||
| 209 | |||
| 210 | path := strings.TrimPrefix(u.Path, "/") | ||
| 211 | if len(path) > 2 { | ||
| 212 | Type = ItemType(path[0]) | ||
| 213 | Selector = path[1:] | ||
| 214 | if u.RawQuery != "" { | ||
| 215 | Selector += "\t" + u.RawQuery | ||
| 216 | } | ||
| 217 | } else if len(path) == 1 { | ||
| 218 | Type = ItemType(path[0]) | ||
| 219 | Selector = "" | ||
| 220 | } else { | ||
| 221 | Type = ItemType(DIRECTORY) | ||
| 222 | Selector = "" | ||
| 223 | } | ||
| 224 | |||
| 225 | i := Item{Type: Type, Selector: Selector, Host: host, Port: port} | ||
| 226 | res := Response{Type: i.Type} | ||
| 227 | |||
| 228 | if i.isDirectoryLike() { | ||
| 229 | d, err := i.FetchDirectory() | ||
| 230 | if err != nil { | ||
| 231 | return nil, err | ||
| 232 | } | ||
| 233 | |||
| 234 | res.Dir = d | ||
| 235 | } else { | ||
| 236 | reader, err := i.FetchFile() | ||
| 237 | if err != nil { | ||
| 238 | return nil, err | ||
| 239 | } | ||
| 240 | |||
| 241 | res.Body = reader | ||
| 242 | } | ||
| 243 | |||
| 244 | return &res, nil | ||
| 245 | } | ||
| 246 | |||
| 247 | // FetchFile fetches data, not directory information. | ||
| 248 | // Calling this on a DIRECTORY Item type | ||
| 249 | // or unsupported type will return an error. | ||
| 250 | func (i *Item) FetchFile() (io.Reader, error) { | ||
| 251 | if i.Type == DIRECTORY { | ||
| 252 | return nil, errors.New("cannot fetch a directory as a file") | ||
| 253 | } | ||
| 254 | |||
| 255 | conn, err := net.Dial("tcp", net.JoinHostPort(i.Host, i.Port)) | ||
| 256 | if err != nil { | ||
| 257 | return nil, err | ||
| 258 | } | ||
| 259 | |||
| 260 | _, err = conn.Write([]byte(i.Selector + CRLF)) | ||
| 261 | if err != nil { | ||
| 262 | conn.Close() | ||
| 263 | return nil, err | ||
| 264 | } | ||
| 265 | |||
| 266 | return conn, nil | ||
| 267 | } | ||
| 268 | |||
| 269 | // FetchDirectory fetches directory information, not data. | ||
| 270 | // Calling this on an Item whose type is not DIRECTORY will return an error. | ||
| 271 | func (i *Item) FetchDirectory() (Directory, error) { | ||
| 272 | if !i.isDirectoryLike() { | ||
| 273 | return Directory{}, errors.New("cannot fetch a file as a directory") | ||
| 274 | } | ||
| 275 | |||
| 276 | conn, err := net.Dial("tcp", net.JoinHostPort(i.Host, i.Port)) | ||
| 277 | if err != nil { | ||
| 278 | return Directory{}, err | ||
| 279 | } | ||
| 280 | |||
| 281 | _, err = conn.Write([]byte(i.Selector + CRLF)) | ||
| 282 | if err != nil { | ||
| 283 | return Directory{}, err | ||
| 284 | } | ||
| 285 | |||
| 286 | reader := bufio.NewReader(conn) | ||
| 287 | scanner := bufio.NewScanner(reader) | ||
| 288 | scanner.Split(bufio.ScanLines) | ||
| 289 | |||
| 290 | var items []*Item | ||
| 291 | |||
| 292 | for scanner.Scan() { | ||
| 293 | line := strings.Trim(scanner.Text(), "\r\n") | ||
| 294 | |||
| 295 | if len(line) == 0 { | ||
| 296 | continue | ||
| 297 | } | ||
| 298 | |||
| 299 | if len(line) == 1 && line[0] == END { | ||
| 300 | break | ||
| 301 | } | ||
| 302 | |||
| 303 | item, err := ParseItem(line) | ||
| 304 | if err != nil { | ||
| 305 | log.Printf("Error parsing %q: %q", line, err) | ||
| 306 | continue | ||
| 307 | } | ||
| 308 | items = append(items, item) | ||
| 309 | } | ||
| 310 | |||
| 311 | return Directory{items}, nil | ||
| 312 | } | ||
| diff --git a/template.go b/gopherproxy/template.go index 6b90cc0..6b90cc0 100644 --- a/template.go +++ b/gopherproxy/template.go | |||
| diff --git a/cmd/gopherproxy/main.go b/main.go index 6e6f48c..f5a82fe 100644 --- a/cmd/gopherproxy/main.go +++ b/main.go | |||
| @@ -4,7 +4,7 @@ import ( | |||
| 4 | "flag" | 4 | "flag" | 
| 5 | "log" | 5 | "log" | 
| 6 | 6 | ||
| 7 | "git.feuerfuchs.dev/Feuerfuchs/gopherproxy" | 7 | "git.feuerfuchs.dev/Feuerfuchs/gopherproxy/gopherproxy" | 
| 8 | ) | 8 | ) | 
| 9 | 9 | ||
| 10 | var ( | 10 | var ( | 
