package gopherproxy import ( "bytes" "crypto/md5" "fmt" "html" "html/template" "io" "io/ioutil" "log" "net/http" "net/url" "regexp" "strings" "github.com/temoto/robotstxt" "github.com/prologic/go-gopher" "github.com/gobuffalo/packr/v2" "github.com/davidbyttow/govips/pkg/vips" ) type Item struct { Link template.URL Type string Text string } type AssetHashList struct { Style string JS string FontW string FontW2 string } func renderDirectory(w http.ResponseWriter, tpl *template.Template, assetHashList AssetHashList, 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:") { tr.Link = template.URL(x.Selector[4:]) } 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( "/%s/%s%s", hostport, string(byte(x.Type)), path, ), ) } out[i] = tr } if title == "" { title = hostport } return tpl.Execute(w, struct { Title string URI string AssetHashList AssetHashList Lines []Item RawText string }{title, fmt.Sprintf("%s/%s", hostport, uri), assetHashList, out, ""}) } // 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, assetHashList AssetHashList, robotsdebug bool, uri string) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { agent := req.UserAgent() path := strings.TrimPrefix(req.URL.Path, "/") 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 { io.WriteString(w, fmt.Sprintf("Error:
%s
", err)) return } res, err := gopher.Get( fmt.Sprintf( "gopher://%s/%s%s", hostport, uri, qs, ), ) if err != nil { io.WriteString(w, fmt.Sprintf("Error:
%s
", err)) return } if res.Body != nil { if len(parts) >= 2 && parts[1] == "0" { //strings.HasSuffix(uri, ".txt") || strings.HasSuffix(uri, ".md") { // handle .txt files buf := new(bytes.Buffer) buf.ReadFrom(res.Body) tpl.Execute(w, struct { Title string URI string AssetHashList AssetHashList RawText string Lines []Item }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetHashList, buf.String(), nil}) } else if 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 := renderDirectory(w, tpl, assetHashList, uri, hostport, res.Dir); err != nil { io.WriteString(w, fmt.Sprintf("Error:
%s
", err)) return } } } } // 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, robotsfile string, robotsdebug bool, vipsconcurrency int, uri string) 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{} } fontwhash := fmt.Sprintf("%x", md5.Sum(fontdataw)) fontdataw2, err := box.Find("iosevka-term-ss03-regular.woff2") if err != nil { fontdataw2 = []byte{} } fontw2hash := fmt.Sprintf("%x", md5.Sum(fontdataw2)) styledata, err := box.Find("style.css") if err != nil { styledata = []byte{} } stylehash := fmt.Sprintf("%x", md5.Sum(styledata)) jsdata, err := box.Find("main.js") if err != nil { jsdata = []byte{} } jshash := fmt.Sprintf("%x", md5.Sum(jsdata)) favicondata, err := box.Find("favicon.ico") if err != nil { favicondata = []byte{} } 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) }, } tpl, err = template.New("gophermenu").Funcs(funcMap).Parse(tpltext) if err != nil { log.Fatal(err) } vips.Startup(&vips.Config{ ConcurrencyLevel: vipsconcurrency, }) http.HandleFunc("/", GopherHandler(tpl, robotsdata, AssetHashList{stylehash, jshash, fontwhash, fontw2hash}, robotsdebug, uri)) http.HandleFunc("/robots.txt", RobotsTxtHandler(robotstxtdata)) http.HandleFunc("/favicon.ico", FaviconHandler(favicondata)) http.HandleFunc("/style-"+stylehash+".css", StyleHandler(styledata)) http.HandleFunc("/main-"+jshash+".js", JavaScriptHandler(jsdata)) http.HandleFunc("/iosevka-term-ss03-regular-"+fontwhash+".woff", FontHandler(false, fontdataw)) http.HandleFunc("/iosevka-term-ss03-regular-"+fontw2hash+".woff2", FontHandler(true, fontdataw2)) //http.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir("assets/")))) return http.ListenAndServe(bind, nil) }