{"title":"This website now supports Gemini","published":"2023-04-09T20:57:56.000Z","html":"<p>Gemini is a protocol similar to HTTP, in that it’s used for transmitting (mostly) text in (usually) a markup language. However, one of the primary goals of Gemini is simplicity. Requests are always a single TLS/<wbr/>TCP connection with the route, and a correct response looks like <code>20 text/<wbr/>gemini\\n\\rhello world\\n</code>. Additionally, Gemini uses a language called “Gemtext” as its markup language. It’s kind of like Markdown, but even simpler. Every line can only contain a single type of data, so for example you can’t have links in the middle of text. Read <a href=\"https://gemini.circumlunar.space/docs/specification.gmi\" rel=\"noopener\">the Gemini spec</a> if you’re interested.</p> <h2>Translating HTML to Gemtext</h2> <p>Anyways, so I decided to make my website support the Gemini protocol for fun. The plan is to make it translate the HTML on my blog into Gemtext, which shouldn’t be <em>too</em> hard considering that HTML is generated from mostly markdown.</p> <p><a href=\"https://github.com/mat-1/matdoesdev/blob/main/src/routes/minecraft-scanning/index.svx\" rel=\"noopener\">Here’s an example of a typical blog post I write, mostly markdown and some HTML.</a></p> <p>At first, I tried using the <a href=\"https://github.com/mathiversen/html-parser\" rel=\"noopener\">html_<wbr/>parser Rust crate</a> to read the HTML and flatten it out. However, I soon ran into <a href=\"https://github.com/mathiversen/html-parser/issues/22\" rel=\"noopener\">issue #22: Incorrectly trimming whitespaces for text nodes</a>. This made text be squished with links, and while technically I could’ve added workarounds by having it add spaces there I figured it’d be better to avoid issues with that in the future by just using a different crate. I looked at other HTML parsing crates and decided on <a href=\"https://github.com/y21/tl\" rel=\"noopener\">tl</a>, which does not suffer from the same issue as html_<wbr/>parser.</p> <p>If you remember from earlier, though, Gemini does not support inline links! I considered other options like putting every link at the end of the post, but I decided to make it dump the links at the end of every paragraph so they’re easy to find while you’re reading.\nTo make images work, I had to make my crawler download them into a directory so the Gemini server could serve them easily. The actual Gemtext for them is straightforward though.</p> <h2>TLS</h2> <p>To actually serve the Gemini site (capsule, technically), I initially thought I was going to use <a href=\"https://github.com/mbrubeck/agate\" rel=\"noopener\">Agate</a>, but I decided it would be more fun to make my own server (and it’d make it easier to integrate with the crawler). The only thing I was kind of worried about implementing was TLS. I started by copy-<wbr/>pasting from the Rustls examples on their docs, but I wasn’t sure how to make the self-<wbr/>signing work. I took a look at how Agate was doing it, and they’re also using Rustls but through <a href=\"https://docs.rs/tokio-rustls\" rel=\"noopener\">tokio_<wbr/>rustls</a>, and using a crate called <a href=\"https://docs.rs/rcgen/latest/rcgen\" rel=\"noopener\">rcgen</a> for generating the certificates.</p> <p>My code for that ended up looking kinda like this:</p> <pre class=\"language-rs\"><code class=\"language-rs\">use rcgen::&#123;Certificate, CertificateParams, DnType&#125;;\nuse tokio_rustls::rustls;\n\nlet mut cert_params = CertificateParams::new(vec![HOSTNAME.to_string()]);\ncert_params\n    .distinguished_name\n    .push(DnType::CommonName, HOSTNAME);\n\nlet cert = Certificate::from_params(cert_params).unwrap();\n\nlet public_key = new_cert.serialize_der().unwrap();\nlet private_key = new_cert.serialize_private_key_der();\n\nlet cert = rustls::Certificate(public_key);\nlet private_key = rustls::PrivateKey(private_key);</code></pre> <p>After I set it up to wrap the TCP connection with TLS, it worked! At least, it worked on <a href=\"https://gmi.skyjake.fi/lagrange/\" rel=\"noopener\">Lagrange</a>, my client of choice. I thought this would be the end of getting my server implementation to work, so I deployed it to a VPS, opened the port on IPv4 and IPv6, and added the A and AAAA records to Cloudflare.</p> <p>(spoiler: it was not the end of getting my server implementation to work)</p> <h2>Making it work everywhere</h2> <p>I realized it may be a good idea to test on more clients, just to make sure it all works properly. The second client I tried was <a href=\"https://git.sr.ht/~julienxx/castor\" rel=\"noopener\">Castor</a>. When I tried loading my capsule on Castor, it didn’t load. I went looking for solutions, and stumbled upon a ”<a href=\"https://github.com/michael-lazar/gemini-diagnostics\" rel=\"noopener\">Gemini server torture test</a>”, which basically does a bunch of crazy requests to servers and makes sure it responds to all of them correctly. When I first ran it, my server was failing most tests. I looked at the failing tests that looked most suspicious, and decided to implement TLS <code>close_<wbr/>notify</code> first, since not implementing it was a violation of the spec I’d initially overlooked. Fortunately implementing it was very easy, just <a href=\"https://github.com/mat-1/matdoesdev-protocols/commit/46b225158e055571f0b212b36079eb74d336fe07\" rel=\"noopener\">a single line change</a>. This fixed the capsule on Castor.</p> <p>I then tried another client, for mobile this time, called <a href=\"https://f-droid.org/packages/corewala.gemini.buran/\" rel=\"noopener\">Buran</a>. It did not load my capsule :sob:. I tried more clients, and the majority seemed to be failing as well. I implemented more fixes, some of which were <a href=\"https://github.com/mat-1/matdoesdev-protocols/commit/e24d5407f8143b706bf43ebeed9528a44babe3e7#diff-1ff98315bc539e9db4f570cc2e6d95a12d4527647ac9e2ff170c314736a1c715L242\" rel=\"noopener\">in the torture test</a>, and some <a href=\"https://github.com/mat-1/matdoesdev-protocols/commit/1ca83ae4d87f030ab08ce519cb338775526ebf03\" rel=\"noopener\">which weren’t</a>. This made the websites accessible when I was hosting locally, but not when it was deployed to my server.</p> <p>I wasn’t sure how this was possible, and I considered the possibility of perhaps my server not supporting TLS 1.<wbr/>2 properly (I knew it supported 1.<wbr/>3 since the torture test tests for that). I found a random <a href=\"https://github.com/cbrews/ignition\" rel=\"noopener\">Gemini client Python library</a> that failed to send requests to my server and modified it to always use TLS 1.<wbr/>3, but this did not resolve it either.</p> <p>I added more logging to my server, and noticed that the clients weren’t even opening a TCP connection. Maybe it’s a DNS issue? DNS seemed to be working fine, but I noticed running <code>print(socket.<wbr/>getaddrinfo('matdoes.<wbr/>dev', 1965))</code> from Python always puts the IPv6 first. Maybe it’s an issue with IPv6 then? The torture test has a check for IPv6 though…\nI removed the AAAA DNS record and waited a few minutes, and this actually worked!? I didn’t want to keep my site IPv4-<wbr/>only though, so I kept trying to track down the source of the issue. Maybe I had to put the IPv6 in expanded form when I pasted it into the DNS records?? (this did not work, of course).</p> <p>After a bit of searching, I found a discussion on <a href=\"https://github.com/tokio-rs/axum/discussions/834\" rel=\"noopener\">Tokio’s Axum web framework</a> that seemed relevant.</p> <blockquote><p>The following results in an Axum which is available on port 3000 via IPv4 only. How can I make it available on IPv6, also? <code>let addr = Socket­Addr::from(([0, 0, 0, 0], 3000));</code></p></blockquote> <blockquote><p>Try with: <code>let addr = \":::3000\".parse().unwrap();</code></p></blockquote> <p>Was this actually the solution? I was under the impression 0.<wbr/>0.0.<wbr/>0 would work for both IPv4 and IPv6. I replaced <code>0.<wbr/>0.0.<wbr/>0</code> with <code>::</code> in my code, and this actually made it work everywhere! :tada: (I later replaced it with <code>[::]</code>, just in case, though I don’t think it was actually necessary).</p> <h2>Caddy issues</h2> <p>This is completely unrelated to Gemini, but I wanted to mention it anyways. Originally, my website was hosted on Cloudflare Pages, since it’s just a static site. However if I wanted to make other ports accessible, I’d have to make it not be proxied by Cloudflare. I decided to just move it to the server I was already hosting my <a href=\"https://matrix.org\" rel=\"noopener\">Matrix</a> and Mastodon (technically <a href=\"https://pleroma.social\" rel=\"noopener\">Pleroma</a>) instances on so I wouldn’t have to buy a new server.</p> <p>I copied <a href=\"https://gist.github.com/mat-1/5cdfc9dff74d98ac1ce8b290d2f057c5\" rel=\"noopener\">a script</a> I wrote a while ago that automatically watches for changes on Git­Hub and runs a shell command when there’s a commit. I know it’s kind of cursed and I should be using a webhook or whatever but this works good enough. So anyways I made it put the build output in <code>/home/<wbr/>ubuntu/<wbr/>matdoesdev/<wbr/>build</code> and told Caddy to have a file-<wbr/>server route on <code>matdoes.<wbr/>dev</code> with that directory as the root.</p> <p>I tried to reload Caddy, but it was taking an unusually long amount of time and eventually timed out. I enabled debug logs but didn’t see anything too suspicious. I then tried to completely restart Caddy, but this made the Matrix and Pleroma instance on the server inaccessible … After waiting about ten minutes, the issue resolved itself and the other routes were accessible again.</p> <p>The other routes. i.<wbr/>e., not the route I was trying to add. This time, though, I was actually getting an error. When I tried to access the domain, I saw an error in the log that said something about not having enough permissions to read the directory. I modified the permissions on the directory and all the files in it to be readable, writable, and executable by every user, but this somehow did not resolve the issue.</p> <p>I found <a href=\"https://caddy.community/t/reverse-proxy-static-file-serving-results-in-403-forbidden-for-static-files/15465\" rel=\"noopener\">a post</a> on the Caddy forums that appeared to be about someone having the same issue as me.</p> <p>The first answer:</p> <blockquote><p>the caddy user still has to have execution access for every parent folder in the path to traverse/<wbr/>reach the file.</p></blockquote> <p>Why??? I don’t want to give the Caddy user permission to access every parent folder. I ended up just making a <code>/www</code> directory and having it copy the build output to there, and I did not come across any more significant issues.</p> <h2>Other stuff</h2> <p>Maybe I’ll support for more protocols to my website in the future? I saw lots of talk about Gopher while I was looking around the Geminispace, and maybe it’d be cool to also make the website be accessible from Telnet or SSH or something.</p> <p><a href=\"https://github.com/mat-1/matdoesdev-protocols\" rel=\"noopener\">Here’s the code for my crawler/<wbr/>translator/<wbr/>Gemini server</a>, it’s not particularly great but it works.</p>"}