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.
[http]: https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol
[gemini]: https://gemini.circumlunar.space/
[gemini]: https://geminiprotocol.net/
## Usage
```
$ 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
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.
@ -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
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
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.
## "kineto"?

170
main.go
View file

@ -108,28 +108,45 @@ var gemtextPage = template.Must(template.
u = ctx.URL.ResolveReference(u)
if u.Scheme == "" || u.Scheme == "gemini" {
if u.Host != ctx.Root.Host {
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.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")
}
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())
},
"safeCSS": func(s string) template.CSS {
@ -383,19 +400,27 @@ input:focus {
}
`
const defaultRobots = `
User-agent: *
Disallow: /
`
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
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
}
type InputContext struct {
@ -427,8 +452,9 @@ func createAnchor(heading string) string {
return strings.ToLower(anchor.String())
}
func proxyGemini(req gemini.Request, external bool, root *url.URL,
w http.ResponseWriter, r *http.Request, css string, externalCSS bool, usePortal bool, portal string) {
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) {
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
defer cancel()
@ -524,16 +550,19 @@ func proxyGemini(req gemini.Request, external bool, root *url.URL,
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,
UsePortal: usePortal,
Portal: portal,
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,
}
var title bool
@ -557,21 +586,40 @@ func proxyGemini(req gemini.Request, external bool, root *url.URL,
func main() {
var (
bind string = ":8080"
css string = defaultCSS
external bool = false
usePortal bool = false
portal string = ""
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
)
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 {
log.Fatal(err)
}
for _, opt := range opts {
switch opt.Option {
case 'P':
disableExternalProxy = true
case 'R':
disableRobots = true
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
case 's':
external = false
cssContent, err := os.ReadFile(opt.Value)
@ -580,12 +628,6 @@ func main() {
} else {
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
}
if r.URL.Path == "/robots.txt" {
w.WriteHeader(http.StatusOK)
w.Write([]byte("User-agent: *\nDisallow: /\n"))
return
if !disableRobots {
if r.URL.Path == "/robots.txt" {
w.WriteHeader(http.StatusOK)
w.Write([]byte(defaultRobots))
return
}
}
req := gemini.Request{}
@ -637,7 +681,7 @@ func main() {
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)
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) {
@ -646,7 +690,7 @@ func main() {
path = append(path, "")
}
if usePortal && path[2] != root.Host {
if path[2] != root.Host && (usePortal || disableExternalProxy) {
w.WriteHeader(http.StatusMethodNotAllowed)
w.Write([]byte("404 Not found"))
return
@ -678,7 +722,7 @@ func main() {
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)
proxyGemini(req, true, root, w, r, css, external, usePortal, portal, disableExternalProxy, useRewrite, rewrite)
}))
log.Printf("HTTP server listening on %s", bind)