Tea & Tech (🍵)

Safe-to-Wake Light, Part 6: Progressive Web App

February 16, 2020

I think I’m going to open this one with a couple screenshots:

As you can see, Squishy Kitty is now a mobile app! There’s even a desktop icon! 🎉

Now, I should clarify that Squishy Kitty is not a native mobile app. Squishy Kitty is a Progressive Web App (PWA for short). Basically, you can take any ol’ website, satisfy a couple special requirements, et voila! You will be able to install your website as an app on your phone!

This blog post is going to be a Long One™, so let’s follow this intro with a table of contents, shall we?

Table Of Contents

  1. Progressive Web App Requirements
  2. Running an Embedded Jetty Webserver
  3. Setting up SSL
  4. Securing the Network
  5. Conclusion

Progressive Web App Requirements

There are lots of great resources online to learn about building Progressive Web Apps, but the abridged version is that we need to meet three requirements:

  1. We need a manifest.json file
  2. We need to register a service worker
  3. We need to serve the app over HTTPS

The first two are straightforward — we just serve up a couple extra files with our other assets. I mostly just copy-pasted example manifest and service worker files from the internet and changed a couple lines to suit my stylistic preferences (add some icons, etc.).

Serving the application over HTTPS is a bit tricker, because it’s an application that’s not meant to be accessible from the outside world. We don’t want strangers controlling the light in my son’s bedroom, after all.

None of this, however, is possible without a webserver, so let’s set up Jetty to start serving assets.

Running an Embedded Jetty Webserver

A few weeks ago, Eric Normand of PurelyFunctional.tv fame published a Clojure Web Servers mini guide that explored available web server options in Clojure.

He recommended using Ring-Jetty for small side projects, and because Squishy Kitty falls squarely into this category, I decided to take his expert advice.

Setting it up took no time at all. The first step was to add some dependencies to my project.clj file:

diff --git a/project.clj b/project.clj
index 11cbf0f..dfa1dae 100644
--- a/project.clj
+++ b/project.clj
@@ -4,7 +4,13 @@
   :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
             :url "https://www.eclipse.org/legal/epl-2.0/"}
   :dependencies [[org.clojure/clojure "1.10.1"]
+                 [compojure "1.6.1"]
+                 [hiccup "1.0.5"]
+                 [ring/ring-core "1.6.3"]
+                 [ring/ring-devel "1.6.3"]
+                 [ring/ring-jetty-adapter "1.6.3"]
+                 [ring/ring-ssl "0.3.0"]
                  [me.raynes/conch "0.8.0"]]

Compojure will be used for routing, and Hiccup will be used for templating.

I should probably emphasize that the techniques used for serving assets over HTTP in this project are probably not what most commercial applications would choose due to the difference in requirements. I would expect there to be, at most, two simultaneous users on Squishy Kitty. In most cases (98% of the time), only one user.

In the interest of simplicity and maintainability, my goal has always to reduce the number of moving pieces, so using an embedded webserver seemed like the obvious choice. It’s the same reason we package our Python scripts in the resources directory and extract them: Deploying and running (new versions of) the app should be as simple as running the java -jar command.

Serving the application involves setting up routes and then running Jetty:

(ns squishy-kitty.routes
  (:require
   [compojure.core :refer [defroutes GET POST]]
   [compojure.handler :as handler]
   [compojure.route :as route]
   [hiccup.middleware :refer [wrap-base-url]]
   [squishy-kitty.handlers :refer [index-page toggle-light schedule-page set-schedule]]))

(defroutes main-routes
  (GET "/" [] (index-page))
  (GET "/schedule" [:as request] (schedule-page (request :params)))
  (POST "/api" [:as request] (toggle-light (request :multipart-params)))
  (POST "/schedule" [:as request] (set-schedule (request :multipart-params)))
  (route/resources "/")
  (route/not-found "Page not found"))

(def app
  (-> (handler/site main-routes)
      (wrap-base-url)))
(ns squishy-kitty.core
  (:require
   [ring.adapter.jetty :refer [run-jetty]]
   [squishy-kitty.routes :refer [app]])
  (:gen-class))

