From c4520f2c84c0d4555bdb02f4ec7b2d1a8bdefca2 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Mon, 23 Jan 2023 16:02:35 +0100 Subject: [PATCH] Automatically bridge gemini links to a gateway on http site --- flake.nix | 1 + src/cmd/load-test-data/test-data.yml | 8 ++++++-- src/gmi/gemtext.go | 27 +++++++++++++++++++++------ src/gmi/gemtext_test.go | 17 ++++++++++++++++- src/http/http.go | 11 +++++++++++ src/http/posts.go | 6 +++++- 6 files changed, 60 insertions(+), 10 deletions(-) diff --git a/flake.nix b/flake.nix index 801ebdb..3a1eb3e 100644 --- a/flake.nix +++ b/flake.nix @@ -43,6 +43,7 @@ export MEDIOCRE_BLOG_HTTP_PUBLIC_URL="$MEDIOCRE_BLOG_ML_PUBLIC_URL" export MEDIOCRE_BLOG_HTTP_LISTEN_PROTO="tcp" export MEDIOCRE_BLOG_HTTP_LISTEN_ADDR=":4000" + export MEDIOCRE_BLOG_HTTP_GEMINI_GATEWAY_URL="https://nightfall.city/x/" # http auth # (password is "bar". This should definitely be changed for prod.) diff --git a/src/cmd/load-test-data/test-data.yml b/src/cmd/load-test-data/test-data.yml index a3fff9d..36e009f 100644 --- a/src/cmd/load-test-data/test-data.yml +++ b/src/cmd/load-test-data/test-data.yml @@ -104,6 +104,12 @@ published_posts: Edgy. + => / Here's a link within the site + + => gemini://mediocregopher.com And here's a link to a gemini capsule + + => https://mediocregopher.com And here's a link to an https site + #### Side-note Did you know that the terms "cyberspace" and "matrix" are attributable to a book from 1984 called _Neuromancer_? @@ -117,8 +123,6 @@ published_posts: This has been a great post. - => / Here's a link outa here! - - id: empty-markdown-test title: Empty Markdown Test description: diff --git a/src/gmi/gemtext.go b/src/gmi/gemtext.go index a1136bd..3e5f6fc 100644 --- a/src/gmi/gemtext.go +++ b/src/gmi/gemtext.go @@ -5,8 +5,9 @@ import ( "errors" "fmt" "io" - "log" + "net/url" "path" + "path/filepath" "regexp" "strings" ) @@ -25,7 +26,10 @@ var linkRegexp = regexp.MustCompile(`^=>\s+(\S+)\s*(.*?)\s*$`) // GemtextToMarkdown reads a gemtext formatted body from the Reader and writes // the markdown version of that body to the Writer. -func GemtextToMarkdown(dst io.Writer, src io.Reader) error { +// +// gmiGateway, if given, is used for all `gemini://` links. The `gemini://` +// prefix will be stripped, and replaced with the given URL. +func GemtextToMarkdown(dst io.Writer, src io.Reader, gmiGateway *url.URL) error { bufSrc := bufio.NewReader(src) @@ -40,7 +44,20 @@ func GemtextToMarkdown(dst io.Writer, src io.Reader) error { if match := linkRegexp.FindStringSubmatch(line); len(match) > 0 { - isImg := hasImgExt(match[1]) + u, err := url.Parse(match[1]) + if err != nil { + return fmt.Errorf("link to invalid url %q: %w", match[1], err) + } + + if u.Scheme == "gemini" && gmiGateway != nil { + + newU := *gmiGateway + newU.Path = filepath.Join(newU.Path, u.Host, u.Path) + newU.RawQuery = u.RawQuery + u = &newU + } + + isImg := hasImgExt(u.Path) descr := match[2] @@ -52,9 +69,7 @@ func GemtextToMarkdown(dst io.Writer, src io.Reader) error { descr = "Link" } - log.Printf("descr:%q", descr) - - line = fmt.Sprintf("[%s](%s)\n", descr, match[1]) + line = fmt.Sprintf("[%s](%s)\n", descr, u.String()) if isImg { line = "!" + line diff --git a/src/gmi/gemtext_test.go b/src/gmi/gemtext_test.go index 23cb97f..75da9df 100644 --- a/src/gmi/gemtext_test.go +++ b/src/gmi/gemtext_test.go @@ -2,6 +2,7 @@ package gmi import ( "bytes" + "net/url" "strconv" "testing" @@ -10,6 +11,8 @@ import ( func TestGemtextToMarkdown(t *testing.T) { + gmiGateway, _ := url.Parse("https://gateway.com/x/") + tests := []struct { in, exp string }{ @@ -37,13 +40,25 @@ func TestGemtextToMarkdown(t *testing.T) { in: "=> img.png description is here ", exp: "![description is here](img.png)\n", }, + { + in: "=> gemini://somewhere.com/foo Somewhere", + exp: "[Somewhere](https://gateway.com/x/somewhere.com/foo)\n", + }, + { + in: "=> gemini://somewhere.com:420/foo Somewhere", + exp: "[Somewhere](https://gateway.com/x/somewhere.com:420/foo)\n", + }, + { + in: "=> gemini://somewhere.com:420/foo?bar=baz Somewhere", + exp: "[Somewhere](https://gateway.com/x/somewhere.com:420/foo?bar=baz)\n", + }, } for i, test := range tests { t.Run(strconv.Itoa(i), func(t *testing.T) { got := new(bytes.Buffer) - err := GemtextToMarkdown(got, bytes.NewBufferString(test.in)) + err := GemtextToMarkdown(got, bytes.NewBufferString(test.in), gmiGateway) assert.NoError(t, err) assert.Equal(t, test.exp, got.String()) }) diff --git a/src/http/http.go b/src/http/http.go index da39374..98cdde3 100644 --- a/src/http/http.go +++ b/src/http/http.go @@ -56,12 +56,17 @@ type Params struct { // AuthRatelimit indicates how much time must pass between subsequent auth // attempts. AuthRatelimit time.Duration + + // GeminiGatewayURL will be used to translate links for `gemini://` into + // `http(s)://`. See gmi.GemtextToMarkdown. + GeminiGatewayURL *url.URL } // SetupCfg implement the cfg.Cfger interface. func (p *Params) SetupCfg(cfg *cfg.Cfg) { publicURLStr := cfg.String("http-public-url", "http://localhost:4000", "URL this service is accessible at") + geminiGatewayURLStr := cfg.String("http-gemini-gateway-url", "", "Optional URL to prefix to all gemini:// links, to make them accessible over https") cfg.StringVar(&p.ListenProto, "http-listen-proto", "tcp", "Protocol to listen for HTTP requests with") cfg.StringVar(&p.ListenAddr, "http-listen-addr", ":4000", "Address/unix socket path to listen for HTTP requests on") @@ -87,6 +92,12 @@ func (p *Params) SetupCfg(cfg *cfg.Cfg) { return fmt.Errorf("parsing -http-public-url: %w", err) } + if *geminiGatewayURLStr != "" { + if p.GeminiGatewayURL, err = url.Parse(*geminiGatewayURLStr); err != nil { + return fmt.Errorf("parsing -http-gemini-gateway-url: %w", err) + } + } + return nil }) } diff --git a/src/http/posts.go b/src/http/posts.go index cae8f43..bb1c899 100644 --- a/src/http/posts.go +++ b/src/http/posts.go @@ -96,7 +96,11 @@ func (a *api) postToPostTplPayload(storedPost post.StoredPost) (postTplPayload, prevBodyBuf := bodyBuf bodyBuf = new(bytes.Buffer) - if err := gmi.GemtextToMarkdown(bodyBuf, prevBodyBuf); err != nil { + err := gmi.GemtextToMarkdown( + bodyBuf, prevBodyBuf, a.params.GeminiGatewayURL, + ) + + if err != nil { return postTplPayload{}, fmt.Errorf("converting gemtext to markdown: %w", err) } }