or: How I Learned to Stop Worrying and Love the WWW
Home RSS

Thoughts on Session Cookies w/ Perl Mojolicious

I've been spending the past few weekends with Mojolicious, a Perl web framework. There's plenty of great web frameworks out there; several of them for Perl if you're a Perl programmer like me (e.g. Dancer2, Catalyst, etc). I plan on making my next few posts about Mojolicious as it really has made web programming fun for me, trying new things and learning new things (sometimes breaking new things). In the process I've been migrating my homepage from Apache/CGI to Mojolicious::Lite.

One thing my website has had is an awesome MIDI (technically an mp3 now) soundtrack, which is great but has merely presented itself as a little player in the upper left corner of the page via this HTML:

<!-- Soundtrack -->
<!-- <embed> doesn't work anymore SAD
<embed src="/misc/Smashmouth_-_All_Star.mid" width="100%" height="60">-->
<audio src="/Music/Smashmouth-All-Star.mp3" autoplay controls
       preload="auto"></audio>

Using <embed> with a MIDI file is NFG nowadays so I've replaced it with an <audio> tag. The controls attritube lets you manually play the song since no modern browser will honor the autoplay attribute. I pieced together via some Stackoverflow posts such as this one that generally a browser won't play media without the user clicking on something. I decided one way to do this is with a fake GDPR compliance banner. People click accept on these all the time without even thinking about it the same way we do with software EULAs. This of course makes them completely useless and society would be better off without them but since they're seemingly here to stay we can at least try to make something useful out of them. What I came up with is some Javascript to play it like so:

<!-- Soundtrack -->
<audio id="soundtrack" src="/Music/Smashmouth-All-Star.mp3" preload="auto">
</audio>
<!-- "GDPR" banner -->
<div id="gdpr">
  <b>Notice:</b> This site uses kickass MIDI technology instead of cookies.
  <img alt="a compact disc playing music" src="/Pictures/Music_CD.gif"
       style="vertical-align: bottom"><br>
  <br>
  <button class="win95button" onclick="playIt();"><u>O</u>K</button>
  <button class="win95button" onclick="closeIt();"><u>C</u>ancel</button>
</div>
<script>
  function closeIt() {
      document.getElementById("gdpr").style.display = "none";
  }

  function playIt() {
      document.getElementById("soundtrack").play();
      closeIt();
  }
</script>

The way this works is the user clicks on “OK” (they always do!), then document.getElementById("soundtrack").play(); reliably triggers the music playback. Mission accomplished but now I have a new grievance: The banner keeps coming back no matter what. The real GDPR banner wouldn't do this even with all its own aggrivations. This is happening because HTTP is a stateless protocol. The irony in my fake GDPR banner is that I'll need to leverage session cookies to maintain state; to tell my homepage, “This user has already heard the joke.” I started by just adding another line to the closeIt() function:

function closeIt() {
    document.getElementById("gdpr").style.display = "none";
    document.cookie = "banner=seen; max-age=600;";
}

This sets my cookie as a plain-text cookie plus I set a max-age so it doesn't linger forever (I'll explain why in a bit). This is fine but because Mojolicious has a more sophisticated way of handling the session I wanted to let Mojolicious set the cookie. That posed a new challenge: HTTP is also a request/response conversation, thus Mojo would want to set its session cookie via a Set-Cookie: response header. However by the time the user sees the banner, the request is completed. I thought of two ways to get around this: Create a new route, let's say /session, then have my accept button point to that. The /session route could then redirect the user back to the value of the Referer: request header (that's the actual spelling) with the Set-Cookie: header in the 301 or 302 response.

Or, since I already have my plain-text cookie in place, why not just look for the presence of this cookie in subsequent requests and if we find it, then include our session cookie in the response? My max-age of 10 min means the plain-text cookie will eventually “roll off” if you will but the fancy, cryptographically signed session cookie will live on with its lifetime of an hour:

