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