aboutsummaryrefslogtreecommitdiffstats
path: root/gopherproxy.go
diff options
context:
space:
mode:
authorFeuerfuchs <git@feuerfuchs.dev>2019-11-26 13:13:02 +0100
committerFeuerfuchs <git@feuerfuchs.dev>2019-11-26 13:13:02 +0100
commit93115f804220c31c2aa10f123560fb11135f06d8 (patch)
treea3c12f68c4263f3e34b1e03f12e7962aab9cfbf5 /gopherproxy.go
parentFix title on startpage (diff)
downloadgopherproxy-93115f804220c31c2aa10f123560fb11135f06d8.tar.gz
gopherproxy-93115f804220c31c2aa10f123560fb11135f06d8.tar.bz2
gopherproxy-93115f804220c31c2aa10f123560fb11135f06d8.zip
Add IPv6 support, general restructuring
Diffstat (limited to 'gopherproxy.go')
-rw-r--r--gopherproxy.go693
1 files changed, 0 insertions, 693 deletions
diff --git a/gopherproxy.go b/gopherproxy.go
deleted file mode 100644
index 8c0bb89..0000000
--- a/gopherproxy.go
+++ /dev/null
@@ -1,693 +0,0 @@
1package gopherproxy
2
3import (
4 "bufio"
5 "bytes"
6 "crypto/md5"
7 "fmt"
8 "html"
9 "html/template"
10 "io"
11 "io/ioutil"
12 "log"
13 "mime"
14 "net/http"
15 "net/url"
16 "regexp"
17 "strings"
18
19 "golang.org/x/net/html/charset"
20 "golang.org/x/text/transform"
21
22 "github.com/temoto/robotstxt"
23
24 "github.com/prologic/go-gopher"
25
26 "github.com/gobuffalo/packr/v2"
27
28 "github.com/davidbyttow/govips/pkg/vips"
29
30 "github.com/NYTimes/gziphandler"
31)
32
33const (
34 ITEM_TYPE_GEMINI_LINE = ""
35 ITEM_TYPE_GEMINI_LINK = " =>"
36)
37
38type Item struct {
39 Link template.URL
40 Type string
41 Text string
42}
43
44type AssetList struct {
45 Style string
46 JS string
47 FontW string
48 FontW2 string
49 PropFontW string
50 PropFontW2 string
51}
52
53func resolveURI(uri string, baseURL *url.URL) (resolvedURI string) {
54 if strings.HasPrefix(uri, "//") {
55 resolvedURI = "/gemini/" + strings.TrimPrefix(uri, "//")
56 } else if strings.HasPrefix(uri, "gemini://") {
57 resolvedURI = "/gemini/" + strings.TrimPrefix(uri, "gemini://")
58 } else if strings.HasPrefix(uri, "gopher://") {
59 resolvedURI = "/gopher/" + strings.TrimPrefix(uri, "gopher://")
60 } else {
61 url, err := url.Parse(uri)
62 if err != nil {
63 return ""
64 }
65 adjustedURI := baseURL.ResolveReference(url)
66 if adjustedURI.Scheme == "gemini" {
67 resolvedURI = "/gemini/" + adjustedURI.Host + adjustedURI.Path
68 } else if adjustedURI.Scheme == "gopher" {
69 resolvedURI = "/gopher/" + adjustedURI.Host + adjustedURI.Path
70 } else {
71 resolvedURI = adjustedURI.String()
72 }
73 }
74
75 return
76}
77
78func renderGopherDirectory(w http.ResponseWriter, tpl *template.Template, assetList AssetList, uri string, hostport string, d gopher.Directory) error {
79 var title string
80
81 out := make([]Item, len(d.Items))
82
83 for i, x := range d.Items {
84 if x.Type == gopher.INFO && x.Selector == "TITLE" {
85 title = x.Description
86 continue
87 }
88
89 tr := Item{
90 Text: x.Description,
91 Type: x.Type.String(),
92 }
93
94 if x.Type == gopher.INFO {
95 out[i] = tr
96 continue
97 }
98
99 if strings.HasPrefix(x.Selector, "URL:") || strings.HasPrefix(x.Selector, "/URL:") {
100 link := strings.TrimPrefix(strings.TrimPrefix(x.Selector, "/"), "URL:")
101 if strings.HasPrefix(link, "gemini://") {
102 link = fmt.Sprintf(
103 "/gemini/%s",
104 strings.TrimPrefix(link, "gemini://"),
105 )
106 } else if strings.HasPrefix(link, "gopher://") {
107 link = fmt.Sprintf(
108 "/gopher/%s",
109 strings.TrimPrefix(link, "gopher://"),
110 )
111 }
112 tr.Link = template.URL(link)
113 } else {
114 var hostport string
115 if x.Port == 70 {
116 hostport = x.Host
117 } else {
118 hostport = fmt.Sprintf("%s:%d", x.Host, x.Port)
119 }
120 path := url.PathEscape(x.Selector)
121 path = strings.Replace(path, "%2F", "/", -1)
122 tr.Link = template.URL(
123 fmt.Sprintf(
124 "/gopher/%s/%s%s",
125 hostport,
126 string(byte(x.Type)),
127 path,
128 ),
129 )
130 }
131
132 out[i] = tr
133 }
134
135 if title == "" {
136 if uri != "" {
137 title = fmt.Sprintf("%s/%s", hostport, uri)
138 } else {
139 title = hostport
140 }
141 }
142
143 return tpl.Execute(w, struct {
144 Title string
145 URI string
146 Assets AssetList
147 Lines []Item
148 RawText string
149 Error bool
150 Protocol string
151 }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, out, "", false, "gopher"})
152}
153
154func parseGeminiDocument(body *bytes.Buffer, uri string, hostport string) (items []Item) {
155 baseURL, err := url.Parse(fmt.Sprintf(
156 "gemini://%s/%s",
157 hostport,
158 uri,
159 ))
160 if err != nil {
161 return []Item{}
162 }
163
164 scanner := bufio.NewScanner(body)
165
166 for scanner.Scan() {
167 line := strings.Trim(scanner.Text(), "\r\n")
168
169 item := Item{
170 Type: ITEM_TYPE_GEMINI_LINE,
171 Text: line,
172 }
173
174 linkMatch := GeminiLinkPattern.FindStringSubmatch(line)
175 if len(linkMatch) != 0 && linkMatch[0] != "" {
176 item.Type = ITEM_TYPE_GEMINI_LINK
177 item.Link = template.URL(resolveURI(linkMatch[1], baseURL))
178 item.Text = linkMatch[2]
179 if item.Text == "" {
180 item.Text = linkMatch[1]
181 }
182 }
183
184 items = append(items, item)
185 }
186
187 return
188}
189
190func DefaultHandler(tpl *template.Template, startpagetext string, assetList AssetList) http.HandlerFunc {
191 return func(w http.ResponseWriter, req *http.Request) {
192 if err := tpl.Execute(w, struct {
193 Title string
194 URI string
195 Assets AssetList
196 RawText string
197 Lines []Item
198 Error bool
199 Protocol string
200 }{"Gopher/Gemini proxy", "", assetList, startpagetext, nil, false, "startpage"}); err != nil {
201 log.Println("Template error: " + err.Error())
202 }
203 }
204}
205
206// GopherHandler returns a Handler that proxies requests
207// to the specified Gopher server as denoated by the first argument
208// to the request path and renders the content using the provided template.
209// The optional robots parameters points to a robotstxt.RobotsData struct
210// to test user agents against a configurable robotst.txt file.
211func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool) http.HandlerFunc {
212 return func(w http.ResponseWriter, req *http.Request) {
213 agent := req.UserAgent()
214 path := strings.TrimPrefix(req.URL.Path, "/gopher/")
215
216 if robotsdata != nil && robotsdebug && !robotsdata.TestAgent(path, agent) {
217 log.Printf("UserAgent %s ignored robots.txt", agent)
218 }
219
220 parts := strings.Split(path, "/")
221 hostport := parts[0]
222
223 if len(hostport) == 0 {
224 http.Redirect(w, req, "/", http.StatusFound)
225 return
226 }
227
228 title := hostport
229
230 var qs string
231
232 if req.URL.RawQuery != "" {
233 qs = fmt.Sprintf("?%s", url.QueryEscape(req.URL.RawQuery))
234 }
235
236 uri, err := url.QueryUnescape(strings.Join(parts[1:], "/"))
237 if err != nil {
238 if e := tpl.Execute(w, struct {
239 Title string
240 URI string
241 Assets AssetList
242 RawText string
243 Lines []Item
244 Error bool
245 Protocol string
246 }{title, hostport, assetList, fmt.Sprintf("Error: %s", err), nil, true, "gopher"}); e != nil {
247 log.Println("Template error: " + e.Error())
248 log.Println(err.Error())
249 }
250 return
251 }
252
253 if uri != "" {
254 title = fmt.Sprintf("%s/%s", hostport, uri)
255 }
256
257 res, err := gopher.Get(
258 fmt.Sprintf(
259 "gopher://%s/%s%s",
260 hostport,
261 uri,
262 qs,
263 ),
264 )
265
266 if err != nil {
267 if e := tpl.Execute(w, struct {
268 Title string
269 URI string
270 Assets AssetList
271 RawText string
272 Lines []Item
273 Error bool
274 Protocol string
275 }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true, "gopher"}); e != nil {
276 log.Println("Template error: " + e.Error())
277 }
278 return
279 }
280
281 if res.Body != nil {
282 if len(parts) < 2 {
283 io.Copy(w, res.Body)
284 } else if strings.HasPrefix(parts[1], "0") && !strings.HasSuffix(uri, ".xml") && !strings.HasSuffix(uri, ".asc") {
285 buf := new(bytes.Buffer)
286 buf.ReadFrom(res.Body)
287 if err := tpl.Execute(w, struct {
288 Title string
289 URI string
290 Assets AssetList
291 RawText string
292 Lines []Item
293 Error bool
294 Protocol string
295 }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, buf.String(), nil, false, "gopher"}); err != nil {
296 log.Println("Template error: " + err.Error())
297 }
298 } else if strings.HasPrefix(parts[1], "T") {
299 _, _, err = vips.NewTransform().
300 Load(res.Body).
301 ResizeStrategy(vips.ResizeStrategyAuto).
302 ResizeWidth(160).
303 Quality(75).
304 Output(w).
305 Apply()
306 } else {
307 io.Copy(w, res.Body)
308 }
309 } else {
310 if err := renderGopherDirectory(w, tpl, assetList, uri, hostport, res.Dir); err != nil {
311 if e := 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 }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true, "gopher"}); e != nil {
320 log.Println("Template error: " + e.Error())
321 log.Println(e.Error())
322 }
323 }
324 }
325 }
326}
327
328func GeminiHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool) http.HandlerFunc {
329 return func(w http.ResponseWriter, req *http.Request) {
330 agent := req.UserAgent()
331 path := strings.TrimPrefix(req.URL.Path, "/gemini/")
332
333 if robotsdata != nil && robotsdebug && !robotsdata.TestAgent(path, agent) {
334 log.Printf("UserAgent %s ignored robots.txt", agent)
335 }
336
337 parts := strings.Split(path, "/")
338 hostport := parts[0]
339
340 if len(hostport) == 0 {
341 http.Redirect(w, req, "/", http.StatusFound)
342 return
343 }
344
345 title := hostport
346
347 var qs string
348
349 if req.URL.RawQuery != "" {
350 qs = fmt.Sprintf("?%s", url.QueryEscape(req.URL.RawQuery))
351 }
352
353 uri, err := url.QueryUnescape(strings.Join(parts[1:], "/"))
354 if err != nil {
355 if e := tpl.Execute(w, struct {
356 Title string
357 URI string
358 Assets AssetList
359 RawText string
360 Lines []Item
361 Error bool
362 Protocol string
363 }{title, hostport, assetList, fmt.Sprintf("Error: %s", err), nil, true, "gemini"}); e != nil {
364 log.Println("Template error: " + e.Error())
365 log.Println(err.Error())
366 }
367 return
368 }
369
370 if uri != "" {
371 title = fmt.Sprintf("%s/%s", hostport, uri)
372 }
373
374 res, err := GeminiGet(
375 fmt.Sprintf(
376 "gemini://%s/%s%s",
377 hostport,
378 uri,
379 qs,
380 ),
381 )
382
383 if err != nil {
384 if e := tpl.Execute(w, struct {
385 Title string
386 URI string
387 Assets AssetList
388 RawText string
389 Lines []Item
390 Error bool
391 Protocol string
392 }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true, "gemini"}); e != nil {
393 log.Println("Template error: " + e.Error())
394 log.Println(err.Error())
395 }
396 return
397 }
398
399 if int(res.Header.Status/10) == 3 {
400 baseURL, err := url.Parse(fmt.Sprintf(
401 "gemini://%s/%s",
402 hostport,
403 uri,
404 ))
405 if err != nil {
406 if e := tpl.Execute(w, struct {
407 Title string
408 URI string
409 Assets AssetList
410 RawText string
411 Lines []Item
412 Error bool
413 Protocol string
414 }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true, "gemini"}); e != nil {
415 log.Println("Template error: " + e.Error())
416 log.Println(err.Error())
417 }
418 return
419 }
420
421 http.Redirect(w, req, resolveURI(res.Header.Meta, baseURL), http.StatusFound)
422 return
423 }
424
425 if int(res.Header.Status/10) != 2 {
426 if err := tpl.Execute(w, struct {
427 Title string
428 URI string
429 Assets AssetList
430 RawText string
431 Lines []Item
432 Error bool
433 Protocol string
434 }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error %d: %s", res.Header.Status, res.Header.Meta), nil, true, "gemini"}); err != nil {
435 log.Println("Template error: " + err.Error())
436 }
437 return
438 }
439
440 if strings.HasPrefix(res.Header.Meta, "text/") {
441 buf := new(bytes.Buffer)
442
443 _, params, err := mime.ParseMediaType(res.Header.Meta)
444 if err != nil {
445 buf.ReadFrom(res.Body)
446 } else {
447 encoding, _ := charset.Lookup(params["charset"])
448 readbuf := new(bytes.Buffer)
449 readbuf.ReadFrom(res.Body)
450
451 writer := transform.NewWriter(buf, encoding.NewDecoder())
452 writer.Write(readbuf.Bytes())
453 writer.Close()
454 }
455
456 var (
457 rawText string
458 items []Item
459 )
460
461 if strings.HasPrefix(res.Header.Meta, MIME_GEMINI) {
462 items = parseGeminiDocument(buf, uri, hostport)
463 } else {
464 rawText = buf.String()
465 }
466
467 if err := tpl.Execute(w, struct {
468 Title string
469 URI string
470 Assets AssetList
471 RawText string
472 Lines []Item
473 Error bool
474 Protocol string
475 }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, rawText, items, false, "gemini"}); err != nil {
476 log.Println("Template error: " + err.Error())
477 }
478 } else {
479 io.Copy(w, res.Body)
480 }
481 }
482}
483
484// RobotsTxtHandler returns the contents of the robots.txt file
485// if configured and valid.
486func RobotsTxtHandler(robotstxtdata []byte) http.HandlerFunc {
487 return func(w http.ResponseWriter, req *http.Request) {
488 if robotstxtdata == nil {
489 http.Error(w, "Not Found", http.StatusNotFound)
490 return
491 }
492
493 w.Header().Set("Content-Type", "text/plain")
494 w.Write(robotstxtdata)
495 }
496}
497
498func FaviconHandler(favicondata []byte) http.HandlerFunc {
499 return func(w http.ResponseWriter, req *http.Request) {
500 if favicondata == nil {
501 http.Error(w, "Not Found", http.StatusNotFound)
502 return
503 }
504
505 w.Header().Set("Content-Type", "image/vnd.microsoft.icon")
506 w.Header().Set("Cache-Control", "max-age=2592000")
507 w.Write(favicondata)
508 }
509}
510
511func StyleHandler(styledata []byte) http.HandlerFunc {
512 return func(w http.ResponseWriter, req *http.Request) {
513 w.Header().Set("Content-Type", "text/css")
514 w.Header().Set("Cache-Control", "max-age=2592000")
515 w.Write(styledata)
516 }
517}
518
519func JavaScriptHandler(jsdata []byte) http.HandlerFunc {
520 return func(w http.ResponseWriter, req *http.Request) {
521 w.Header().Set("Content-Type", "text/javascript")
522 w.Header().Set("Cache-Control", "max-age=2592000")
523 w.Write(jsdata)
524 }
525}
526
527func FontHandler(woff2 bool, fontdata []byte) http.HandlerFunc {
528 return func(w http.ResponseWriter, req *http.Request) {
529 if fontdata == nil {
530 http.Error(w, "Not Found", http.StatusNotFound)
531 return
532 }
533
534 if woff2 {
535 w.Header().Set("Content-Type", "font/woff2")
536 } else {
537 w.Header().Set("Content-Type", "font/woff")
538 }
539 w.Header().Set("Cache-Control", "max-age=2592000")
540
541 w.Write(fontdata)
542 }
543}
544
545// ListenAndServe creates a listening HTTP server bound to
546// the interface specified by bind and sets up a Gopher to HTTP
547// proxy proxying requests as requested and by default will prozy
548// to a Gopher server address specified by uri if no servers is
549// specified by the request. The robots argument is a pointer to
550// a robotstxt.RobotsData struct for testing user agents against
551// a configurable robots.txt file.
552func ListenAndServe(bind, startpagefile string, robotsfile string, robotsdebug bool, vipsconcurrency int) error {
553 var (
554 tpl *template.Template
555 robotsdata *robotstxt.RobotsData
556 )
557
558 robotstxtdata, err := ioutil.ReadFile(robotsfile)
559 if err != nil {
560 log.Printf("error reading robots.txt: %s", err)
561 robotstxtdata = nil
562 } else {
563 robotsdata, err = robotstxt.FromBytes(robotstxtdata)
564 if err != nil {
565 log.Printf("error reading robots.txt: %s", err)
566 robotstxtdata = nil
567 }
568 }
569
570 box := packr.New("assets", "./assets")
571
572 fontdataw, err := box.Find("iosevka-term-ss03-regular.woff")
573 if err != nil {
574 fontdataw = []byte{}
575 }
576 fontwAsset := fmt.Sprintf("/iosevka-term-ss03-regular-%x.woff", md5.Sum(fontdataw))
577
578 fontdataw2, err := box.Find("iosevka-term-ss03-regular.woff2")
579 if err != nil {
580 fontdataw2 = []byte{}
581 }
582 fontw2Asset := fmt.Sprintf("/iosevka-term-ss03-regular-%x.woff2", md5.Sum(fontdataw2))
583
584 propfontdataw, err := box.Find("iosevka-aile-regular.woff")
585 if err != nil {
586 propfontdataw = []byte{}
587 }
588 propfontwAsset := fmt.Sprintf("/iosevka-aile-regular-%x.woff", md5.Sum(propfontdataw))
589
590 propfontdataw2, err := box.Find("iosevka-aile-regular.woff2")
591 if err != nil {
592 propfontdataw2 = []byte{}
593 }
594 propfontw2Asset := fmt.Sprintf("/iosevka-aile-regular-%x.woff2", md5.Sum(propfontdataw2))
595
596 styledata, err := box.Find("style.css")
597 if err != nil {
598 styledata = []byte{}
599 }
600 styleAsset := fmt.Sprintf("/style-%x.css", md5.Sum(styledata))
601
602 jsdata, err := box.Find("main.js")
603 if err != nil {
604 jsdata = []byte{}
605 }
606 jsAsset := fmt.Sprintf("/main-%x.js", md5.Sum(jsdata))
607
608 favicondata, err := box.Find("favicon.ico")
609 if err != nil {
610 favicondata = []byte{}
611 }
612
613 startpagedata, err := ioutil.ReadFile(startpagefile)
614 if err != nil {
615 startpagedata, err = box.Find("startpage.txt")
616 if err != nil {
617 startpagedata = []byte{}
618 }
619 }
620 startpagetext := string(startpagedata)
621
622 tpldata, err := ioutil.ReadFile(".template")
623 if err == nil {
624 tpltext = string(tpldata)
625 }
626
627 funcMap := template.FuncMap{
628 "safeHtml": func(s string) template.HTML {
629 return template.HTML(s)
630 },
631 "safeCss": func(s string) template.CSS {
632 return template.CSS(s)
633 },
634 "safeJs": func(s string) template.JS {
635 return template.JS(s)
636 },
637 "HTMLEscape": func(s string) string {
638 return html.EscapeString(s)
639 },
640 "split": strings.Split,
641 "last": func(s []string) string {
642 return s[len(s)-1]
643 },
644 "pop": func(s []string) []string {
645 return s[:len(s)-1]
646 },
647 "replace": func(pattern, output string, input interface{}) string {
648 var re = regexp.MustCompile(pattern)
649 var inputStr = fmt.Sprintf("%v", input)
650 return re.ReplaceAllString(inputStr, output)
651 },
652 "trimLeftChar": func(s string) string {
653 for i := range s {
654 if i > 0 {
655 return s[i:]
656 }
657 }
658 return s[:0]
659 },
660 "hasPrefix": func(s string, prefix string) bool {
661 return strings.HasPrefix(s, prefix)
662 },
663 "title": func(s string) string {
664 return strings.Title(s)
665 },
666 }
667
668 tpl, err = template.New("gophermenu").Funcs(funcMap).Parse(tpltext)
669 if err != nil {
670 log.Fatal(err)
671 }
672
673 vips.Startup(&vips.Config{
674 ConcurrencyLevel: vipsconcurrency,
675 })
676
677 assets := AssetList{styleAsset, jsAsset, fontwAsset, fontw2Asset, propfontwAsset, propfontw2Asset}
678
679 http.Handle("/", gziphandler.GzipHandler(DefaultHandler(tpl, startpagetext, assets)))
680 http.Handle("/gopher/", gziphandler.GzipHandler(GopherHandler(tpl, robotsdata, assets, robotsdebug)))
681 http.Handle("/gemini/", gziphandler.GzipHandler(GeminiHandler(tpl, robotsdata, assets, robotsdebug)))
682 http.Handle("/robots.txt", gziphandler.GzipHandler(RobotsTxtHandler(robotstxtdata)))
683 http.Handle("/favicon.ico", gziphandler.GzipHandler(FaviconHandler(favicondata)))
684 http.Handle(styleAsset, gziphandler.GzipHandler(StyleHandler(styledata)))
685 http.Handle(jsAsset, gziphandler.GzipHandler(JavaScriptHandler(jsdata)))
686 http.HandleFunc(fontwAsset, FontHandler(false, fontdataw))
687 http.HandleFunc(fontw2Asset, FontHandler(true, fontdataw2))
688 http.HandleFunc(propfontwAsset, FontHandler(false, propfontdataw))
689 http.HandleFunc(propfontw2Asset, FontHandler(true, propfontdataw2))
690 //http.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir("assets/"))))
691
692 return http.ListenAndServe(bind, nil)
693}