<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Katashift - Web</title>
    <subtitle>Vaguely directed ramblings</subtitle>
    <link rel="self" type="application/atom+xml" href="https://maguire.tech/tags/web/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://maguire.tech"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2025-10-18T00:00:00+00:00</updated>
    <id>https://maguire.tech/tags/web/atom.xml</id>
    <entry xml:lang="en">
        <title>Shove</title>
        <published>2025-10-18T00:00:00+00:00</published>
        <updated>2025-10-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Jack Maguire
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://maguire.tech/posts/shove/"/>
        <id>https://maguire.tech/posts/shove/</id>
        
        <content type="html" xml:base="https://maguire.tech/posts/shove/">&lt;p&gt;Say hello to &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;BurntNail&#x2F;shove&quot;&gt;shove&lt;&#x2F;a&gt;, my brand new HTTP file server and content manager!&lt;&#x2F;p&gt;
&lt;p&gt;Shove is an S3-backed HTTP file server that handles live-reloads, partial updates and even basic HTTP auth and it has now replaced caddy as the HTTP server that serves this very blog&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-0-1&quot;&gt;&lt;a href=&quot;#fn-0&quot;&gt;1&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;! This blog post will be part explanation, part advertisement and part me being excited about my latest project!&lt;&#x2F;p&gt;
&lt;h2 id=&quot;current-functionality&quot;&gt;Current Functionality&lt;&#x2F;h2&gt;
&lt;p&gt;Currently, there’s three main commands inside &lt;code&gt;shove&lt;&#x2F;code&gt; - &lt;code&gt;protect&lt;&#x2F;code&gt;, &lt;code&gt;upload&lt;&#x2F;code&gt; and &lt;code&gt;serve&lt;&#x2F;code&gt;, which I’ll briefly explain in that order. I’ll then go over some of the interesting things I did with each.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;protect&quot;&gt;Protect&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;shove protect&lt;&#x2F;code&gt; deals with the HTTP Basic Auth that &lt;code&gt;shove&lt;&#x2F;code&gt; has - internally it’s modelled as a bunch of users (each of which has a username, a password and a uuid), and a list of realms (each of which has a pattern to protect and a list of uuids that can access it).&lt;&#x2F;p&gt;
&lt;p&gt;You run &lt;code&gt;shove protect&lt;&#x2F;code&gt; from wherever you’re uploading from (to get easy access to environment variables), add the users and add the realms. The default behaviour for paths which don’t match any patterns is just to allow anyone and everyone access to those files - for example, my blog currently has no access control in place.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;protect-1&quot;&gt;Protect&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;shove upload&lt;&#x2F;code&gt; uploads the given directory to the given S3 bucket - as far as the user cares, that’s all it does.&lt;&#x2F;p&gt;
&lt;p&gt;The complexity comes from the fact that it only uploads new files and deletes old files - to achieve this there’s a file in the root directory that stores hashes of each file. &lt;code&gt;shove protect&lt;&#x2F;code&gt; first reads in all the files, then gets that file, checks the hashes and only uploads the files that have changed.&lt;&#x2F;p&gt;
&lt;p&gt;That’s why &lt;code&gt;shove protect&lt;&#x2F;code&gt; can’t upload a ‘current directory’ - it has to have somewhere to put all the files in the bucket where there’s a guarantee the data file (and the auth file) won’t clash.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;serve&quot;&gt;Serve&lt;&#x2F;h3&gt;
&lt;p&gt;By far however, the most complicated part is &lt;code&gt;shove serve&lt;&#x2F;code&gt; - this is the general lifecycle:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Read in the data file from S3&lt;&#x2F;li&gt;
&lt;li&gt;Find all the files&lt;&#x2F;li&gt;
&lt;li&gt;Read them in from S3 and put them into a cache&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Then, when we need to serve a file it does this:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Check for access control - if that fails, then send a &lt;a href=&quot;https:&#x2F;&#x2F;http.cat&#x2F;status&#x2F;401&quot;&gt;401&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Check if that file is in the cache - if so, send it&lt;&#x2F;li&gt;
&lt;li&gt;If it isn’t: grab it from S3, put that in the cache and serve it&lt;&#x2F;li&gt;
&lt;li&gt;If we couldn’t find it in S3, serve a &lt;a href=&quot;https:&#x2F;&#x2F;http.cat&#x2F;status&#x2F;404&quot;&gt;404&lt;&#x2F;a&gt; page.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;That’s all 

&lt;span id=&quot;animated-918806491&quot;&gt;&lt;&#x2F;span&gt;
&lt;script&gt;
    let personToThankForThisSickAnimation=&quot;https:&#x2F;&#x2F;codepen.io&#x2F;zachkrall&#x2F;pen&#x2F;MWWGMPx&quot;
    let delay = 250;
    let outerspan = document.getElementById(&quot;animated-918806491&quot;);

    outerspan.innerHTML = &quot;relatively&quot;
        .split(&quot;&quot;) 
        .map(letter =&gt; {
            return `&lt;span&gt;` + letter + `&lt;&#x2F;span&gt;`;
        })
        .join(&quot;&quot;);

    Array.from(outerspan.children).forEach((span, index) =&gt; {
        setTimeout(() =&gt; {
            span.classList.add(&quot;wavy&quot;);
        }, index * 60 + delay);
    });
