home | O'Reilly's CD bookshelfs | FreeBSD | Linux | Cisco | Cisco Exam  


Writing Apache Modules with Perl and C
By:   Lincoln Stein and Doug MacEachern
Published:   O'Reilly & Associates, Inc.  - March 1999

Copyright © 1999 by O'Reilly & Associates, Inc.


 


   Show Contents   Previous Page   Next Page

Chapter 5 - Maintaining State
Maintaining State with Cookies

The other main client-side technique we'll consider uses HTTP cookies to store state information. HTTP cookies are named bits of information that are transmitted between the server and browser within the HTTP header. Ordinarily the server creates a cookie by including a Set-Cookie field in the HTTP header. The browser then stashes away the cookie information in a small in-memory or on-disk database. The next time the browser makes a request from that particular server, it returns that cookie in a Cookie field.

Cookies are relatively flexible. You can create cookies that will be returned to only one specific server or to any server in your domain. You can set them up so that they're returned only when users access a particular part of the document tree or any URI in the document hierarchy. They can be set to expire immediately when the user exits the browser, or they can be made to persist on the user's disk database for an extended period of time. You can also create secure cookies that are only returned to the server when a secure protocol, such as SSL, is in effect. This prevents cookies from being intercepted in transit by network eavesdroppers.

The exact format of HTTP cookies is somewhat involved and is described in the HTTP specification at http://www.w3.org/Protocols. Fortunately it's easy to make cookies in the right format using the CGI::Cookie module. To create a cookie with the name Hangman, a value equal to the hangman state variable $state, and an expiration time one month from now, you would call CGI::Cookie::new() in this way:

$cookie = CGI::Cookie->new(-name    => 'Hangman',
                          -value   => {WORD => 'terpitude',
                                       GAMENO => 1},
                          -expires => '+1M');

You can now send the cookie to the browser among the HTTP header fields using the -cookie argument to CGI.pm's header() method as shown here:

print header(-cookie => $cookie);

On subsequent invocations of the program you can retrieve named cookies sent by the browser with CGI.pm's cookie() method:

%cookie = cookie('Hangman');

Note that CGI.pm allows you to set and retrieve cookies that consist of entire hashes.

If you want to bypass CGI.pm and do the cookie management yourself within the Perl Apache API, you can use CGI::Cookie to create and parse the cookie format and then get the cookies in and out of the HTTP header using the Apache header_in() and header_out() methods. The experimental Apache::Request module also has cookie-handling functions.

Using the Perl Apache API, here's how to add a cookie to the HTTP header:

$r->header_out('Set-Cookie' => $cookie);

Here's how to retrieve and parse the cookies from the HTTP header and then find the one named Hangman:

%cookies = CGI::Cookie->parse($r->header_in('Cookie'));
$cookie = $cookies{'Hangman'};

Because we already require it for the hangman game, we'll use the CGI.pm shortcuts for cookie management. We only need to make a few changes to reimplement the hangman game to use cookies for state maintenance. The updated subroutines are shown in Example 5-2.

use CGI::Cookie ();
# retrieve the state
my $state = get_state() unless param('clear');

At the top of the file, in addition to importing functions from CGI.pm, we bring in the CGI::Cookie module. This isn't strictly necessary, since CGI.pm will do it for us, but it makes the code clearer. We retrieve the state as before by calling get_state(), but now we do it only if the CGI parameter clear is not defined. We'll see why we made this change later.

$state    = initialize($state) if !$state or param('restart');
my($message, $status) = process_guess(param('guess') || '', $state);
print header(-cookie => save_state($state)),
   start_html(-Title   => 'Hangman 2',
              -bgcolor => 'white',
              -onLoad  => 'if (document.gf) document.gf.guess.focus()'),
   h1('Hangman 2');

Next, having retrieved the state, we (re)initialize it if necessary in order to choose a fresh word at the beginning of a new game. We process the user's guess by calling process_guess() and then print out the HTTP header. Here's where we find the first big difference. Instead of sending the state information to the browser within the HTML body, we need to save it in the HTTP header. We call save_state() in order to create a correctly formatted cookie, then send it down the wire to the browser by passing it to CGI.pm's header() method as the value of the -cookie argument.

