Sunday, 14 June 2015

Sessions and Cookies in Perl with Dancer



Cookie Monster: Me need cookie! Me make cookie! But how?

Sir Ian McKellen: Well ... how many ways can a cookie crumble?


In this article we demonstrate three archetypal ways of implementing sessions with cookies using session factories in Dancer2.

A core principle behind the HTTP protocol is that it's stateless.  Every time you click on a link, your browser sends a request to a web server asking for a web page determined by the URL and any other data your browser sent. The server doesn't know or care who you are or what you've done - it simply responds to the information you've provided.

The simplicity of this protocol is one reason the World Wide Web took off so quickly - but on its own, services such as Internet banking would not be possible.

The solution to this is the cookie. A cookie is a snippet of data which is issued by the web server in response to a request. On subsequent requests, the browser passes it back. Each time the server gets a request with this cookie, it stores data against that cookie indicating that, for example, the cookie has been passed in by a user who knows the password of a particular account. As such they have permission to take actions only available to that person. The sequence of requests and responses where the server 'knows' who it's talking to is called a session.

There are many ways for the server to manage sessions in Dancer2, and we'll explain three techniques which cover all the common approaches.


Demonstration app - the Count von Count family tree


Here's some code for easily experimenting with the different approaches to sessions. The behaviour we will describe is tested when running under Dancer2 v0.160003.


#!/usr/bin/env perl

use Dancer2;
use Lingua::EN::Numbers qw/num2en_ordinal/;
set session => 'Simple';


get '/' => sub {
  if (session('user')) {
    session count => session('count') + 1;
    return
      '<a href="/">Click here</a> for the name of the eldest son of Count '.
      session('user').' von Count the '.
      '<b>'.num2en_ordinal(session('count')).'</b>';
  }
  return 'Cookie Monster!';
};

get '/login/:user' => sub {
    session user   => params->{user};
    session count  => 0;
    redirect '/';
};

dance;

Run this by simply storing it in a script like 'cookie.pl', make it executable and run it. If you just visit the index (/) URL it will just respond with 'Cookie Monster'. If you start by visiting the URL http://<hostname>/login/Andrew you'll get

Click here for the name of the eldest son of Count Andrew von Count the first

and as you click on the link you'll get responses

Click here for the name of the eldest son of Count Andrew von Count the second
Click here for the name of the eldest son of Count Andrew von Count the third
...


Approach 1: The ephemeral cookie key


In the code above, the line

set session => 'Simple';

was actually unnecessary since Simple sessions are switched on by default. The intention is to highlight to the reader the fact that we're using the Dancer2::Session::Simple session factory.

The principle is that when you visit the site, the app responds with a cookie (some random unique string) in the header. On subsequent requests, your browser sends that cookie along in the header and Dancer looks it up to find all the data you've stored against it in the session.

Now this seems to be working fine, but your site is getting popular so to improve performance you run it under Starman and Plack so you can serve the pages more quickly using middleware to gzip the images.

$ plackup -s Starman --port 3000 --workers 1  ./cookie.pl 

This is still working nicely but your site is getting so many Sesame street fans you realise you'll need another worker process managing all these HTTP requests, so you run:

$ plackup -s Starman --port 3000 --workers 2  ./cookie.pl

and things stop working. Give it a try! You visit the login page but then within the first couple of clicks you're back to the Cookie Monster response.

To give yourself a hint at what's going wrong, print the process id ($$) on each page. You'll find that as soon as Starman delegates an HTTP request to a different worker, it has no idea about the cookie and creates a new one with no session data stored against it.

The reason for this is that you've got two sessions, and each session factory stores the cookies in its own hash which is a variable. Variables are not shared across processes.


Approach 2: The fat cookie


To solve the problem above, you can replace

set session => 'Simple'

with


set engines => {
    session => {
        Cookie => { secret_key => 'my_secret_key' }
    }
};
set session => 'Cookie' ;

The Dancer2::Session::Cookie factory works by encrypting the hash of all the session data as a string and regarding that encryption as the cookie. In this way the server doesn't need to store anything at all, but if you have a lot of data in your session, it could slow things down considerably.



Approach 3: The persistent cookie key


Simply install memcached and change the settings to:

set engines => {
  session => {
    Memcached => { memcached_servers => 'localhost:11211' }
  }
};
set session => 'Memcached';

Memcached provides memory which can be shared between the worker processes on your host, and even between processes on different hosts. As such you can load balance between workers and they pass the cookie to the memcached server to retrieve the session data.


Conclusion

Apart from Simple, Cookie and Memcached there are at least eight other session factories implemented on top of Dancer2 but they all model the third approach above, using all types of server side storage from text files to databases. The conclusion to draw from this is that in production servers, the persistent cookie key is the standard approach.

1 comment: