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
- Progressive Web App Requirements
- Running an Embedded Jetty Webserver
- Setting up SSL
- Securing the Network
- 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:
- We need a manifest.json file
- We need to register a service worker
- 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 š