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
FontW string
FontW2 string
PropFontW string
PropFontW2 string
}
type TemplateVariables struct {
Title string
URI string
Assets AssetList
RawText string
Lines []Item
Error bool
Protocol 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, TemplateVariables{
Title: "Gopher/Gemini proxy",
Assets: assetList,
RawText: startpagetext,
Protocol: "startpage",
}); 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
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))
//
// 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)
//
//
var allFiles []string
files, err := ioutil.ReadDir("./tpl")
if err != nil {
fmt.Println(err)
}
for _, file := range files {
filename := file.Name()
if strings.HasSuffix(filename, ".html") {
allFiles = append(allFiles, "./tpl/"+filename)
}
}
templates, err = template.ParseFiles(allFiles...)
//
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)
},
}
//
startpageTpl := templates.Lookup("startpage.html").Funcs(funcMap)
geminiTpl := templates.Lookup("gemini.html").Funcs(funcMap)
gopherTpl := templates.Lookup("gopher.html").Funcs(funcMap)
//
//
vips.Startup(&vips.Config{
ConcurrencyLevel: vipsconcurrency,
})
assets := AssetList{
Style: styleAsset,
JS: jsAsset,
FontW: fontwAsset,
FontW2: fontw2Asset,
PropFontW: propfontwAsset,
PropFontW2: propfontw2Asset,
}
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(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)
}