From d86f26f9947c814566e6e815bdb6951a0cdf67a0 Mon Sep 17 00:00:00 2001 From: Fuwn Date: Thu, 2 Sep 2021 02:08:25 -0700 Subject: refactor(main): main.go -> capybara.go --- capybara.go | 666 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 666 ------------------------------------------------------------ 2 files changed, 666 insertions(+), 666 deletions(-) create mode 100644 capybara.go delete mode 100644 main.go diff --git a/capybara.go b/capybara.go new file mode 100644 index 0000000..2e6c5be --- /dev/null +++ b/capybara.go @@ -0,0 +1,666 @@ +package main + +import ( + "context" + "fmt" + "html/template" + "io" + "io/ioutil" + "log" + "mime" + "net/http" + "net/url" + "os" + "strings" + "time" + + "git.sr.ht/~adnano/go-gemini" + "git.sr.ht/~sircmpwn/getopt" +) + +var gemtextPage = template.Must(template. + New("gemtext"). + Funcs(template.FuncMap{ + "heading": func(line gemini.Line) *GemtextHeading { + switch l := line.(type) { + case gemini.LineHeading1: + return &GemtextHeading{1, string(l)} + case gemini.LineHeading2: + return &GemtextHeading{2, string(l)} + case gemini.LineHeading3: + return &GemtextHeading{3, string(l)} + default: + return nil + } + }, + "link": func(line gemini.Line) *gemini.LineLink { + switch l := line.(type) { + case gemini.LineLink: + return &l + default: + return nil + } + }, + "li": func(line gemini.Line) *gemini.LineListItem { + switch l := line.(type) { + case gemini.LineListItem: + return &l + default: + return nil + } + }, + "pre_toggle_on": func(ctx *GemtextContext, line gemini.Line) *gemini.LinePreformattingToggle { + switch l := line.(type) { + case gemini.LinePreformattingToggle: + if ctx.Pre%4 == 0 { + ctx.Pre += 1 + return &l + } + ctx.Pre += 1 + return nil + default: + return nil + } + }, + "pre_toggle_off": func(ctx *GemtextContext, line gemini.Line) *gemini.LinePreformattingToggle { + switch l := line.(type) { + case gemini.LinePreformattingToggle: + if ctx.Pre%4 == 3 { + ctx.Pre += 1 + return &l + } + ctx.Pre += 1 + return nil + default: + return nil + } + }, + "pre": func(line gemini.Line) *gemini.LinePreformattedText { + switch l := line.(type) { + case gemini.LinePreformattedText: + return &l + default: + return nil + } + }, + "quote": func(line gemini.Line) *gemini.LineQuote { + switch l := line.(type) { + case gemini.LineQuote: + return &l + default: + return nil + } + }, + "text": func(line gemini.Line) *gemini.LineText { + switch l := line.(type) { + case gemini.LineText: + return &l + default: + return nil + } + }, + "url": func(ctx *GemtextContext, s string) template.URL { + u, err := url.Parse(s) + if err != nil { + return "error" + } + u = ctx.URL.ResolveReference(u) + + if u.Scheme == "" || u.Scheme == "gemini" { + if u.Host != ctx.Root.Host { + u.Path = fmt.Sprintf("/proxy/%s%s", u.Host, u.Path) + } + u.Scheme = "" + u.Host = "" + } + return template.URL(u.String()) + }, + "safeCSS": func(s string) template.CSS { + return template.CSS(s) + }, + "safeURL": func(s string) template.URL { + return template.URL(s) + }, + }). + Parse(` + + + +{{- if .CSS }} +{{- if .ExternalCSS }} + +{{- else }} + +{{- end }} +{{- end }} +{{.Title}} + + {{ $ctx := . -}} + {{- $isList := false -}} + {{- range .Lines -}} + {{- if and $isList (not (. | li)) }} + + {{- $isList = false -}} + {{- end -}} + + {{- with . | heading }} + {{- $isList = false -}} + {{.Text}} + {{- end -}} + + {{- with . | link }} + {{- $isList = false -}} +

+ {{if .Name}}{{.Name}}{{else}}{{.URL}}{{end}} + {{- end -}} + + {{- with . | quote }} + {{- $isList = false -}} +

+ {{slice .String 1}} +
+ {{- end -}} + + {{- with . | pre_toggle_on $ctx }} +
+ +
+ {{- end -}} + + {{- with . | text }} + {{- $isList = false }} +

{{.}} + {{- end -}} + + {{- with . | li }} + {{- if not $isList }} +

+ {{- end }} + +
+ + Proxied content from {{.URL.String}} + {{if .External}} + (external content) + {{end}} + +

Gemini request details: +

+
Original URL
+
{{.URL.String}}
+
Status code
+
{{.Resp.Status}}
+
Meta
+
{{.Resp.Meta}}
+
Proxied by
+
Capybara
+
+

Be advised that no attempt was made to verify the remote SSL certificate. +

+`)) + +var inputPage = template.Must(template. + New("input"). + Funcs(template.FuncMap{ + "safeCSS": func(s string) template.CSS { + return template.CSS(s) + }, + }). + Parse(` + + + +{{- if .CSS }} +{{- if .ExternalCSS }} + +{{- else }} + +{{- end }} +{{- end }} +{{.Prompt}} +
+ + {{ if .Secret }} + + {{ else }} + + {{ end }} +
+`)) + +// 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; +} + +li:not(:last-child) { + margin-bottom: 0.5rem; +} + +a { + position: relative; +} + +a:before { + content: '⇒'; + color: #999; + text-decoration: none; + font-weight: bold; + position: absolute; + left: -1.25rem; +} + +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; +} + +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; + } + + a { + color: #0087BD; + } + + a:visited { + color: #333399; + } +} + +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); +} +` + +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 +} + +type InputContext struct { + CSS string + ExternalCSS bool + Prompt string + Secret bool + URL *url.URL +} + +type GemtextHeading struct { + Level int + Text string +} + +func proxyGemini(req gemini.Request, external bool, root *url.URL, + w http.ResponseWriter, r *http.Request, css string, externalCSS bool) { + + ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second) + defer cancel() + + client := gemini.Client{} + resp, err := client.Do(ctx, &req) + if err != nil { + w.WriteHeader(http.StatusBadGateway) + fmt.Fprintf(w, "Gateway error: %v", err) + return + } + defer resp.Body.Close() + + switch resp.Status { + 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, + }) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(fmt.Sprintf("%v", err))) + } + return + case 20: + break // OK + case 30, 31: + to, err := url.Parse(resp.Meta) + if err != nil { + w.WriteHeader(http.StatusBadGateway) + fmt.Fprintf(w, "Gateway error: bad redirect: %v", err) + } + next := req.URL.ResolveReference(to) + if next.Scheme != "gemini" { + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "This page is redirecting you to %s", next) + return + } + if external { + next.Path = fmt.Sprintf("/proxy/%s/%s", next.Host, next.Path) + } + next.Host = r.URL.Host + next.Scheme = r.URL.Scheme + w.Header().Add("Location", next.String()) + w.WriteHeader(http.StatusFound) + fmt.Fprintf(w, "Redirecting to %s", next) + return + case 40, 41, 42, 43, 44: + w.WriteHeader(http.StatusServiceUnavailable) + fmt.Fprintf(w, "The remote server returned %d: %s", resp.Status, resp.Meta) + return + case 50, 51: + w.WriteHeader(http.StatusNotFound) + fmt.Fprintf(w, "The remote server returned %d: %s", resp.Status, resp.Meta) + return + case 52, 53, 59: + w.WriteHeader(http.StatusServiceUnavailable) + fmt.Fprintf(w, "The remote server returned %d: %s", resp.Status, resp.Meta) + return + default: + w.WriteHeader(http.StatusNotImplemented) + fmt.Fprintf(w, "Proxy does not understand Gemini response status %d", resp.Status) + return + } + + m, params, err := mime.ParseMediaType(resp.Meta) + if err != nil { + w.WriteHeader(http.StatusBadGateway) + fmt.Fprintf(w, "Gateway error: %d %s: %v", resp.Status, resp.Meta, err) + return + } + + if m != "text/gemini" { + w.Header().Add("Content-Type", resp.Meta) + io.Copy(w, resp.Body) + 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"] + + w.Header().Add("Content-Type", "text/html") + gemctx := &GemtextContext{ + CSS: css, + ExternalCSS: externalCSS, + External: external, + Resp: resp, + Title: req.URL.Host + " " + req.URL.Path, + Lang: lang, + URL: req.URL, + Root: root, + } + + var title bool + gemini.ParseLines(resp.Body, func(line gemini.Line) { + gemctx.Lines = append(gemctx.Lines, line) + if !title { + if h, ok := line.(gemini.LineHeading1); ok { + gemctx.Title = string(h) + title = true + } + } + }) + + err = gemtextPage.Execute(w, gemctx) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "%v", err) + return + } +} + +func performIfEnv(key string, do func()) { + if len(os.Getenv(key)) != 0 { + do() + } +} + +func main() { + var ( + bind string = ":8080" + css string = defaultCSS + external bool = false + ) + + opts, optind, err := getopt.Getopts(os.Args, "b:c:s:e:") + if err != nil { + log.Fatal(err) + } + + performIfEnv("BIND", func() { + bind = os.Getenv("BIND") + }) + performIfEnv("CSS", func() { + external = false + cssContent, err := ioutil.ReadFile(os.Getenv("CSS")) + if err == nil { + css = string(cssContent) + } else { + log.Fatalf("Error opening custom CSS from '%s': %v", os.Getenv("CSS"), err) + } + }) + performIfEnv("CSS_EXTERNAL", func() { + external = true + css = os.Getenv("CSS_EXTERNAL") + }) + + for _, opt := range opts { + switch opt.Option { + case 'b': + bind = opt.Value + case 's': + external = false + cssContent, err := ioutil.ReadFile(opt.Value) + if err == nil { + css = string(cssContent) + } else { + log.Fatalf("Error opening custom CSS from '%s': %v", opt.Value, err) + } + case 'e': + external = true + css = opt.Value + } + } + + args := os.Args[optind:] + var ( + envRoot string + root *url.URL + ) + if len(args) != 1 { + envRoot = os.Getenv("ROOT") + if len(envRoot) == 0 { + log.Fatalf("Usage: %s ", os.Args[0]) + } + } else { + root, err = url.Parse(args[0]) + } + if len(envRoot) != 0 { + root, err = url.Parse(envRoot) + } + 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 + } + + 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 + } + + req := gemini.Request{} + 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) + })) + + http.Handle("/proxy/", 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 + } + + if r.Method != "GET" { + w.WriteHeader(http.StatusMethodNotAllowed) + w.Write([]byte("404 Not found")) + return + } + + path := strings.SplitN(r.URL.Path, "/", 4) + if len(path) != 4 { + path = append(path, "") + } + 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) + })) + + log.Printf("HTTP server listening on %s", bind) + log.Fatal(http.ListenAndServe(bind, nil)) +} diff --git a/main.go b/main.go deleted file mode 100644 index 2e6c5be..0000000 --- a/main.go +++ /dev/null @@ -1,666 +0,0 @@ -package main - -import ( - "context" - "fmt" - "html/template" - "io" - "io/ioutil" - "log" - "mime" - "net/http" - "net/url" - "os" - "strings" - "time" - - "git.sr.ht/~adnano/go-gemini" - "git.sr.ht/~sircmpwn/getopt" -) - -var gemtextPage = template.Must(template. - New("gemtext"). - Funcs(template.FuncMap{ - "heading": func(line gemini.Line) *GemtextHeading { - switch l := line.(type) { - case gemini.LineHeading1: - return &GemtextHeading{1, string(l)} - case gemini.LineHeading2: - return &GemtextHeading{2, string(l)} - case gemini.LineHeading3: - return &GemtextHeading{3, string(l)} - default: - return nil - } - }, - "link": func(line gemini.Line) *gemini.LineLink { - switch l := line.(type) { - case gemini.LineLink: - return &l - default: - return nil - } - }, - "li": func(line gemini.Line) *gemini.LineListItem { - switch l := line.(type) { - case gemini.LineListItem: - return &l - default: - return nil - } - }, - "pre_toggle_on": func(ctx *GemtextContext, line gemini.Line) *gemini.LinePreformattingToggle { - switch l := line.(type) { - case gemini.LinePreformattingToggle: - if ctx.Pre%4 == 0 { - ctx.Pre += 1 - return &l - } - ctx.Pre += 1 - return nil - default: - return nil - } - }, - "pre_toggle_off": func(ctx *GemtextContext, line gemini.Line) *gemini.LinePreformattingToggle { - switch l := line.(type) { - case gemini.LinePreformattingToggle: - if ctx.Pre%4 == 3 { - ctx.Pre += 1 - return &l - } - ctx.Pre += 1 - return nil - default: - return nil - } - }, - "pre": func(line gemini.Line) *gemini.LinePreformattedText { - switch l := line.(type) { - case gemini.LinePreformattedText: - return &l - default: - return nil - } - }, - "quote": func(line gemini.Line) *gemini.LineQuote { - switch l := line.(type) { - case gemini.LineQuote: - return &l - default: - return nil - } - }, - "text": func(line gemini.Line) *gemini.LineText { - switch l := line.(type) { - case gemini.LineText: - return &l - default: - return nil - } - }, - "url": func(ctx *GemtextContext, s string) template.URL { - u, err := url.Parse(s) - if err != nil { - return "error" - } - u = ctx.URL.ResolveReference(u) - - if u.Scheme == "" || u.Scheme == "gemini" { - if u.Host != ctx.Root.Host { - u.Path = fmt.Sprintf("/proxy/%s%s", u.Host, u.Path) - } - u.Scheme = "" - u.Host = "" - } - return template.URL(u.String()) - }, - "safeCSS": func(s string) template.CSS { - return template.CSS(s) - }, - "safeURL": func(s string) template.URL { - return template.URL(s) - }, - }). - Parse(` - - - -{{- if .CSS }} -{{- if .ExternalCSS }} - -{{- else }} - -{{- end }} -{{- end }} -{{.Title}} - - {{ $ctx := . -}} - {{- $isList := false -}} - {{- range .Lines -}} - {{- if and $isList (not (. | li)) }} - - {{- $isList = false -}} - {{- end -}} - - {{- with . | heading }} - {{- $isList = false -}} - {{.Text}} - {{- end -}} - - {{- with . | link }} - {{- $isList = false -}} -

