Add new flags for robots.txt and URL rewrites

This commit is contained in:
Erick Ruiz de Chavez 2025-01-10 07:24:45 -05:00
parent c36b3b6cb0
commit 32c87fbc39
2 changed files with 124 additions and 66 deletions

View file

@ -6,15 +6,24 @@ can proxy to any domain in order to facilitate linking to the rest of
Geminispace, but it defaults to a specific domain. Geminispace, but it defaults to a specific domain.
[http]: https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol [http]: https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol
[gemini]: https://gemini.circumlunar.space/ [gemini]: https://geminiprotocol.net/
## Usage ## Usage
``` ```
$ go build $ go build
$ ./kineto [-b 127.0.0.1:8080] [-s style.css] [-e style.css] [-p https://portal.mozz.us] gemini://example.org $ ./kineto [-P] [-R] [-b 127.0.0.1:8080] [-r /learn-more-about-gemini] [-s style.css] [-e style.css] [-p https://portal.mozz.us] gemini://example.org
``` ```
The -P argument is optional and allows you to completely disable proxying
and URL rewriting external gemini URLs. The user visiting your site will need
to have a browser or browser extension that understands how to handle
gemini:// links.
The -R argument is optional and allows you to completely disables serving a
custom robots.txt response. When enabled the request will be proxied to your
gemini site.
The -b argument is optional and allows you to bind to an arbitrary address; by The -b argument is optional and allows you to bind to an arbitrary address; by
default kineto will bind to `:8080`. You should set up some external reverse default kineto will bind to `:8080`. You should set up some external reverse
proxy like nginx to forward traffic to this port and add TLS. proxy like nginx to forward traffic to this port and add TLS.
@ -30,8 +39,13 @@ in a `<style>` block like with the -s flag. The given stylesheet can be a
relative link, for instance `-e /main.css` will serve `main.css` from the root relative link, for instance `-e /main.css` will serve `main.css` from the root
of the proxied Gemini capsule. of the proxied Gemini capsule.
The -r argument is optional and allows you rewrite external gemini links to a
custom URL. Use this if you have external gemini links but instead of proxying
them you prefer to invite your visitors to learn more about the Gemini protocol
and download a Gemini browser. This can be a relative or absolute URL.
The -p argument is optional and allows you to specify a custom Portal to proxy The -p argument is optional and allows you to specify a custom Portal to proxy
external gemini requests. This option will effectively disable the use of `/x/` external Gemini requests. This option will effectively disable the use of `/x/`
for all external URLs, that is al URLs whose host does not match the proxied site. for all external URLs, that is al URLs whose host does not match the proxied site.
## "kineto"? ## "kineto"?

170
main.go
View file

@ -108,28 +108,45 @@ var gemtextPage = template.Must(template.
u = ctx.URL.ResolveReference(u) u = ctx.URL.ResolveReference(u)
if u.Scheme == "" || u.Scheme == "gemini" { if u.Scheme != "" && u.Scheme != "gemini" {
if u.Host != ctx.Root.Host { return template.URL(u.String())
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/gemini/%s%s", p.Path, u.Host, u.Path)
u.Scheme = p.Scheme
u.Host = p.Host
} else {
u.Path = fmt.Sprintf("/x/%s%s", u.Host, u.Path)
u.Scheme = ""
u.Host = ""
}
} else {
u.Scheme = ""
u.Host = ""
}
} }
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")
}
return template.URL(r.String())
}
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 = ""
return template.URL(u.String()) return template.URL(u.String())
}, },
"safeCSS": func(s string) template.CSS { "safeCSS": func(s string) template.CSS {
@ -383,19 +400,27 @@ input:focus {
} }
` `
const defaultRobots = `
User-agent: *
Disallow: /
`
type GemtextContext struct { type GemtextContext struct {
CSS string CSS string
ExternalCSS bool ExternalCSS bool
External bool External bool
Lines []gemini.Line Lines []gemini.Line
Pre int Pre int
Resp *gemini.Response Resp *gemini.Response
Title string Title string
Lang string Lang string
URL *url.URL URL *url.URL
Root *url.URL Root *url.URL
UsePortal bool UsePortal bool
Portal string Portal string
DisableExternalProxy bool
UseRewrite bool
Rewrite string
} }
type InputContext struct { type InputContext struct {
@ -427,8 +452,9 @@ func createAnchor(heading string) string {
return strings.ToLower(anchor.String()) return strings.ToLower(anchor.String())
} }
func proxyGemini(req gemini.Request, external bool, root *url.URL, func proxyGemini(req gemini.Request, external bool, root *url.URL, w http.ResponseWriter,
w http.ResponseWriter, r *http.Request, css string, externalCSS bool, usePortal bool, portal string) { r *http.Request, css string, externalCSS bool, usePortal bool, portal string, disableExternalProxy bool,
useRewrite bool, rewrite string) {
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second) ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
defer cancel() defer cancel()
@ -524,16 +550,19 @@ func proxyGemini(req gemini.Request, external bool, root *url.URL,
w.Header().Add("Content-Type", "text/html") w.Header().Add("Content-Type", "text/html")
gemctx := &GemtextContext{ gemctx := &GemtextContext{
CSS: css, CSS: css,
ExternalCSS: externalCSS, ExternalCSS: externalCSS,
External: external, External: external,
Resp: resp, Resp: resp,
Title: req.URL.Host + " " + req.URL.Path, Title: req.URL.Host + " " + req.URL.Path,
Lang: lang, Lang: lang,
URL: req.URL, URL: req.URL,
Root: root, Root: root,
UsePortal: usePortal, UsePortal: usePortal,
Portal: portal, Portal: portal,
DisableExternalProxy: disableExternalProxy,
UseRewrite: useRewrite,
Rewrite: rewrite,
} }
var title bool var title bool
@ -557,21 +586,40 @@ func proxyGemini(req gemini.Request, external bool, root *url.URL,
func main() { func main() {
var ( var (
bind string = ":8080" bind string = ":8080"
css string = defaultCSS css string = defaultCSS
external bool = false external bool = false
usePortal bool = false usePortal bool = false
portal string = "" portal string = ""
useRewrite bool = false
rewrite string = ""
disableExternalProxy bool = false
disableRobots bool = false
) )
opts, optind, err := getopt.Getopts(os.Args, "b:c:s:e:p:") opts, optind, err := getopt.Getopts(os.Args, "PRb:c:e:p:r:s:")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
for _, opt := range opts { for _, opt := range opts {
switch opt.Option { switch opt.Option {
case 'P':
disableExternalProxy = true
case 'R':
disableRobots = true
case 'b': case 'b':
bind = opt.Value bind = opt.Value
case 'e':
external = true
css = opt.Value
case 'p':
usePortal = true
portal = opt.Value
case 'r':
useRewrite = true
rewrite = opt.Value
case 's': case 's':
external = false external = false
cssContent, err := os.ReadFile(opt.Value) cssContent, err := os.ReadFile(opt.Value)
@ -580,12 +628,6 @@ func main() {
} else { } else {
log.Fatalf("Error opening custom CSS from '%s': %v", opt.Value, err) log.Fatalf("Error opening custom CSS from '%s': %v", opt.Value, err)
} }
case 'e':
external = true
css = opt.Value
case 'p':
usePortal = true
portal = opt.Value
} }
} }
@ -625,10 +667,12 @@ func main() {
return return
} }
if r.URL.Path == "/robots.txt" { if !disableRobots {
w.WriteHeader(http.StatusOK) if r.URL.Path == "/robots.txt" {
w.Write([]byte("User-agent: *\nDisallow: /\n")) w.WriteHeader(http.StatusOK)
return w.Write([]byte(defaultRobots))
return
}
} }
req := gemini.Request{} req := gemini.Request{}
@ -637,7 +681,7 @@ func main() {
req.URL.Host = root.Host req.URL.Host = root.Host
req.URL.Path = r.URL.Path req.URL.Path = r.URL.Path
req.URL.RawQuery = r.URL.RawQuery req.URL.RawQuery = r.URL.RawQuery
proxyGemini(req, false, root, w, r, css, external, usePortal, portal) proxyGemini(req, false, root, w, r, css, external, usePortal, portal, disableExternalProxy, useRewrite, rewrite)
})) }))
http.Handle("/x/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Handle("/x/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -646,7 +690,7 @@ func main() {
path = append(path, "") path = append(path, "")
} }
if usePortal && path[2] != root.Host { if path[2] != root.Host && (usePortal || disableExternalProxy) {
w.WriteHeader(http.StatusMethodNotAllowed) w.WriteHeader(http.StatusMethodNotAllowed)
w.Write([]byte("404 Not found")) w.Write([]byte("404 Not found"))
return return
@ -678,7 +722,7 @@ func main() {
req.URL.Path = "/" + path[3] req.URL.Path = "/" + path[3]
req.URL.RawQuery = r.URL.RawQuery req.URL.RawQuery = r.URL.RawQuery
log.Printf("%s (external) %s%s", r.Method, r.URL.Host, r.URL.Path) log.Printf("%s (external) %s%s", r.Method, r.URL.Host, r.URL.Path)
proxyGemini(req, true, root, w, r, css, external, usePortal, portal) proxyGemini(req, true, root, w, r, css, external, usePortal, portal, disableExternalProxy, useRewrite, rewrite)
})) }))
log.Printf("HTTP server listening on %s", bind) log.Printf("HTTP server listening on %s", bind)