Developer

Cookies in Perl

Perl can be an effective way write cookies. We'll walk you through the code and get you on your way.


By Nathan Torkington

The Web is designed to be stateless. Each document sent from server to browser is a unique transaction. Having sent a document, the server forgets about it, which greatly simplifies programming a Web server. But it doesn't simplify writing Web applications. To implement shopping carts, user preferences, or detailed visitor tracking, you need to recognize a user from one page to the next. By the time the Web needed this level of sophistication, it was too late to turn around and make it support states. So Netscape invented cookies.

A cookie is like an ID card that a server issues to a browser. Every time the browser requests something from the server, it flashes the ID card, with all of the information left by the server, as part of the transaction. To track people on your Web site, you issue them a cookie with identifying information on their first visit to your server. Every subsequent request they make will include that cookie, from which you can extract their identity.

In this article, I'll show you how to set and inspect cookies from CGI scripts, then demonstrate how to use them for useful purposes such as efficient, secure authentication.

Gotchas
Cookies aren't foolproof. Browsers often store cookie data in readable files, so don't use them to hold information such as credit card numbers or passwords. And the browser, which must send cookies back to the server, may not have them enabled. It's informative to clear the cookies from your Web browser, disable cookie functionality, then surf for a while to see how many of your favorite sites no longer work.

Cookies are also pretty limited in what they can hold. Netscape's original specification set limits at 4K of cookie data, 20 cookies per domain, and 300 cookies per browser. Browsers past their limit may prematurely expire, that is delete, older cookies. Some browsers go beyond these limits, but you can't rely on it.

Cookie attributes
Cookies contain a lot of information, the most useful being the name, a string that identifies the cookie, and the value, the data associated with that name.

If Web server www.example.com sends a cookie to your browser, your browser won't send it back to just any old server (www.malicioushacker.com, for example). The cookie has an associated domain, (such as .example.com or .intranet.example.com), and the browser sends the cookie only to servers that match that domain.

You can even limit which pages on the server receive the cookie with the path cookie option. A cookie with the path /staff will be sent to /staff/contacts.html or /staff-images/kevin.jpg, but not to /~loser/cookie-grabber.cgi.

In addition, you can specify that the cookie should be sent only for secure (HTTPS) requests. You might use this if you don't trust the network to be secure, but you do trust the client machine. However, remember that the Web browser may record the cookie in a file, so if that file can be read by a malicious hacker, it defeats the purpose of securing the network transmission.

And finally, you can set an expiration time for the cookie. A cookie with no expiration time disappears when the browser shuts down. You must specify expiration times precisely, such as Sun, 10-10-00 15:02:19 GMT, but dates such as these are inconvenient to work with. Fortunately, the CGI.pm module that we'll use to create and manipulate cookies has a simple notation for dates relative to the current date and time. The format for these relative dates and times is a positive or negative offset number, and a character representing the units (days, months, minutes, and so on). For example:



Creating a cookie
Let's begin by writing a program that creates a cookie and sends it to the browser. This program has two parts: a form for the user to enter something we'll remember, and a page to display when the cookie is set.

First, the form:
#!/usr/bin/perl -w
# cookie-set.cgi - set a cookie
 
use CGI qw(:standard);
 
unless (param()) {
   # display form
   print
      header(),
      start_html("Cookie Baker"),
      h1("Cookie Baker"),
      start_form(),
      p("What's your name?", textfield("NAME")),
      submit(),
     end_form(),
      end_html();
# cookie-set.cgi will be continued ...

Setting a cookie is a two-part process: first, create it with the cookie() function, then pass it to the browser when you send the HTTP header. In the remaining code, $to_set holds the cookie we create, and the -cookie argument to the header() function passes it to the browser.
# cookie-set.cgi continues ...
} else {
   # process form and set cookie
   $name = param("NAME");
   $to_set = cookie(-name => "username",
      -value => $name,
      -expires => "+30s",
      -path => "/~gnat/zd",
      );
   print
      header(-cookie => $to_set),
      start_html("Thanks!"),
      h1("Thanks for using the Cookie Baker"),
      p("I set your name to ", b($name),
      " and I will remember this if you visit ",
      a({-href => "cookie-get.cgi"}, "here"),
      " within the next 30 seconds."),
      end_html();
}