&lt;&#x2F;script&gt; simple&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-5-1&quot;&gt;&lt;a href=&quot;#fn-5&quot;&gt;2&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;. The complicated part is the live reloading which goes a little something like this:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Check if we need to reload - Tigris has a webhook, or we can just re-fetch the upload data every 60s and see if that file has changed.&lt;&#x2F;li&gt;
&lt;li&gt;Work out which files have changed, and add their new versions to our cache.&lt;&#x2F;li&gt;
&lt;li&gt;Work out which files have gone missing, and evict them from our cache.&lt;&#x2F;li&gt;
&lt;li&gt;Send a notice to all the websockets connected to reload their pages.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;fun-tricks-stories&quot;&gt;Fun Tricks &amp;amp; Stories&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;protect-2&quot;&gt;Protect&lt;&#x2F;h3&gt;
&lt;p&gt;HTTP Basic Auth was certainly interesting to get working - I wanted to have some form of authentication, and got 90% the way through implementing SCRAM-SHA-256&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-10-1&quot;&gt;&lt;a href=&quot;#fn-10&quot;&gt;3&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;, before I realised that this is supposed to be for server-&amp;gt;server authentication and no web browser lets you just put in a password for that.&lt;&#x2F;p&gt;
&lt;p&gt;I then redirected that work into my HTTP Basic Auth implementation. I’d originally planned to more closely copy Caddy (which this project is designed to replace for me), but I kinda ended up going ham on the access control. I’d always been annoyed that I had to SSH-in, use &lt;code&gt;caddy hash-password&lt;&#x2F;code&gt;, update the caddyfile and restart caddy to change the auth&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-20-1&quot;&gt;&lt;a href=&quot;#fn-20&quot;&gt;4&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt; but &lt;code&gt;shove&lt;&#x2F;code&gt; auth just updates an encrypted file in the bucket that gets regularly checked for updates!&lt;&#x2F;p&gt;
&lt;p&gt;On that front, the auth benefits from the livereloading as well - &lt;code&gt;shove serve&lt;&#x2F;code&gt; never changes the auth, so we know that if it’s been changed then we need to update. Technically, if multiple people were to start changing the protections and finishing their work in weird orders, there could be a race condition where work would be lost, but I’m not concerned about this at the moment&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-30-1&quot;&gt;&lt;a href=&quot;#fn-30&quot;&gt;5&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;upload&quot;&gt;Upload&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;upload&lt;&#x2F;code&gt; was (&lt;strong&gt;comparatively&lt;&#x2F;strong&gt;) simple - we just read in the provided directory, calculate hashes, read in the upload data from S3, and deal with the S3 bits. The only vaguely interesting parts are that I’ve used things like &lt;code&gt;FuturesUnordered&lt;&#x2F;code&gt; to get a load of uploads going at once.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;serve-1&quot;&gt;Serve&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;serve&lt;&#x2F;code&gt; is by far the most complicated, because it handles so damn much. This section will be more an explanation of how stuff works, rather than just tricks and stories. With &lt;a href=&quot;https:&#x2F;&#x2F;lib.rs&#x2F;crates&#x2F;axum&quot;&gt;&lt;code&gt;axum&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&#x2F;&lt;a href=&quot;https:&#x2F;&#x2F;lib.rs&#x2F;crates&#x2F;hyper&quot;&gt;&lt;code&gt;hyper&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;&#x2F;&lt;a href=&quot;https:&#x2F;&#x2F;lib.rs&#x2F;crates&#x2F;tower&quot;&gt;&lt;code&gt;tower&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; projects, I always find a fun way to gauge the complexity is to have a look at the &lt;code&gt;State&lt;&#x2F;code&gt; that the services use, and this is mine:&lt;&#x2F;p&gt;
&lt;pre data-lang=&quot;rust&quot; style=&quot;background-color:#2b303b;color:#c0c5ce;&quot; class=&quot;language-rust &quot;&gt;&lt;code class=&quot;language-rust&quot; data-lang=&quot;rust&quot;&gt;&lt;span&gt;#[&lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;derive&lt;&#x2F;span&gt;&lt;span&gt;(Clone)]
&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;pub struct &lt;&#x2F;span&gt;&lt;span&gt;State {
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;bucket&lt;&#x2F;span&gt;&lt;span&gt;: Box&amp;lt;Bucket&amp;gt;,
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;pub &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;tigris_token&lt;&#x2F;span&gt;&lt;span&gt;: Option&amp;lt;Arc&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color:#b48ead;&quot;&gt;str&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;gt;,
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;pages&lt;&#x2F;span&gt;&lt;span&gt;: Pages,
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;live_reloader&lt;&#x2F;span&gt;&lt;span&gt;: LiveReloader,
&lt;&#x2F;span&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;span style=&quot;color:#bf616a;&quot;&gt;auth&lt;&#x2F;span&gt;&lt;span&gt;: AuthChecker,
&lt;&#x2F;span&gt;&lt;span&gt;}
&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Yeah, that’s not a lot but still considerable, especially considering three of them are custom structs with lots of other functionality. They also happen to be relatively neat lines to draw for explanations and stories about &lt;code&gt;serve&lt;&#x2F;code&gt; so I’ll give a general explanation, and then dive into those three parts (the live reloader, the auth and the pages).&lt;&#x2F;p&gt;
&lt;h4 id=&quot;uploaddata&quot;&gt;&lt;code&gt;UploadData&lt;&#x2F;code&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;The &lt;code&gt;UploadData&lt;&#x2F;code&gt; is the struct that holds all of the hashes for all of the files in S3 and was where this whole project started. Whenever the reload is triggered&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-40-1&quot;&gt;&lt;a href=&quot;#fn-40&quot;&gt;6&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;, we check whether any of the upload data has changed and if so, change it. Once I got hashing working, this was relatively simple.&lt;&#x2F;p&gt;
&lt;p&gt;It doesn’t store any of the actual data, just a map which links a path to a hash, and the name of the root directory we’re serving from.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;livereloader&quot;&gt;&lt;code&gt;LiveReloader&lt;&#x2F;code&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;To use the livereloader, sites must include a small snippet in their javascript that creates a websocket connection to the server and reloads the page when it receives a &lt;code&gt;reload&lt;&#x2F;code&gt; message. Technically, they can open a websocket connection to &lt;em&gt;any&lt;&#x2F;em&gt; path as the HTTP server only checks whether the requests are for websocket upgrades and not what path they’re looking for. This works here and for now because I know there won’t be any other reasons to open websockets&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-50-1&quot;&gt;&lt;a href=&quot;#fn-50&quot;&gt;7&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;But how does that get handled in the server? So, whenever we receive an upgrade request&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-60-1&quot;&gt;&lt;a href=&quot;#fn-60&quot;&gt;8&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;, we send back an upgrade response (kindfully handled by &lt;a href=&quot;https:&#x2F;&#x2F;lib.rs&#x2F;crates&#x2F;soketto&quot;&gt;&lt;code&gt;soketto&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;), and then create a &lt;code&gt;Sender&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;Receiver&lt;&#x2F;code&gt; pair for messages. Then, whenever we need to reload the pages (handled by a channel&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-70-1&quot;&gt;&lt;a href=&quot;#fn-70&quot;&gt;9&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;), we just send a &lt;code&gt;reload&lt;&#x2F;code&gt; message to all of the senders!&lt;&#x2F;p&gt;
&lt;p&gt;If you’ve had a peek over the source code though, it might look like &lt;code&gt;livereload.rs&lt;&#x2F;code&gt; is doing a fair bit more than that, and it is because we have to deal with closed sockets. That gets dealt with by another thread that pings all of the sockets every 60 seconds to make sure that they’re still alive and stops keeping track of the dead ones. It could be argued that this is excessive, but it’s only a server-side load and I’m happy with it. If you’ve got a better solution, feel free to submit a PR! The code is also slightly complicated because it uses a &lt;a href=&quot;https:&#x2F;&#x2F;docs.rs&#x2F;futures&#x2F;latest&#x2F;futures&#x2F;prelude&#x2F;stream&#x2F;struct.FuturesUnordered.html&quot;&gt;&lt;code&gt;FuturesUnordered&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; to deal with dead senders as their pings finish, rather than needing to wait for all of them.&lt;&#x2F;p&gt;
&lt;p&gt;There’s also some code there for closing sockets when the server finishes.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;authchecker&quot;&gt;&lt;code&gt;AuthChecker&lt;&#x2F;code&gt;&lt;&#x2F;h4&gt;
&lt;p&gt;Shove also has ways of dealing with HTTP basic auth. That’s when you open a webpage and the browser asks for a username and password, rather than the webpage. On the plus side, it’s relatively easy to implement from the server, but also it involves sending the entered password over the wire in plaintext which means that this &lt;strong&gt;must only&lt;&#x2F;strong&gt; be used with HTTPS deployments. In addition, you have to implement things like ratelimiting and it’s a faff.&lt;&#x2F;p&gt;
&lt;p&gt;How do I deal with it? I’ll firstly explain how the auth works, and then how it applies itself. So, when we load a page we firstly get a list of every user that has access to that page, and the hash of their password&lt;sup class=&quot;footnote-reference&quot; id=&quot;fr-80-1&quot;&gt;&lt;a href=&quot;#fn-80&quot;&gt;10&lt;&#x2F;a&gt;&lt;&#x2F;sup&gt;. That function deliberately returns an &lt;code&gt;Option&amp;lt;HashMap&amp;gt;&lt;&#x2F;code&gt; so that we can encode three states:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;None&lt;&#x2F;code&gt; signifies that there’s no auth needed - in that case we just return the page as normal!&lt;&#x2F;li&gt;
&lt;li&gt;A &lt;code&gt;Some&lt;&#x2F;code&gt; with an empty &lt;code&gt;HashMap&lt;&#x2F;code&gt; signifies that nobody has access. This could be useful for pages that are only necessary for a specific job and that job isn’t always filled.&lt;&#x2F;li&gt;
&lt;li&gt;A &lt;code&gt;Some&lt;&#x2F;code&gt; with a filled &lt;code&gt;HashMap&lt;&#x2F;code&gt; contains the users we need to auth against.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Once we’ve got the list of users and confirmed that the page needs auth, we check that user’s IP against a ratelimiter. At any point past here, if we fail then we return a response that tells the browser that the user is unauthenticated and needs to provide the correct password. If they pass that check, we then check the &lt;code&gt;Authorization&lt;&#x2F;code&gt; header in their request - if it isn’t there then we fail them and their browser prompts them for a username and password. We then try to find their user in the list we retreieved earlier. If we couldn’t find it then we fail them, but not before running a hash on a fake password - this ensures that they can’t use the round-trip-time to work out which users are valid and invalid. If we could find their password hash, then we hash what they provided and compare the two. If they match, then we serve the page and if not we fail them.&lt;&#x2F;p&gt;
&lt;p&gt;But how do we know when to apply the auth? As I mentioned earlier, I am not necessarily a fan of the way Caddy does things and I wanted to have some fun here - there’s a whole system of Users and Realms. There’s a list of Users which are just uuid-name-hash combinations, and of Realms which can match on paths (there’s exact match, regex, etc) and then we can link together users and realms for authentication. For me, this system just makes sense.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;&#x2F;h2&gt;
&lt;p&gt;This project was a blast to work on - just large enough to have some fun complicated parts, but not so large that it becomes painful to work on. It’s also legitimately useful for me which is a nice side effect. This was also my first project deployed with &lt;code&gt;fly.io&lt;&#x2F;code&gt;, which has been incredible to work with - speedy support, great documentation, reasonable prices, and the CLI works very very well.&lt;&#x2F;p&gt;
&lt;section class=&quot;footnotes&quot;&gt;
&lt;ol class=&quot;footnotes-list&quot;&gt;
&lt;li id=&quot;fn-0&quot;&gt;
&lt;p&gt;I honestly don’t understand why more people don’t &lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Eating_your_own_dog_food&quot;&gt;dogfood&lt;&#x2F;a&gt; their stuff - it’s been so incredible for finding bugs and new features. &lt;a href=&quot;#fr-0-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id=&quot;fn-5&quot;&gt;
&lt;p&gt;Yes I spent far too long getting wavy text working - even with the help of &lt;a href=&quot;https:&#x2F;&#x2F;codepen.io&#x2F;zachkrall&#x2F;pen&#x2F;MWWGMPx&quot;&gt;a sick codepen&lt;&#x2F;a&gt;, it was certainly interesting learning more about Hugo &amp;amp; Zola and how to get a version that was repeatable on dynamic pages working. &lt;a href=&quot;#fr-5-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id=&quot;fn-10&quot;&gt;
&lt;p&gt;From the RFC alone, I might add. Definitely because I wanted the challenge, not because there were’t any rust crates I could find to do it for me ;). And yes, I am aware of how horrific an idea rolling my own cryptography is. &lt;a href=&quot;#fr-10-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id=&quot;fn-20&quot;&gt;
&lt;p&gt;And I just updated the files through &lt;code&gt;rclone&lt;&#x2F;code&gt; for reference. &lt;a href=&quot;#fr-20-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id=&quot;fn-30&quot;&gt;
&lt;p&gt;But if you are, feel free to open a PR! My first thought would be some kind of lockfile in the bucket which is the first thing created and the last thing destroyed in the &lt;code&gt;protect&lt;&#x2F;code&gt; flow. &lt;a href=&quot;#fr-30-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id=&quot;fn-40&quot;&gt;
&lt;p&gt;Either by a webhook which lives on &lt;code&gt;&#x2F;reload&lt;&#x2F;code&gt; or on a timer every 60s. &lt;a href=&quot;#fr-40-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id=&quot;fn-50&quot;&gt;
&lt;p&gt;If I ever need some kind of control&#x2F;management plane, I’ll probably just do it with a raw TCP socket. &lt;a href=&quot;#fr-50-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id=&quot;fn-60&quot;&gt;
&lt;p&gt;A special kind of request that signals to a server: &lt;em&gt;Heyo! I’d love to open a new websocket connection, can I?&lt;&#x2F;em&gt; &lt;a href=&quot;#fr-60-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id=&quot;fn-70&quot;&gt;
&lt;p&gt;That channel gets set off by either a 60s timer that checks for a different content hash in S3, or a Tigris webhook that lives on &lt;code&gt;&#x2F;reload&lt;&#x2F;code&gt;. &lt;a href=&quot;#fr-70-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li id=&quot;fn-80&quot;&gt;
&lt;p&gt;For persistence, this data is stored in an encrypted file in S3. &lt;a href=&quot;#fr-80-1&quot;&gt;↩&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;section&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Vent</title>
        <published>2023-11-11T00:00:00+00:00</published>
        <updated>2024-04-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Jack Maguire
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://maguire.tech/posts/vent/"/>
        <id>https://maguire.tech/posts/vent/</id>
        
        <content type="html" xml:base="https://maguire.tech/posts/vent/">&lt;p&gt;You know how recipes often have a reputation for rambling sections about the origin of those recipes? Well, let’s just say that I finally understand why.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;BurntNail&#x2F;vent&quot;&gt;Vent&lt;&#x2F;a&gt; is my biggest project ever, at time of writing (over 4k LOC of Rust, not including the SQL or the templates 😲 which is pretty damn huge for me) and has been my baby for the last 8 months. It’s finally proper time that I actually show it off and explain the backend (this definitely isn’t a disguised way for me to re-familiarise  myself with the whole thing 😉).&lt;&#x2F;p&gt;
