package port import ( "bufio" "bytes" "fmt" "html/template" "io" "log" "mime" "net/http" "net/url" "regexp" "strings" "golang.org/x/net/html/charset" "golang.org/x/text/transform" "git.vulpes.one/Feuerfuchs/port/pkg/libgemini" "github.com/temoto/robotstxt" ) type SectionType byte const ( RAW_TEXT = SectionType(0) REFLOW_TEXT = SectionType(1) LINK = SectionType(2) ) type Section struct { Type SectionType Text string URL template.URL } type templateVariables struct { Title string URI string Assets AssetList Sections []Section } var ( TermEscapeSGRPattern = regexp.MustCompile("\\[\\d+(;\\d+)*m") ) func resolveURI(uri string, baseURL *url.URL) (resolvedURI string) { if strings.HasPrefix(uri, "//") { resolvedURI = "/gemini/" + strings.TrimPrefix(uri, "//") } else if strings.HasPrefix(uri, "gemini://") { resolvedURI = "/gemini/" + strings.TrimPrefix(uri, "gemini://") } else if strings.HasPrefix(uri, "gopher://") { resolvedURI = "/gopher/" + strings.TrimPrefix(uri, "gopher://") } else { url, err := url.Parse(uri) if err != nil { return "" } adjustedURI := baseURL.ResolveReference(url) path := adjustedURI.Path if !strings.HasPrefix(path, "/") { path = "/" + path } if adjustedURI.Scheme == "gemini" { resolvedURI = "/gemini/" + adjustedURI.Host + path } else if adjustedURI.Scheme == "gopher" { resolvedURI = "/gopher/" + adjustedURI.Host + path } else { resolvedURI = adjustedURI.String() } } return } func parseGeminiDocument(body *bytes.Buffer, uri string, hostport string) (sections []Section) { baseURL, err := url.Parse(fmt.Sprintf( "gemini://%s/%s", hostport, uri, )) if err != nil { return []Section{} } skipSection := true section := Section{ Type: RAW_TEXT, } scanner := bufio.NewScanner(body) for scanner.Scan() { line := strings.Trim(scanner.Text(), "\r\n") line = TermEscapeSGRPattern.ReplaceAllString(line, "") linkMatch := libgemini.LinkPattern.FindStringSubmatch(line) if len(linkMatch) != 0 && linkMatch[0] != "" { curType := section.Type if !skipSection { sections = append(sections, section) } label := linkMatch[2] if label == "" { label = linkMatch[1] } sections = append(sections, Section{ Type: LINK, Text: label, URL: template.URL(resolveURI(linkMatch[1], baseURL)), }) skipSection = false section = Section{ Type: curType, } } else { reflowModeMatch := libgemini.ReflowModePattern.FindStringSubmatch(line) if len(reflowModeMatch) != 0 { newType := RAW_TEXT if section.Type == RAW_TEXT { newType = REFLOW_TEXT } if !skipSection { sections = append(sections, section) } skipSection = false section = Section{ Type: newType, } } else { section.Text = section.Text + "\n" + line } } } if !skipSection { sections = append(sections, section) } 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, templateVariables{ Title: title, URI: hostport, Assets: assetList, Sections: []Section{{ Type: RAW_TEXT, Text: fmt.Sprintf("Error: %s", err), }}, }); 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, templateVariables{ Title: title, URI: fmt.Sprintf("%s/%s", hostport, uri), Assets: assetList, Sections: []Section{{ Type: RAW_TEXT, Text: fmt.Sprintf("Error: %s", err), }}, }); 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, templateVariables{ Title: title, URI: fmt.Sprintf("%s/%s", hostport, uri), Assets: assetList, Sections: []Section{{ Type: RAW_TEXT, Text: fmt.Sprintf("Error: %s", err), }}, }); e != nil { log.Println("Template error: " + e.Error()) log.Println(err.Error()) } return } http.Redirect(w, req, resolveURI(res.Header.Meta, baseURL), http.StatusFound) return } if int(res.Header.Status/10) != 2 { if err := tpl.Execute(w, templateVariables{ Title: title, URI: fmt.Sprintf("%s/%s", hostport, uri), Assets: assetList, Sections: []Section{{ Type: RAW_TEXT, Text: fmt.Sprintf("Error %d: %s", res.Header.Status, res.Header.Meta), }}, }); err != nil { log.Println("Template error: " + err.Error()) } return } if strings.HasPrefix(res.Header.Meta, "text/") { 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 []Section if strings.HasPrefix(res.Header.Meta, libgemini.MIME_GEMINI) { sections = parseGeminiDocument(buf, uri, hostport) } else { sections = append(sections, Section{ Type: RAW_TEXT, Text: buf.String(), }) } if err := tpl.Execute(w, templateVariables{ Title: title, URI: fmt.Sprintf("%s/%s", hostport, uri), Assets: assetList, Sections: sections, }); err != nil { log.Println("Template error: " + err.Error()) } } else { io.Copy(w, res.Body) } } }