If we were setting multiple cookies, we'd pass an array reference to the header() function:
header(-cookie => [$name_cookie, $age_cookie, $city_cookie])

Fetch!
Fetching cookies is even easier. Just call the cookie() function with the name of the cookie. The function returns the value of the cookie. The CGI.pm module cannot return the other parameters of a cookie, such as domain, path, and expiration time.
#!/usr/bin/perl -w
# cookie-get.cgi - fetch the value of a cookie
use CGI qw(:standard);
 
$name = cookie("username");
 
print
   header(),
   start_html("Hello $name"),
   h1("Hello " . $name || "Stranger");
if ($name) {
   print p("See, I remembered your name!");
} else {
   print p("The cookie must have expired.");
}
print end_html();

Cookies with authentication
Cookies are often used to hold preferences, usernames, or session identifiers. One uncommon application, though, is as a complement to authentication. If you have an authentication system that is slow or otherwise consumes a lot of resources, using cookies can ease the load on your system.

It works like this: You have an htaccess-protected page where users log in. When they provide the necessary authentication, they're given a cookie and redirected to an unprotected CGI script. This script displays content to users who have valid cookies and redirects those who don't back to the protected login page.

This is known as a ticket system. The cookie acts as a ticket that users show to prove they have access. Think of it as the stamp you get on your hand when you go into a nightclub—you've shown your ID once and can buy as many drinks as you like without having to show ID again, as long as you display the stamp.

Enough talk, let's code it! Here's the directory structure:
/secure/.htaccess .htaccess file that sets authentication
/secure/index.cgi Authenticated program that sets cookie
/insecure/index.cgi Unauthenticated program that mediates access
/insecure/..... Files protected by /insecure/index.cgi

Here's a sample .htaccess file for the secure area:
AuthType Basic
AuthName "My Web site"
AuthUserFile /home/gnat/etc/.htpasswd-zd
require valid-user

And here's the secure/index.cgi program that sets the cookie:
#!/usr/bin/perl -w
# secure/index.cgi - set cookie showing user has authenticated
 
use CGI qw(:standard);
 
$cookie = cookie(-name => "ticket",
   -value => "admit 1",
   -expires => "+2m",
   -path => "/");
print redirect(-cookie => $cookie,
   -url => "../insecure/");

To simplify, let's make the Web server redirect all requests for insecure pages to the insecure/index.cgi program:
Alias /~gnat/zd/insecure/ /home/gnat/public_html/zd/insecure/index.cgi/

So that if the browser requests
/~gnat/zd/insecure/secrets.html

the server will redirect that request to the file
/home/gnat/public_html/zd/insecure/index.cgi/secrets.html

Now our insecure/index.cgi program need only look at path_info() (the stuff after the program name in the URL) to determine which file the user has requested. The full insecure/index.cgi program is here.

Notice that the script has to duplicate a lot of the Web server's behavior: working out which file to access, what type it is, and handling requests for nonexistent files. If you're concerned about this code duplication or the performance issues of having a CGI script mediate every request, move to mod_perl handlers, where you can use Apache's own code for the tricky stuff. It's easy to code, and the performance boost is incredible.

Authentication with mod_perl
Let's recode the insecure/index.cgi part of our ticketing system as a mod_perl handler. We'll use an authentication handler, configured in the httpd.conf file, thus:
PerlUse Apache::TicketSecure <Location /~gnat/zd/insecure>
   SetHandler perl-script
   PerlAuthzHandler Apache::TicketSecure
</Location>

Here's the source to the Apache::TicketSecure module:
package Apache::TicketSecure;
 