# Handle the session
under sub {
    my ($c) = @_;

    if ($c->cookie('banner') eq 'seen') {
        $c->session->{banner} //= 'seen'
    }

    1;
};

I'm using under in my Perl code (the controller) to save myself from having to repeat this logic in every route. Here's the end result according to the response headers:

[daniel@threadlake ~]$ curl -I -k -b 'banner=seen' https://www.swagg.net
HTTP/2 200
content-type: text/html;charset=UTF-8
set-cookie: mojolicious=eyJiYW5uZXIiOiJzZWVuIiwiZXhwaXJlcyI6MTYxNTc2Mjc0OX0---22455b29015766360fe791cb13cd16778e4fb197; expires=Sun, 14 Mar 2021 22:59:09 GMT; path=/; HttpOnly; SameSite=Lax
x-cloud-trace-context: 06fdb28b1bbd08a2b04c3d5d668a5afd;o=1
content-length: 5529
date: Sun, 14 Mar 2021 21:59:09 GMT
server: Google Frontend
via: 1.1 google
alt-svc: clear

We can decode the bit of the session cookie to the left of the three hyphens with, what else, perl:

[daniel@threadlake ~]$ perl -MMIME::Base64 -e 'print decode_base64("eyJiYW5uZXIiOiJzZWVuIiwiZXhwaXJlcyI6MTYxNTc2Mjc0OX0"), "\n";'
{"banner":"seen","expires":1615762749}

Expires in an hour rather than the 600 seconds (aka 10 minutes) of our plain-text cookie:

[daniel@threadlake ~]$ date -d @1615762749; echo "it is now: `date`"
Sun Mar 14 06:59:09 PM EDT 2021
it is now: Sun Mar 14 06:04:13 PM EDT 2021

That “stuff” to the right of the three hyphens is currently still a mystery to me. I imagine it has something to do with the HMAC-SHA1 signature that makes the session cookie tamper-proof. That's about all I can say about session cookies for one weekend. One last thing I want to share is another bonus use I found for my newfangled session. I can look for it in my templates/layouts to save the client from even having to download the mp3 file or GDPR banner resources on subsequent requests as well, sparing them some bandwidth:

<% unless (session('banner') eq 'seen') { %>
<!-- Soundtrack -->
<!-- <embed> doesn't work anymore SAD
<embed src="/misc/Smashmouth_-_All_Star.mid" width="100%" height="60">-->
<audio id="soundtrack" src="/Music/Smashmouth-All-Star.mp3" preload="auto">
</audio>
<!-- "GDPR" banner -->
<div id="gdpr">
  <b>Notice:</b> This site uses kickass MIDI technology instead of cookies.
  <img alt="a compact disc playing music" src="/Pictures/Music_CD.gif"
       style="vertical-align: bottom"><br>
  <br>
  <button class="win95button" onclick="playIt();"><u>O</u>K</button>
  <button class="win95button" onclick="closeIt();"><u>C</u>ancel</button>
</div>
<script>
  function closeIt() {
      document.getElementById("gdpr").style.display = "none";
      document.cookie = "banner=seen; max-age=600;";
  }

  function playIt() {
      document.getElementById("soundtrack").play();
      closeIt();
  }
</script>
<% } %>

Layouts and templates are also fun Mojo things I'll want to say more about in later posts. I look forward to sharing how I've actually deployed this thing via Docker container as I was pleasantly surprised on how painless this can be for someone who cut their teeth on Apache and CGI. Diving into Mojo has exposed me to newfangled concepts that I frankly wasn't particulary excited about before (I still maintain that “serverless” is a damn oxymoron!). I find myself more open to exploring them now and hopefully will give me cool things to muse on here in the future.

Source code for my cool new web 2.0 page is on my Codeberg and is, as always, “under construction”.

Happy GDPR-compliant cookie acceptance,

Dan

EDIT: Fixed some markdown formatting errors

#perl
#mojolicious