From 93115f804220c31c2aa10f123560fb11135f06d8 Mon Sep 17 00:00:00 2001 From: Feuerfuchs Date: Tue, 26 Nov 2019 13:13:02 +0100 Subject: Add IPv6 support, general restructuring --- gopherproxy.go | 693 --------------------------------------------------------- 1 file changed, 693 deletions(-) delete mode 100644 gopherproxy.go (limited to 'gopherproxy.go') diff --git a/gopherproxy.go b/gopherproxy.go deleted file mode 100644 index 8c0bb89..0000000 --- a/gopherproxy.go +++ /dev/null @@ -1,693 +0,0 @@ -package gopherproxy - -import ( - "bufio" - "bytes" - "crypto/md5" - "fmt" - "html" - "html/template" - "io" - "io/ioutil" - "log" - "mime" - "net/http" - "net/url" - "regexp" - "strings" - - "golang.org/x/net/html/charset" - "golang.org/x/text/transform" - - "github.com/temoto/robotstxt" - - "github.com/prologic/go-gopher" - - "github.com/gobuffalo/packr/v2" - - "github.com/davidbyttow/govips/pkg/vips" - - "github.com/NYTimes/gziphandler" -) - -const ( - ITEM_TYPE_GEMINI_LINE = "" - ITEM_TYPE_GEMINI_LINK = " =>" -) - -type Item struct { - Link template.URL - Type string - Text string -} - -type AssetList struct { - Style string - JS string - FontW string - FontW2 string - PropFontW string - PropFontW2 string -} - -func resolveURI(uri string, baseURL *url.URL) (resolvedURI string) { - if strings.HasPrefix(uri, "//") { - resolvedURI = "/gemini/" + strings.TrimPrefix(uri, "//") - } else if strings.HasPrefix(uri, "gemini://") { - resolvedURI = "/gemini/" + strings.TrimPrefix(uri, "gemini://") - } else if strings.HasPrefix(uri, "gopher://") { - resolvedURI = "/gopher/" + strings.TrimPrefix(uri, "gopher://") - } else { - url, err := url.Parse(uri) - if err != nil { - return "" - } - adjustedURI := baseURL.ResolveReference(url) - if adjustedURI.Scheme == "gemini" { - resolvedURI = "/gemini/" + adjustedURI.Host + adjustedURI.Path - } else if adjustedURI.Scheme == "gopher" { - resolvedURI = "/gopher/" + adjustedURI.Host + adjustedURI.Path - } else { - resolvedURI = adjustedURI.String() - } - } - - return -} - -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)) - - for i, x := range d.Items { - if x.Type == gopher.INFO && x.Selector == "TITLE" { - title = x.Description - continue - } - - tr := Item{ - Text: x.Description, - Type: x.Type.String(), - } - - if x.Type == gopher.INFO { - out[i] = tr - continue - } - - if strings.HasPrefix(x.Selector, "URL:") || strings.HasPrefix(x.Selector, "/URL:") { - link := strings.TrimPrefix(strings.TrimPrefix(x.Selector, "/"), "URL:") - if strings.HasPrefix(link, "gemini://") { - link = fmt.Sprintf( - "/gemini/%s", - strings.TrimPrefix(link, "gemini://"), - ) - } else if strings.HasPrefix(link, "gopher://") { - link = fmt.Sprintf( - "/gopher/%s", - strings.TrimPrefix(link, "gopher://"), - ) - } - tr.Link = template.URL(link) - } else { - var hostport string - if x.Port == 70 { - hostport = x.Host - } else { - hostport = fmt.Sprintf("%s:%d", x.Host, x.Port) - } - path := url.PathEscape(x.Selector) - path = strings.Replace(path, "%2F", "/", -1) - tr.Link = template.URL( - fmt.Sprintf( - "/gopher/%s/%s%s", - hostport, - string(byte(x.Type)), - path, - ), - ) - } - - out[i] = tr - } - - if title == "" { - if uri != "" { - title = fmt.Sprintf("%s/%s", hostport, uri) - } else { - title = hostport - } - } - - return tpl.Execute(w, struct { - 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(body *bytes.Buffer, uri string, hostport string) (items []Item) { - baseURL, err := url.Parse(fmt.Sprintf( - "gemini://%s/%s", - hostport, - uri, - )) - if err != nil { - return []Item{} - } - - scanner := bufio.NewScanner(body) - - 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] != "" { - item.Type = ITEM_TYPE_GEMINI_LINK - item.Link = template.URL(resolveURI(linkMatch[1], baseURL)) - item.Text = linkMatch[2] - if item.Text == "" { - item.Text = linkMatch[1] - } - } - - items = append(items, item) - } - - return -} - -func DefaultHandler(tpl *template.Template, startpagetext string, assetList AssetList) http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - if err := tpl.Execute(w, struct { - Title string - URI string - Assets AssetList - RawText string - Lines []Item - Error bool - Protocol string - }{"Gopher/Gemini proxy", "", assetList, startpagetext, nil, false, "startpage"}); err != nil { - log.Println("Template error: " + err.Error()) - } - } -} - -// GopherHandler returns a Handler that proxies requests -// to the specified Gopher server as denoated by the first argument -// to the request path and renders the content using the provided template. -// The optional robots parameters points to a robotstxt.RobotsData struct -// to test user agents against a configurable robotst.txt file. -func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool) http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - agent := req.UserAgent() - path := strings.TrimPrefix(req.URL.Path, "/gopher/") - - 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, "/", http.StatusFound) - return - } - - title := hostport - - 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 { - if e := tpl.Execute(w, struct { - Title string - URI string - Assets AssetList - RawText string - Lines []Item - Error bool - Protocol string - }{title, hostport, assetList, fmt.Sprintf("Error: %s", err), nil, true, "gopher"}); e != nil { - log.Println("Template error: " + e.Error()) - log.Println(err.Error()) - } - return - } - - if uri != "" { - title = fmt.Sprintf("%s/%s", hostport, uri) - } - - res, err := gopher.Get( - fmt.Sprintf( - "gopher://%s/%s%s", - hostport, - uri, - qs, - ), - ) - - if err != nil { - if e := tpl.Execute(w, struct { - Title string - URI string - Assets AssetList - RawText string - Lines []Item - Error bool - Protocol string - }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true, "gopher"}); e != nil { - log.Println("Template error: " + e.Error()) - } - return - } - - if res.Body != nil { - if len(parts) < 2 { - io.Copy(w, res.Body) - } else if strings.HasPrefix(parts[1], "0") && !strings.HasSuffix(uri, ".xml") && !strings.HasSuffix(uri, ".asc") { - buf := new(bytes.Buffer) - buf.ReadFrom(res.Body) - if err := tpl.Execute(w, struct { - Title string - URI string - Assets AssetList - RawText string - Lines []Item - Error bool - Protocol string - }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, buf.String(), nil, false, "gopher"}); err != nil { - log.Println("Template error: " + err.Error()) - } - } else if strings.HasPrefix(parts[1], "T") { - _, _, err = vips.NewTransform(). - Load(res.Body). - ResizeStrategy(vips.ResizeStrategyAuto). - ResizeWidth(160). - Quality(75). - Output(w). - Apply() - } else { - io.Copy(w, res.Body) - } - } else { - if err := renderGopherDirectory(w, tpl, assetList, uri, hostport, res.Dir); err != nil { - if e := tpl.Execute(w, struct { - Title string - URI string - Assets AssetList - RawText string - Lines []Item - Error bool - Protocol string - }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true, "gopher"}); e != nil { - log.Println("Template error: " + e.Error()) - log.Println(e.Error()) - } - } - } - } -} - -func GeminiHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool) 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, "/", http.StatusFound) - return - } - - title := hostport - - 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 { - if e := tpl.Execute(w, struct { - Title string - URI string - Assets AssetList - RawText string - Lines []Item - Error bool - Protocol string - }{title, hostport, assetList, fmt.Sprintf("Error: %s", err), nil, true, "gemini"}); e != nil { - log.Println("Template error: " + e.Error()) - log.Println(err.Error()) - } - return - } - - if uri != "" { - title = fmt.Sprintf("%s/%s", hostport, uri) - } - - res, err := GeminiGet( - fmt.Sprintf( - "gemini://%s/%s%s", - hostport, - uri, - qs, - ), - ) - - if err != nil { - if e := tpl.Execute(w, struct { - Title string - URI string - Assets AssetList - RawText string - Lines []Item - Error bool - Protocol string - }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true, "gemini"}); e != nil { - log.Println("Template error: " + e.Error()) - log.Println(err.Error()) - } - return - } - - if int(res.Header.Status/10) == 3 { - baseURL, err := url.Parse(fmt.Sprintf( - "gemini://%s/%s", - hostport, - uri, - )) - if err != nil { - if e := tpl.Execute(w, struct { - Title string - URI string - Assets AssetList - RawText string - Lines []Item - Error bool - Protocol string - }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true, "gemini"}); e != nil { - log.Println("Template error: " + e.Error()) - log.Println(err.Error()) - } - return - } - - http.Redirect(w, req, resolveURI(res.Header.Meta, baseURL), http.StatusFound) - return - } - - if int(res.Header.Status/10) != 2 { - if err := tpl.Execute(w, struct { - Title string - URI string - Assets AssetList - RawText string - Lines []Item - Error bool - Protocol string - }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error %d: %s", res.Header.Status, res.Header.Meta), nil, true, "gemini"}); err != nil { - log.Println("Template error: " + err.Error()) - } - return - } - - if strings.HasPrefix(res.Header.Meta, "text/") { - buf := new(bytes.Buffer) - - _, params, err := mime.ParseMediaType(res.Header.Meta) - if err != nil { - buf.ReadFrom(res.Body) - } else { - encoding, _ := charset.Lookup(params["charset"]) - readbuf := new(bytes.Buffer) - readbuf.ReadFrom(res.Body) - - writer := transform.NewWriter(buf, encoding.NewDecoder()) - writer.Write(readbuf.Bytes()) - writer.Close() - } - - var ( - rawText string - items []Item - ) - - if strings.HasPrefix(res.Header.Meta, MIME_GEMINI) { - items = parseGeminiDocument(buf, uri, hostport) - } else { - rawText = buf.String() - } - - if err := tpl.Execute(w, struct { - Title string - URI string - Assets AssetList - RawText string - Lines []Item - Error bool - Protocol string - }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, rawText, items, false, "gemini"}); err != nil { - log.Println("Template error: " + err.Error()) - } - } 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 { - return func(w http.ResponseWriter, req *http.Request) { - if robotstxtdata == nil { - http.Error(w, "Not Found", http.StatusNotFound) - return - } - - w.Header().Set("Content-Type", "text/plain") - w.Write(robotstxtdata) - } -} - -func FaviconHandler(favicondata []byte) http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - if favicondata == nil { - http.Error(w, "Not Found", http.StatusNotFound) - return - } - - w.Header().Set("Content-Type", "image/vnd.microsoft.icon") - w.Header().Set("Cache-Control", "max-age=2592000") - w.Write(favicondata) - } -} - -func StyleHandler(styledata []byte) http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", "text/css") - w.Header().Set("Cache-Control", "max-age=2592000") - w.Write(styledata) - } -} - -func JavaScriptHandler(jsdata []byte) http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", "text/javascript") - w.Header().Set("Cache-Control", "max-age=2592000") - w.Write(jsdata) - } -} - -func FontHandler(woff2 bool, fontdata []byte) http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - if fontdata == nil { - http.Error(w, "Not Found", http.StatusNotFound) - return - } - - if woff2 { - w.Header().Set("Content-Type", "font/woff2") - } else { - w.Header().Set("Content-Type", "font/woff") - } - w.Header().Set("Cache-Control", "max-age=2592000") - - w.Write(fontdata) - } -} - -// ListenAndServe creates a listening HTTP server bound to -// the interface specified by bind and sets up a Gopher to HTTP -// proxy proxying requests as requested and by default will prozy -// to a Gopher server address specified by uri if no servers is -// specified by the request. The robots argument is a pointer to -// a robotstxt.RobotsData struct for testing user agents against -// a configurable robots.txt file. -func ListenAndServe(bind, startpagefile string, robotsfile string, robotsdebug bool, vipsconcurrency int) error { - var ( - tpl *template.Template - robotsdata *robotstxt.RobotsData - ) - - robotstxtdata, err := ioutil.ReadFile(robotsfile) - if err != nil { - log.Printf("error reading robots.txt: %s", err) - robotstxtdata = nil - } else { - robotsdata, err = robotstxt.FromBytes(robotstxtdata) - if err != nil { - log.Printf("error reading robots.txt: %s", err) - robotstxtdata = nil - } - } - - box := packr.New("assets", "./assets") - - fontdataw, err := box.Find("iosevka-term-ss03-regular.woff") - if err != nil { - fontdataw = []byte{} - } - fontwAsset := fmt.Sprintf("/iosevka-term-ss03-regular-%x.woff", md5.Sum(fontdataw)) - - fontdataw2, err := box.Find("iosevka-term-ss03-regular.woff2") - if err != nil { - fontdataw2 = []byte{} - } - fontw2Asset := fmt.Sprintf("/iosevka-term-ss03-regular-%x.woff2", md5.Sum(fontdataw2)) - - propfontdataw, err := box.Find("iosevka-aile-regular.woff") - if err != nil { - propfontdataw = []byte{} - } - propfontwAsset := fmt.Sprintf("/iosevka-aile-regular-%x.woff", md5.Sum(propfontdataw)) - - propfontdataw2, err := box.Find("iosevka-aile-regular.woff2") - if err != nil { - propfontdataw2 = []byte{} - } - propfontw2Asset := fmt.Sprintf("/iosevka-aile-regular-%x.woff2", md5.Sum(propfontdataw2)) - - styledata, err := box.Find("style.css") - if err != nil { - styledata = []byte{} - } - styleAsset := fmt.Sprintf("/style-%x.css", md5.Sum(styledata)) - - jsdata, err := box.Find("main.js") - if err != nil { - jsdata = []byte{} - } - jsAsset := fmt.Sprintf("/main-%x.js", md5.Sum(jsdata)) - - favicondata, err := box.Find("favicon.ico") - if err != nil { - favicondata = []byte{} - } - - startpagedata, err := ioutil.ReadFile(startpagefile) - if err != nil { - startpagedata, err = box.Find("startpage.txt") - if err != nil { - startpagedata = []byte{} - } - } - startpagetext := string(startpagedata) - - tpldata, err := ioutil.ReadFile(".template") - if err == nil { - tpltext = string(tpldata) - } - - funcMap := template.FuncMap{ - "safeHtml": func(s string) template.HTML { - return template.HTML(s) - }, - "safeCss": func(s string) template.CSS { - return template.CSS(s) - }, - "safeJs": func(s string) template.JS { - return template.JS(s) - }, - "HTMLEscape": func(s string) string { - return html.EscapeString(s) - }, - "split": strings.Split, - "last": func(s []string) string { - return s[len(s)-1] - }, - "pop": func(s []string) []string { - return s[:len(s)-1] - }, - "replace": func(pattern, output string, input interface{}) string { - var re = regexp.MustCompile(pattern) - var inputStr = fmt.Sprintf("%v", input) - return re.ReplaceAllString(inputStr, output) - }, - "trimLeftChar": func(s string) string { - for i := range s { - if i > 0 { - return s[i:] - } - } - return s[:0] - }, - "hasPrefix": func(s string, prefix string) bool { - return strings.HasPrefix(s, prefix) - }, - "title": func(s string) string { - return strings.Title(s) - }, - } - - tpl, err = template.New("gophermenu").Funcs(funcMap).Parse(tpltext) - if err != nil { - log.Fatal(err) - } - - vips.Startup(&vips.Config{ - ConcurrencyLevel: vipsconcurrency, - }) - - assets := AssetList{styleAsset, jsAsset, fontwAsset, fontw2Asset, propfontwAsset, propfontw2Asset} - - http.Handle("/", gziphandler.GzipHandler(DefaultHandler(tpl, startpagetext, assets))) - http.Handle("/gopher/", gziphandler.GzipHandler(GopherHandler(tpl, robotsdata, assets, robotsdebug))) - http.Handle("/gemini/", gziphandler.GzipHandler(GeminiHandler(tpl, robotsdata, assets, robotsdebug))) - http.Handle("/robots.txt", gziphandler.GzipHandler(RobotsTxtHandler(robotstxtdata))) - http.Handle("/favicon.ico", gziphandler.GzipHandler(FaviconHandler(favicondata))) - http.Handle(styleAsset, gziphandler.GzipHandler(StyleHandler(styledata))) - http.Handle(jsAsset, gziphandler.GzipHandler(JavaScriptHandler(jsdata))) - http.HandleFunc(fontwAsset, FontHandler(false, fontdataw)) - http.HandleFunc(fontw2Asset, FontHandler(true, fontdataw2)) - http.HandleFunc(propfontwAsset, FontHandler(false, propfontdataw)) - http.HandleFunc(propfontw2Asset, FontHandler(true, propfontdataw2)) - //http.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir("assets/")))) - - return http.ListenAndServe(bind, nil) -} -- cgit v1.2.3-54-g00ecf