{"title":"This website now supports Gemini","published":"2023-04-09T20:57:56.000Z","html":"
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/20 text/. 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 the Gemini spec if youâre interested.
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 too hard considering that HTML is generated from mostly markdown.
Hereâs an example of a typical blog post I write, mostly markdown and some HTML.
At first, I tried using the html_
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.
To actually serve the Gemini site (capsule, technically), I initially thought I was going to use Agate, 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-
My code for that ended up looking kinda like this:
use rcgen::{Certificate, CertificateParams, DnType};\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); After I set it up to wrap the TCP connection with TLS, it worked! At least, it worked on Lagrange, 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.
(spoiler: it was not the end of getting my server implementation to work)
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 Castor. When I tried loading my capsule on Castor, it didnât load. I went looking for solutions, and stumbled upon a âGemini server torture testâ, 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 close_ first, since not implementing it was a violation of the spec Iâd initially overlooked. Fortunately implementing it was very easy, just a single line change. This fixed the capsule on Castor.
I then tried another client, for mobile this time, called Buran. 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 in the torture test, and some which werenât. This made the websites accessible when I was hosting locally, but not when it was deployed to my server.
I wasnât sure how this was possible, and I considered the possibility of perhaps my server not supporting TLS 1.
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 print(socket. 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-
After a bit of searching, I found a discussion on Tokioâs Axum web framework that seemed relevant.
The following results in an Axum which is available on port 3000 via IPv4 only. How can I make it available on IPv6, also?
let addr = SocketÂAddr::from(([0, 0, 0, 0], 3000));
Try with:
let addr = \":::3000\".parse().unwrap();
Was this actually the solution? I was under the impression 0.0. with :: in my code, and this actually made it work everywhere! :tada: (I later replaced it with [::], just in case, though I donât think it was actually necessary).
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 Matrix and Mastodon (technically Pleroma) instances on so I wouldnât have to buy a new server.
I copied a script 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 /home/ and told Caddy to have a file-matdoes. with that directory as the root.
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.
The other routes. i.
I found a post on the Caddy forums that appeared to be about someone having the same issue as me.
The first answer:
the caddy user still has to have execution access for every parent folder in the path to traverse/
reach the file.
Why??? I donât want to give the Caddy user permission to access every parent folder. I ended up just making a /www directory and having it copy the build output to there, and I did not come across any more significant issues.
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.
Hereâs the code for my crawler/