234 lines
5.6 KiB
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
|
|
}
|
|
|