Permalink
Browse files

Added cookie support, including per-request cookies (request and resp…

…onse) and session persistant cookies

One or two small bug fixes
  • Loading branch information...
pokeb committed Aug 25, 2008
1 parent d001857 commit bc988eeaa7f29a5a3acb597be247643b90844bd1
View
@@ -0,0 +1,34 @@
+//
+// ASIHTTPCookie.h
+// asi-http-request
+//
+// Created by Ben Copsey on 25/08/2008.
+// Copyright 2008 All-Seeing Interactive. All rights reserved.
+//
+
+#import <Cocoa/Cocoa.h>
+
+
+@interface ASIHTTPCookie : NSObject {
+ NSString *name;
+ NSString *value;
+ NSDate *expires;
+ NSString *path;
+ NSString *domain;
+ BOOL requiresHTTPS;
+}
+
+- (void)setValue:(NSString *)newValue forProperty:(NSString *)property;
+
++ (NSMutableArray *)cookiesFromHeader:(NSString *)header;
++ (NSString *)urlEncodedValue:(NSString *)string;
++ (NSString *)urlDecodedValue:(NSString *)string;
+
+@property (retain) NSString *name;
+@property (retain) NSString *value;
+@property (retain) NSDate *expires;
+@property (retain) NSString *path;
+@property (retain) NSString *domain;
+@property (assign) BOOL requiresHTTPS;
+
+@end
View
@@ -0,0 +1,100 @@
+//
+// ASIHTTPCookie.m
+// asi-http-request
+//
+// Created by Ben Copsey on 25/08/2008.
+// Copyright 2008 All-Seeing Interactive. All rights reserved.
+//
+
+#import "ASIHTTPCookie.h"
+
+@implementation ASIHTTPCookie
+
+- (void)setValue:(NSString *)newValue forProperty:(NSString *)property
+{
+ NSString *prop = [property lowercaseString];
+ if ([prop isEqualToString:@"expires"]) {
+ //[self setExpires:[NSDate dateFrom
+ return;
+ } else if ([prop isEqualToString:@"domain"]) {
+ [self setDomain:newValue];
+ return;
+ } else if ([prop isEqualToString:@"path"]) {
+ [self setPath:newValue];
+ return;
+ } else if ([prop isEqualToString:@"secure"]) {
+ [self setRequiresHTTPS:[newValue isEqualToString:@"1"]];
+ return;
+ }
+ if (![self name] && ![self value]) {
+ [self setName:property];
+ [self setValue:newValue];
+ }
+}
+
+
+// I know this looks like a really ugly way to parse the Set-Cookie header, but I'd guess this is probably one of the simplest methods!
+// You can't rely on a comma being a cookie delimeter, since it's quite likely that the expiry date for a cookie will contain a comma
+
+
++ (NSMutableArray *)cookiesFromHeader:(NSString *)header
+{
+ NSMutableArray *cookies = [[[NSMutableArray alloc] init] autorelease];
+ ASIHTTPCookie *cookie = [[[ASIHTTPCookie alloc] init] autorelease];
+
+ NSArray *parts = [header componentsSeparatedByString:@"="];
+ int i;
+ NSString *name;
+ NSString *value;
+ NSArray *components;
+ NSString *newKey;
+ NSString *terminator;
+ for (i=0; i<[parts count]; i++) {
+ NSString *part = [parts objectAtIndex:i];
+ if (i == 0) {
+ name = part;
+ continue;
+ } else if (i == [parts count]-1) {
+ [cookie setValue:[ASIHTTPCookie urlDecodedValue:part] forProperty:name];
+ [cookies addObject:cookie];
+ continue;
+ }
+ components = [part componentsSeparatedByString:@" "];
+ newKey = [components lastObject];
+ value = [part substringWithRange:NSMakeRange(0,[part length]-[newKey length]-2)];
+ [cookie setValue:[ASIHTTPCookie urlDecodedValue:value] forProperty:name];
+
+ terminator = [part substringWithRange:NSMakeRange([part length]-[newKey length]-2,1)];
+ if ([terminator isEqualToString:@","]) {
+ [cookies addObject:cookie];
+ cookie = [[[ASIHTTPCookie alloc] init] autorelease];
+ }
+ name = newKey;
+ }
+
+ return cookies;
+
+}
+
++ (NSString *)urlDecodedValue:(NSString *)string
+{
+ NSMutableString *s = [NSMutableString stringWithString:[string stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
+ //Also swap plus signs for spaces
+ [s replaceOccurrencesOfString:@"+" withString:@" " options:NSLiteralSearch range:NSMakeRange(0, [s length])];
+ return [NSString stringWithString:s];
+}
+
++ (NSString *)urlEncodedValue:(NSString *)string
+{
+ return [string stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
+}
+
+@synthesize name;
+@synthesize value;
+@synthesize expires;
+@synthesize path;
+@synthesize domain;
+@synthesize requiresHTTPS;
+@end
+
+
View
@@ -32,9 +32,18 @@
//Dictionary for custom HTTP request headers
NSMutableDictionary *requestHeaders;
- //Will be populate with HTTP response headers from the server
+ //Will be populated with HTTP response headers from the server
NSDictionary *responseHeaders;
+ //Can be used to manually insert cookie headers to a request, but it's more likely that sessionCookies will do this for you
+ NSMutableArray *requestCookies;
+
+ //Will be populated with Cookies
+ NSMutableArray *responseCookies;
+
+ //If use cokie persistance is true, network requests will present valid cookies from previous requests
+ BOOL useCookiePersistance;
+
//If useKeychainPersistance is true, network requests will attempt to read credentials from the keychain, and will save them in the keychain when they are successfully presented
BOOL useKeychainPersistance;
@@ -215,6 +224,17 @@
// Remove credentials from the keychain
+ (void)removeCredentialsForHost:(NSString *)host port:(int)port protocol:(NSString *)protocol realm:(NSString *)realm;
+// Store cookies for a particular request in the session
++ (void)recordCookiesInSessionForRequest:(ASIHTTPRequest *)request;
+
++ (void)setSessionCookies:(NSMutableArray *)newSessionCookies;
++ (NSMutableArray *)sessionCookies;
+
+// Dump all session data (authentication and cookies)
++ (void)clearSession;
+
+
+
@property (retain) NSString *username;
@property (retain) NSString *password;
@property (retain) NSString *domain;
@@ -232,6 +252,9 @@
@property (retain) NSError *error;
@property (assign,readonly) BOOL complete;
@property (retain) NSDictionary *responseHeaders;
+@property (retain) NSMutableArray *requestCookies;
+@property (retain) NSMutableArray *responseCookies;
+@property (assign) BOOL useCookiePersistance;
@property (retain) NSDictionary *requestCredentials;
@property (assign) int responseStatusCode;
@property (retain) NSMutableData *receivedData;
View
@@ -11,6 +11,7 @@
// See: http://developer.apple.com/samplecode/ImageClient/listing37.html
#import "ASIHTTPRequest.h"
+#import "ASIHTTPCookie.h"
static NSString *NetworkRequestErrorDomain = @"com.Your-Company.Your-Product.NetworkError.";
@@ -21,6 +22,7 @@
static CFHTTPAuthenticationRef sessionAuthentication = NULL;
static NSMutableDictionary *sessionCredentials = nil;
+static NSMutableArray *sessionCookies = nil;
static void ReadStreamClientCallBack(CFReadStreamRef readStream, CFStreamEventType type, void *clientCallBackInfo) {
@@ -51,6 +53,8 @@ - (id)initWithURL:(NSURL *)newURL
responseHeaders = nil;
[self setUseKeychainPersistance:NO];
[self setUseSessionPersistance:YES];
+ [self setUseCookiePersistance:YES];
+ [self setRequestCookies:[[[NSMutableArray alloc] init] autorelease]];
didFinishSelector = @selector(requestFinished:);
didFailSelector = @selector(requestFailed:);
delegate = nil;
@@ -164,6 +168,55 @@ - (void)main
//Set your own boundary string only if really obsessive. We don't bother to check if post data contains the boundary, since it's pretty unlikely that it does.
NSString *stringBoundary = @"0xKhTmLbOuNdArY";
+ //Add cookies from session
+ if (useCookiePersistance && [[ASIHTTPRequest sessionCookies] count] > 0) {
+ ASIHTTPCookie *requestCookie;
+ ASIHTTPCookie *storedCookie;
+ for (storedCookie in sessionCookies) {
+ BOOL foundExistingCookie = NO;
+ //Look for existing cookies in the request - these will always take precedence over session stored cookies
+ for (requestCookie in requestCookies) {
+ if ([[requestCookie domain] isEqualToString:[storedCookie domain]]) {
+ if ([[requestCookie path] isEqualToString:[storedCookie path]] || (![requestCookie path] && ![storedCookie path])) {
+ if ([[requestCookie name] isEqualToString:[storedCookie name]]) {
+ foundExistingCookie = YES;
+ break;
+ }
+ }
+ }
+ }
+ if (!foundExistingCookie) {
+ [requestCookies addObject:storedCookie];
+ }
+ }
+ }
+
+ //Apply request cookies
+ if ([requestCookies count] > 0) {
+ ASIHTTPCookie *cookie;
+ NSString *cookieHeader = nil;
+ for (cookie in requestCookies) {
+ //Ensure the cookie is valid for this request
+ if ([[[url host] substringWithRange:NSMakeRange([[url host] length]-[[cookie domain] length],[[cookie domain] length])] isEqualToString:[cookie domain]]) {
+ if ([[[url path] substringWithRange:NSMakeRange(0,[[cookie path] length])] isEqualToString:[cookie path]]) {
+ if (![cookie requiresHTTPS] || [[url port] intValue] == 443) {
+ if (![cookie expires] || [[cookie expires] timeIntervalSinceNow] > 0) {
+ if (!cookieHeader) {
+ cookieHeader = [NSString stringWithFormat: @"%@=%@",[cookie name],[ASIHTTPCookie urlEncodedValue:[cookie value]]];
+ } else {
+ cookieHeader = [NSString stringWithFormat: @"%@; %@=%@",cookieHeader,[cookie name],[ASIHTTPCookie urlEncodedValue:[cookie value]]];
+ }
+ }
+ }
+ }
+ }
+ }
+ if (cookieHeader) {
+ [self addRequestHeader:@"Cookie" value:cookieHeader];
+ }
+ }
+
+
//Add custom headers
NSString *header;
for (header in requestHeaders) {
@@ -207,8 +260,6 @@ - (void)main
}
[postBody appendData:[[NSString stringWithFormat:@"\r\n--%@--\r\n",stringBoundary] dataUsingEncoding:NSUTF8StringEncoding]];
-
- NSString *foo = [[[NSString alloc] initWithBytes:[postBody bytes] length:[postBody length] encoding:NSUTF8StringEncoding] autorelease];
// Set the body.
CFHTTPMessageSetBody(request, (CFDataRef)postBody);
@@ -422,6 +473,16 @@ - (BOOL)readResponseHeadersReturningAuthenticationFailure
[self performSelectorOnMainThread:@selector(resetDownloadProgress:) withObject:[NSNumber numberWithDouble:contentLength] waitUntilDone:YES];
}
}
+
+ //Handle cookies
+ NSString *cookieHeader = [responseHeaders valueForKey:@"Set-Cookie"];
+ if (cookieHeader) {
+ [self setResponseCookies:[ASIHTTPCookie cookiesFromHeader:cookieHeader]];
+ if (useCookiePersistance) {
+ [ASIHTTPRequest recordCookiesInSessionForRequest:self];
+ }
+ }
+
}
}
@@ -768,6 +829,57 @@ + (void)removeCredentialsForHost:(NSString *)host port:(int)port protocol:(NSStr
}
++ (void)recordCookiesInSessionForRequest:(ASIHTTPRequest *)request
+{
+ if (!sessionCookies) {
+ [self setSessionCookies:[[[NSMutableArray alloc] init] autorelease]];
+ }
+ ASIHTTPCookie *newCookie;
+ ASIHTTPCookie *storedCookie;
+ for (newCookie in [request responseCookies]) {
+ //If we didn't get a domain for the cookie, let's add the one from this request, so we aren't sending cookies from the wrong server later on
+ if (![newCookie domain]) {
+ [newCookie setDomain:[[request url] host]];
+ }
+ int i = 0;
+ BOOL foundExistingCookie = NO;
+ for (storedCookie in sessionCookies) {
+ if ([[storedCookie domain] isEqualToString:[newCookie domain]]) {
+ if ([[storedCookie path] isEqualToString:[newCookie path]] || (![storedCookie path] && ![newCookie path])) {
+ if ([[storedCookie name] isEqualToString:[newCookie name]]) {
+ foundExistingCookie = YES;
+ [sessionCookies replaceObjectAtIndex:i withObject:newCookie];
+ break;
+ }
+ }
+ }
+ i++;
+ }
+ if (!foundExistingCookie) {
+ [sessionCookies addObject:newCookie];
+ }
+ }
+}
+
++ (NSMutableArray *)sessionCookies
+{
+ return sessionCookies;
+}
+
++ (void)setSessionCookies:(NSMutableArray *)newSessionCookies
+{
+ [sessionCookies release];
+ sessionCookies = [newSessionCookies retain];
+}
+
+// Dump all session data (authentication and cookies)
++ (void)clearSession
+{
+ [ASIHTTPRequest setSessionAuthentication:NULL];
+ [ASIHTTPRequest setSessionCredentials:nil];
+ [ASIHTTPRequest setSessionCookies:nil];
+}
+
@synthesize username;
@synthesize password;
@@ -778,13 +890,16 @@ + (void)removeCredentialsForHost:(NSString *)host port:(int)port protocol:(NSStr
@synthesize downloadProgressDelegate;
@synthesize useKeychainPersistance;
@synthesize useSessionPersistance;
+@synthesize useCookiePersistance;
@synthesize downloadDestinationPath;
@synthesize didFinishSelector;
@synthesize didFailSelector;
@synthesize authenticationRealm;
@synthesize error;
@synthesize complete;
@synthesize responseHeaders;
+@synthesize responseCookies;
+@synthesize requestCookies;
@synthesize requestCredentials;
@synthesize responseStatusCode;
@synthesize receivedData;
View
@@ -14,5 +14,5 @@
- (void)testBasicDownload;
- (void)testOperationQueue;
-
+- (void)testCookies;
@end
Oops, something went wrong.

0 comments on commit bc988ee

Please sign in to comment.