//
you're reading...

Programming

Implementing Your Own Cookie Storage

When you’re writing a client application of a web-based service that supports multiple logins you could find a situation where you need a private “cookie space” for each login. Especially when the server app relies on cookies Web Cookie Monsterto keep track of each account’s session. This is more apparent in 3rd party client applications where you couldn’t easily request changes to the server side and have the cookie-based session tracking removed.

I encountered this exact problem while working on Scuttlebutt – our upcoming Yammer client. Although Yammer provides a REST API for 3rd party clients, there are a number functionalities that are absent from the API and thus need to fall back to Yammer’s web interface. Unfortunately the web interface doesn’t make use of the OAuth2 access token normally used by the REST API but instead it relies on a number of cookies that were set just after the user logs in. If Scuttlebutt only support one account, this shouldn’t be a problem since it can just use Cocoa’s URL loading system to handle those cookies. But supporting multiple Yammer accounts means needing to have a separate “cookie space” for each account and that means I needed to implement my own cookie storage and plug it in to Cocoa’s web client infrastructure.

How Cookies Work

In simple terms, an HTTP cookie is a key-value pair set by a web server as it sends a web page back to the browser. In turn the web browser is expected to return those key-value pairs back to the web server as it requests subsequent pages from the same server. Of course there are variations to that generic flow such as the cookie’s scope – which web pages that can also receive those cookies apart from its originating web page, how long should the browser keep the cookie, and various other tidbits. Wikipedia has a more extensive overview on HTTP cookies if you like to dive deeper on this.

Because cookies are set by the server and then sent back by the browser, there are two integration points where you need to handle cookies from a web client’s perspective:

  1. Extract the cookies from an HTTP response and save them somewhere.
  2. For each HTTP requests, decide which cookies that are appropriate and embed it in the request.

Plugging-in Your Custom Cookie Storage

By default the URL Loading System handles cookies for you and will save these cookies in a shared location. In OS X, cookies are shared on a per-user basis. All applications will share the same set of cookies as far as the user is concerned, but different users of the same machine will have their own set of cookies shared across all applications that they use. However there is no notion of multiple users on an iOS device and thus cookies are private for each application.

Both of those approaches imply that an application cannot present the illusion of more than a single user for a given website. To do that, you need to disable Cocoa’s cookie handling system and plug in your own implementation.

Plain HTTP Request / Response

To plug in your own cookie handling system to HTTP requests you make yourself, you’ll need to call [NSMutableURLRequest setHTTPShouldHandleCookies: NO] to disable the system’s built-in cookie handling. Then you’ll need to add in those cookies to the HTTP headers just before you run the request.

The code for a request could look like this:

// requestURL is the target URL
NSMutableURLRequest* request = [NSMutableURLRequest requestURL:requestURL];

// disable the default cookie handling
[request setHTTPShouldHandleCookies:NO];

// cookieStorage is your custom cookie storage object 
// which should add in the appropriate cookies as HTTP header values
[cookieStorage handleCookiesInRequest:request];

// run the request
NSData* result = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error]

Whereas the code for handling a response looks like the following:

NSHTTPURLResponse* response = …;

// cookieStorage is your custom cookie storage object 
// which should scan for cookies in the response's HTTP headers and save them for later.
[cookieStorage handleCookiesInResponse:response];

You can use a similar approach to the above when you use AFNetworking‘s AFHTTPRequestOperation class. I’ll explain more on cookieStorage and handleCookiesInRequest: later in this article.

Plugging Your Own Custom Cookie Storage to the WebView class

Adapting a WebView to use your own cookie storage is also quite similar. The primary difference is that instead of creating your own NSHTTPRequest and NSHTTPResponse instances, you’ll need to implement WebView’s delegate methods so you can modify the ones that it uses. You do this in your WebResourceLoadDelegate protocol implementation and set your implementation object as the WebView‘s resourceLoadDelegate. Implement these methods of the protocol to hook WebView‘s URL loading system to your cookie handling code:

  • webView: resource: didReceiveResponse: fromDataSource:
  • webView: willSendRequest: redirectResponse: fromDataSource:

The following code snippet shows you how to do this:

- (void)webView:(WebView *)sender resource:(id)identifier didReceiveResponse:(NSURLResponse *)response fromDataSource:(WebDataSource *)dataSource
{
	if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
		// cookieStorage is your cookie storage implementation.
		BSHTTPCookieStorage* cookieStorage = ...;
		[cookieStorage handleCookiesInResponse:(NSHTTPURLResponse*) response];
	}
}


