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