- {{if .Name}}{{.Name}}{{else}}{{.URL}}{{end}} - {{- end -}} - - {{- with . | quote }} - {{- $isList = false -}} -

- {{slice .String 1}} -
- {{- end -}} - - {{- with . | pre_toggle_on $ctx }} -
- -
- {{- end -}} - - {{- with . | text }} - {{- $isList = false }} -

{{.}} - {{- end -}} - - {{- with . | li }} - {{- if not $isList }} -

    - {{- end -}} - - {{- $isList = true }} -
  • {{slice .String 1}}
  • - {{- end -}} - - {{- end }} - {{- if $isList }} -
- {{- end }} - -
- - Proxied content from {{.URL.String}} - {{if .External}} - (external content) - {{end}} - -

Gemini request details: -

-
Original URL
-
{{.URL.String}}
-
Status code
-
{{.Resp.Status}}
-
Meta
-
{{.Resp.Meta}}
-
Proxied by
-
Capybara
-
-

Be advised that no attempt was made to verify the remote SSL certificate. -

-`)) - -var inputPage = template.Must(template. - New("input"). - Funcs(template.FuncMap{ - "safeCSS": func(s string) template.CSS { - return template.CSS(s) - }, - }). - Parse(` - - - -{{- if .CSS }} -{{- if .ExternalCSS }} - -{{- else }} - -{{- end }} -{{- end }} -{{.Prompt}} -
- - {{ if .Secret }} - - {{ else }} - - {{ end }} -
-`)) - -// 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; -} - -li:not(:last-child) { - margin-bottom: 0.5rem; -} - -a { - position: relative; -} - -a:before { - content: '⇒'; - color: #999; - text-decoration: none; - font-weight: bold; - position: absolute; - left: -1.25rem; -} - -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; -} - -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; - } - - a { - color: #0087BD; - } - - a:visited { - color: #333399; - } -} - -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); -} -` - -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 -} - -type InputContext struct { - CSS string - ExternalCSS bool - Prompt string - Secret bool - URL *url.URL -} - -type GemtextHeading struct { - Level int - Text string -} - -func proxyGemini(req gemini.Request, external bool, root *url.URL, - w http.ResponseWriter, r *http.Request, css string, externalCSS bool) { - - ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second) - defer cancel() - - client := gemini.Client{} - resp, err := client.Do(ctx, &req) - if err != nil { - w.WriteHeader(http.StatusBadGateway) - fmt.Fprintf(w, "Gateway error: %v", err) - return - } - defer resp.Body.Close() - - switch resp.Status { - 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, - }) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(fmt.Sprintf("%v", err))) - } - return - case 20: - break // OK - case 30, 31: - to, err := url.Parse(resp.Meta) - if err != nil { - w.WriteHeader(http.StatusBadGateway) - fmt.Fprintf(w, "Gateway error: bad redirect: %v", err) - } - next := req.URL.ResolveReference(to) - if next.Scheme != "gemini" { - w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, "This page is redirecting you to %s", next) - return - } - if external { - next.Path = fmt.Sprintf("/proxy/%s/%s", next.Host, next.Path) - } - next.Host = r.URL.Host - next.Scheme = r.URL.Scheme - w.Header().Add("Location", next.String()) - w.WriteHeader(http.StatusFound) - fmt.Fprintf(w, "Redirecting to %s", next) - return - case 40, 41, 42, 43, 44: - w.WriteHeader(http.StatusServiceUnavailable) - fmt.Fprintf(w, "The remote server returned %d: %s", resp.Status, resp.Meta) - return - case 50, 51: - w.WriteHeader(http.StatusNotFound) - fmt.Fprintf(w, "The remote server returned %d: %s", resp.Status, resp.Meta) - return - case 52, 53, 59: - w.WriteHeader(http.StatusServiceUnavailable) - fmt.Fprintf(w, "The remote server returned %d: %s", resp.Status, resp.Meta) - return - default: - w.WriteHeader(http.StatusNotImplemented) - fmt.Fprintf(w, "Proxy does not understand Gemini response status %d", resp.Status) - return - } - - m, params, err := mime.ParseMediaType(resp.Meta) - if err != nil { - w.WriteHeader(http.StatusBadGateway) - fmt.Fprintf(w, "Gateway error: %d %s: %v", resp.Status, resp.Meta, err) - return - } - - if m != "text/gemini" { - w.Header().Add("Content-Type", resp.Meta) - io.Copy(w, resp.Body) - 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"] - - w.Header().Add("Content-Type", "text/html") - gemctx := &GemtextContext{ - CSS: css, - ExternalCSS: externalCSS, - External: external, - Resp: resp, - Title: req.URL.Host + " " + req.URL.Path, - Lang: lang, - URL: req.URL, - Root: root, - } - - var title bool - gemini.ParseLines(resp.Body, func(line gemini.Line) { - gemctx.Lines = append(gemctx.Lines, line) - if !title { - if h, ok := line.(gemini.LineHeading1); ok { - gemctx.Title = string(h) - title = true - } - } - }) - - err = gemtextPage.Execute(w, gemctx) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, "%v", err) - return - } -} - -func performIfEnv(key string, do func()) { - if len(os.Getenv(key)) != 0 { - do() - } -} - -func main() { - var ( - bind string = ":8080" - css string = defaultCSS - external bool = false - ) - - opts, optind, err := getopt.Getopts(os.Args, "b:c:s:e:") - if err != nil { - log.Fatal(err) - } - - performIfEnv("BIND", func() { - bind = os.Getenv("BIND") - }) - performIfEnv("CSS", func() { - external = false - cssContent, err := ioutil.ReadFile(os.Getenv("CSS")) - if err == nil { - css = string(cssContent) - } else { - log.Fatalf("Error opening custom CSS from '%s': %v", os.Getenv("CSS"), err) - } - }) - performIfEnv("CSS_EXTERNAL", func() { - external = true - css = os.Getenv("CSS_EXTERNAL") - }) - - for _, opt := range opts { - switch opt.Option { - case 'b': - bind = opt.Value - case 's': - external = false - cssContent, err := ioutil.ReadFile(opt.Value) - if err == nil { - css = string(cssContent) - } else { - log.Fatalf("Error opening custom CSS from '%s': %v", opt.Value, err) - } - case 'e': - external = true - css = opt.Value - } - } - - args := os.Args[optind:] - var ( - envRoot string - root *url.URL - ) - if len(args) != 1 { - envRoot = os.Getenv("ROOT") - if len(envRoot) == 0 { - log.Fatalf("Usage: %s ", os.Args[0]) - } - } else { - root, err = url.Parse(args[0]) - } - if len(envRoot) != 0 { - root, err = url.Parse(envRoot) - } - 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 - } - - 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 - } - - req := gemini.Request{} - 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) - })) - - http.Handle("/proxy/", 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 - } - - if r.Method != "GET" { - w.WriteHeader(http.StatusMethodNotAllowed) - w.Write([]byte("404 Not found")) - return - } - - path := strings.SplitN(r.URL.Path, "/", 4) - if len(path) != 4 { - path = append(path, "") - } - 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) - })) - - log.Printf("HTTP server listening on %s", bind) - log.Fatal(http.ListenAndServe(bind, nil)) -} -- cgit v1.2.3