- (NSURLRequest *)webView:(WebView *)sender resource:(id)identifier willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse fromDataSource:(WebDataSource *)dataSource
{
	// cookieStorage is your custom cookie storage implementation.
	BSHTTPCookieStorage* cookieStorage = ...;
	if ([redirectResponse isKindOfClass:[NSHTTPURLResponse class]]) {
		[cookieStorage handleCookiesInResponse:(NSHTTPURLResponse*) redirectResponse];
	}
	
	NSMutableURLRequest* modifiedRequest = [request mutableCopy];
	[modifiedRequest setHTTPShouldHandleCookies:NO];
	[cookieStorage handleCookiesInRequest:modifiedRequest];
	return modifiedRequest;
}

You’ll need to capture cookies both when the WebView receives a response and when it receives a redirect. That’s why there are calls to handleCookiesInResponse in both method implementations. Whereas sending cookies is only done in one place,  webView: willSendRequest: redirectResponse: fromDataSource: that is called when the WebView is about to execute an NSURLRequest and gives you a chance to modify that request.

Onwards to Implementation!

As you can see from the above usage scenarios, these are the bare minimum external interfaces that you need to write a cookie storage class:

  • -(void) handleCookiesInResponse: (NSHTTPURLResponse*) response;
  • -(void) handleCookiesInRequest: (NSMutableURLRequest*) request;

The first method above reads HTTP response headers for cookies and saves those cookies when found. Whereas the second method uses the URL in the request and modifies the HTTP headers to add in the appropriate cookies for that URL.

Accordingly the class should also need an internal method to save cookies are found in a response and another one to determine what cookies that are applicable for a given URL:

  • - (void) setCookie: (NSHTTPCookie *) aCookie;
  • - (NSArray *) cookiesForURL: (NSURL *) theURL;

Caveat Emptor!

If you need to persist your cookies, please be aware that NSHTTPCookie doesn’t support serialization. As of OS X 10.8 and iOS 6.0 the class still doesn’t implement the NSCoding interface that’s needed to serialize the object into persistent storage. Fortunately it’s pretty straightforward to add serialization support to the class via an Objective-C Category (hint: use the properties and initWithProperties: methods to convert the between NSHTTPCookie and NSDictionary objects).

Introducing BSHTTPCookieStorage

I’ve coded up a bare-bones HTTP cookie storage implementation class that you can find below. It’s a pretty basic implementation and doesn’t support cookie expiration (e.g. cookies are retained indefinitely until overwritten by another one). However the cookie storage supports NSCopying and NSCoding protocols and thus can be cloned and saved to persistent storage. This is the same class that Scuttlebutt uses as part of its Yammer API access classes.

BSHTTPCookieStorage.h

//
//  BSHTTPCookieStorage.h
//
//      Created by Sasmito Adibowo on 02-07-12.
//  Copyright (c) 2012 Basil Salad Software. All rights reserved.
//  http://basilsalad.com
//
//  Licensed under the BSD License <http://www.opensource.org/licenses/bsd-license>
//  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
//  EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
//  OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
//  SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
//  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
//  TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
//  BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
//  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
//  THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#import <Foundation/Foundation.h>


/**
 Stores cookies.
 */
@interface BSHTTPCookieStorage : NSObject<NSCoding,NSCopying>

- (NSArray *)cookiesForURL:(NSURL *)theURL;

- (void)setCookie:(NSHTTPCookie *)aCookie;

/**
 Removes all stored cookies from this storage
 */

-(void) reset;

-(void) loadCookies:(id) cookies;
-(void) handleCookiesInRequest:(NSMutableURLRequest*) request;
-(void) handleCookiesInResponse:(NSHTTPURLResponse*) response;


@end


// ---


@interface NSHTTPCookie (BSHTTPCookieStorage) <NSCoding>

@end

// ---

BSHTTPCookieStorage.m

//
//  BSHTTPCookieStorage.m
//
//  Created by Sasmito Adibowo on 02-07-12.
//  Copyright (c) 2012 Basil Salad Software. All rights reserved.
//  http://basilsalad.com
//
//  Licensed under the BSD License <http://www.opensource.org/licenses/bsd-license>
//  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
//  EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
//  OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
//  SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
//  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
//  TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
//  BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
//  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
//  THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


// this is ARC code.
#if !__has_feature(objc_arc)
#error Need automatic reference counting to compile this.
#endif

#import "BSHTTPCookieStorage.h"


@interface BSHTTPCookieStorage() <NSCoding>

