goldberry/gemini.go

234 lines
5.6 KiB
Go

package main
import (
"crypto/tls"
"fmt"
"io/ioutil"
"net"
"net/url"
"strconv"
"strings"
"time"
)
var TlsTimeout time.Duration = time.Duration(5) * time.Second
var redirectCount int = 0
func RetrieveGemini(u *url.URL) (string, error) {
conf := &tls.Config{
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: true,
}
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: time.Duration(settings.TimeOutSeconds) * time.Second}, "tcp", u.Host, conf)
if err != nil {
return "", fmt.Errorf("TLS Dial Error: %s. URL: %+v", err.Error(), u)
}
defer conn.Close()
_, err = conn.Write([]byte(u.String() + "\r\n"))
if err != nil {
return "", err
}
result, err := ioutil.ReadAll(conn)
if err != nil {
return "", err
}
return string(result), nil
}
func splitHeaderBody(resp string) (string, string) {
r := strings.SplitN(resp, "\r\n", 2)
var header, body string
if len(r) == 2 {
header = r[0]
body = r[1]
} else if len(r) == 1 {
header = r[0]
}
return strings.TrimSpace(header), strings.TrimSpace(body)
}
func splitStatusMeta(header string) (int, string) {
h := strings.SplitN(header, " ", 2)
if len(h) != 2 {
h = strings.SplitN(header, "\t", 2)
}
if len(h) != 2 || len(h[0]) < 1 {
return -1, "Invalid response from server; mangled header"
}
code, err := strconv.Atoi(h[0])
if err != nil {
code = 99
}
return code, h[1]
}
func HandleRelativeURL(relLink string, current *url.URL) string {
rel, err := url.Parse(relLink)
if err != nil {
return relLink
}
return current.ResolveReference(rel).String()
}
func ParseGeminiBody(body string, u *url.URL) string {
// Check mime type. It wont always be text/gemini
// Base64 encode images and return an img tag with data src
var out strings.Builder
splitContent := strings.Split(body, "\n")
inPreBlock := false
inList := false
for i, ln := range splitContent {
splitContent[i] = strings.Trim(ln, "\r\n")
isPreBlockDeclaration := strings.HasPrefix(ln, "```")
isLinkLine := strings.HasPrefix(ln, "=>") && len([]rune(ln)) > 3
isListItem := strings.HasPrefix(ln, "* ")
if inList && !isListItem {
out.WriteString("</ul>\n")
inList = false
}
if inPreBlock {
if isPreBlockDeclaration {
inPreBlock = false
out.WriteString("</pre>\n")
} else {
out.WriteString(ln)
out.WriteRune('\n')
}
} else {
ln = strings.TrimSpace(ln)
if isPreBlockDeclaration {
inPreBlock = !inPreBlock
alt := ""
if len(ln) > 3 {
alt = strings.TrimSpace(ln[3:])
}
if alt == "" {
out.WriteString("<pre>")
} else {
out.WriteString("<pre aria-label=\"")
out.WriteString(alt)
out.WriteString("\">")
}
} else if isLinkLine {
var link, decorator string
subLn := strings.Trim(ln[2:], "\r\n\t \a")
splitPoint := strings.IndexAny(subLn, " \t")
if splitPoint < 0 || len([]rune(subLn))-1 <= splitPoint {
link = subLn
decorator = subLn
} else {
link = strings.Trim(subLn[:splitPoint], "\t\n\r \a")
decorator = strings.Trim(subLn[splitPoint:], "\t\n\r \a")
}
if strings.Index(link, "://") < 0 {
link = HandleRelativeURL(link, u)
}
out.WriteString(fmt.Sprintf("<a href=\"%s\">%s</a><br>\n", link, decorator))
} else if isListItem {
if !inList {
inList = true
out.WriteString("<ul>\n")
}
out.WriteString("\t<li>\n\t\t")
if len([]rune(ln)) > 2 {
out.WriteString(ln[2:])
}
out.WriteString("\n\t</li>\n")
} else if strings.HasPrefix(ln, "####") {
out.WriteString(ln)
} else if strings.HasPrefix(ln, "###") {
out.WriteString("<h3>\n\t")
if len([]rune(ln)) > 3 {
out.WriteString(ln[3:])
}
out.WriteString("\n</h3>\n")
} else if strings.HasPrefix(ln, "##") {
out.WriteString("<h2>\n\t")
if len([]rune(ln)) > 2 {
out.WriteString(ln[2:])
}
out.WriteString("\n</h2>\n")
} else if strings.HasPrefix(ln, "#") {
out.WriteString("<h1>\n\t")
if len([]rune(ln)) > 1 {
out.WriteString(ln[1:])
}
out.WriteString("\n</h1>\n")
} else if strings.HasPrefix(ln, ">") {
out.WriteString("<blockquote>\n\t")
if len([]rune(ln)) > 1 {
out.WriteString(ln[1:])
}
out.WriteString("\n</blockquote>\n")
} else {
out.WriteString("<p>\n\t")
out.WriteString(strings.TrimSpace(ln))
out.WriteString("\n</p>\n")
}
}
}
return out.String()
}
func GetGemini(u *url.URL) (string, error) {
resp, err := RetrieveGemini(u)
if err != nil {
return ConvertErrorToHTML(92, err.Error()), nil
}
header, body := splitHeaderBody(resp)
status, meta := splitStatusMeta(header)
meta = strings.ToLower(meta)
switch status {
case 10, 1:
return RenderSearchForm(meta, u)
case 20, 2:
redirectCount = 0
if strings.HasPrefix(meta, "text/gemini") {
return ParseGeminiBody(body, u), nil
} else if strings.HasPrefix(meta, "text/markdown") || strings.HasPrefix(meta, "text/x-markdown") {
return ParseMarkdown(body), nil
} else if strings.HasPrefix(meta, "text") {
return strings.ReplaceAll(body, "\n", "<br>\n"), nil
} else if strings.HasPrefix(meta, "image") {
return MakeImage(body, meta), nil
} else {
appCtrl.SaveFile(body)
return "", fmt.Errorf("File Saved")
}
case 30, 31, 3:
redirectCount++
if redirectCount < 5 {
u, err = url.Parse(meta)
if err != nil {
return ConvertErrorToHTML(98, "Invalid redirect"), nil
}
return GetGemini(u)
} else {
return ConvertErrorToHTML(99, "Too many redirects"), nil
}
case 40, 41, 42, 43, 44, 4, 5, 50, 51, 52, 53, 59:
redirectCount = 0
return ConvertErrorToHTML(status, meta), nil
default:
redirectCount = 0
return ConvertErrorToHTML(status, "Unknown or unsupported response status"), nil
}
return WTFError, nil
}