From 9bed6157351eea6fdbccff69aba5cc967a0b2a56 Mon Sep 17 00:00:00 2001 From: Feuerfuchs Date: Sat, 16 Nov 2019 00:55:25 +0100 Subject: Initial Gemini support --- gopherproxy.go | 261 ++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 221 insertions(+), 40 deletions(-) (limited to 'gopherproxy.go') diff --git a/gopherproxy.go b/gopherproxy.go index 87a1ad0..9a60507 100644 --- a/gopherproxy.go +++ b/gopherproxy.go @@ -1,6 +1,7 @@ package gopherproxy import ( + "bufio" "bytes" "crypto/md5" "fmt" @@ -25,6 +26,11 @@ import ( "github.com/NYTimes/gziphandler" ) +const ( + ITEM_TYPE_GEMINI_LINE = "" + ITEM_TYPE_GEMINI_LINK = " =>" +) + type Item struct { Link template.URL Type string @@ -40,7 +46,7 @@ type AssetList struct { PropFontW2 string } -func renderDirectory(w http.ResponseWriter, tpl *template.Template, assetList AssetList, uri string, hostport string, d gopher.Directory) error { +func renderGopherDirectory(w http.ResponseWriter, tpl *template.Template, assetList AssetList, uri string, hostport string, d gopher.Directory) error { var title string out := make([]Item, len(d.Items)) @@ -76,7 +82,7 @@ func renderDirectory(w http.ResponseWriter, tpl *template.Template, assetList As path = strings.Replace(path, "%2F", "/", -1) tr.Link = template.URL( fmt.Sprintf( - "/%s/%s%s", + "/gopher/%s/%s%s", hostport, string(byte(x.Type)), path, @@ -92,13 +98,81 @@ func renderDirectory(w http.ResponseWriter, tpl *template.Template, assetList As } return tpl.Execute(w, struct { - Title string - URI string - Assets AssetList - Lines []Item - RawText string - Error bool - }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, out, "", false}) + Title string + URI string + Assets AssetList + Lines []Item + RawText string + Error bool + Protocol string + }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, out, "", false, "gopher"}) +} + +func parseGeminiDocument(response *GeminiResponse, uri string, hostport string) (items []Item) { + scanner := bufio.NewScanner(response.Body) + scanner.Split(bufio.ScanLines) + + baseUrl, err := url.Parse(fmt.Sprintf( + "gemini://%s/%s", + hostport, + uri, + )) + if err != nil { + return []Item{} + } + + for scanner.Scan() { + line := strings.Trim(scanner.Text(), "\r\n") + + item := Item{ + Type: ITEM_TYPE_GEMINI_LINE, + Text: line, + } + + linkMatch := GeminiLinkPattern.FindStringSubmatch(line) + if len(linkMatch) != 0 && linkMatch[0] != "" { + link := linkMatch[1] + + if strings.HasPrefix(link, "//") { + link = "/gemini/" + strings.TrimPrefix(link, "//") + } else if strings.HasPrefix(link, "gemini://") { + link = "/gemini/" + strings.TrimPrefix(link, "gemini://") + } else if strings.HasPrefix(link, "gopher://") { + link = "/gopher/" + strings.TrimPrefix(link, "gopher://") + } else { + linkUrl, err := url.Parse(link) + if err != nil { + continue + } + adjustedUrl := baseUrl.ResolveReference(linkUrl) + if adjustedUrl.Scheme == "gemini" { + link = "/gemini/" + adjustedUrl.Host + adjustedUrl.Path + } else if adjustedUrl.Scheme == "gopher" { + link = "/gopher/" + adjustedUrl.Host + adjustedUrl.Path + } else { + link = adjustedUrl.String() + } + } + + item.Type = ITEM_TYPE_GEMINI_LINK + item.Link = template.URL(link) + if linkMatch[2] != "" { + item.Text = linkMatch[2] + } else { + item.Text = linkMatch[1] + } + } + + items = append(items, item) + } + + return +} + +func DefaultHandler(tpl *template.Template, uri string) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + http.Redirect(w, req, "/"+uri, http.StatusFound) + } } // GopherHandler returns a Handler that proxies requests @@ -109,7 +183,7 @@ func renderDirectory(w http.ResponseWriter, tpl *template.Template, assetList As func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool, uri string) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { agent := req.UserAgent() - path := strings.TrimPrefix(req.URL.Path, "/") + path := strings.TrimPrefix(req.URL.Path, "/gopher/") if robotsdata != nil && robotsdebug && !robotsdata.TestAgent(path, agent) { log.Printf("UserAgent %s ignored robots.txt", agent) @@ -132,13 +206,14 @@ func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass uri, err := url.QueryUnescape(strings.Join(parts[1:], "/")) if err != nil { tpl.Execute(w, struct { - Title string - URI string - Assets AssetList - RawText string - Lines []Item - Error bool - }{"", hostport, assetList, fmt.Sprintf("Error: %s", err), nil, true}) + Title string + URI string + Assets AssetList + RawText string + Lines []Item + Error bool + Protocol string + }{"", hostport, assetList, fmt.Sprintf("Error: %s", err), nil, true, "gopher"}) return } @@ -153,13 +228,14 @@ func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass if err != nil { tpl.Execute(w, struct { - Title string - URI string - Assets AssetList - RawText string - Lines []Item - Error bool - }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true}) + Title string + URI string + Assets AssetList + RawText string + Lines []Item + Error bool + Protocol string + }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true, "gopher"}) return } @@ -171,13 +247,14 @@ func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass buf := new(bytes.Buffer) buf.ReadFrom(res.Body) tpl.Execute(w, struct { - Title string - URI string - Assets AssetList - RawText string - Lines []Item - Error bool - }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, buf.String(), nil, false}) + Title string + URI string + Assets AssetList + RawText string + Lines []Item + Error bool + Protocol string + }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, buf.String(), nil, false, "gopher"}) } else if strings.HasPrefix(parts[1], "T") { _, _, err = vips.NewTransform(). Load(res.Body). @@ -190,21 +267,123 @@ func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass io.Copy(w, res.Body) } } else { - if err := renderDirectory(w, tpl, assetList, uri, hostport, res.Dir); err != nil { + if err := renderGopherDirectory(w, tpl, assetList, uri, hostport, res.Dir); err != nil { tpl.Execute(w, struct { - Title string - URI string - Assets AssetList - RawText string - Lines []Item - Error bool - }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true}) + Title string + URI string + Assets AssetList + RawText string + Lines []Item + Error bool + Protocol string + }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true, "gopher"}) return } } } } +func GeminiHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool, uri string) 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, "/"+uri, http.StatusFound) + return + } + + 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 { + tpl.Execute(w, struct { + Title string + URI string + Assets AssetList + RawText string + Lines []Item + Error bool + Protocol string + }{"", hostport, assetList, fmt.Sprintf("Error: %s", err), nil, true, "gemini"}) + return + } + + res, err := GeminiGet( + fmt.Sprintf( + "gemini://%s/%s%s", + hostport, + uri, + qs, + ), + ) + + if err != nil { + tpl.Execute(w, struct { + Title string + URI string + Assets AssetList + RawText string + Lines []Item + Error bool + Protocol string + }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true, "gemini"}) + return + } + + if int(res.Header.Status/10) != 2 { + tpl.Execute(w, struct { + Title string + URI string + Assets AssetList + RawText string + Lines []Item + Error bool + Protocol string + }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error %d: %s", res.Header.Status, res.Header.Meta), nil, true, "gemini"}) + return + } + + if strings.HasPrefix(res.Header.Meta, MIME_GEMINI) { + items := parseGeminiDocument(res, uri, hostport) + tpl.Execute(w, struct { + Title string + URI string + Assets AssetList + RawText string + Lines []Item + Error bool + Protocol string + }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, "", items, false, "gemini"}) + } else if strings.HasPrefix(res.Header.Meta, "text/") { + buf := new(bytes.Buffer) + buf.ReadFrom(res.Body) + tpl.Execute(w, struct { + Title string + URI string + Assets AssetList + RawText string + Lines []Item + Error bool + Protocol string + }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, buf.String(), nil, false, "gemini"}) + } 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 { @@ -386,7 +565,9 @@ func ListenAndServe(bind, robotsfile string, robotsdebug bool, vipsconcurrency i ConcurrencyLevel: vipsconcurrency, }) - http.Handle("/", gziphandler.GzipHandler(GopherHandler(tpl, robotsdata, AssetList{styleAsset, jsAsset, fontwAsset, fontw2Asset, propfontwAsset, propfontw2Asset}, robotsdebug, uri))) + http.Handle("/", gziphandler.GzipHandler(DefaultHandler(tpl, uri))) + http.Handle("/gopher/", gziphandler.GzipHandler(GopherHandler(tpl, robotsdata, AssetList{styleAsset, jsAsset, fontwAsset, fontw2Asset, propfontwAsset, propfontw2Asset}, robotsdebug, uri))) + http.Handle("/gemini/", gziphandler.GzipHandler(GeminiHandler(tpl, robotsdata, AssetList{styleAsset, jsAsset, fontwAsset, fontw2Asset, propfontwAsset, propfontw2Asset}, robotsdebug, uri))) http.Handle("/robots.txt", gziphandler.GzipHandler(RobotsTxtHandler(robotstxtdata))) http.Handle("/favicon.ico", gziphandler.GzipHandler(FaviconHandler(favicondata))) http.Handle(styleAsset, gziphandler.GzipHandler(StyleHandler(styledata))) -- cgit v1.3.1