kineto/main.go

772 lines
16 KiB
Go
Raw Normal View History

2020-10-11 12:56:17 -04:00
package main
import (
2021-02-24 23:32:39 -05:00
"context"
2020-10-11 12:56:17 -04:00
"fmt"
"html/template"
2020-11-05 17:51:26 -05:00
"io"
2020-10-11 12:56:17 -04:00
"log"
"mime"
"net/http"
"net/url"
"os"
2025-01-17 13:35:08 -05:00
"path/filepath"
2020-10-11 12:56:17 -04:00
"strings"
2020-11-05 17:51:26 -05:00
"time"
"unicode"
2020-10-11 12:56:17 -04:00
2020-10-26 12:28:16 -04:00
"git.sr.ht/~adnano/go-gemini"
2020-10-11 12:56:17 -04:00
"git.sr.ht/~sircmpwn/getopt"
)
2025-01-17 13:35:08 -05:00
// Source: https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types
var imgexts = []string{".apng", ".png", ".avif", ".gif", ".jpg", ".jpeg", ".jfif", ".pjpeg", ".pjp", ".png", ".svg", ".webp", ".bmp", ".ico", ".cur", ".tif", ".tiff"}
2020-10-11 12:56:17 -04:00
var gemtextPage = template.Must(template.
New("gemtext").
Funcs(template.FuncMap{
2020-10-26 12:28:16 -04:00
"heading": func(line gemini.Line) *GemtextHeading {
2020-10-11 12:56:17 -04:00
switch l := line.(type) {
2020-10-26 12:28:16 -04:00
case gemini.LineHeading1:
return &GemtextHeading{1, string(l), createAnchor(string(l))}
2020-10-26 12:28:16 -04:00
case gemini.LineHeading2:
return &GemtextHeading{2, string(l), createAnchor(string(l))}
2020-10-26 12:28:16 -04:00
case gemini.LineHeading3:
return &GemtextHeading{3, string(l), createAnchor(string(l))}
2020-10-11 12:56:17 -04:00
default:
return nil
}
},
2020-10-26 12:28:16 -04:00
"link": func(line gemini.Line) *gemini.LineLink {
2020-10-11 12:56:17 -04:00
switch l := line.(type) {
2020-10-26 12:28:16 -04:00
case gemini.LineLink:
2020-10-11 12:56:17 -04:00
return &l
default:
return nil
}
},
2020-10-26 12:28:16 -04:00
"li": func(line gemini.Line) *gemini.LineListItem {
2020-10-11 12:56:17 -04:00
switch l := line.(type) {
2020-10-26 12:28:16 -04:00
case gemini.LineListItem:
2020-10-11 12:56:17 -04:00
return &l
default:
return nil
}
},
2020-10-26 12:28:16 -04:00
"pre_toggle_on": func(ctx *GemtextContext, line gemini.Line) *gemini.LinePreformattingToggle {
2020-10-11 12:56:17 -04:00
switch l := line.(type) {
2020-10-26 12:28:16 -04:00
case gemini.LinePreformattingToggle:
2020-11-05 17:51:26 -05:00
if ctx.Pre%4 == 0 {
2020-10-11 12:56:17 -04:00
ctx.Pre += 1
return &l
}
ctx.Pre += 1
return nil
default:
return nil
}
},
2020-10-26 12:28:16 -04:00
"pre_toggle_off": func(ctx *GemtextContext, line gemini.Line) *gemini.LinePreformattingToggle {
2020-10-11 12:56:17 -04:00
switch l := line.(type) {
2020-10-26 12:28:16 -04:00
case gemini.LinePreformattingToggle:
2020-11-05 17:51:26 -05:00
if ctx.Pre%4 == 3 {
2020-10-11 12:56:17 -04:00
ctx.Pre += 1
return &l
}
ctx.Pre += 1
return nil
default:
return nil
}
},
2020-10-26 12:28:16 -04:00
"pre": func(line gemini.Line) *gemini.LinePreformattedText {
2020-10-11 12:56:17 -04:00
switch l := line.(type) {
2020-10-26 12:28:16 -04:00
case gemini.LinePreformattedText:
2020-10-11 12:56:17 -04:00
return &l
default:
return nil
}
},
2020-10-26 12:28:16 -04:00
"quote": func(line gemini.Line) *gemini.LineQuote {
2020-10-11 12:56:17 -04:00
switch l := line.(type) {
2020-10-26 12:28:16 -04:00
case gemini.LineQuote:
2020-10-11 12:56:17 -04:00
return &l
default:
return nil
}
},
2020-10-26 12:28:16 -04:00
"text": func(line gemini.Line) *gemini.LineText {
2020-10-11 12:56:17 -04:00
switch l := line.(type) {
2020-10-26 12:28:16 -04:00
case gemini.LineText:
2020-10-11 12:56:17 -04:00
return &l
default:
return nil
}
},
2025-01-17 13:35:08 -05:00
"isImage": func(s string) bool {
2025-01-11 05:19:22 -05:00
u, err := url.Parse(s)
if err != nil {
return false
}
2025-01-17 13:35:08 -05:00
ext := strings.ToLower(filepath.Ext(u.Path))
log.Printf("Testing if %s is a known extension", ext)
for _, l := range imgexts {
log.Printf("Testing if %s == %s", ext, l)
if ext == l {
log.Printf("It is a known image!")
return true
}
}
log.Printf("Not an image")
return false
2025-01-11 05:19:22 -05:00
},
2020-10-11 12:56:17 -04:00
"url": func(ctx *GemtextContext, s string) template.URL {
u, err := url.Parse(s)
2020-10-11 12:56:17 -04:00
if err != nil {
return template.URL("error")
}
2020-11-05 17:51:26 -05:00
u = ctx.URL.ResolveReference(u)
if u.Scheme != "" && u.Scheme != "gemini" {
return template.URL(u.String())
}
if u.Host == ctx.Root.Host {
u.Scheme = ""
u.Host = ""
return template.URL(u.String())
}
if ctx.DisableExternalProxy {
return template.URL(u.String())
}
if ctx.UseRewrite {
r, err := url.Parse(ctx.Rewrite)
if err != nil {
return template.URL("error")
2020-10-11 12:56:17 -04:00
}
return template.URL(r.String())
2020-10-11 12:56:17 -04:00
}
if ctx.UsePortal {
p, err := url.Parse(ctx.Portal)
if err != nil {
return template.URL("error")
}
p = ctx.URL.ResolveReference(p)
u.Path = fmt.Sprintf("%s/%s/%s%s", p.Path, u.Scheme, u.Host, u.Path)
u.Scheme = p.Scheme
u.Host = p.Host
return template.URL(u.String())
}
u.Path = fmt.Sprintf("/x/%s%s", u.Host, u.Path)
u.Scheme = ""
u.Host = ""
2020-11-05 17:51:26 -05:00
return template.URL(u.String())
2020-10-11 12:56:17 -04:00
},
"safeCSS": func(s string) template.CSS {
return template.CSS(s)
},
"safeURL": func(s string) template.URL {
return template.URL(s)
},
}).
Parse(`<!doctype html>
<html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1" />
{{- if .CSS }}
{{- if .ExternalCSS }}
<link rel="stylesheet" type="text/css" href="{{.CSS | safeCSS}}">
{{- else }}
2020-10-11 12:56:17 -04:00
<style>
{{.CSS | safeCSS}}
</style>
{{- end }}
{{- end }}
2020-10-11 12:56:17 -04:00
<title>{{.Title}}</title>
<article{{if .Lang}} lang="{{.Lang}}"{{end}}>
2020-10-11 12:56:17 -04:00
{{ $ctx := . -}}
{{- $isList := false -}}
2025-01-11 05:19:22 -05:00
2020-10-11 12:56:17 -04:00
{{- range .Lines -}}
2025-01-11 05:19:22 -05:00
2020-10-11 12:56:17 -04:00
{{- if and $isList (not (. | li)) }}
</ul>
{{- $isList = false -}}
{{- end -}}
{{- with . | heading }}
{{- $isList = false -}}
<h{{.Level}} id="{{.Anchor}}">{{.Text}}</h{{.Level}}>
2020-10-11 12:56:17 -04:00
{{- end -}}
{{- with . | link }}
{{- $isList = false -}}
<p>
2025-01-17 13:35:08 -05:00
<a
href="{{.URL | url $ctx}}"
>
{{- if ( .URL | isImage ) -}}
2025-01-11 05:19:22 -05:00
<img
src="{{.URL | url $ctx}}"
{{if .Name}}alt="{{.Name}}" title="{{.Name}}"{{end}}
>
{{- else -}}
2025-01-17 13:35:08 -05:00
{{if .Name}}{{.Name}}{{else}}{{.URL}}{{end}}
2020-10-11 12:56:17 -04:00
{{- end -}}
2025-01-17 13:35:08 -05:00
</a>
2025-01-11 05:19:22 -05:00
</p>
{{- end -}}
2020-10-11 12:56:17 -04:00
{{- with . | quote }}
{{- $isList = false -}}
<blockquote>
{{slice .String 1}}
</blockquote>
{{- end -}}
{{- with . | pre_toggle_on $ctx }}
<div aria-label="{{slice .String 3}}">
<pre aria-hidden="true" alt="{{slice .String 3}}">
{{- $isList = false -}}
{{- end -}}
{{- with . | pre -}}
{{- $isList = false -}}
{{.}}
{{ end -}}
{{- with . | pre_toggle_off $ctx -}}
{{- $isList = false -}}
</pre>
</div>
{{- end -}}
{{- with . | text }}
{{- $isList = false }}
2025-01-11 05:19:22 -05:00
<p>{{.}}</p>
2020-10-11 12:56:17 -04:00
{{- end -}}
{{- with . | li }}
{{- if not $isList }}
<ul>
{{- end -}}
{{- $isList = true }}
<li>{{slice .String 1}}</li>
{{- end -}}
{{- end }}
{{- if $isList }}
</ul>
{{- end }}
</article>
<details>
<summary>
Proxied content from <a href="{{.URL.String | safeURL}}">{{.URL.String}}</a>
{{if .External}}
(external content)
{{end}}
</summary>
<p>Gemini request details:
<dl>
<dt>Original URL</dt>
<dd><a href="{{.URL.String | safeURL}}">{{.URL.String}}</a></dd>
<dt>Status code</dt>
<dd>{{.Resp.Status}}</dd>
<dt>Meta</dt>
<dd>{{.Resp.Meta}}</dd>
<dt>Proxied by</dt>
<dd><a href="https://sr.ht/~sircmpwn/kineto">kineto</a></dd>
</dl>
<p>Be advised that no attempt was made to verify the remote SSL certificate.
</details>
`))
2020-11-08 10:24:27 -05:00
var inputPage = template.Must(template.
New("input").
Funcs(template.FuncMap{
"safeCSS": func(s string) template.CSS {
return template.CSS(s)
},
}).
Parse(`<!doctype html>
<html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1" />
{{- if .CSS }}
{{- if .ExternalCSS }}
<link rel="stylesheet" type="text/css" href="{{.CSS | safeCSS}}">
{{- else }}
2020-11-08 10:24:27 -05:00
<style>
{{.CSS | safeCSS}}
</style>
{{- end }}
{{- end }}
2020-11-08 10:24:27 -05:00
<title>{{.Prompt}}</title>
<form method="POST">
2020-11-08 10:24:27 -05:00
<label for="input">{{.Prompt}}</label>
{{ if .Secret }}
<input type="password" id="input" name="q" />
{{ else }}
<input type="text" id="input" name="q" />
{{ end }}
</form>
`))
2020-10-11 12:56:17 -04:00
// TODO: let user customize this
const defaultCSS = `html {
font-family: sans-serif;
color: #080808;
}
body {
max-width: 920px;
margin: 0 auto;
padding: 1rem 2rem;
}
blockquote {
background-color: #eee;
border-left: 3px solid #444;
margin: 1rem -1rem 1rem calc(-1rem - 3px);
padding: 1rem;
}
ul {
margin-left: 0;
padding: 0;
}
li {
padding: 0;
}
2020-11-01 13:38:18 -05:00
li:not(:last-child) {
2020-11-01 13:39:28 -05:00
margin-bottom: 0.5rem;
2020-11-01 13:38:18 -05:00
}
2025-01-11 05:19:22 -05:00
img {
max-width: 100%;
}
a {
position: relative;
}
a:before {
content: '⇒';
color: #999;
text-decoration: none;
font-weight: bold;
position: absolute;
left: -1.25rem;
}
2025-01-17 13:35:08 -05:00
a:has(img):before {
content: '';
}
2020-10-11 12:56:17 -04:00
pre {
background-color: #eee;
margin: 0 -1rem;
padding: 1rem;
overflow-x: auto;
}
details:not([open]) summary,
details:not([open]) summary a {
color: gray;
}
details summary a:before {
display: none;
}
2020-10-11 12:56:17 -04:00
dl dt {
font-weight: bold;
}
dl dt:not(:first-child) {
margin-top: 0.5rem;
}
@media(prefers-color-scheme:dark) {
html {
background-color: #111;
color: #eee;
}
blockquote {
background-color: #000;
}
pre {
background-color: #222;
}
2021-02-14 17:42:51 -05:00
a {
color: #0087BD;
}
a:visited {
color: #333399;
}
}
2020-11-08 10:24:27 -05:00
label {
display: block;
font-weight: bold;
margin-bottom: 0.5rem;
}
input {
display: block;
border: 1px solid #888;
padding: .375rem;
line-height: 1.25rem;
transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
width: 100%;
}
input:focus {
outline: 0;
border-color: #80bdff;
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
}
2020-10-11 12:56:17 -04:00
`
const defaultRobots = `
User-agent: *
Disallow: /
`
2020-10-11 12:56:17 -04:00
type GemtextContext struct {
CSS string
ExternalCSS bool
External bool
Lines []gemini.Line
Pre int
Resp *gemini.Response
Title string
Lang string
URL *url.URL
Root *url.URL
UsePortal bool
Portal string
DisableExternalProxy bool
UseRewrite bool
Rewrite string
2020-10-11 12:56:17 -04:00
}
2020-11-08 10:24:27 -05:00
type InputContext struct {
CSS string
ExternalCSS bool
Prompt string
Secret bool
URL *url.URL
2020-11-08 10:24:27 -05:00
}
2020-10-11 12:56:17 -04:00
type GemtextHeading struct {
Level int
Text string
Anchor string
}
func createAnchor(heading string) string {
var anchor strings.Builder
prev := '-'
for _, c := range heading {
if unicode.IsLetter(c) || unicode.IsDigit(c) {
anchor.WriteRune(unicode.ToLower(c))
prev = c
} else if (unicode.IsSpace(c) || c == '-') && prev != '-' {
anchor.WriteRune('-')
prev = '-'
}
}
return strings.ToLower(anchor.String())
2020-10-11 12:56:17 -04:00
}
func proxyGemini(req gemini.Request, external bool, root *url.URL, w http.ResponseWriter,
r *http.Request, css string, externalCSS bool, usePortal bool, portal string, disableExternalProxy bool,
useRewrite bool, rewrite string) {
2020-11-05 17:51:26 -05:00
2021-02-24 23:32:39 -05:00
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
defer cancel()
2020-10-11 12:56:17 -04:00
2021-02-24 23:32:39 -05:00
client := gemini.Client{}
resp, err := client.Do(ctx, &req)
2020-10-11 12:56:17 -04:00
if err != nil {
w.WriteHeader(http.StatusBadGateway)
2020-11-05 17:51:27 -05:00
fmt.Fprintf(w, "Gateway error: %v", err)
2020-10-11 12:56:17 -04:00
return
}
2020-11-05 17:51:26 -05:00
defer resp.Body.Close()
2020-10-11 12:56:17 -04:00
switch resp.Status {
2020-11-08 10:24:27 -05:00
case 10, 11:
w.Header().Add("Content-Type", "text/html")
err = inputPage.Execute(w, &InputContext{
CSS: css,
ExternalCSS: externalCSS,
Prompt: resp.Meta,
Secret: resp.Status == 11,
URL: req.URL,
2020-11-08 10:24:27 -05:00
})
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf("%v", err)))
}
return
2020-10-11 12:56:17 -04:00
case 20:
break // OK
2020-11-05 17:51:27 -05:00
case 30, 31:
2020-11-08 10:24:27 -05:00
to, err := url.Parse(resp.Meta)
if err != nil {
w.WriteHeader(http.StatusBadGateway)
fmt.Fprintf(w, "Gateway error: bad redirect: %v", err)
2020-11-08 10:24:27 -05:00
}
next := req.URL.ResolveReference(to)
if next.Scheme != "gemini" {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "This page is redirecting you to %s", next)
2020-11-08 10:24:27 -05:00
return
}
if external {
next.Path = fmt.Sprintf("/x/%s/%s", next.Host, next.Path)
}
next.Host = r.URL.Host
2020-11-08 10:24:27 -05:00
next.Scheme = r.URL.Scheme
w.Header().Add("Location", next.String())
w.WriteHeader(http.StatusFound)
fmt.Fprintf(w, "Redirecting to %s", next)
2020-10-11 12:56:17 -04:00
return
2020-11-05 17:51:27 -05:00
case 40, 41, 42, 43, 44:
2020-10-11 12:56:17 -04:00
w.WriteHeader(http.StatusServiceUnavailable)
2020-11-05 17:51:27 -05:00
fmt.Fprintf(w, "The remote server returned %d: %s", resp.Status, resp.Meta)
2020-10-11 12:56:17 -04:00
return
2020-11-05 17:51:27 -05:00
case 50, 51:
2020-10-11 12:56:17 -04:00
w.WriteHeader(http.StatusNotFound)
2020-11-05 17:51:27 -05:00
fmt.Fprintf(w, "The remote server returned %d: %s", resp.Status, resp.Meta)
2020-10-11 12:56:17 -04:00
return
2020-11-05 17:51:27 -05:00
case 52, 53, 59:
2020-10-11 12:56:17 -04:00
w.WriteHeader(http.StatusServiceUnavailable)
2020-11-05 17:51:27 -05:00
fmt.Fprintf(w, "The remote server returned %d: %s", resp.Status, resp.Meta)
2020-10-11 12:56:17 -04:00
return
default:
w.WriteHeader(http.StatusNotImplemented)
2020-11-05 17:51:27 -05:00
fmt.Fprintf(w, "Proxy does not understand Gemini response status %d", resp.Status)
2020-10-11 12:56:17 -04:00
return
}
m, params, err := mime.ParseMediaType(resp.Meta)
2020-10-11 12:56:17 -04:00
if err != nil {
w.WriteHeader(http.StatusBadGateway)
fmt.Fprintf(w, "Gateway error: %d %s: %v", resp.Status, resp.Meta, err)
2020-10-11 12:56:17 -04:00
return
}
if m != "text/gemini" {
w.Header().Add("Content-Type", resp.Meta)
2020-11-05 17:51:26 -05:00
io.Copy(w, resp.Body)
2020-10-11 12:56:17 -04:00
return
}
if charset, ok := params["charset"]; ok {
charset = strings.ToLower(charset)
if charset != "utf-8" {
w.WriteHeader(http.StatusNotImplemented)
fmt.Fprintf(w, "Unsupported charset: %s", charset)
return
}
}
lang := params["lang"]
2020-10-11 12:56:17 -04:00
w.Header().Add("Content-Type", "text/html")
2021-02-24 23:32:39 -05:00
gemctx := &GemtextContext{
CSS: css,
ExternalCSS: externalCSS,
External: external,
Resp: resp,
Title: req.URL.Host + " " + req.URL.Path,
Lang: lang,
URL: req.URL,
Root: root,
UsePortal: usePortal,
Portal: portal,
DisableExternalProxy: disableExternalProxy,
UseRewrite: useRewrite,
Rewrite: rewrite,
2020-10-11 12:56:17 -04:00
}
2020-11-05 17:51:26 -05:00
var title bool
gemini.ParseLines(resp.Body, func(line gemini.Line) {
2021-02-24 23:32:39 -05:00
gemctx.Lines = append(gemctx.Lines, line)
2020-11-05 17:51:26 -05:00
if !title {
if h, ok := line.(gemini.LineHeading1); ok {
2021-02-24 23:32:39 -05:00
gemctx.Title = string(h)
2020-11-05 17:51:26 -05:00
title = true
}
2020-10-11 12:56:17 -04:00
}
2020-11-05 17:51:26 -05:00
})
2021-02-24 23:32:39 -05:00
err = gemtextPage.Execute(w, gemctx)
2020-10-11 12:56:17 -04:00
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
2020-11-05 17:51:27 -05:00
fmt.Fprintf(w, "%v", err)
2020-10-11 12:56:17 -04:00
return
}
}
func main() {
var (
bind string = ":8080"
css string = defaultCSS
external bool = false
usePortal bool = false
portal string = ""
useRewrite bool = false
rewrite string = ""
disableExternalProxy bool = false
disableRobots bool = false
2020-10-11 12:56:17 -04:00
)
opts, optind, err := getopt.Getopts(os.Args, "PRb:c:e:p:r:s:")
2020-10-11 12:56:17 -04:00
if err != nil {
log.Fatal(err)
}
2020-10-11 12:56:17 -04:00
for _, opt := range opts {
switch opt.Option {
case 'P':
disableExternalProxy = true
case 'R':
disableRobots = true
2020-10-11 12:56:17 -04:00
case 'b':
bind = opt.Value
case 'e':
external = true
css = opt.Value
case 'p':
usePortal = true
portal = opt.Value
case 'r':
useRewrite = true
rewrite = opt.Value
2021-02-14 17:42:51 -05:00
case 's':
external = false
2025-01-10 07:19:12 -05:00
cssContent, err := os.ReadFile(opt.Value)
if err == nil {
2021-02-14 17:42:51 -05:00
css = string(cssContent)
} else {
log.Fatalf("Error opening custom CSS from '%s': %v", opt.Value, err)
}
}
2020-10-11 12:56:17 -04:00
}
args := os.Args[optind:]
if len(args) != 1 {
log.Fatalf("Usage: %s <gemini root>", os.Args[0])
}
root, err := url.Parse(args[0])
if err != nil {
log.Fatal(err)
}
http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
r.ParseForm()
if q, ok := r.Form["q"]; !ok {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Bad request"))
} else {
w.Header().Add("Location", "?"+q[0])
w.WriteHeader(http.StatusFound)
w.Write([]byte("Redirecting"))
}
return
}
2020-10-11 12:56:17 -04:00
log.Printf("%s %s", r.Method, r.URL.Path)
if r.Method != "GET" {
w.WriteHeader(http.StatusMethodNotAllowed)
w.Write([]byte("404 Not found"))
return
}
if r.URL.Path == "/favicon.ico" {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("404 Not found"))
return
}
if !disableRobots {
if r.URL.Path == "/robots.txt" {
w.WriteHeader(http.StatusOK)
w.Write([]byte(defaultRobots))
return
}
}
2020-10-26 12:28:16 -04:00
req := gemini.Request{}
2020-10-11 12:56:17 -04:00
req.URL = &url.URL{}
req.URL.Scheme = root.Scheme
req.URL.Host = root.Host
req.URL.Path = r.URL.Path
req.URL.RawQuery = r.URL.RawQuery
proxyGemini(req, false, root, w, r, css, external, usePortal, portal, disableExternalProxy, useRewrite, rewrite)
2020-10-11 12:56:17 -04:00
}))
http.Handle("/x/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := strings.SplitN(r.URL.Path, "/", 4)
if len(path) != 4 {
path = append(path, "")
}
if path[2] != root.Host && (usePortal || disableExternalProxy) {
w.WriteHeader(http.StatusMethodNotAllowed)
w.Write([]byte("404 Not found"))
return
}
if r.Method == "POST" {
r.ParseForm()
if q, ok := r.Form["q"]; !ok {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Bad request"))
} else {
w.Header().Add("Location", "?"+q[0])
w.WriteHeader(http.StatusFound)
w.Write([]byte("Redirecting"))
}
return
}
2020-10-11 12:56:17 -04:00
if r.Method != "GET" {
w.WriteHeader(http.StatusMethodNotAllowed)
w.Write([]byte("404 Not found"))
return
}
2020-10-26 12:28:16 -04:00
req := gemini.Request{}
req.URL = &url.URL{}
req.URL.Scheme = "gemini"
req.URL.Host = path[2]
req.URL.Path = "/" + path[3]
req.URL.RawQuery = r.URL.RawQuery
log.Printf("%s (external) %s%s", r.Method, r.URL.Host, r.URL.Path)
proxyGemini(req, true, root, w, r, css, external, usePortal, portal, disableExternalProxy, useRewrite, rewrite)
2020-10-11 12:56:17 -04:00
}))
log.Printf("HTTP server listening on %s", bind)
log.Fatal(http.ListenAndServe(bind, nil))
}