package gopherproxy import ( "crypto/md5" "fmt" "html" "html/template" "io/ioutil" "log" "net/http" "regexp" "strings" "github.com/NYTimes/gziphandler" "github.com/davidbyttow/govips/pkg/vips" "github.com/gobuffalo/packr/v2" "github.com/temoto/robotstxt" ) type AssetList struct { Style string JS string FontRegularW string FontRegularW2 string FontBoldW string FontBoldW2 string } type startTemplateVariables struct { Title string URL string Assets AssetList Content string } func DefaultHandler(tpl *template.Template, startpagetext string, assetList AssetList) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { if err := tpl.Execute(w, startTemplateVariables{ Title: "Gopher/Gemini proxy", Assets: assetList, Content: startpagetext, }); err != nil { log.Println("Template error: " + err.Error()) } } } // 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 { box := packr.New("assets", "../../assets") // // Robots var 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 } } // // Fonts fontRegularWData, err := box.Find("iosevka-fixed-ss03-regular.woff") if err != nil { fontRegularWData = []byte{} } fontRegularWAsset := fmt.Sprintf("/iosevka-fixed-ss03-regular-%x.woff", md5.Sum(fontRegularWData)) fontRegularW2Data, err := box.Find("iosevka-fixed-ss03-regular.woff2") if err != nil { fontRegularW2Data = []byte{} } fontRegularW2Asset := fmt.Sprintf("/iosevka-fixed-ss03-regular-%x.woff2", md5.Sum(fontRegularW2Data)) fontBoldWData, err := box.Find("iosevka-fixed-ss03-bold.woff") if err != nil { fontBoldWData = []byte{} } fontBoldWAsset := fmt.Sprintf("/iosevka-fixed-ss03-bold-%x.woff", md5.Sum(fontBoldWData)) fontBoldW2Data, err := box.Find("iosevka-fixed-ss03-bold.woff2") if err != nil { fontBoldW2Data = []byte{} } fontBoldW2Asset := fmt.Sprintf("/iosevka-fixed-ss03-bold-%x.woff2", md5.Sum(fontBoldW2Data)) // // Stylesheet styledata, err := box.Find("style.css") if err != nil { styledata = []byte{} } styleAsset := fmt.Sprintf("/style-%x.css", md5.Sum(styledata)) // // JavaScript jsdata, err := box.Find("main.js") if err != nil { jsdata = []byte{} } jsAsset := fmt.Sprintf("/main-%x.js", md5.Sum(jsdata)) // // Favicon favicondata, err := box.Find("favicon.ico") if err != nil { favicondata = []byte{} } // // Start page text startpagedata, err := ioutil.ReadFile(startpagefile) if err != nil { startpagedata, err = box.Find("startpage.txt") if err != nil { startpagedata = []byte{} } } startpagetext := string(startpagedata) // // 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) }, "hasSuffix": func(s string, suffix string) bool { return strings.HasSuffix(s, suffix) }, "title": func(s string) string { return strings.Title(s) }, "string": func(s interface{}) string { return fmt.Sprint(s) }, } // tplBox := packr.New("templates", "./tpl") templates := template.New("main.html").Funcs(funcMap) for _, filename := range tplBox.List() { if strings.HasSuffix(filename, ".html") { tplStr, _ := tplBox.FindString(filename) templates, _ = templates.New(filename).Parse(tplStr) } } // startpageTpl := templates.Lookup("startpage.html") geminiTpl := templates.Lookup("gemini.html") gopherTpl := templates.Lookup("gopher.html") // // vips.Startup(&vips.Config{ ConcurrencyLevel: vipsconcurrency, }) assets := AssetList{ Style: styleAsset, JS: jsAsset, FontRegularW: fontRegularWAsset, FontRegularW2: fontRegularW2Asset, FontBoldW: fontBoldWAsset, FontBoldW2: fontBoldW2Asset, } http.Handle("/", gziphandler.GzipHandler(DefaultHandler(startpageTpl, startpagetext, assets))) http.Handle("/gopher/", gziphandler.GzipHandler(GopherHandler(gopherTpl, robotsdata, assets, robotsdebug))) http.Handle("/gemini/", gziphandler.GzipHandler(GeminiHandler(geminiTpl, 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(fontRegularWAsset, FontHandler(false, fontRegularWData)) http.HandleFunc(fontRegularW2Asset, FontHandler(true, fontRegularW2Data)) http.HandleFunc(fontBoldWAsset, FontHandler(false, fontBoldWData)) http.HandleFunc(fontBoldW2Asset, FontHandler(true, fontBoldW2Data)) //http.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir("assets/")))) return http.ListenAndServe(bind, nil) }