(defn -main []
  (run-jetty app {:port (Integer/parseInt (or (System/getenv "PORT") "8000"))))

We’ll expand on these when it comes to adding SSL, but that’s the gist of it. Because we identified squishy-kitty.core as our main entrypoint in our project.clj, we’ll launch Jetty as soon as we run our compiled JAR file with java -jar.

Setting up SSL

Setting up SSL means we need certificates, and when it comes to certificates, Let’s Encrypt makes it very fast and easy to do.

I installed the certificate directly onto the Raspberry Pi Zero like so:

sudo apt-get update
sudo apt-get install certbot
sudo certbot certonly --standalone

In order for the third step to complete successfully, I needed to follow the prompts, open some ports temporarily to the Pi, and set up some DNS rules as instructed.

Once certbot is done spitting out some files, we need to use them to create a PKCS12 Keystore. Jetty uses the keystore to store the private key and certificates used for establishing HTTPS connections.

That was accomplished with the following:

export CERTBOT_OUTPUT="/path/to/files/output/by/certbot"
export SK_SSL_PASSWORD="you'll use this password later, too"
export SK_KEYSTORE="/path/to/keystore.p12" # You are creating this file; it doesn't exist yet

cd $CERTBOT_OUTPUT

openssl pkcs12 -export -in fullchain.pem -inkey privkey.pem -out $SK_KEYSTORE -name squishy-kitty -CAfile chain.pem -caname root -password pass:$SK_SSL_PASSWORD

chmod a+r $SK_KEYSTORE

I recommend configuring your Pi to set the 3 environment variables above at boot time so they’re always defined when the Pi starts up.

Now that we have a keystore and a password defined, we can configure Jetty to serve them:

diff --git a/src/squishy_kitty/routes.clj b/src/squishy_kitty/routes.clj
index efe9b18..247eb4a 100644
--- a/src/squishy_kitty/routes.clj
+++ b/src/squishy_kitty/routes.clj
@@ -4,6 +4,7 @@
    [compojure.handler :as handler]
    [compojure.route :as route]
    [hiccup.middleware :refer [wrap-base-url]]
+   [ring.middleware.ssl :refer [wrap-ssl-redirect]]
    [squishy-kitty.handlers :refer [index-page toggle-light schedule-page set-schedule]]))

 (defroutes main-routes
@@ -16,4 +17,6 @@

 (def app
   (-> (handler/site main-routes)
+      (wrap-base-url)
+      (wrap-ssl-redirect {:ssl-port (Integer/parseInt (or (System/getenv "SSL_PORT")
+                                                          "8443"))})))
-      (wrap-base-url)))
diff --git a/src/squishy_kitty/core.clj b/src/squishy_kitty/core.clj
index 539da08..3d03a34 100644
--- a/src/squishy_kitty/core.clj
+++ b/src/squishy_kitty/core.clj
@@ -5,4 +5,8 @@
   (:gen-class))

 (defn -main []
+  (run-jetty app {:port (Integer/parseInt (or (System/getenv "PORT") "8000"))
+                  :ssl? true
+                  :ssl-port (Integer/parseInt (or (System/getenv "SSL_PORT") "8443"))
+                  :keystore (System/getenv "SK_KEYSTORE")
+                  :key-password (System/getenv "SK_SSL_PASSWORD")}))
-  (run-jetty app {:port (Integer/parseInt (or (System/getenv "PORT") "8000"))))

Now, when running our Squishy Kitty app, we just need to make sure that our environment variables are set, and we’ll be serving everything over HTTPS! That is, as long as our network topology is configure correctly…

Securing the Network

The secret sauce to getting Squishy Kitty running only on the local wifi while still being served over HTTPS is a touch of DNS magic. It might not make sense out of context, so let’s start with my local network topology:

INTERNET --> Verizon Router +--> Home Wifi --> Squishy Kitty (Pi Zero)
                            +-->  Pi Hole (DNS/Adblock)

My Verizon Fios router is locked down. It does not forward any ports, and the only incoming traffic it allows is traffic initiated by an internal request.

My Home Wifi router is configured to use the Pi Hole as its DNS server. No ads while you’re on the wifi; it’s fantastic!

The Pi Hole, in addition to falling back to 1.1.1.1 (Cloudflare) as a DNS server for valid requests, has a hosts file that is used to resolve domain names for devices on the network. THIS is the secret sauce to creating a Progressive Web App that only works on an internal network.

PWAs require valid SSL certificates, so you can’t use an internal IP address and get PWA functionality, because the certificate expects to be associated with a specific domain name (not an IP address).

Instead, you create an entry in your hosts file to forward squishy-kitty.your-domain.com to an internal IP address, any mobile device on your network will resolve that address to a local IP. In that way, the certificate you created will appear valid (because the device is using a URL), and all PWA functionality will work as intended.

In other words:

# in the pi-hole hosts file:
192.168.1.5     squishy-kitty.ajpierce.com

This ensures that Squishy Kitty can only be controlled by devices connected to the local wifi. The squishy-kitty subdomain doesn’t point to any webservers on the public internet, so the only place the app will resolve is on my local network, where it will forward you to an internal IP.

You may be tempted to forward ports externally so you can drive Squishy Kitty while you’re out and about. You can certainly do that if you choose! But I’d like to go on record stating that I think having any paths open to “smart devices” (even obfuscated paths!) inside your house is a Bad Idea™.

Conclusion

At some point in the future I may release the entire source code to the application, but it’s honestly nothing special. I didn’t invest much energy into the website, probably because I build websites all day for a living, so that wasn’t the interesting or challenging part to building Squishy Kitty.

Given enough time and energy, it could be a neat, interactive experience with a bleeding-edge tech stack, but none of that was necessary to get Squishy Kitty out into the wild. What’s more, it’s not like I need to worry about user retention or conversion, because it’s just me and Katie who will be using it.

Does it work? Yep!

Is it intuitive? Yep!

Is the technology new and interesting? Nope! I’m just serving static HTML and some hand-spun CSS.

The best part about Squishy Kitty is the experience of having built something by hand for the people I love. I learned a lot in the process, and I got to enjoy sharing it with all of you, too!

The ironic thing? Squishy Kitty has been busy changing colors for my son for some time now, but he still declines the opportunity to leave his room unless Katie or I come and get him 😂


Andrew J. Pierce collects Yixing teapots and lives in Virginia with his wife, son, and Ziggy the cat. You can follow him on Twitter.