package port
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)
}