sub get_state {
   my %cookie = cookie(COOKIE_NAME);
   return undef unless %cookie;
   return \%cookie;
}
sub save_state {
   my $state = shift;
   return CGI::Cookie->new(-name => COOKIE_NAME,
                           -value => $state,
                           -expires => '+1M');
}

Turning our attention to the pivotal get_state() and save_state() functions, we see that get_state() calls CGI.pm's cookie() method to retrieve the value of the cookie named Hangman (stored in the constant COOKIE_NAME). cookie() takes care of flattening and expanding arrays and hashes for us (but not more complex structures, unfortunately), so we don't need to copy any fields to a separate $state variable, we just return a reference to the cookie hash itself! Similarly, in save_state(), we just turn the entire state structure into a cookie by passing it to CGI::Cookie::new(). We specify an expiration time of one month in the future (+1M). This allows the cookie to persist between browser sessions.

Because we don't have to mess around with hidden fields in this example, the show_guess_form() subroutine doesn't need to call save_state(). Likewise, we can remove the call to save_state() from show_restart_form(). The latter subroutine has an additional modification, the addition of a checkbox labeled "Clear scores" (see Figure 5-2). If the user selects this checkbox before pressing the new game button, the program clears out the state entirely, treating get_state() as if it returned an undefined value.

Figure 5-2. The improved version of the hangman game allows users to clear their aggregate scores and start over.

The rationale for this feature is to capitalize on a bonus that you get when you use persistent cookies. Because the cookie is stored on the user's disk until it expires, the user can quit the browser completely and come back to the game some days later to find it in exactly the state he left it. It's eerie and wonderful at the same time. Of course, the user might want to start out fresh, particularly if he hasn't been doing so well. The "Clear scores" checkbox lets him wipe the slate clean.

Example 5-2. The Hangman Game Using Cookies for State Maintenance

# file: hangman2.cgi
# hangman game using cookies to save state
use IO::File ();
use CGI qw(:standard);
use CGI::Cookie ();
use strict;
use constant WORDS => '/usr/games/lib/hangman-words';
use constant ICONS => '/icons/hangman';
use constant COOKIE_NAME => 'Hangman';
use constant TRIES => 6;
# retrieve the state
my $state = get_state() unless param('clear');
# reinitialize if we need to
$state    = initialize($state) if !$state or param('restart');
# process the current guess, if any
my($message, $status) = process_guess(param('guess') || '', $state);
# start the page
print header(-cookie => save_state($state)),
   start_html(-Title   => 'Hangman 2',
              -bgcolor => 'white',
              -onLoad  => 'if (document.gf) document.gf.guess.focus()'), 
h1('Hangman 2: Cookies');
. . . nothing in the middle is different . . .
# print the fill-out form for requesting input
sub show_guess_form {
   my $state = shift;
   print start_form(-name => 'gf'),
         "Your guess: ",
         textfield(-name => 'guess', -value => '', -override => 1),
         submit(-value => 'Guess');
   print end_form;
}
# ask the user if he wants to start over
sub show_restart_form {
   my $state = shift;
   print start_form,
         "Do you want to play again?",
         submit(-name => 'restart', -value => 'Another game'),
         checkbox(-name => 'clear', -label => 'Clear scores');
   delete $state->{WORD};
   print end_form;
}
# Retrieve an existing state
sub get_state {
   my %cookie = cookie(COOKIE_NAME);
   return undef unless %cookie;
   return \%cookie;
}
# Save the current state
sub save_state {
   my $state = shift;
   return CGI::Cookie->new(-name => COOKIE_NAME,
                           -value => $state,
                           -expires => '+1M');
}

Footnotes

2 Lincoln was very gratified when he tested the first working version of the game on his wife. She took over the computer and refused to give it back for hours!

   Show Contents   Previous Page   Next Page
Copyright © 1999 by O'Reilly & Associates, Inc.