20 April 2016

On Monday I decided I would finally get around to setting up TLS on my website. I had heard about Let's Encrypt a while back, but I had been under the impression that they mainly supported Apache, with some experimental support for nginx. It turns out that they just need to verify that you have control over a domain, and one way to do that is by allowing their client to write files that are accessible via HTTP, as dictated by the ACME protocol.

Overall, the entire process of moving from an HTTP-only setup to one serving both HTTP and HTTPS took a bit over an hour, and was overall one of the smoothest software changes I've ever made.

First, I needed to have a server listening for HTTPS requests in addition to the one listening to HTTP requests. My website is built as a wai application and uses warp as the web server. For serving HTTPS traffic, there's the warp-tls package, which functions in essentially the same way. The code change was essentially changing from

runSettings (setPort port $ defaultSettings) ((>>=) . application)

to

do
    let runHttpServer =
            runSettings
                (setPort port $ defaultSettings)
                ((>>=) . application)
        runTlsServer =
            runTLS
                (tlsSettings tlsCertFile tlsKeyFile)
                (setPort tlsPort $ defaultSettings)
                ((>>=) . application)
    void $ concurrently runHttpServer runTlsServer

Then I needed to be able to serve the files that are relevant for the ACME challenge, which letsencrypt (the Let's Encrypt client) puts in a .well-known subdirectory. I already had code to serve subtrees of static content for content like css and javascript, so it was a simple matter of adding Route (SubtreePattern [".well-known"]) (staticR ".well-known/" ".well-known/") to the list of routes.

Amusingly, I forgot to recompile and restart my webserver the first time I tried running the letsencrypt client, so of course all of the challenges failed. Other than that, everything worked with no issues. After setting the appropriate file paths for the test certificates, I verified that I could get to the live site over HTTPS (with warnings about the certificate being invalid). Then I reran letsencrypt without the --test-cert flag to get real, signed certificates, restarted the webserver, and I was done!

Well, almost. I use two external resources on my site: the Istok Web font and MathJax. I had been loading these over HTTP, so they got blocked by my browser when the main page was HTTPS. Not a difficult problem to fix, but something that's easy to overlook at first.

Finally, since Let's Encrypt's certificates are only valid for 90 days, I need to make sure that my certificates get renewed regularly. This is supposed to be as easy as making letsencrypt renew run as a cron job, and the renew command is designed to have no interaction required. I'll find out in two months when my certificate comes close to expiring, but based on my experience so far I don't expect any issues.

Additionally, because I got three separate people asking me if my blog had an RSS feed (this was three more people than I expected, by the way), I decided that I'd look into RSS. The format seemed simple enough, and the rss package gave a nice interface for generating the actual XML.

I should mention that rather than using a content management system for my blog, I store the posts as files, and have the metadata directly in my server's source code. This means that both the posts and the metadata are checked in via git, and I already have the metadata that would be going into the RSS feed. This is what the main part of the code looks like:

do
    let blogFeedItem entry = do                                                            
            entryContent <- renderHtml <$> content entry                                   
            return $ 
                [ Title (Text.unpack (title entry))                                        
                , Link (pathURI $ "/blog/" <> (Text.unpack (id_ entry)))                   
                , PubDate (UTCTime                                                         
                    { utctDay = calendarDay (date entry)                                   
                    , utctDayTime = 86399
                    })
                , Description (LBS.unpack entryContent)
                ]   
    items <- traverse blogFeedItem (take 5 . reverse $ all_entries)                        
    let rss = RSS feedTitle (pathURI "/blog") "Thoughts about whatever comes to mind." [] items                                                                                         
    return $ responseLBS status200 [("Content-Type", "application/xml")] (fromString . showXML . rssToXML $ rss)

In reality, there's another 23 lines that are just there to deal with the fact that I'm using two different names. I'm not sure how many people have noticed, but you can get to this site either via www.brianhamrick.com or via www.extratricky.com. They have the same content, with the main difference being which name shows up in the title element. So those other 23 lines are adjusting the title of the feed and the domain for links accordingly. The feed is at /blog/feed.xml.

There's one other site improvement that I want to have, and haven't finished yet: comments. I actually put together a mostly working implementation a few weeks ago with the main blocker being styling the comment elements so that they look reasonable. I won't make any promises about when I'll get around to finishing it up, but I have had one person ask me why there are no comments on my blog, so I figure some others might be wondering the same thing.