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