aboutsummaryrefslogblamecommitdiffstats
path: root/internal/port/main.go
blob: 5fb3dae967dbec6f9bb7dfbfc0708e68eac9ee0f (plain) (tree)


















                                                




                            
 



                                    


                                                                                                         


                                                                









































































                                                                                                                 
                                                  



















                                                                       
                                                                            
                       
                                           
         
                                                                                                          
 
                                                                              
                       
                                            
         
                                                                                                             
 
                                                                      
                       
                                        
         
                                                                                                 
 
                                                                        
                       
                                         
         
                                                                                                    








































                                                                      


































                                                                                   

                                                                 

                                                

                                                      


          
                                                 
 

                                                             
                                                         
                                                                            

                 



                                                          







                                                  




                                                  







                                                                                                                   


                                                                                 


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