&lt;p&gt;You know the saying &lt;em&gt;Vexation breeds innovation&lt;&#x2F;em&gt;? Maybe not? Well, regardless it’s definitely true for this project. I’d just come into possession of a monstrous spreadsheet for managing various events, as well as a OneDrive folder where the photos got dumped. The old spreadsheet was exclusively for managing the details of the events and had no facilities for managing who was attending the event - this was dealt with by a series of newsletters that died out as people got bored and, in the late stages, teams messages which people were told to react with a 👍 to. The photos weren’t organised at all, were a pain to manage and nobody could see them unless you just asked and then they had to be manually dug out.&lt;&#x2F;p&gt;
&lt;p&gt;No more! To fix this, we go to a self-hosted 100% custom solution with &lt;code&gt;Rust&lt;&#x2F;code&gt; using &lt;a href=&quot;https:&#x2F;&#x2F;lib.rs&#x2F;crates&#x2F;axum&quot;&gt;&lt;code&gt;axum&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; and &lt;code&gt;postgres&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Jack, did you need to do this&lt;&#x2F;em&gt;? God no.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;Jack, was anyone asking you to do this?&lt;&#x2F;em&gt; Also, no.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;em&gt;Weren’t there any prebuilt solutions?&lt;&#x2F;em&gt; Alas, I couldn’t bring myself to look when I saw the potential for such an exciting project.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The plan for the rest of this is to go over the project as it is now, then to go over the creation and some of my interesting hurdles.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;overview&quot;&gt;Overview&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;technical-details&quot;&gt;Technical Details&lt;&#x2F;h3&gt;
&lt;p&gt;As of right now, the project basically just acts as an access-managed front-end for a &lt;code&gt;postgres&lt;&#x2F;code&gt; database serving &lt;code&gt;GET&lt;&#x2F;code&gt; requests using &lt;a href=&quot;https:&#x2F;&#x2F;shopify.github.io&#x2F;liquid&#x2F;&quot;&gt;liquid&lt;&#x2F;a&gt;, and handles all interactive elements via &lt;code&gt;POST&lt;&#x2F;code&gt; requests. For CSS, I’m using &lt;a href=&quot;https:&#x2F;&#x2F;getbootstrap.com&#x2F;docs&#x2F;5.3&#x2F;getting-started&#x2F;introduction&#x2F;&quot;&gt;Bootstrap&lt;&#x2F;a&gt;, which feels 2015-esque but is the easiest way for me to get some vaguely-competent looking CSS. I like doing things myself rather than using pre-built solutions, but I’m willing to limit this project to one new main thing - the sentence above.&lt;&#x2F;p&gt;
&lt;p&gt;If you’re wondering where I’m going to mention what Javascript framework I’m using, the answer is none. I’m a systems dude who’s trying to learn, and I’ll be honest - this is as much a project to learn the ropes of postgres, deployment, back-ends and the web as it is a project to manage events. I’ve tried lots of the java&#x2F;type-script-y frameworks, and they frankly don’t really appeal to me. I’ve become pretty damn hooked on the Rust programming language due to the package management, documentation, tooling and unique features like discriminated unions which are missing from loads of other languages so I didn’t want to really use any. I’ve tried &lt;em&gt;(trust me I’ve really actually tried)&lt;&#x2F;em&gt;, but I can’t quite get javascript to fit inside my head - I had the most success with &lt;code&gt;Svelte&lt;&#x2F;code&gt; but it just doesn’t work for me ;(.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;user-story&quot;&gt;User Story&lt;&#x2F;h3&gt;
&lt;p&gt;So, the main way this project works is through events and peoples’ links to events. An event has a number of properties (that came from the original spreadsheet - one of the project requirements was to be able to import stuff from that &lt;em&gt;relatively&lt;&#x2F;em&gt; easy), and then a bunch of links. They all have links to the people participating, the people looking managing the event, and the photos that have been added. Participant-Event links also can be marked as &lt;strong&gt;verified&lt;&#x2F;strong&gt; - this is useful for making sure people have actually attended the event. A manager-level user has to do that after the event, and in my experience the photo comes in very useful for this.&lt;&#x2F;p&gt;
&lt;p&gt;Finally, there’s a rewards system where after completing a certain number of events, you can be eligible for different rewards. There’s also a system in place for two different entry points each of which have their own requirements for the rewards.&lt;&#x2F;p&gt;
&lt;p&gt;The access control works pretty sensibly imho - for example, someone with manager-level permissions can add and remove whomever they please from events to satisfy reality, but a participant-level user can only add and remove themselves. I won’t go into insane detail - just know that there’s 4 levels (Participant, Manager, Admin, Dev), where the admins can add &amp;amp; remove users, and the dev gets stuff that the admin wouldn’t find as interesting like reloading partial templates.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-story&quot;&gt;The Story&lt;&#x2F;h2&gt;
&lt;p&gt;Here’s the story of my application - a few items have been moved around to better tell the story, but it’s mostly accurate and if you want the 100% accurate version then feel free to read the &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;BurntNail&#x2F;vent&#x2F;commits&#x2F;main&#x2F;&quot;&gt;Commit History&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;beginning&quot;&gt;Beginning&lt;&#x2F;h3&gt;
&lt;p&gt;This project started with the beginners guide for the &lt;code&gt;axum&lt;&#x2F;code&gt; crate (for this was my first axum project which slowly expanded in capability as I learnt more about axum &amp;amp; async rust), where you get started with a basic GET&#x2F;POST which worked well for me. I then added a &lt;code&gt;liquid&lt;&#x2F;code&gt; based templating system inspired by &lt;a href=&quot;https:&#x2F;&#x2F;fasterthanli.me&quot;&gt;Amos Wenger&lt;&#x2F;a&gt; (specifically &lt;a href=&quot;https:&#x2F;&#x2F;fasterthanli.me&#x2F;articles&#x2F;a-new-website-for-2020&quot;&gt;this one&lt;&#x2F;a&gt; iirc), who’s writings I cannot recommend enough. I then slowly by surely added the &lt;em&gt;other things&lt;&#x2F;em&gt; like participants and photos. This part of the project was probably the second most fun, where I could move mountains with little effort - partially because I hadn’t done much yet so any change was big, and partially because I was free to look at all kinds of different approaches before I got entrenched in a pretty specific style.&lt;&#x2F;p&gt;
&lt;p&gt;I also had a few interesting constraints to work within:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;I wanted to be able to run the whole thing on just the one tiny VPS (a Linode Nanode which has 1GB of RAM, 1 vCPUs and 25GB of storage) I rent whilst still keeping some space for other stuff (like this blog!). This was mainly an attempt to reduce both vendor lock-in as well as costs.&lt;&#x2F;li&gt;
&lt;li&gt;I wanted this to keep backwards compatibility with the existing spreadsheets - that meant making sure that I had lots of import&#x2F;export tools.&lt;&#x2F;li&gt;
&lt;li&gt;I wanted this to be easily used by lots of more non-technical users - a JSON feed would never be acceptable and I needed to put lots of work in to get something looking halfway decent (a challenge even with the aid of &lt;code&gt;Bootstrap&lt;&#x2F;code&gt;).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;authentication&quot;&gt;Authentication&lt;&#x2F;h3&gt;
&lt;p&gt;I then took it to the person who actually had final say on adoption on this system and he said that I needed to follow all relevant guidelines that he had to, which in this case included making sure that you couldn’t see photos without logging in. My initial thoughts were something along the lines of &lt;em&gt;balls. i don’t really want to do this, because the more user data i store the more impact my screwups have&lt;&#x2F;em&gt;, but I wanted adoption so I just nodded and said yes. Turns out I was still right, just not in the way I thought I was.&lt;&#x2F;p&gt;
&lt;p&gt;At this point I was pretty happy with the general &lt;code&gt;axum&lt;&#x2F;code&gt; ecosystem, and I came across two main crates that seem to be used for logging in - &lt;a href=&quot;https:&#x2F;&#x2F;lib.rs&#x2F;crates&#x2F;axum-auth&quot;&gt;&lt;code&gt;axum-auth&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; &amp;amp; &lt;a href=&quot;https:&#x2F;&#x2F;lib.rs&#x2F;crates&#x2F;axum-login&quot;&gt;&lt;code&gt;axum-login&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;. &lt;code&gt;axum-auth&lt;&#x2F;code&gt; seemed to have more downloads but was just one rust file to check headers and get them. &lt;code&gt;axum-login&lt;&#x2F;code&gt; seemed to have more features and even &lt;strong&gt;drumroll please&lt;&#x2F;strong&gt; - Session Management, although it had fewer downloads. Generally, I like writing things myself (as has already been mentioned, often for the learning opportunity), but if I’ve learnt one thing in my years of Tom Scott&#x2F;Computerphile viewership - it’s that you leave password management to other people, preferably open source people with code that’s been explored &amp;amp; checked over. That having been said, it still wasn’t the easiest to work with - there were a few annoyances like having to rewrite the &lt;code&gt;PostgresStore&lt;&#x2F;code&gt; for sessions to work with my system, as well as getting passwords to work right. I started with the &lt;code&gt;0.6&lt;&#x2F;code&gt; version, which has recently been updated with more breaking changes than I’ve had hot dinners (future update coming - &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;BurntNail&#x2F;vent&#x2F;issues&#x2F;187&quot;&gt;issue here&lt;&#x2F;a&gt;). I then used &lt;code&gt;bcrypt&lt;&#x2F;code&gt; to store the &lt;em&gt;hashed&lt;&#x2F;em&gt; passwords.&lt;&#x2F;p&gt;
&lt;p&gt;One fun solution to a problem is signing up. Since I know my entire userbase in advance, I can just import them all via CSV with blank passwords set. My original solution was to just set the password to whatever the user entered on first login, before I realised that I’d have a sea of password resets as people logged into other peoples’ accounts for shits &amp;amp; giggles. I eventually ended up going with emailed magic links to set the password.&lt;&#x2F;p&gt;
&lt;p&gt;Eventually I got all that working, with the access control coming not long after. As I alluded to earlier, there was still a pain point. This was just managing the access control levels across the whole application - I have checks in a whole lot of places to make sure someone’s not trying to be sneaky with direct &lt;code&gt;POST&lt;&#x2F;code&gt; requests to the server and making sure that no PII leaks.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-modern-web&quot;&gt;The Modern Web&lt;&#x2F;h3&gt;
&lt;p&gt;At this point though it was just something running on my machine, or just via direct HTTP from my registrar (originally &lt;a href=&quot;https:&#x2F;&#x2F;hover.com&quot;&gt;Hover&lt;&#x2F;a&gt;, but I transferred to &lt;a href=&quot;https:&#x2F;&#x2F;www.cloudflare.com&#x2F;en-gb&#x2F;learning&#x2F;dns&#x2F;what-is-cloudflare-registrar&#x2F;&quot;&gt;Cloudflare&lt;&#x2F;a&gt; for cheaper renewal fees) to my VPS (&lt;a href=&quot;https:&#x2F;&#x2F;www.linode.com&#x2F;&quot;&gt;Linode&lt;&#x2F;a&gt; seems to give a decent compromise between cost, configuration &amp;amp; ease of use). I &lt;em&gt;needed&lt;&#x2F;em&gt; HTTPS if I wanted people to send passwords through here - there’s nothing particularly important, and I’ve got the password hygiene not to use the same password everywhere but I can’t say that for all my users. Luckily, one of my very good friends (&lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;HandyHat&quot;&gt;HandyHat&lt;&#x2F;a&gt;) had experience here, and walked me through the entire setup of &lt;a href=&quot;https:&#x2F;&#x2F;caddyserver.com&#x2F;&quot;&gt;Caddy&lt;&#x2F;a&gt; for Cloudflare using Reverse DNS to get HTTPS certificates.&lt;&#x2F;p&gt;
&lt;p&gt;You might’ve noticed that I’ve put all of my eggs into one basket - if Cloudflare goes down then I’m down. I understand the concern, but I also trust that if Cloudflare goes down then the one instance I run of Vent personally will be of little concerns.&lt;&#x2F;p&gt;
&lt;p&gt;I also chose to use a VPS here, rather than a Docker Container running on a &lt;a href=&quot;https:&#x2F;&#x2F;www.digitalocean.com&#x2F;products&#x2F;droplets&quot;&gt;Droplet&lt;&#x2F;a&gt; or something - this is for multiple reasons. First is that I’m definitely not just using the VPS for this so I save the more projects I use. I also find it insanely convenient to quickly fix small issues in things like SQL queries in production without needing some complicated deployment process - I just &lt;code&gt;ssh&lt;&#x2F;code&gt; in, edit the relevant file with &lt;a href=&quot;https:&#x2F;&#x2F;micro-editor.github.io&#x2F;&quot;&gt;&lt;code&gt;micro&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;, then build the project and restart it in a &lt;code&gt;tmux&lt;&#x2F;code&gt; session I keep around.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;adoption&quot;&gt;Adoption&lt;&#x2F;h3&gt;
&lt;p&gt;Eventually, I started some user groups on the system and when nothing went wrong (apart from some misspelt usernames which were easily fixed) I went for the full rollout. Whilst some people still don’t mark themselves as planning to attend, it’s now the de-facto system and those people probably check when and where events are on the main page. I’m really happy with how it went.&lt;&#x2F;p&gt;
&lt;p&gt;No first plan survives contact with the enemy though, so I had to rapidly issue a few updates like publicly marking who uploads photos, and improving my logging infrastructure which at that point just forwarded whatever domain specific error it got from whatever caused the error via &lt;a href=&quot;https:&#x2F;&#x2F;lib.rs&#x2F;crates&#x2F;thiserror&quot;&gt;&lt;code&gt;thiserror&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; &amp;amp; the &lt;a href=&quot;https:&#x2F;&#x2F;lib.rs&#x2F;crates&#x2F;tracing&quot;&gt;&lt;code&gt;tracing&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; ecosystem. You don’t want to know how long its taken to tune the logs 😔.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fun-technical-things&quot;&gt;Fun Technical Things&lt;&#x2F;h2&gt;
&lt;p&gt;You didn’t think I’d finish here, did you? Nah - I’ve got a few fun things which I’ll add to as I remember from the making of this project.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;threads-not-of-the-zuck-variety&quot;&gt;Threads (not of the Zuck variety)&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;axum&lt;&#x2F;code&gt; has a &lt;code&gt;State&lt;&#x2F;code&gt; extractor you can use with all of your methods, and whilst from what I’ve seen most people don’t use it as a state manager and just use it as various components I’ve got it all tricked out. For example, I publish an ICS but don’t want the faff of re-making the file for every request (which definitely isn’t what I was doing for a while which caused event duplication issues 😉) so I have one persistent file that gets updated. How do I know when to update it? Every time an event gets updated in such a way that would affect the calendar, I call a function on the State which sends a blank message through a channel to the calendar updater.&lt;&#x2F;p&gt;
&lt;p&gt;I’ve also got threads to delete old sessions (hopefully no longer needed in &lt;code&gt;axum-login&lt;&#x2F;code&gt; 0.7?), and send emails to users.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;templating&quot;&gt;Templating&lt;&#x2F;h3&gt;
&lt;p&gt;I’ve mentioned that I use liquid templating, but given no more insight than that. Currently, the partials get cached but the actual templates don’t (there’s an &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;BurntNail&#x2F;vent&#x2F;issues&#x2F;180&quot;&gt;issue&lt;&#x2F;a&gt; and it’s next on my list after &lt;code&gt;axum-login&lt;&#x2F;code&gt; 0.7). The main reason for this is that &lt;code&gt;liquid&lt;&#x2F;code&gt; templates are entirely synchronous and the compiler is created in an environment with no way to access &lt;code&gt;async&lt;&#x2F;code&gt; APIs other than caching them beforehand 🤷, so if ever anyone looks at my code and gets confused then that’s why.&lt;&#x2F;p&gt;
&lt;p&gt;If you look over my templates, I’ve also got a couple of invariants (like people always being ordered by group in the view all page when logged in) that allow me to commit some &lt;em&gt;horrendous&lt;&#x2F;em&gt; crimes that make me giggle every time I look at them, and sigh every time I have to think about doing something with that part.&lt;&#x2F;p&gt;
&lt;p&gt;Having now got experience with liquid, I can say that it probably wasn’t the right choice - it can be annoyingly limited to work with (something like &lt;a href=&quot;https:&#x2F;&#x2F;lib.rs&#x2F;crates&#x2F;handlebars&quot;&gt;&lt;code&gt;handlebars&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; might’ve been better in that respect), but I’m also happy to accept that it’s good enough and that maybe everything doesn’t always need to be perfect.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gotchas-for-future-me&quot;&gt;Gotchas for future me&lt;&#x2F;h2&gt;
&lt;p&gt;These are just a few pointers for people ever embarking on similar projects:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;You need to setup a &lt;a href=&quot;https:&#x2F;&#x2F;developers.cloudflare.com&#x2F;support&#x2F;page-rules&#x2F;understanding-and-configuring-cloudflare-page-rules-page-rules-tutorial&#x2F;&quot;&gt;page rule&lt;&#x2F;a&gt; to turn off SSL for &lt;code&gt;DOMAIN&#x2F;.well_known&#x2F;acme_challenge&#x2F;*&lt;&#x2F;code&gt; because that prevents Caddy from setting up the certificate.&lt;&#x2F;li&gt;
&lt;li&gt;Linode blocks email-related ports (presumably for spam&#x2F;DDOS reasons?) and doesn’t ever say clearly (just in some &lt;a href=&quot;https:&#x2F;&#x2F;www.linode.com&#x2F;blog&#x2F;linode&#x2F;a-new-policy-to-help-fight-spam&#x2F;&quot;&gt;random blog post&lt;&#x2F;a&gt;) so you need to contact support to get them unblocked.&lt;&#x2F;li&gt;
&lt;li&gt;If you’re tee-ing JSON logs into a file and then serving that file, you might need to parse that file and reverse all the elements because otherwise all the most recent logs (which are probably the important ones) are right at the bottom.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;axum::debug_handler&lt;&#x2F;code&gt; compiles to nothing in release mode - cover your project in them because all they’ll do is slightly increase compile times and they’ll massively help in errors.&lt;&#x2F;li&gt;
&lt;li&gt;Use &lt;a href=&quot;https:&#x2F;&#x2F;dbeaver.io&#x2F;&quot;&gt;DBeaver&lt;&#x2F;a&gt; - it’s insanely useful for quickly editing database records and doing other database-adjacent things like complete migrations.&lt;&#x2F;li&gt;
&lt;li&gt;People aren’t joking about backups - you wouldn’t believe how easy it is to completely accidentally wipe something important from (or just wipe the whole) production database. It doesn’t matter how many flags you’ve setup in &lt;code&gt;DBeaver&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;www.hostinger.co.uk&#x2F;&quot;&gt;Hostinger&lt;&#x2F;a&gt; can shut off servers with too high activity - make sure to double check that doesn’t happen to you, because I didn’t get emailed about it or realise until I checked wanting to see the events and saw that production was down because &lt;code&gt;Docker&lt;&#x2F;code&gt; was being screwy.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
</feed>
