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
}
current := false
if i == len(parts)-1 || (len(parts) == 2 && i == 0) {
current = true
}
items = append(items, GopherNavItem{
Label: part,
URL: partialURL,
Current: current,
})
}
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())
}
}
}
}
}