298 lines
7.1 KiB
Go
298 lines
7.1 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
func getURL(u *url.URL) (string, *url.URL, error) {
|
|
switch u.Scheme {
|
|
case "http", "https":
|
|
resp, err := http.Get(u.String())
|
|
if err != nil {
|
|
return "", u, err
|
|
}
|
|
defer resp.Body.Close()
|
|
loc, err := resp.Location()
|
|
if err == nil {
|
|
u = loc
|
|
}
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "", u, err
|
|
}
|
|
return string(body), u, nil
|
|
case "gemini":
|
|
status, meta, resp, actualU := GeminiRequest(u, 0)
|
|
if meta == "error" {
|
|
return "", actualU, errors.New(resp)
|
|
} else if status == 1 {
|
|
fmt.Print(meta + ": ")
|
|
os.Exit(gemGetLine)
|
|
} else if status == 2 {
|
|
meta = strings.ToLower(meta)
|
|
if strings.Contains(meta, "html") {
|
|
return resp, actualU, nil
|
|
} else if strings.Contains(meta, "gemini") {
|
|
return gem2html(resp), actualU, nil
|
|
} else if strings.Contains(meta, "markdown") || strings.HasSuffix(u.String(), ".md") {
|
|
return md2html(resp), actualU, nil
|
|
} else {
|
|
return "<html><body><pre>" + resp + "</pre></body></html>", actualU, nil
|
|
}
|
|
} else {
|
|
return fmt.Sprintf(`<html><body><h1>%d Error (Gemini)</h1><p>%s</p></body></html>`, status, resp), actualU, nil
|
|
}
|
|
case "gopher":
|
|
isGopher = true
|
|
if u.Port() == "" {
|
|
u.Host = u.Host + ":70"
|
|
}
|
|
conn, err := net.Dial("tcp", u.Host)
|
|
if err != nil {
|
|
return "", u, err
|
|
}
|
|
defer conn.Close()
|
|
gType := "1"
|
|
p := u.Path
|
|
if len(u.Path) < 2 {
|
|
p = "/" + "\n"
|
|
} else {
|
|
gType = p[1:2]
|
|
p = p[2:] + "\n"
|
|
}
|
|
_, err = conn.Write([]byte(p))
|
|
if err != nil {
|
|
return "", u, err
|
|
}
|
|
resp, err := io.ReadAll(conn)
|
|
if err != nil {
|
|
return "", u, err
|
|
}
|
|
switch gType {
|
|
case "0":
|
|
return `<html><body><pre>` + string(resp) + `</pre></body></html>`, u, nil
|
|
case "1":
|
|
return gopher2html(string(resp)), u, nil
|
|
case "7":
|
|
fmt.Print("Query: ")
|
|
os.Exit(gophGetLine)
|
|
default:
|
|
return "", u, errors.New("Unsupported gopher type")
|
|
}
|
|
default:
|
|
return "", u, fmt.Errorf("Unsupported URL scheme: %s\n", u.Scheme)
|
|
}
|
|
return "", u, nil
|
|
}
|
|
|
|
type gopherLine struct {
|
|
url string
|
|
text string
|
|
gType rune
|
|
}
|
|
|
|
func splitGopherLine(l string) gopherLine {
|
|
var out gopherLine
|
|
parts := strings.SplitN(l, "\t", -1)
|
|
|
|
if len(parts[0]) > 0 {
|
|
out.gType = rune(parts[0][0])
|
|
}
|
|
if len(parts[0]) > 1 {
|
|
out.text = parts[0][1:]
|
|
out.text = strings.TrimRight(parts[0][1:], "\n\r ")
|
|
}
|
|
if len(parts) >= 4 {
|
|
out.url = "gopher://" + parts[2] + ":" + parts[3] + "/" + string(out.gType) + parts[1]
|
|
} else {
|
|
out.gType = 'i'
|
|
out.text = ""
|
|
}
|
|
return out
|
|
}
|
|
|
|
func gopher2html(s string) string {
|
|
var b strings.Builder
|
|
b.WriteString(`<html><body>`)
|
|
var inPre bool
|
|
for _, line := range strings.SplitN(s, "\n", -1) {
|
|
line = strings.TrimSpace(line)
|
|
if line == "." {
|
|
break
|
|
}
|
|
gl := splitGopherLine(line)
|
|
if gl.gType == 'i' && !inPre {
|
|
b.WriteString(`<pre>`)
|
|
b.WriteString(makeEntities(gl.text))
|
|
inPre = true
|
|
} else if gl.gType == 'i' {
|
|
b.WriteString("<br>")
|
|
b.WriteString(makeEntities(gl.text))
|
|
} else if inPre {
|
|
inPre = false
|
|
b.WriteString(fmt.Sprintf(`</pre><br><a href="%s">%s</a></br>`, gl.url, makeEntities(gl.text)))
|
|
} else {
|
|
b.WriteString(fmt.Sprintf(`<a href="%s">%s</a><br>`, gl.url, makeEntities(gl.text)))
|
|
}
|
|
}
|
|
b.WriteString(`</body></html>`)
|
|
return b.String()
|
|
}
|
|
|
|
func gem2html(s string) string {
|
|
var b strings.Builder
|
|
b.WriteString("<html><body>")
|
|
inPre := false
|
|
inList := false
|
|
for _, l := range strings.SplitN(s, "\n", -1) {
|
|
li := strings.HasPrefix(l, "* ")
|
|
if li && !inList {
|
|
inList = true
|
|
b.WriteString("<ul>")
|
|
} else if inList {
|
|
b.WriteString("</ul>")
|
|
}
|
|
if strings.HasPrefix(l, "### ") && len(l) > 4 {
|
|
b.WriteString("<h3>")
|
|
b.WriteString(makeEntities(l[3:]))
|
|
b.WriteString("</h3>\n")
|
|
} else if strings.HasPrefix(l, "## ") && len(l) > 3 {
|
|
b.WriteString("<h2>")
|
|
b.WriteString(makeEntities(l[2:]))
|
|
b.WriteString("</h2>\n")
|
|
} else if strings.HasPrefix(l, "# ") && len(l) > 2 {
|
|
b.WriteString("<h1>")
|
|
b.WriteString(makeEntities(l[2:]))
|
|
b.WriteString("</h1>\n")
|
|
} else if strings.HasPrefix(l, "> ") && len(l) > 2 {
|
|
b.WriteString("<blockquote>")
|
|
b.WriteString(makeEntities(l[2:]))
|
|
b.WriteString("</blockquote>\n")
|
|
} else if strings.HasPrefix(l, "=>") && len(l) > 2 {
|
|
linkline := strings.TrimSpace(l[2:])
|
|
sep := strings.IndexAny(linkline, " \t")
|
|
var body, href string
|
|
if sep < 0 {
|
|
body = linkline
|
|
href = linkline
|
|
} else {
|
|
href = strings.TrimSpace(linkline[:sep])
|
|
body = makeEntities(strings.TrimSpace(linkline[sep:]))
|
|
}
|
|
b.WriteString(fmt.Sprintf(`<a href="%s">%s</a><br>`, href, body))
|
|
} else if li && len(l) > 2 {
|
|
b.WriteString("<li>")
|
|
b.WriteString(makeEntities(l[2:]))
|
|
b.WriteString("</li>")
|
|
} else if strings.HasPrefix(l, "```") {
|
|
if inPre {
|
|
inPre = false
|
|
b.WriteString("</pre>\n")
|
|
} else {
|
|
inPre = true
|
|
b.WriteString("<pre>\n")
|
|
}
|
|
} else {
|
|
if inPre {
|
|
b.WriteString(l)
|
|
b.WriteString("\n")
|
|
} else {
|
|
b.WriteString(makeEntities(strings.TrimSpace(l)))
|
|
b.WriteString("<br>")
|
|
}
|
|
}
|
|
}
|
|
b.WriteString("</body></html>")
|
|
return b.String()
|
|
}
|
|
|
|
func makeEntities(line string) string {
|
|
line = strings.ReplaceAll(line, "<", "<")
|
|
line = strings.ReplaceAll(line, ">", ">")
|
|
return line
|
|
}
|
|
|
|
func md2html(s string) string {
|
|
return s
|
|
}
|
|
|
|
func GeminiRequest(u *url.URL, redirectCount int) (int, string, string, *url.URL) {
|
|
if redirectCount >= 10 {
|
|
return 3, "error", "Too many redirects", u
|
|
}
|
|
if u.Port() == "" {
|
|
u.Host = u.Host + ":1965"
|
|
}
|
|
conf := &tls.Config{
|
|
MinVersion: tls.VersionTLS12,
|
|
InsecureSkipVerify: true,
|
|
}
|
|
conn, err := tls.Dial("tcp", u.Host, conf)
|
|
if err != nil {
|
|
return -1, "error", err.Error(), u
|
|
}
|
|
defer conn.Close()
|
|
|
|
_, err = conn.Write([]byte(u.String() + "\r\n"))
|
|
if err != nil {
|
|
return -1, "error", err.Error(), u
|
|
}
|
|
res, err := io.ReadAll(conn)
|
|
if err != nil {
|
|
return -1, "error", err.Error(), u
|
|
}
|
|
resp := strings.SplitN(string(res), "\r\n", 2)
|
|
if len(resp) != 2 {
|
|
if err != nil {
|
|
return -1, "error", "Invalid response from server", u
|
|
}
|
|
}
|
|
header := strings.SplitN(resp[0], " ", 2)
|
|
if len([]rune(header[0])) != 2 {
|
|
header = strings.SplitN(resp[0], "\t", 2)
|
|
if len([]rune(header[0])) != 2 {
|
|
return -1, "error", "Invalid response format from server", u
|
|
}
|
|
}
|
|
|
|
// Get status code single digit form
|
|
status, err := strconv.Atoi(string(header[0][0]))
|
|
if err != nil {
|
|
return -1, "error", "Invalid status response from server", u
|
|
}
|
|
|
|
if status != 2 {
|
|
switch status {
|
|
case 1:
|
|
resp[1] = header[1]
|
|
case 3:
|
|
// This does not support relative redirects
|
|
// TODO add support
|
|
newUrl, err := url.Parse(header[1])
|
|
if err != nil {
|
|
resp[1] = "Redirect attempted to invalid URL"
|
|
break
|
|
}
|
|
return GeminiRequest(newUrl, redirectCount+1)
|
|
case 4:
|
|
resp[1] = fmt.Sprintf("Temporary failure; %s", header[1])
|
|
case 5:
|
|
resp[1] = fmt.Sprintf("Permanent failure; %s", header[1])
|
|
case 6:
|
|
resp[1] = "Client certificate required (unsupported by 'net-get')"
|
|
default:
|
|
resp[1] = "Invalid response status from server"
|
|
}
|
|
}
|
|
return status, header[1], resp[1], u
|
|
}
|