diff options
Diffstat (limited to 'gopherproxy.go')
-rw-r--r-- | gopherproxy.go | 261 |
1 files changed, 221 insertions, 40 deletions
diff --git a/gopherproxy.go b/gopherproxy.go index 87a1ad0..9a60507 100644 --- a/gopherproxy.go +++ b/gopherproxy.go | |||
@@ -1,6 +1,7 @@ | |||
1 | package gopherproxy | 1 | package gopherproxy |
2 | 2 | ||
3 | import ( | 3 | import ( |
4 | "bufio" | ||
4 | "bytes" | 5 | "bytes" |
5 | "crypto/md5" | 6 | "crypto/md5" |
6 | "fmt" | 7 | "fmt" |
@@ -25,6 +26,11 @@ import ( | |||
25 | "github.com/NYTimes/gziphandler" | 26 | "github.com/NYTimes/gziphandler" |
26 | ) | 27 | ) |
27 | 28 | ||
29 | const ( | ||
30 | ITEM_TYPE_GEMINI_LINE = "" | ||
31 | ITEM_TYPE_GEMINI_LINK = " =>" | ||
32 | ) | ||
33 | |||
28 | type Item struct { | 34 | type Item struct { |
29 | Link template.URL | 35 | Link template.URL |
30 | Type string | 36 | Type string |
@@ -40,7 +46,7 @@ type AssetList struct { | |||
40 | PropFontW2 string | 46 | PropFontW2 string |
41 | } | 47 | } |
42 | 48 | ||
43 | func renderDirectory(w http.ResponseWriter, tpl *template.Template, assetList AssetList, uri string, hostport string, d gopher.Directory) error { | 49 | func renderGopherDirectory(w http.ResponseWriter, tpl *template.Template, assetList AssetList, uri string, hostport string, d gopher.Directory) error { |
44 | var title string | 50 | var title string |
45 | 51 | ||
46 | out := make([]Item, len(d.Items)) | 52 | out := make([]Item, len(d.Items)) |
@@ -76,7 +82,7 @@ func renderDirectory(w http.ResponseWriter, tpl *template.Template, assetList As | |||
76 | path = strings.Replace(path, "%2F", "/", -1) | 82 | path = strings.Replace(path, "%2F", "/", -1) |
77 | tr.Link = template.URL( | 83 | tr.Link = template.URL( |
78 | fmt.Sprintf( | 84 | fmt.Sprintf( |
79 | "/%s/%s%s", | 85 | "/gopher/%s/%s%s", |
80 | hostport, | 86 | hostport, |
81 | string(byte(x.Type)), | 87 | string(byte(x.Type)), |
82 | path, | 88 | path, |
@@ -92,13 +98,81 @@ func renderDirectory(w http.ResponseWriter, tpl *template.Template, assetList As | |||
92 | } | 98 | } |
93 | 99 | ||
94 | return tpl.Execute(w, struct { | 100 | return tpl.Execute(w, struct { |
95 | Title string | 101 | Title string |
96 | URI string | 102 | URI string |
97 | Assets AssetList | 103 | Assets AssetList |
98 | Lines []Item | 104 | Lines []Item |
99 | RawText string | 105 | RawText string |
100 | Error bool | 106 | Error bool |
101 | }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, out, "", false}) | 107 | Protocol string |
108 | }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, out, "", false, "gopher"}) | ||
109 | } | ||
110 | |||
111 | func parseGeminiDocument(response *GeminiResponse, uri string, hostport string) (items []Item) { | ||
112 | scanner := bufio.NewScanner(response.Body) | ||
113 | scanner.Split(bufio.ScanLines) | ||
114 | |||
115 | baseUrl, err := url.Parse(fmt.Sprintf( | ||
116 | "gemini://%s/%s", | ||
117 | hostport, | ||
118 | uri, | ||
119 | )) | ||
120 | if err != nil { | ||
121 | return []Item{} | ||
122 | } | ||
123 | |||
124 | for scanner.Scan() { | ||
125 | line := strings.Trim(scanner.Text(), "\r\n") | ||
126 | |||
127 | item := Item{ | ||
128 | Type: ITEM_TYPE_GEMINI_LINE, | ||
129 | Text: line, | ||
130 | } | ||
131 | |||
132 | linkMatch := GeminiLinkPattern.FindStringSubmatch(line) | ||
133 | if len(linkMatch) != 0 && linkMatch[0] != "" { | ||
134 | link := linkMatch[1] | ||
135 | |||
136 | if strings.HasPrefix(link, "//") { | ||
137 | link = "/gemini/" + strings.TrimPrefix(link, "//") | ||
138 | } else if strings.HasPrefix(link, "gemini://") { | ||
139 | link = "/gemini/" + strings.TrimPrefix(link, "gemini://") | ||
140 | } else if strings.HasPrefix(link, "gopher://") { | ||
141 | link = "/gopher/" + strings.TrimPrefix(link, "gopher://") | ||
142 | } else { | ||
143 | linkUrl, err := url.Parse(link) | ||
144 | if err != nil { | ||
145 | continue | ||
146 | } | ||
147 | adjustedUrl := baseUrl.ResolveReference(linkUrl) | ||
148 | if adjustedUrl.Scheme == "gemini" { | ||
149 | link = "/gemini/" + adjustedUrl.Host + adjustedUrl.Path | ||
150 | } else if adjustedUrl.Scheme == "gopher" { | ||
151 | link = "/gopher/" + adjustedUrl.Host + adjustedUrl.Path | ||
152 | } else { | ||
153 | link = adjustedUrl.String() | ||
154 | } | ||
155 | } | ||
156 | |||
157 | item.Type = ITEM_TYPE_GEMINI_LINK | ||
158 | item.Link = template.URL(link) | ||
159 | if linkMatch[2] != "" { | ||
160 | item.Text = linkMatch[2] | ||
161 | } else { | ||
162 | item.Text = linkMatch[1] | ||
163 | } | ||
164 | } | ||
165 | |||
166 | items = append(items, item) | ||
167 | } | ||
168 | |||
169 | return | ||
170 | } | ||
171 | |||
172 | func DefaultHandler(tpl *template.Template, uri string) http.HandlerFunc { | ||
173 | return func(w http.ResponseWriter, req *http.Request) { | ||
174 | http.Redirect(w, req, "/"+uri, http.StatusFound) | ||
175 | } | ||
102 | } | 176 | } |
103 | 177 | ||
104 | // GopherHandler returns a Handler that proxies requests | 178 | // GopherHandler returns a Handler that proxies requests |
@@ -109,7 +183,7 @@ func renderDirectory(w http.ResponseWriter, tpl *template.Template, assetList As | |||
109 | func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool, uri string) http.HandlerFunc { | 183 | func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool, uri string) http.HandlerFunc { |
110 | return func(w http.ResponseWriter, req *http.Request) { | 184 | return func(w http.ResponseWriter, req *http.Request) { |
111 | agent := req.UserAgent() | 185 | agent := req.UserAgent() |
112 | path := strings.TrimPrefix(req.URL.Path, "/") | 186 | path := strings.TrimPrefix(req.URL.Path, "/gopher/") |
113 | 187 | ||
114 | if robotsdata != nil && robotsdebug && !robotsdata.TestAgent(path, agent) { | 188 | if robotsdata != nil && robotsdebug && !robotsdata.TestAgent(path, agent) { |
115 | log.Printf("UserAgent %s ignored robots.txt", agent) | 189 | log.Printf("UserAgent %s ignored robots.txt", agent) |
@@ -132,13 +206,14 @@ func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass | |||
132 | uri, err := url.QueryUnescape(strings.Join(parts[1:], "/")) | 206 | uri, err := url.QueryUnescape(strings.Join(parts[1:], "/")) |
133 | if err != nil { | 207 | if err != nil { |
134 | tpl.Execute(w, struct { | 208 | tpl.Execute(w, struct { |
135 | Title string | 209 | Title string |
136 | URI string | 210 | URI string |
137 | Assets AssetList | 211 | Assets AssetList |
138 | RawText string | 212 | RawText string |
139 | Lines []Item | 213 | Lines []Item |
140 | Error bool | 214 | Error bool |
141 | }{"", hostport, assetList, fmt.Sprintf("Error: %s", err), nil, true}) | 215 | Protocol string |
216 | }{"", hostport, assetList, fmt.Sprintf("Error: %s", err), nil, true, "gopher"}) | ||
142 | return | 217 | return |
143 | } | 218 | } |
144 | 219 | ||
@@ -153,13 +228,14 @@ func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass | |||
153 | 228 | ||
154 | if err != nil { | 229 | if err != nil { |
155 | tpl.Execute(w, struct { | 230 | tpl.Execute(w, struct { |
156 | Title string | 231 | Title string |
157 | URI string | 232 | URI string |
158 | Assets AssetList | 233 | Assets AssetList |
159 | RawText string | 234 | RawText string |
160 | Lines []Item | 235 | Lines []Item |
161 | Error bool | 236 | Error bool |
162 | }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true}) | 237 | Protocol string |
238 | }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true, "gopher"}) | ||
163 | return | 239 | return |
164 | } | 240 | } |
165 | 241 | ||
@@ -171,13 +247,14 @@ func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass | |||
171 | buf := new(bytes.Buffer) | 247 | buf := new(bytes.Buffer) |
172 | buf.ReadFrom(res.Body) | 248 | buf.ReadFrom(res.Body) |
173 | tpl.Execute(w, struct { | 249 | tpl.Execute(w, struct { |
174 | Title string | 250 | Title string |
175 | URI string | 251 | URI string |
176 | Assets AssetList | 252 | Assets AssetList |
177 | RawText string | 253 | RawText string |
178 | Lines []Item | 254 | Lines []Item |
179 | Error bool | 255 | Error bool |
180 | }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, buf.String(), nil, false}) | 256 | Protocol string |
257 | }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, buf.String(), nil, false, "gopher"}) | ||
181 | } else if strings.HasPrefix(parts[1], "T") { | 258 | } else if strings.HasPrefix(parts[1], "T") { |
182 | _, _, err = vips.NewTransform(). | 259 | _, _, err = vips.NewTransform(). |
183 | Load(res.Body). | 260 | Load(res.Body). |
@@ -190,21 +267,123 @@ func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass | |||
190 | io.Copy(w, res.Body) | 267 | io.Copy(w, res.Body) |
191 | } | 268 | } |
192 | } else { | 269 | } else { |
193 | if err := renderDirectory(w, tpl, assetList, uri, hostport, res.Dir); err != nil { | 270 | if err := renderGopherDirectory(w, tpl, assetList, uri, hostport, res.Dir); err != nil { |
194 | tpl.Execute(w, struct { | 271 | tpl.Execute(w, struct { |
195 | Title string | 272 | Title string |
196 | URI string | 273 | URI string |
197 | Assets AssetList | 274 | Assets AssetList |
198 | RawText string | 275 | RawText string |
199 | Lines []Item | 276 | Lines []Item |
200 | Error bool | 277 | Error bool |
201 | }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true}) | 278 | Protocol string |
279 | }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true, "gopher"}) | ||
202 | return | 280 | return |
203 | } | 281 | } |
204 | } | 282 | } |
205 | } | 283 | } |
206 | } | 284 | } |
207 | 285 | ||
286 | func GeminiHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool, uri string) http.HandlerFunc { | ||
287 | return func(w http.ResponseWriter, req *http.Request) { | ||
288 | agent := req.UserAgent() | ||
289 | path := strings.TrimPrefix(req.URL.Path, "/gemini/") | ||
290 | |||
291 | if robotsdata != nil && robotsdebug && !robotsdata.TestAgent(path, agent) { | ||
292 | log.Printf("UserAgent %s ignored robots.txt", agent) | ||
293 | } | ||
294 | |||
295 | parts := strings.Split(path, "/") | ||
296 | hostport := parts[0] | ||
297 | |||
298 | if len(hostport) == 0 { | ||
299 | http.Redirect(w, req, "/"+uri, http.StatusFound) | ||
300 | return | ||
301 | } | ||
302 | |||
303 | var qs string | ||
304 | |||
305 | if req.URL.RawQuery != "" { | ||
306 | qs = fmt.Sprintf("?%s", url.QueryEscape(req.URL.RawQuery)) | ||
307 | } | ||
308 | |||
309 | uri, err := url.QueryUnescape(strings.Join(parts[1:], "/")) | ||
310 | if err != nil { | ||
311 | tpl.Execute(w, struct { | ||
312 | Title string | ||
313 | URI string | ||
314 | Assets AssetList | ||
315 | RawText string | ||
316 | Lines []Item | ||
317 | Error bool | ||
318 | Protocol string | ||
319 | }{"", hostport, assetList, fmt.Sprintf("Error: %s", err), nil, true, "gemini"}) | ||
320 | return | ||
321 | } | ||
322 | |||
323 | res, err := GeminiGet( | ||
324 | fmt.Sprintf( | ||
325 | "gemini://%s/%s%s", | ||
326 | hostport, | ||
327 | uri, | ||
328 | qs, | ||
329 | ), | ||
330 | ) | ||
331 | |||
332 | if err != nil { | ||
333 | tpl.Execute(w, struct { | ||
334 | Title string | ||
335 | URI string | ||
336 | Assets AssetList | ||
337 | RawText string | ||
338 | Lines []Item | ||
339 | Error bool | ||
340 | Protocol string | ||
341 | }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true, "gemini"}) | ||
342 | return | ||
343 | } | ||
344 | |||
345 | if int(res.Header.Status/10) != 2 { | ||
346 | tpl.Execute(w, struct { | ||
347 | Title string | ||
348 | URI string | ||
349 | Assets AssetList | ||
350 | RawText string | ||
351 | Lines []Item | ||
352 | Error bool | ||
353 | Protocol string | ||
354 | }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error %d: %s", res.Header.Status, res.Header.Meta), nil, true, "gemini"}) | ||
355 | return | ||
356 | } | ||
357 | |||
358 | if strings.HasPrefix(res.Header.Meta, MIME_GEMINI) { | ||
359 | items := parseGeminiDocument(res, uri, hostport) | ||
360 | tpl.Execute(w, struct { | ||
361 | Title string | ||
362 | URI string | ||
363 | Assets AssetList | ||
364 | RawText string | ||
365 | Lines []Item | ||
366 | Error bool | ||
367 | Protocol string | ||
368 | }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, "", items, false, "gemini"}) | ||
369 | } else if strings.HasPrefix(res.Header.Meta, "text/") { | ||
370 | buf := new(bytes.Buffer) | ||
371 | buf.ReadFrom(res.Body) | ||
372 | tpl.Execute(w, struct { | ||
373 | Title string | ||
374 | URI string | ||
375 | Assets AssetList | ||
376 | RawText string | ||
377 | Lines []Item | ||
378 | Error bool | ||
379 | Protocol string | ||
380 | }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, buf.String(), nil, false, "gemini"}) | ||
381 | } else { | ||
382 | io.Copy(w, res.Body) | ||
383 | } | ||
384 | } | ||
385 | } | ||
386 | |||
208 | // RobotsTxtHandler returns the contents of the robots.txt file | 387 | // RobotsTxtHandler returns the contents of the robots.txt file |
209 | // if configured and valid. | 388 | // if configured and valid. |
210 | func RobotsTxtHandler(robotstxtdata []byte) http.HandlerFunc { | 389 | func RobotsTxtHandler(robotstxtdata []byte) http.HandlerFunc { |
@@ -386,7 +565,9 @@ func ListenAndServe(bind, robotsfile string, robotsdebug bool, vipsconcurrency i | |||
386 | ConcurrencyLevel: vipsconcurrency, | 565 | ConcurrencyLevel: vipsconcurrency, |
387 | }) | 566 | }) |
388 | 567 | ||
389 | http.Handle("/", gziphandler.GzipHandler(GopherHandler(tpl, robotsdata, AssetList{styleAsset, jsAsset, fontwAsset, fontw2Asset, propfontwAsset, propfontw2Asset}, robotsdebug, uri))) | 568 | http.Handle("/", gziphandler.GzipHandler(DefaultHandler(tpl, uri))) |
569 | http.Handle("/gopher/", gziphandler.GzipHandler(GopherHandler(tpl, robotsdata, AssetList{styleAsset, jsAsset, fontwAsset, fontw2Asset, propfontwAsset, propfontw2Asset}, robotsdebug, uri))) | ||
570 | http.Handle("/gemini/", gziphandler.GzipHandler(GeminiHandler(tpl, robotsdata, AssetList{styleAsset, jsAsset, fontwAsset, fontw2Asset, propfontwAsset, propfontw2Asset}, robotsdebug, uri))) | ||
390 | http.Handle("/robots.txt", gziphandler.GzipHandler(RobotsTxtHandler(robotstxtdata))) | 571 | http.Handle("/robots.txt", gziphandler.GzipHandler(RobotsTxtHandler(robotstxtdata))) |
391 | http.Handle("/favicon.ico", gziphandler.GzipHandler(FaviconHandler(favicondata))) | 572 | http.Handle("/favicon.ico", gziphandler.GzipHandler(FaviconHandler(favicondata))) |
392 | http.Handle(styleAsset, gziphandler.GzipHandler(StyleHandler(styledata))) | 573 | http.Handle(styleAsset, gziphandler.GzipHandler(StyleHandler(styledata))) |