/*
 Cookie storage is stored in the order of
 domain -> path -> name
 
 This one stores cookies that are subdomain specific
 */
@property (nonatomic,strong,readonly) NSMutableDictionary* subdomainCookies;

/*
 Cookie storage is stored in the order of
 domain -> path -> name
 
 This one stores cookies global for a domain.
 */
@property (nonatomic,strong,readonly) NSMutableDictionary* domainGlobalCookies;


@end


@implementation BSHTTPCookieStorage

@synthesize subdomainCookies = _subdomainCookies;
@synthesize domainGlobalCookies = _domainGlobalCookies;

- (void)setCookie:(NSHTTPCookie *)aCookie
{
    // only domain names are case insensitive
    NSString* domain = [[aCookie domain] lowercaseString]; 
    NSString* path = [aCookie path];
    NSString* name = [aCookie name];
    
    NSMutableDictionary* domainStorage = [domain hasPrefix:@"."] ? self.domainGlobalCookies : self.subdomainCookies;
    
    NSMutableDictionary* pathStorage = [domainStorage objectForKey:domain];
    if (!pathStorage) {
        pathStorage = [NSMutableDictionary new];
        [domainStorage setObject:pathStorage forKey:domain];
    }
    NSMutableDictionary* nameStorage = [pathStorage objectForKey:path];
    if (!nameStorage) {
        nameStorage = [NSMutableDictionary new];
        [pathStorage setObject:nameStorage forKey:path];
    }

    [nameStorage setObject:aCookie forKey:name];
}


- (NSArray *)cookiesForURL:(NSURL *)theURL
{
    NSMutableArray* resultCookies = [NSMutableArray new];
    NSString* cookiePath = [theURL path];
    
    void (^cookieFinder)(NSString*,NSDictionary*) = ^(NSString* domainKey,NSDictionary* domainStorage) {
        NSMutableDictionary* pathStorage = [domainStorage objectForKey:domainKey];
        if (!pathStorage) {
            return;
        }
        for (NSString* path in pathStorage) {
            if ([path isEqualToString:@"/"] || [cookiePath hasPrefix:path]) {
                NSMutableDictionary* nameStorage = [pathStorage objectForKey:path];
                [resultCookies addObjectsFromArray:[nameStorage allValues]];
            }
        }
    };

    NSString* cookieDomain = [[theURL host] lowercaseString];
    
    cookieFinder(cookieDomain,self.subdomainCookies);
    
    // delete the fist subdomain
    NSRange range = [cookieDomain rangeOfString:@"."];
    if (range.location != NSNotFound) {
        NSString* globalDomain = [cookieDomain substringFromIndex:range.location];
        cookieFinder(globalDomain,self.domainGlobalCookies);
    }
    
    return resultCookies;
}


-(void) loadCookies:(id) cookies
{
    for (NSHTTPCookie* cookie in cookies) {
        [self setCookie:cookie];
    }
}


-(void) handleCookiesInRequest:(NSMutableURLRequest*) request
{
    NSURL* url = request.URL;
    NSArray* cookies = [self cookiesForURL:url];
    NSDictionary* headers = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
    
    NSUInteger count = [headers count];
    __unsafe_unretained id keys[count], values[count];
    [headers getObjects:values andKeys:keys];
    
    for (NSUInteger i=0;i_subdomainCookies = [self.subdomainCookies mutableCopy];
        copy->_domainGlobalCookies = [self.domainGlobalCookies mutableCopy];
    }
    return copy;
}

@end

// ---

@implementation NSHTTPCookie (BSHTTPCookieStorage)

-(id)initWithCoder:(NSCoder *)aDecoder
{
    NSDictionary* cookieProperties = [aDecoder decodeObjectForKey:@"cookieProperties"];
    if (![cookieProperties isKindOfClass:[NSDictionary class]]) {
        // cookies are always immutable, so there's no point to return anything here if its properties cannot be found.
        return nil;
    }
    self = [self initWithProperties:cookieProperties];
    return self;
}


-(void) encodeWithCoder:(NSCoder *)aCoder
{
    NSDictionary* cookieProperties = self.properties;
    if (cookieProperties) {
        [aCoder encodeObject:cookieProperties forKey:@"cookieProperties"];
    }
}

@end

// ---

The code above is also available as a Github Gist.

That’s all for now. Thanks for reading this ^_^



Do you enjoy this post? Enter your e-mail address below to receive articles like this one in your mailbox.

Free Updates!

Learn how to grow your indie business while keeping your day job.

Categories

Archives

Keep updated!

Don't miss out on new articles!
%d bloggers like this: