5.3 Working with cookies

by Marko Riedel

5.3.1 Idea

Cookies are a mechanism by which the HTTP protocol, which is stateless, can add persistence to a session. This is initiated by the server, which sends a cookie that the client stores locally. The client subsequently includes the cookie in its HTTP headers when it requests a page from the server (precisely what servers will receive a cookie is controlled by the path and domain properties of the cookie; cookies are sent with requests whose URL matches the path and the domain). The server could use cookies to store information that it has received from the client through a form submission. Usually just one cookie is used, however, namely the session id. The server maps the session id to the session state data.

We add a category to NSMutableDictionary that contains a method that initializes the dictionary with the variables that were passed in through the environment variable  HTTP_COOKIE. We also implement a method that outputs the content of a dictionary as a series of HTTP Set-Cookie headers.

Our test application adds two cookies to the set of cookies it has received from the client. These cookies have a lifetime of thirty seconds. The value of each cookie is the time since the Epoch (00:00:00 UTC, January 1, 1970), measured in seconds, so that the server can output the remaining time for each cookie it receives.

The syntax of the Set-Cookie header is like this (all of it on one line):

            Set-Cookie: KEY=VALUE  
            [; expires=DATE] [; path=PATH]  
            [; domain=DOMAIN_NAME] [; secure]

The date is required to have the format Wdy, DD-Mon-YYYY HH:MM:SS GMT.

The Cookie header that is returned by the client looks like this:

            KEY1=VALUE1; KEY2=VALUE2; ...

Note that the protocol as we use it here lets the server receive multiple key-value pairs in a single Cookie header, whereas we must send one Set-Cookie header for each pair we wish to set.

5.3.2 Preliminaries

We use a shell script as a wrapper; it should go into the CGI-BIN directory of your webserver, e.g. you could save it as /cgi-bin/cookies.sh.

#! /bin/sh  
export GNUSTEP_SYSTEM_ROOT=/usr/GNUstep  
export TZ=Europe/Berlin  
. $GNUSTEP_SYSTEM_ROOT/System/Makefiles/GNUstep.sh  

5.3.3 Implementation

Start by declaring PERSIST, a constant that defines the persistence of the cookies we produce. The method initWithCookie parses HTTP_COOKIE and stores the key-value pairs in the dictionary. The method cookieToString returns a string that is suitable for inclusion among the headers sent to the client. The special keys expires, domain, path and secure can be used to set the properties of the cookie.

#include <Foundation/Foundation.h> 
#define PERSIST 30 
@interface NSMutableDictionary (Cookies) 
- (NSMutableDictionary *)initWithCookie; 
- (NSString *)cookieToString; 

The first step is to retrieve the cookie from the process environment. There is work to be done if HTTP_COOKIE was not empty.

@implementation NSMutableDictionary (Cookies) 
- (id)initWithCookie 
    NSString *cookie = 
        [[[NSProcessInfo processInfo] environment] 
    [self initWithCapacity:1]; 

The algorithm works works like this: scan up to the first semicolon and split the key-value pair on the equal sign. Skip whatever whitespace you may find. Repeat until you reach the end of the string.

We need a character set for whitespace so that we may skip those characters. We intialize the scanner with the cookie and start a loop that ends when the scanner reaches the end of the string.

        NSCharacterSet *space = 
            [NSCharacterSet whitespaceAndNewlineCharacterSet]; 
        NSScanner *scn = 
            [NSScanner scannerWithString:cookie]; 
        while([scn isAtEnd]==NO){

We scan up to the first semicolon; pair holds those characters, but does not include the semicolon. We split the pair on the equal sign.

            NSString *pair; 
            if([scn scanUpToString:@”;” intoString:&pair]==YES){ 
                NSArray *entry = 
                     [pair componentsSeparatedByString:@”=”]; 
                NSString *key, *value;

We do some error checking. There should be exactly two fields: a key and a value. We raise an exception if this is not the case.

                if([entry count]!=2){ 
                         cookie, [entry count]]; 

Neither the key nor the value may be empty.

                key = [entry objectAtIndex:0]; 
                value = [entry objectAtIndex:1]; 
                if(![key length] ∣∣ ![value length]){ 
                         cookie, [key length], [value length]]; 

If we pass the two checks then we have found a good key-value pair and add it to the dictionary. We skip over the semicolon and any whitespace. This takes us to the first character of the next pair or to the end of the string.

                [self setObject:value forKey:key]; 
                [scn scanString:@”;” intoString:NULL]; 
                [scn scanCharactersFromSet:space 

We return the dictionary when we are done scanning.

    return self; 

The method cookieToString builds a set of Set-Cookie headers from the contents of the dictionary. It takes special care to process the cookie properties properly. There are two steps: first, processs all keys in the dictionary to build the cookie’s properties as well as an array of key-value assignments and second, output a Set-Cookie header for each pair, using the properties from the first step.

The array data holds key-value pairs and the array properties holds properties. There is a boolean to indicate whether the cookie is marked secure or not. The variable field holds a single item being added to one of the arrays. We iterate over all keys of the dictionary.

- (NSString *)cookieToString 
    NSMutableArray *data = 
        [NSMutableArray arrayWithCapacity:1]; 
    NSMutableArray *properties = 
        [NSMutableArray arrayWithCapacity:1]; 
    BOOL secure = NO; 
    NSString *field; 
    NSEnumerator *keyEnum = [self keyEnumerator]; 
    NSString *key; 
    while((key=[keyEnum nextObject])!=nil){

The properties domain and path are easy: we build the key-value pair and add it to the array of properties.

        if([key isEqualToString:@”domain”]==YES ∣∣ 
           [key isEqualToString:@”path”]==YES){ 
            field = 
                [NSString stringWithFormat:@”%@=%@;”, 
                           key, [self objectForKey:key]]; 
            [properties addObject:field]; 

The property expires is assumed to be a date object. We require a date string in the format that the protocol uses. We use a calendar format to obtain the result; note that we must use GMT for the time zone.

        else if([key isEqualToString:@”expires”]==YES){ 
           NSString *dateStr = 
               [[self objectForKey:key] 
                    timeZone:[NSTimeZone timeZoneWithName:@”GMT”] 
            field = 
                [NSString stringWithFormat:@”%@=%@;”, 
                           key, dateStr]; 
            [properties addObject:field]; 

We set the boolean indicator for the property secure if it is among the keys in the dictionary. All other entries of the dictionary are assumed to be data, i.e. key-value pairs. We format each pair according to the requirements and store it in the array data. This completes the first step.

        else if([key isEqualToString:@”secure”]==YES){ 
            secure = YES; 
            field = 
                [NSString stringWithFormat:@”%@=%@;”, 
                           key, [self objectForKey:key]]; 
            [data addObject:field]; 

We now have all the properties. This means that we can construct the property string, which stays the same for all pairs. We build a string that contains all properties separated by single spaces. If the property secure has been set, than it is included at the end of the property string.

    NSString *props = 
        [properties componentsJoinedByString:@”_”]; 
        props = [props stringByAppendingString:@”_secure”]; 

The second step is to build the result string, which starts out empty. We iterate over all data lines and construct a Set-Cookie header by including first the data, and then the properties. We return the result when we are done.

    NSString *result = @””, *line; 
    NSEnumerator *lineEnum = [data objectEnumerator]; 
    while((line=[lineEnum nextObject])!=nil){ 
        NSString *allFields = 
                line, props]; 
        result = 
            [result stringByAppendingString:allFields]; 
    return result; 

The remainder of this section shows how we test the code that we discussed above. Recall that our test program should add two new cookies each time it is invoked and display the cookies that it received and the time that remains until they expire.

We begin by obtaining the cookies that may have been passed in through HTTP_COOKIE. We also obtain a date that is thirty seconds in the future, or some other defined value. It determines the expiry of the new cookies that we are about to set.

#define NEWKEYS 2 
int main(int argc, char** argv, char **env) 
    NSAutoreleasePool *pool = [NSAutoreleasePool new]; 
    NSMutableDictionary *cookieValues = 
        [[NSMutableDictionary alloc] initWithCookie]; 
    NSDate *date = 
        [NSDate dateWithTimeIntervalSinceNow:PERSIST];

The new cookie set starts out containing two properties, the date and the path.

    NSMutableDictionary *newCookie = 
            dictionaryWithObjectsAndKeys:date, @”expires”, 
            @”/cgi-bin/cookies.sh”, @”path”, nil];

We must now add the key-value pairs, NEWKEYS items to be precise. The key should be a string containing the character ’k’, the last four digits of the time, and a three digit index to make it unique, and its value, most importantly, should be the current time, i.e. the time when it was created.

    long now = time(NULL); int k; 
    for(k=0; k<NEWKEYS; k++){ 
            *key = 
            [NSString stringWithFormat:@”k%04ld%03d”, 
                       now%10000, k], 
            *value = 
            [NSString stringWithFormat:@”%ld”, 
        [newCookie setObject:value forKey:key]; 

We are now ready to output HTML code. There are two sections: a list of all cookies that we received and how much longer each will last, some auxiliary information and a button that invokes cookies.sh. The set of cookies comes first, followed by the header and the beginning of the body of the document.

    printf(”%s”, [[newCookie cookieToString] cString]); 

We prepare to iterate over the key-value pairs that we received. There is an enumerator and a counter. The data will not use HTML markup, so we output a PRE tag.

    NSEnumerator *en; 
    NSString *key, *value; 
    int count = 1; 
    long rem; 

First we print the data (key-value pairs) that we received; this helps to debug the program. We sort the keys in lexical order and prepare to iterate over the array thus obtained.

           [[cookieValues cookieToString] cString]); 
    en = [[[cookieValues allKeys] 
    while((key = [en nextObject])!=nil){

We lookup up the value associated to each key. The remaining time for this key is the difference between the time that has elapsed since its creation and the total persistence time. Note that differences between the clocks of the server and the client can cause the client to remove the cookie a bit sooner or a bit later than required. We construct a string that contains the key, the value, and the remaining time, print it and increment the counter.

        value = [cookieValues objectForKey:key]; 
        rem = (long)PERSIST-(now-atol([value cString])); 
        NSString *item = 
            [NSString stringWithFormat:@”key_%d:_%@\n” 
                       count, key, count, value, 
                       count, rem]; 
        printf(”%s\n”, [item cString]); 

We output an indicator phrase that shows whether a cookie was received. We also print the Set-Cookie headers that we sent earlier, so the user knows what cookies to expect when he submits the form.

           ([cookieValues count]? ”Did” : ”Did_not”)); 
    printf(”%s\n”, [[newCookie cookieToString] cString]);

The second section of the document contains a button that reloads the script, thereby forcing the client to send the current set of cookies to the server.

    [pool release];