use Apache::Constants qw(:common);
use CGI::Cookie;
 
sub handler {
   my $r = shift;
   my %cookies = CGI::Cookie->parse($r->header_in("Cookie"));
   my $ticket = "";
 
   if (exists $cookies{ticket}) {
      $ticket = $cookies{ticket}->name || "";
   }
 
   if ($ticket ne "admit 1") {
      $r->header_out("Location" => "../secure/");
      return REDIRECT;
   }
   return OK;
}
 
1;

Much simpler! If the cookie doesn't check out, redirect to the authenticator in /secure/. If the cookie does check out, the authentication handler is done. The rest of the code in the original CGI program—filenames, type guessing, and returning errors—is left to the Web server, as such things should be.

But wait, there's more
There are a lot of security issues to consider. Cookies are basically sent in plaintext, so unless you're transmitting across an HTTPS connection, snoopers will be able to read the cookie and use it to get access to your site. You might want to generate a different cookie for each user, perhaps a convolution of each user's IP address and browser agent, to counter this. At some point, validating cookies could take as much processing bandwidth as regular authentication, so, as always, this is a matter of checks and balances.

CGI::Cookie
The CGI::Cookie module, which you can download from CPAN, gives a more powerful object-oriented interface to cookies than the standard CGI module. Unlike the CGI module, CGI::Cookie provides access to a cookie's expiration time, domain, and other attributes besides name and value. Each cookie is an object, and methods such as name, expires, and domain give you access to its attributes.

It is a simple change to make our initial cookie-set.cgi program use CGI::Cookie:
#!/usr/bin/perl -w
# cc-set.cgi - set cookie with CGI::Cookie
use CGI qw(:standard);
use CGI::Cookie;   # this line is new
 
unless (param()) {
   # display form
   print
      header(),
      start_html("Cookie Baker"),
      h1("Cookie Baker"),
      start_form(),
      p("What's your name?", textfield("NAME")),
      submit(),
      end_form(),
     end_html();
} else {
   # process form and set cookie
   $name = param("NAME");
   $to_set = CGI::Cookie->new(-name => "username",
      -value => $name,
      -expires   => "+30s",
      -path => "/~gnat/zd",
      );   # this has changed
   print
     header(-cookie => $to_set),
      start_html("Thanks!"),
      h1("Thanks for using the Cookie Baker"),
      p("I set your name to ", b($name),
       " and I will remember this if you visit ",
       a({-href => "cc-get.cgi"}, "here"),
      " within the next 30 seconds."),
   end_html();
}

As you can see, we needed to make only two changes: including CGI::Cookie and creating the cookie using CGI::Cookie. Truth be told, there's not much value in using CGI::Cookie over the regular CGI module for this program. There's not even a gain if we use CGI::Cookie to get the cookie:
#!/usr/bin/perl -w
# cc-get.cgi - get cookie with CGI::Cookie
use CGI qw(:standard);
use CGI::Cookie;
 
%cookies = CGI::Cookie->fetch;
$name = $cookies{username}->value;
 
print
   header(),
   start_html("Hello $name"),
   h1("Hello " . $name || "Stranger");
if ($name) {
   print p("See, I remembered your name!");
} else {
   print p("The cookie must have expired.");
}
print end_html();

I've found only two reasons to use CGI::Cookie: when you need access to the other cookie attributes and when you are using mod_perl. Using the CGI module just for its cookie handling would be wasteful, and CGI::Cookie is a fast, lightweight alternative.

Futher reading
The CGI module comes with extensive documentation, including information on the cookie() function. The CGI::Cookie module has similar documentation on its operations. There's a section on cookies in the second edition of CGI Programming with Perl, published by O'Reilly and Associates. And if you're using mod_perl, you can use a number of ticketing and session systems: Apache::AuthCookie, Apache::AuthTicket, Apache::Session, and Apache::ASP are just some of your options. All are available from CPAN.

Editor's Picks

Free Newsletters, In your Inbox