aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--Makefile4
-rw-r--r--go.mod1
-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.go312
-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
diff --git a/.gitignore b/.gitignore
index e036b02..fe92af3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,6 @@ dist
3*.bak 3*.bak
4coverage.txt 4coverage.txt
5 5
6gopherproxy 6gopherproxy.bin
7 7
8.vscode 8.vscode
diff --git a/Makefile b/Makefile
index 1d65cf0..75a8f2e 100644
--- a/Makefile
+++ b/Makefile
@@ -3,7 +3,7 @@
3all: dev 3all: dev
4 4
5dev: build 5dev: build
6 ./gopherproxy -bind 127.0.0.1:8000 6 ./gopherproxy.bin -bind 127.0.0.1:8000
7 7
8build: clean 8build: 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
17profile: 17profile:
18 @go test -cpuprofile cpu.prof -memprofile mem.prof -v -bench . 18 @go test -cpuprofile cpu.prof -memprofile mem.prof -v -bench .
diff --git a/go.mod b/go.mod
index 503bb87..6cde9a1 100644
--- a/go.mod
+++ b/go.mod
@@ -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
33const ( 32const (
@@ -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
78func renderGopherDirectory(w http.ResponseWriter, tpl *template.Template, assetList AssetList, uri string, hostport string, d gopher.Directory) error { 81func 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 @@
1package gopherproxy 1package libgemini
2 2
3import ( 3import (
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
49var ( 51var (
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
54type GeminiHeader struct { 56type Header struct {
55 Status int 57 Status int
56 Meta string 58 Meta string
57} 59}
58 60
59type GeminiResponse struct { 61type Response struct {
60 Header *GeminiHeader 62 Header *Header
61 Body io.Reader 63 Body io.Reader
62} 64}
63 65
64func GeminiGet(uri string) (*GeminiResponse, error) { 66func 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
125func ParseGeminiHeader(line string) (header *GeminiHeader, err error) { 117func 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 @@
1package libgopher
2
3import (
4 "bufio"
5 "errors"
6 "io"
7 "log"
8 "net"
9 "net/url"
10 "strings"
11)
12
13// Item Types
14const (
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
41const (
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
56type ItemType byte
57
58// Return a human friendly represation of an ItemType
59func (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.
105type 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
117func 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
161func (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
173type 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).
181type Response struct {
182 Type ItemType
183 Dir Directory
184 Body io.Reader
185}
186
187// Get fetches a Gopher resource by URI
188func 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.
250func (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.
271func (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
10var ( 10var (