From 38485ede265d22e11a316b4d66de1bf0feb945be Mon Sep 17 00:00:00 2001 From: Volpeon Date: Sat, 24 Jul 2021 09:46:59 +0200 Subject: Update --- .gitignore | 2 +- Dockerfile | 10 +- Makefile | 4 +- README.md | 8 +- cmd/gopherproxy/main.go | 25 +++ cmd/port/main.go | 25 --- go.mod | 2 +- internal/gopherproxy/gemini.go | 292 +++++++++++++++++++++++++++++++ internal/gopherproxy/gopher.go | 284 ++++++++++++++++++++++++++++++ internal/gopherproxy/main.go | 300 ++++++++++++++++++++++++++++++++ internal/gopherproxy/tpl/_fonts.html | 16 ++ internal/gopherproxy/tpl/_modals.html | 24 +++ internal/gopherproxy/tpl/gemini.html | 65 +++++++ internal/gopherproxy/tpl/gopher.html | 50 ++++++ internal/gopherproxy/tpl/startpage.html | 30 ++++ internal/port/gemini.go | 292 ------------------------------- internal/port/gopher.go | 284 ------------------------------ internal/port/main.go | 300 -------------------------------- internal/port/tpl/_fonts.html | 16 -- internal/port/tpl/_modals.html | 24 --- internal/port/tpl/gemini.html | 65 ------- internal/port/tpl/gopher.html | 50 ------ internal/port/tpl/startpage.html | 30 ---- port.bin | Bin 0 -> 13478712 bytes 24 files changed, 1099 insertions(+), 1099 deletions(-) create mode 100644 cmd/gopherproxy/main.go delete mode 100644 cmd/port/main.go create mode 100644 internal/gopherproxy/gemini.go create mode 100644 internal/gopherproxy/gopher.go create mode 100644 internal/gopherproxy/main.go create mode 100644 internal/gopherproxy/tpl/_fonts.html create mode 100644 internal/gopherproxy/tpl/_modals.html create mode 100644 internal/gopherproxy/tpl/gemini.html create mode 100644 internal/gopherproxy/tpl/gopher.html create mode 100644 internal/gopherproxy/tpl/startpage.html delete mode 100644 internal/port/gemini.go delete mode 100644 internal/port/gopher.go delete mode 100644 internal/port/main.go delete mode 100644 internal/port/tpl/_fonts.html delete mode 100644 internal/port/tpl/_modals.html delete mode 100644 internal/port/tpl/gemini.html delete mode 100644 internal/port/tpl/gopher.html delete mode 100644 internal/port/tpl/startpage.html create mode 100755 port.bin diff --git a/.gitignore b/.gitignore index f02946e..fe92af3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,6 @@ dist *.bak coverage.txt -port.bin +gopherproxy.bin .vscode diff --git a/Dockerfile b/Dockerfile index 5db4ab0..6ac0724 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,16 +2,16 @@ FROM golang:alpine EXPOSE 80/tcp -ENTRYPOINT ["port"] +ENTRYPOINT ["gopherproxy"] RUN \ apk add --update git && \ rm -rf /var/cache/apk/* -RUN mkdir -p /go/src/git.vulpes.one/Feuerfuchs/port -WORKDIR /go/src/git.vulpes.one/Feuerfuchs/port +RUN mkdir -p /go/src/git.vulpes.one/gopherproxy +WORKDIR /go/src/git.vulpes.one/gopherproxy -COPY . /go/src/git.vulpes.one/Feuerfuchs/port +COPY . /go/src/git.vulpes.one/gopherproxy RUN go get -v -d -RUN go install -v git.vulpes.one/Feuerfuchs/port/... +RUN go install -v git.vulpes.one/gopherproxy/... diff --git a/Makefile b/Makefile index a537070..0d99fd0 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ all: dev dev: build - ./port.bin -bind 127.0.0.1:8000 + ./gopherproxy.bin -bind 127.0.0.1:8000 build: clean sassc -t compressed css/main.scss assets/style.css @@ -12,7 +12,7 @@ build: clean #pyftsubset fonts/iosevka-fixed-ss03-regular.ttf "*" --name-IDs+=0,4,6 --flavor='woff2' --output-file='assets/iosevka-fixed-ss03-regular.woff2' #pyftsubset fonts/iosevka-fixed-ss03-bold.ttf "*" --name-IDs+=0,4,6 --flavor='woff' --with-zopfli --output-file='assets/iosevka-fixed-ss03-bold.woff' #pyftsubset fonts/iosevka-fixed-ss03-bold.ttf "*" --name-IDs+=0,4,6 --flavor='woff2' --output-file='assets/iosevka-fixed-ss03-bold.woff2' - go build -o ./port.bin ./cmd/port + go build -o ./gopherproxy.bin ./cmd/gopherproxy profile: @go test -cpuprofile cpu.prof -memprofile mem.prof -v -bench . diff --git a/README.md b/README.md index 40ed18a..da48e89 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Gopher (RFC 1436) Web Proxy -port is a Gopher (RFC 1436) and Gemini (gopher://zaibatsu.circumlunar.space/1/~solderpunk/gemini/docs/) Web Proxy that acts as a gateway into Gopherspace/Geminispace by proxying standard Web HTTP requests to Gopher/Gemini requests of the target server. +gopherproxy is a Gopher (RFC 1436) and Gemini (gopher://zaibatsu.circumlunar.space/1/~solderpunk/gemini/docs/) Web Proxy that acts as a gateway into Gopherspace/Geminispace by proxying standard Web HTTP requests to Gopher/Gemini requests of the target server. -port is a fork of [https://github.com/prologic/port](https://github.com/prologic/port). +gopherproxy is a fork of [https://github.com/prologic/gopherproxy](https://github.com/prologic/gopherproxy). Demo: https://proxy.vulpes.one/ @@ -15,14 +15,14 @@ Demo: https://proxy.vulpes.one/ ## Installation ```#!bash -$ go install git.vulpes.one/Feuerfuchs/port/... +$ go install git.vulpes.one/gopherproxy/... ``` ## Usage ```#!bash -$ port +$ gopherproxy ``` Arguments: diff --git a/cmd/gopherproxy/main.go b/cmd/gopherproxy/main.go new file mode 100644 index 0000000..1b69671 --- /dev/null +++ b/cmd/gopherproxy/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "flag" + "log" + + gopherproxy "git.vulpes.one/gopherproxy/internal/gopherproxy" +) + +var ( + // TODO: Allow config file and environment vars + // (opt -> env -> config -> default) + bind = flag.String("bind", "0.0.0.0:8000", "[int]:port to bind to") + startpagefile = flag.String("startpage-file", "startpage.txt", "Default page to display if no URL is specified") + robotsfile = flag.String("robots-file", "robots.txt", "robots.txt file") + robotsdebug = flag.Bool("robots-debug", false, "print output about ignored robots.txt") + vipsconcurrency = flag.Int("vips-concurrency", 1, "Concurrency level of libvips") +) + +func main() { + flag.Parse() + + // Use a gopherproxy struct + log.Fatal(gopherproxy.ListenAndServe(*bind, *startpagefile, *robotsfile, *robotsdebug, *vipsconcurrency)) +} diff --git a/cmd/port/main.go b/cmd/port/main.go deleted file mode 100644 index 6cdccc6..0000000 --- a/cmd/port/main.go +++ /dev/null @@ -1,25 +0,0 @@ -package main - -import ( - "flag" - "log" - - port "git.vulpes.one/Feuerfuchs/port/internal/port" -) - -var ( - // TODO: Allow config file and environment vars - // (opt -> env -> config -> default) - bind = flag.String("bind", "0.0.0.0:8000", "[int]:port to bind to") - startpagefile = flag.String("startpage-file", "startpage.txt", "Default page to display if no URL is specified") - robotsfile = flag.String("robots-file", "robots.txt", "robots.txt file") - robotsdebug = flag.Bool("robots-debug", false, "print output about ignored robots.txt") - vipsconcurrency = flag.Int("vips-concurrency", 1, "Concurrency level of libvips") -) - -func main() { - flag.Parse() - - // Use a config struct - log.Fatal(port.ListenAndServe(*bind, *startpagefile, *robotsfile, *robotsdebug, *vipsconcurrency)) -} diff --git a/go.mod b/go.mod index f6759d3..8d5600c 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module git.vulpes.one/Feuerfuchs/port +module git.vulpes.one/gopherproxy require ( github.com/NYTimes/gziphandler v1.1.1 diff --git a/internal/gopherproxy/gemini.go b/internal/gopherproxy/gemini.go new file mode 100644 index 0000000..89d820c --- /dev/null +++ b/internal/gopherproxy/gemini.go @@ -0,0 +1,292 @@ +package gopherproxy + +import ( + "bytes" + "fmt" + "html/template" + "io" + "log" + "mime" + "net/http" + "net/url" + "strings" + + "golang.org/x/net/html/charset" + "golang.org/x/text/transform" + + "git.vulpes.one/gopherproxy/pkg/libgemini" + + "github.com/temoto/robotstxt" +) + +type GeminiTemplateVariables struct { + Title string + URL string + Assets AssetList + Sections []GeminiSection + Nav []GeminiNavItem + IsPlain bool +} + +type GeminiNavItem struct { + Label string + URL string + Current bool +} + +type GeminiSection struct { + Type string + Text string + URL template.URL + Items []string +} + +func urlToGeminiNav(url string) (items []GeminiNavItem) { + partialURL := "/gemini" + parts := strings.Split(url, "/") + + if len(parts) != 0 && parts[len(parts)-1] == "" { + parts = parts[:len(parts)-1] + } + + for _, part := range parts { + partialURL = partialURL + "/" + part + + items = append(items, GeminiNavItem{ + Label: part, + URL: partialURL, + Current: false, + }) + } + + items[len(items)-1].Current = true + + return +} + +func resolveURL(uri string, baseURL *url.URL) (resolvedURL string) { + if strings.HasPrefix(uri, "//") { + resolvedURL = "/gemini/" + strings.TrimPrefix(uri, "//") + } else if strings.HasPrefix(uri, "gemini://") { + resolvedURL = "/gemini/" + strings.TrimPrefix(uri, "gemini://") + } else if strings.HasPrefix(uri, "gopher://") { + resolvedURL = "/gopher/" + strings.TrimPrefix(uri, "gopher://") + } else { + url, err := url.Parse(uri) + if err != nil { + return "" + } + adjustedURL := baseURL.ResolveReference(url) + path := adjustedURL.Path + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + if adjustedURL.Scheme == "gemini" { + resolvedURL = "/gemini/" + adjustedURL.Host + path + } else if adjustedURL.Scheme == "gopher" { + resolvedURL = "/gopher/" + adjustedURL.Host + path + } else { + resolvedURL = adjustedURL.String() + } + } + + return +} + +func parseGeminiDocument(body *bytes.Buffer, uri string, hostport string) (sections []GeminiSection) { + baseURL, err := url.Parse(fmt.Sprintf( + "gemini://%s/%s", + hostport, + uri, + )) + if err != nil { + return + } + + unpreppedSections := libgemini.ParseGeminiDocument(body) + + for _, section := range unpreppedSections { + if section.Type != libgemini.LINK { + sections = append(sections, GeminiSection{ + Type: section.Type.String(), + Text: section.Text, + URL: template.URL(section.URL), + Items: section.Items, + }) + } else { + sections = append(sections, GeminiSection{ + Type: section.Type.String(), + Text: section.Text, + URL: template.URL(resolveURL(section.URL, baseURL)), + Items: section.Items, + }) + } + } + + return +} + +func GeminiHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + agent := req.UserAgent() + path := strings.TrimPrefix(req.URL.Path, "/gemini/") + + 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, "/", http.StatusFound) + return + } + + title := hostport + + 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 { + if e := tpl.Execute(w, GeminiTemplateVariables{ + Title: title, + URL: hostport, + Assets: assetList, + Sections: []GeminiSection{{ + Type: libgemini.RAW_TEXT.String(), + Text: fmt.Sprintf("Error: %s", err), + }}, + Nav: urlToGeminiNav(hostport), + IsPlain: true, + }); e != nil { + log.Println("Template error: " + e.Error()) + log.Println(err.Error()) + } + return + } + + if uri != "" { + title = fmt.Sprintf("%s/%s", hostport, uri) + } + + res, err := libgemini.Get( + fmt.Sprintf( + "gemini://%s/%s%s", + hostport, + uri, + qs, + ), + ) + + if err != nil { + if e := tpl.Execute(w, GeminiTemplateVariables{ + Title: title, + URL: fmt.Sprintf("%s/%s", hostport, uri), + Assets: assetList, + Sections: []GeminiSection{{ + Type: libgemini.RAW_TEXT.String(), + Text: fmt.Sprintf("Error: %s", err), + }}, + Nav: urlToGeminiNav(fmt.Sprintf("%s/%s", hostport, uri)), + IsPlain: true, + }); e != nil { + log.Println("Template error: " + e.Error()) + log.Println(err.Error()) + } + return + } + + if int(res.Header.Status/10) == 3 { + baseURL, err := url.Parse(fmt.Sprintf( + "gemini://%s/%s", + hostport, + uri, + )) + if err != nil { + if e := tpl.Execute(w, GeminiTemplateVariables{ + Title: title, + URL: fmt.Sprintf("%s/%s", hostport, uri), + Assets: assetList, + Sections: []GeminiSection{{ + Type: libgemini.RAW_TEXT.String(), + Text: fmt.Sprintf("Error: %s", err), + }}, + Nav: urlToGeminiNav(fmt.Sprintf("%s/%s", hostport, uri)), + IsPlain: true, + }); e != nil { + log.Println("Template error: " + e.Error()) + log.Println(err.Error()) + } + return + } + + http.Redirect(w, req, resolveURL(res.Header.Meta, baseURL), http.StatusFound) + return + } + + if int(res.Header.Status/10) != 2 { + if err := tpl.Execute(w, GeminiTemplateVariables{ + Title: title, + URL: fmt.Sprintf("%s/%s", hostport, uri), + Assets: assetList, + Sections: []GeminiSection{{ + Type: libgemini.RAW_TEXT.String(), + Text: fmt.Sprintf("Error %d: %s", res.Header.Status, res.Header.Meta), + }}, + Nav: urlToGeminiNav(fmt.Sprintf("%s/%s", hostport, uri)), + IsPlain: true, + }); err != nil { + log.Println("Template error: " + err.Error()) + } + return + } + + if strings.HasPrefix(res.Header.Meta, "text/") && !strings.HasPrefix(res.Header.Meta, "text/html") && !strings.HasPrefix(res.Header.Meta, "text/css") { + buf := new(bytes.Buffer) + + _, params, err := mime.ParseMediaType(res.Header.Meta) + if err != nil { + buf.ReadFrom(res.Body) + } else { + encoding, _ := charset.Lookup(params["charset"]) + readbuf := new(bytes.Buffer) + readbuf.ReadFrom(res.Body) + + writer := transform.NewWriter(buf, encoding.NewDecoder()) + writer.Write(readbuf.Bytes()) + writer.Close() + } + + var sections []GeminiSection + isPlain := true + + if strings.HasPrefix(res.Header.Meta, libgemini.MIME_GEMINI) { + sections = parseGeminiDocument(buf, uri, hostport) + isPlain = false + } else { + sections = append(sections, GeminiSection{ + Type: libgemini.RAW_TEXT.String(), + Text: buf.String(), + }) + } + + if err := tpl.Execute(w, GeminiTemplateVariables{ + Title: title, + URL: fmt.Sprintf("%s/%s", hostport, uri), + Assets: assetList, + Sections: sections, + Nav: urlToGeminiNav(fmt.Sprintf("%s/%s", hostport, uri)), + IsPlain: isPlain, + }); err != nil { + log.Println("Template error: " + err.Error()) + } + } else { + io.Copy(w, res.Body) + } + } +} diff --git a/internal/gopherproxy/gopher.go b/internal/gopherproxy/gopher.go new file mode 100644 index 0000000..5f4b39f --- /dev/null +++ b/internal/gopherproxy/gopher.go @@ -0,0 +1,284 @@ +package gopherproxy + +import ( + "bytes" + "fmt" + "html/template" + "io" + "log" + "net" + "net/http" + "net/url" + "strings" + + "git.vulpes.one/gopherproxy/pkg/libgopher" + + "github.com/davidbyttow/govips/pkg/vips" + "github.com/temoto/robotstxt" +) + +type gopherTemplateVariables struct { + Title string + URL string + Assets AssetList + Lines []GopherItem + Nav []GopherNavItem + IsPlain bool +} + +type GopherNavItem struct { + Label string + URL string + Current bool +} + +type GopherItem struct { + Link template.URL + Type string + Text string +} + +func trimLeftChars(s string, n int) string { + m := 0 + for i := range s { + if m >= n { + return s[i:] + } + m++ + } + return s[:0] +} + +func urlToGopherNav(url string) (items []GopherNavItem) { + partialURL := "/gopher" + parts := strings.Split(url, "/") + + if len(parts) != 0 && parts[len(parts)-1] == "" { + parts = parts[:len(parts)-1] + } + + for i, part := range parts { + if i == 1 { + partialURL = partialURL + "/1" + part = trimLeftChars(part, 1) + + if part == "" { + continue + } + } else { + partialURL = partialURL + "/" + part + } + + items = append(items, GopherNavItem{ + Label: part, + URL: partialURL, + Current: false, + }) + } + + items[len(items)-1].Current = true + + return +} + +func renderGopherDirectory(w http.ResponseWriter, tpl *template.Template, assetList AssetList, uri string, hostport string, d libgopher.Directory) error { + var title string + + out := make([]GopherItem, len(d.Items)) + + for i, x := range d.Items { + if x.Type == libgopher.INFO && x.Selector == "TITLE" { + title = x.Description + continue + } + + tr := GopherItem{ + Text: x.Description, + Type: x.Type.String(), + } + + if x.Type == libgopher.INFO { + out[i] = tr + continue + } + + if strings.HasPrefix(x.Selector, "URL:") || strings.HasPrefix(x.Selector, "/URL:") { + link := strings.TrimPrefix(strings.TrimPrefix(x.Selector, "/"), "URL:") + if strings.HasPrefix(link, "gemini://") { + link = fmt.Sprintf( + "/gemini/%s", + strings.TrimPrefix(link, "gemini://"), + ) + } else if strings.HasPrefix(link, "gopher://") { + link = fmt.Sprintf( + "/gopher/%s", + strings.TrimPrefix(link, "gopher://"), + ) + } + tr.Link = template.URL(link) + } else { + var linkHostport string + if x.Port != "70" { + linkHostport = net.JoinHostPort(x.Host, x.Port) + } else { + linkHostport = x.Host + } + + path := url.PathEscape(x.Selector) + path = strings.Replace(path, "%2F", "/", -1) + tr.Link = template.URL( + fmt.Sprintf( + "/gopher/%s/%s%s", + linkHostport, + string(byte(x.Type)), + path, + ), + ) + } + + out[i] = tr + } + + if title == "" { + if uri != "" { + title = fmt.Sprintf("%s/%s", hostport, uri) + } else { + title = hostport + } + } + + return tpl.Execute(w, gopherTemplateVariables{ + Title: title, + URL: fmt.Sprintf("%s/%s", hostport, uri), + Assets: assetList, + Lines: out, + Nav: urlToGopherNav(fmt.Sprintf("%s/%s", hostport, uri)), + }) +} + +// 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, assetList AssetList, robotsdebug bool) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + agent := req.UserAgent() + path := strings.TrimPrefix(req.URL.Path, "/gopher/") + + 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, "/", http.StatusFound) + return + } + + title := hostport + + 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 { + if e := tpl.Execute(w, gopherTemplateVariables{ + Title: title, + URL: hostport, + Assets: assetList, + Lines: []GopherItem{{ + Text: fmt.Sprintf("Error: %s", err), + }}, + Nav: urlToGopherNav(hostport), + IsPlain: true, + }); e != nil { + log.Println("Template error: " + e.Error()) + log.Println(err.Error()) + } + return + } + + if uri != "" { + title = fmt.Sprintf("%s/%s", hostport, uri) + } + + res, err := libgopher.Get( + fmt.Sprintf( + "gopher://%s/%s%s", + hostport, + uri, + qs, + ), + ) + + if err != nil { + if e := tpl.Execute(w, gopherTemplateVariables{ + Title: title, + URL: fmt.Sprintf("%s/%s", hostport, uri), + Assets: assetList, + Lines: []GopherItem{{ + Text: fmt.Sprintf("Error: %s", err), + }}, + Nav: urlToGopherNav(fmt.Sprintf("%s/%s", hostport, uri)), + IsPlain: true, + }); e != nil { + log.Println("Template error: " + e.Error()) + } + return + } + + if res.Body != nil { + if len(parts) < 2 { + io.Copy(w, res.Body) + } else if strings.HasPrefix(parts[1], "0") && !strings.HasSuffix(uri, ".xml") && !strings.HasSuffix(uri, ".asc") { + buf := new(bytes.Buffer) + buf.ReadFrom(res.Body) + + if err := tpl.Execute(w, gopherTemplateVariables{ + Title: title, + URL: fmt.Sprintf("%s/%s", hostport, uri), + Assets: assetList, + Lines: []GopherItem{{ + Text: buf.String(), + }}, + Nav: urlToGopherNav(fmt.Sprintf("%s/%s", hostport, uri)), + IsPlain: true, + }); err != nil { + log.Println("Template error: " + err.Error()) + } + } else if strings.HasPrefix(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 := renderGopherDirectory(w, tpl, assetList, uri, hostport, res.Dir); err != nil { + if e := tpl.Execute(w, gopherTemplateVariables{ + Title: title, + URL: fmt.Sprintf("%s/%s", hostport, uri), + Assets: assetList, + Lines: []GopherItem{{ + Text: fmt.Sprintf("Error: %s", err), + }}, + Nav: urlToGopherNav(fmt.Sprintf("%s/%s", hostport, uri)), + IsPlain: false, + }); e != nil { + log.Println("Template error: " + e.Error()) + log.Println(e.Error()) + } + } + } + } +} diff --git a/internal/gopherproxy/main.go b/internal/gopherproxy/main.go new file mode 100644 index 0000000..33230c1 --- /dev/null +++ b/internal/gopherproxy/main.go @@ -0,0 +1,300 @@ +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) +} diff --git a/internal/gopherproxy/tpl/_fonts.html b/internal/gopherproxy/tpl/_fonts.html new file mode 100644 index 0000000..a947222 --- /dev/null +++ b/internal/gopherproxy/tpl/_fonts.html @@ -0,0 +1,16 @@ + diff --git a/internal/gopherproxy/tpl/_modals.html b/internal/gopherproxy/tpl/_modals.html new file mode 100644 index 0000000..3bbdef2 --- /dev/null +++ b/internal/gopherproxy/tpl/_modals.html @@ -0,0 +1,24 @@ + diff --git a/internal/gopherproxy/tpl/gemini.html b/internal/gopherproxy/tpl/gemini.html new file mode 100644 index 0000000..8d20da1 --- /dev/null +++ b/internal/gopherproxy/tpl/gemini.html @@ -0,0 +1,65 @@ + + + + + + {{ .Title }} - Gemini proxy + + {{- template "_fonts.html" . -}} + + +
+
+ gemini://:// + {{- range $i, $item := .Nav -}} + {{- if ne $i 0 -}} + / + {{- end -}} + {{- if .Current -}} + {{ .Label }} + {{- else -}} + {{ .Label }} + {{- end -}} + {{- end -}} +
+
+
+
+
+ +
+
+ {{- range .Sections -}} + {{- if eq .Type "RAW_TEXT" -}} +
```
{{- .Text -}}
+ {{- else if eq .Type "REFLOW_TEXT" -}} +

{{- .Text -}}

+ {{- else if eq .Type "LINK" -}} + {{- $linkCls := "link" -}} + {{- $url := string .URL -}} + {{- if or (hasSuffix $url ".jpg") (hasSuffix $url ".jpeg") (hasSuffix $url ".png") (hasSuffix $url ".gif") -}} + {{- $linkCls = "link--IMG" -}} + {{- end -}} + + {{- else if eq .Type "HEADING_1" -}} +
#

{{- .Text -}}

+ {{- else if eq .Type "HEADING_2" -}} +
##

{{- .Text -}}

+ {{- else if eq .Type "HEADING_3" -}} +
###

{{- .Text -}}

+ {{- else if eq .Type "LIST" -}} +
    + {{- range .Items -}} +
  • {{- . -}}
  • + {{- end -}} +
+ {{- end -}} + {{- end -}} +
+
+ + {{- template "_modals.html" . -}} + + + + diff --git a/internal/gopherproxy/tpl/gopher.html b/internal/gopherproxy/tpl/gopher.html new file mode 100644 index 0000000..6eb607c --- /dev/null +++ b/internal/gopherproxy/tpl/gopher.html @@ -0,0 +1,50 @@ + + + + + + {{ .Title }} - Gopher proxy + + {{- template "_fonts.html" . -}} + + +
+
+ gopher://:// + {{- range $i, $item := .Nav -}} + {{- if ne $i 0 -}} + / + {{- end -}} + {{- if .Current -}} + {{ .Label }} + {{- else -}} + {{ .Label }} + {{- end -}} + {{- end -}} +
+
+ {{- if .IsPlain -}} + + {{- end -}} +
+
+
+ +
+
+ {{- $page := . -}} + {{- range .Lines -}} + {{- if .Link -}} +
{{- .Type -}}{{- .Text -}}
+ {{- else -}} +
{{- .Text -}}
+ {{- end -}} + {{- end -}} +
+
+ + {{- template "_modals.html" . -}} + + + + diff --git a/internal/gopherproxy/tpl/startpage.html b/internal/gopherproxy/tpl/startpage.html new file mode 100644 index 0000000..772ac90 --- /dev/null +++ b/internal/gopherproxy/tpl/startpage.html @@ -0,0 +1,30 @@ + + + + + + {{ .Title }} + + {{- template "_fonts.html" . -}} + + +
+ +
+
+
+
+ +
+
+        {{- .Content -}}
+			
+
+ + {{- template "_modals.html" . -}} + + + + diff --git a/internal/port/gemini.go b/internal/port/gemini.go deleted file mode 100644 index f574816..0000000 --- a/internal/port/gemini.go +++ /dev/null @@ -1,292 +0,0 @@ -package port - -import ( - "bytes" - "fmt" - "html/template" - "io" - "log" - "mime" - "net/http" - "net/url" - "strings" - - "golang.org/x/net/html/charset" - "golang.org/x/text/transform" - - "git.vulpes.one/Feuerfuchs/port/pkg/libgemini" - - "github.com/temoto/robotstxt" -) - -type GeminiTemplateVariables struct { - Title string - URL string - Assets AssetList - Sections []GeminiSection - Nav []GeminiNavItem - IsPlain bool -} - -type GeminiNavItem struct { - Label string - URL string - Current bool -} - -type GeminiSection struct { - Type string - Text string - URL template.URL - Items []string -} - -func urlToGeminiNav(url string) (items []GeminiNavItem) { - partialURL := "/gemini" - parts := strings.Split(url, "/") - - if len(parts) != 0 && parts[len(parts)-1] == "" { - parts = parts[:len(parts)-1] - } - - for _, part := range parts { - partialURL = partialURL + "/" + part - - items = append(items, GeminiNavItem{ - Label: part, - URL: partialURL, - Current: false, - }) - } - - items[len(items)-1].Current = true - - return -} - -func resolveURL(uri string, baseURL *url.URL) (resolvedURL string) { - if strings.HasPrefix(uri, "//") { - resolvedURL = "/gemini/" + strings.TrimPrefix(uri, "//") - } else if strings.HasPrefix(uri, "gemini://") { - resolvedURL = "/gemini/" + strings.TrimPrefix(uri, "gemini://") - } else if strings.HasPrefix(uri, "gopher://") { - resolvedURL = "/gopher/" + strings.TrimPrefix(uri, "gopher://") - } else { - url, err := url.Parse(uri) - if err != nil { - return "" - } - adjustedURL := baseURL.ResolveReference(url) - path := adjustedURL.Path - if !strings.HasPrefix(path, "/") { - path = "/" + path - } - if adjustedURL.Scheme == "gemini" { - resolvedURL = "/gemini/" + adjustedURL.Host + path - } else if adjustedURL.Scheme == "gopher" { - resolvedURL = "/gopher/" + adjustedURL.Host + path - } else { - resolvedURL = adjustedURL.String() - } - } - - return -} - -func parseGeminiDocument(body *bytes.Buffer, uri string, hostport string) (sections []GeminiSection) { - baseURL, err := url.Parse(fmt.Sprintf( - "gemini://%s/%s", - hostport, - uri, - )) - if err != nil { - return - } - - unpreppedSections := libgemini.ParseGeminiDocument(body) - - for _, section := range unpreppedSections { - if section.Type != libgemini.LINK { - sections = append(sections, GeminiSection{ - Type: section.Type.String(), - Text: section.Text, - URL: template.URL(section.URL), - Items: section.Items, - }) - } else { - sections = append(sections, GeminiSection{ - Type: section.Type.String(), - Text: section.Text, - URL: template.URL(resolveURL(section.URL, baseURL)), - Items: section.Items, - }) - } - } - - return -} - -func GeminiHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool) http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - agent := req.UserAgent() - path := strings.TrimPrefix(req.URL.Path, "/gemini/") - - 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, "/", http.StatusFound) - return - } - - title := hostport - - 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 { - if e := tpl.Execute(w, GeminiTemplateVariables{ - Title: title, - URL: hostport, - Assets: assetList, - Sections: []GeminiSection{{ - Type: libgemini.RAW_TEXT.String(), - Text: fmt.Sprintf("Error: %s", err), - }}, - Nav: urlToGeminiNav(hostport), - IsPlain: true, - }); e != nil { - log.Println("Template error: " + e.Error()) - log.Println(err.Error()) - } - return - } - - if uri != "" { - title = fmt.Sprintf("%s/%s", hostport, uri) - } - - res, err := libgemini.Get( - fmt.Sprintf( - "gemini://%s/%s%s", - hostport, - uri, - qs, - ), - ) - - if err != nil { - if e := tpl.Execute(w, GeminiTemplateVariables{ - Title: title, - URL: fmt.Sprintf("%s/%s", hostport, uri), - Assets: assetList, - Sections: []GeminiSection{{ - Type: libgemini.RAW_TEXT.String(), - Text: fmt.Sprintf("Error: %s", err), - }}, - Nav: urlToGeminiNav(fmt.Sprintf("%s/%s", hostport, uri)), - IsPlain: true, - }); e != nil { - log.Println("Template error: " + e.Error()) - log.Println(err.Error()) - } - return - } - - if int(res.Header.Status/10) == 3 { - baseURL, err := url.Parse(fmt.Sprintf( - "gemini://%s/%s", - hostport, - uri, - )) - if err != nil { - if e := tpl.Execute(w, GeminiTemplateVariables{ - Title: title, - URL: fmt.Sprintf("%s/%s", hostport, uri), - Assets: assetList, - Sections: []GeminiSection{{ - Type: libgemini.RAW_TEXT.String(), - Text: fmt.Sprintf("Error: %s", err), - }}, - Nav: urlToGeminiNav(fmt.Sprintf("%s/%s", hostport, uri)), - IsPlain: true, - }); e != nil { - log.Println("Template error: " + e.Error()) - log.Println(err.Error()) - } - return - } - - http.Redirect(w, req, resolveURL(res.Header.Meta, baseURL), http.StatusFound) - return - } - - if int(res.Header.Status/10) != 2 { - if err := tpl.Execute(w, GeminiTemplateVariables{ - Title: title, - URL: fmt.Sprintf("%s/%s", hostport, uri), - Assets: assetList, - Sections: []GeminiSection{{ - Type: libgemini.RAW_TEXT.String(), - Text: fmt.Sprintf("Error %d: %s", res.Header.Status, res.Header.Meta), - }}, - Nav: urlToGeminiNav(fmt.Sprintf("%s/%s", hostport, uri)), - IsPlain: true, - }); err != nil { - log.Println("Template error: " + err.Error()) - } - return - } - - if strings.HasPrefix(res.Header.Meta, "text/") && !strings.HasPrefix(res.Header.Meta, "text/html") && !strings.HasPrefix(res.Header.Meta, "text/css") { - buf := new(bytes.Buffer) - - _, params, err := mime.ParseMediaType(res.Header.Meta) - if err != nil { - buf.ReadFrom(res.Body) - } else { - encoding, _ := charset.Lookup(params["charset"]) - readbuf := new(bytes.Buffer) - readbuf.ReadFrom(res.Body) - - writer := transform.NewWriter(buf, encoding.NewDecoder()) - writer.Write(readbuf.Bytes()) - writer.Close() - } - - var sections []GeminiSection - isPlain := true - - if strings.HasPrefix(res.Header.Meta, libgemini.MIME_GEMINI) { - sections = parseGeminiDocument(buf, uri, hostport) - isPlain = false - } else { - sections = append(sections, GeminiSection{ - Type: libgemini.RAW_TEXT.String(), - Text: buf.String(), - }) - } - - if err := tpl.Execute(w, GeminiTemplateVariables{ - Title: title, - URL: fmt.Sprintf("%s/%s", hostport, uri), - Assets: assetList, - Sections: sections, - Nav: urlToGeminiNav(fmt.Sprintf("%s/%s", hostport, uri)), - IsPlain: isPlain, - }); err != nil { - log.Println("Template error: " + err.Error()) - } - } else { - io.Copy(w, res.Body) - } - } -} diff --git a/internal/port/gopher.go b/internal/port/gopher.go deleted file mode 100644 index fc7b754..0000000 --- a/internal/port/gopher.go +++ /dev/null @@ -1,284 +0,0 @@ -package port - -import ( - "bytes" - "fmt" - "html/template" - "io" - "log" - "net" - "net/http" - "net/url" - "strings" - - "git.vulpes.one/Feuerfuchs/port/pkg/libgopher" - - "github.com/davidbyttow/govips/pkg/vips" - "github.com/temoto/robotstxt" -) - -type gopherTemplateVariables struct { - Title string - URL string - Assets AssetList - Lines []GopherItem - Nav []GopherNavItem - IsPlain bool -} - -type GopherNavItem struct { - Label string - URL string - Current bool -} - -type GopherItem struct { - Link template.URL - Type string - Text string -} - -func trimLeftChars(s string, n int) string { - m := 0 - for i := range s { - if m >= n { - return s[i:] - } - m++ - } - return s[:0] -} - -func urlToGopherNav(url string) (items []GopherNavItem) { - partialURL := "/gopher" - parts := strings.Split(url, "/") - - if len(parts) != 0 && parts[len(parts)-1] == "" { - parts = parts[:len(parts)-1] - } - - for i, part := range parts { - if i == 1 { - partialURL = partialURL + "/1" - part = trimLeftChars(part, 1) - - if part == "" { - continue - } - } else { - partialURL = partialURL + "/" + part - } - - items = append(items, GopherNavItem{ - Label: part, - URL: partialURL, - Current: false, - }) - } - - items[len(items)-1].Current = true - - return -} - -func renderGopherDirectory(w http.ResponseWriter, tpl *template.Template, assetList AssetList, uri string, hostport string, d libgopher.Directory) error { - var title string - - out := make([]GopherItem, len(d.Items)) - - for i, x := range d.Items { - if x.Type == libgopher.INFO && x.Selector == "TITLE" { - title = x.Description - continue - } - - tr := GopherItem{ - Text: x.Description, - Type: x.Type.String(), - } - - if x.Type == libgopher.INFO { - out[i] = tr - continue - } - - if strings.HasPrefix(x.Selector, "URL:") || strings.HasPrefix(x.Selector, "/URL:") { - link := strings.TrimPrefix(strings.TrimPrefix(x.Selector, "/"), "URL:") - if strings.HasPrefix(link, "gemini://") { - link = fmt.Sprintf( - "/gemini/%s", - strings.TrimPrefix(link, "gemini://"), - ) - } else if strings.HasPrefix(link, "gopher://") { - link = fmt.Sprintf( - "/gopher/%s", - strings.TrimPrefix(link, "gopher://"), - ) - } - tr.Link = template.URL(link) - } else { - var linkHostport string - if x.Port != "70" { - linkHostport = net.JoinHostPort(x.Host, x.Port) - } else { - linkHostport = x.Host - } - - path := url.PathEscape(x.Selector) - path = strings.Replace(path, "%2F", "/", -1) - tr.Link = template.URL( - fmt.Sprintf( - "/gopher/%s/%s%s", - linkHostport, - string(byte(x.Type)), - path, - ), - ) - } - - out[i] = tr - } - - if title == "" { - if uri != "" { - title = fmt.Sprintf("%s/%s", hostport, uri) - } else { - title = hostport - } - } - - return tpl.Execute(w, gopherTemplateVariables{ - Title: title, - URL: fmt.Sprintf("%s/%s", hostport, uri), - Assets: assetList, - Lines: out, - Nav: urlToGopherNav(fmt.Sprintf("%s/%s", hostport, uri)), - }) -} - -// 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, assetList AssetList, robotsdebug bool) http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - agent := req.UserAgent() - path := strings.TrimPrefix(req.URL.Path, "/gopher/") - - 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, "/", http.StatusFound) - return - } - - title := hostport - - 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 { - if e := tpl.Execute(w, gopherTemplateVariables{ - Title: title, - URL: hostport, - Assets: assetList, - Lines: []GopherItem{{ - Text: fmt.Sprintf("Error: %s", err), - }}, - Nav: urlToGopherNav(hostport), - IsPlain: true, - }); e != nil { - log.Println("Template error: " + e.Error()) - log.Println(err.Error()) - } - return - } - - if uri != "" { - title = fmt.Sprintf("%s/%s", hostport, uri) - } - - res, err := libgopher.Get( - fmt.Sprintf( - "gopher://%s/%s%s", - hostport, - uri, - qs, - ), - ) - - if err != nil { - if e := tpl.Execute(w, gopherTemplateVariables{ - Title: title, - URL: fmt.Sprintf("%s/%s", hostport, uri), - Assets: assetList, - Lines: []GopherItem{{ - Text: fmt.Sprintf("Error: %s", err), - }}, - Nav: urlToGopherNav(fmt.Sprintf("%s/%s", hostport, uri)), - IsPlain: true, - }); e != nil { - log.Println("Template error: " + e.Error()) - } - return - } - - if res.Body != nil { - if len(parts) < 2 { - io.Copy(w, res.Body) - } else if strings.HasPrefix(parts[1], "0") && !strings.HasSuffix(uri, ".xml") && !strings.HasSuffix(uri, ".asc") { - buf := new(bytes.Buffer) - buf.ReadFrom(res.Body) - - if err := tpl.Execute(w, gopherTemplateVariables{ - Title: title, - URL: fmt.Sprintf("%s/%s", hostport, uri), - Assets: assetList, - Lines: []GopherItem{{ - Text: buf.String(), - }}, - Nav: urlToGopherNav(fmt.Sprintf("%s/%s", hostport, uri)), - IsPlain: true, - }); err != nil { - log.Println("Template error: " + err.Error()) - } - } else if strings.HasPrefix(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 := renderGopherDirectory(w, tpl, assetList, uri, hostport, res.Dir); err != nil { - if e := tpl.Execute(w, gopherTemplateVariables{ - Title: title, - URL: fmt.Sprintf("%s/%s", hostport, uri), - Assets: assetList, - Lines: []GopherItem{{ - Text: fmt.Sprintf("Error: %s", err), - }}, - Nav: urlToGopherNav(fmt.Sprintf("%s/%s", hostport, uri)), - IsPlain: false, - }); e != nil { - log.Println("Template error: " + e.Error()) - log.Println(e.Error()) - } - } - } - } -} diff --git a/internal/port/main.go b/internal/port/main.go deleted file mode 100644 index 5fb3dae..0000000 --- a/internal/port/main.go +++ /dev/null @@ -1,300 +0,0 @@ -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) -} diff --git a/internal/port/tpl/_fonts.html b/internal/port/tpl/_fonts.html deleted file mode 100644 index a947222..0000000 --- a/internal/port/tpl/_fonts.html +++ /dev/null @@ -1,16 +0,0 @@ - diff --git a/internal/port/tpl/_modals.html b/internal/port/tpl/_modals.html deleted file mode 100644 index 3bbdef2..0000000 --- a/internal/port/tpl/_modals.html +++ /dev/null @@ -1,24 +0,0 @@ - diff --git a/internal/port/tpl/gemini.html b/internal/port/tpl/gemini.html deleted file mode 100644 index 8d20da1..0000000 --- a/internal/port/tpl/gemini.html +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - {{ .Title }} - Gemini proxy - - {{- template "_fonts.html" . -}} - - -
-
- gemini://:// - {{- range $i, $item := .Nav -}} - {{- if ne $i 0 -}} - / - {{- end -}} - {{- if .Current -}} - {{ .Label }} - {{- else -}} - {{ .Label }} - {{- end -}} - {{- end -}} -
-
-
-
-
- -
-
- {{- range .Sections -}} - {{- if eq .Type "RAW_TEXT" -}} -
```
{{- .Text -}}
- {{- else if eq .Type "REFLOW_TEXT" -}} -

{{- .Text -}}

- {{- else if eq .Type "LINK" -}} - {{- $linkCls := "link" -}} - {{- $url := string .URL -}} - {{- if or (hasSuffix $url ".jpg") (hasSuffix $url ".jpeg") (hasSuffix $url ".png") (hasSuffix $url ".gif") -}} - {{- $linkCls = "link--IMG" -}} - {{- end -}} - - {{- else if eq .Type "HEADING_1" -}} -
#

{{- .Text -}}

- {{- else if eq .Type "HEADING_2" -}} -
##

{{- .Text -}}

- {{- else if eq .Type "HEADING_3" -}} -
###

{{- .Text -}}

- {{- else if eq .Type "LIST" -}} -
    - {{- range .Items -}} -
  • {{- . -}}
  • - {{- end -}} -
- {{- end -}} - {{- end -}} -
-
- - {{- template "_modals.html" . -}} - - - - diff --git a/internal/port/tpl/gopher.html b/internal/port/tpl/gopher.html deleted file mode 100644 index 6eb607c..0000000 --- a/internal/port/tpl/gopher.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - {{ .Title }} - Gopher proxy - - {{- template "_fonts.html" . -}} - - -
-
- gopher://:// - {{- range $i, $item := .Nav -}} - {{- if ne $i 0 -}} - / - {{- end -}} - {{- if .Current -}} - {{ .Label }} - {{- else -}} - {{ .Label }} - {{- end -}} - {{- end -}} -
-
- {{- if .IsPlain -}} - - {{- end -}} -
-
-
- -
-
- {{- $page := . -}} - {{- range .Lines -}} - {{- if .Link -}} -
{{- .Type -}}{{- .Text -}}
- {{- else -}} -
{{- .Text -}}
- {{- end -}} - {{- end -}} -
-
- - {{- template "_modals.html" . -}} - - - - diff --git a/internal/port/tpl/startpage.html b/internal/port/tpl/startpage.html deleted file mode 100644 index 772ac90..0000000 --- a/internal/port/tpl/startpage.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - {{ .Title }} - - {{- template "_fonts.html" . -}} - - -
- -
-
-
-
- -
-
-        {{- .Content -}}
-			
-
- - {{- template "_modals.html" . -}} - - - - diff --git a/port.bin b/port.bin new file mode 100755 index 0000000..eff9e5d Binary files /dev/null and b/port.bin differ -- cgit v1.2.3-70-g09d2