Ben Copsey

Added cookie support, including per-request cookies (request and response) and s…

…ession persistant cookies
One or two small bug fixes
//
// 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
... ...
//
// 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
... ...
... ... @@ -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;
... ...
... ... @@ -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 const CFOptionFlags kNetworkEvents = kCFStreamEventOpenCompleted |
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 @@ static void ReadStreamClientCallBack(CFReadStreamRef readStream, CFStreamEventTy
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 @@ static void ReadStreamClientCallBack(CFReadStreamRef readStream, CFStreamEventTy
//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) {
... ... @@ -208,8 +261,6 @@ static void ReadStreamClientCallBack(CFReadStreamRef readStream, CFStreamEventTy
[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 @@ static void ReadStreamClientCallBack(CFReadStreamRef readStream, CFStreamEventTy
[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 @@ static void ReadStreamClientCallBack(CFReadStreamRef readStream, CFStreamEventTy
}
+ (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,6 +890,7 @@ static void ReadStreamClientCallBack(CFReadStreamRef readStream, CFStreamEventTy
@synthesize downloadProgressDelegate;
@synthesize useKeychainPersistance;
@synthesize useSessionPersistance;
@synthesize useCookiePersistance;
@synthesize downloadDestinationPath;
@synthesize didFinishSelector;
@synthesize didFailSelector;
... ... @@ -785,6 +898,8 @@ static void ReadStreamClientCallBack(CFReadStreamRef readStream, CFStreamEventTy
@synthesize error;
@synthesize complete;
@synthesize responseHeaders;
@synthesize responseCookies;
@synthesize requestCookies;
@synthesize requestCredentials;
@synthesize responseStatusCode;
@synthesize receivedData;
... ...
... ... @@ -14,5 +14,5 @@
- (void)testBasicDownload;
- (void)testOperationQueue;
- (void)testCookies;
@end
... ...
This diff is collapsed. Click to expand it.
... ... @@ -147,7 +147,8 @@
- (void)authSheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
ASIHTTPRequest *request = (ASIHTTPRequest *)contextInfo;
if (returnCode == NSOKButton) {
[request setUsername:[[[username stringValue] copy] autorelease] andPassword:[[[password stringValue] copy] autorelease]];
[request setUsername:[[[username stringValue] copy] autorelease]];
[request setPassword:[[[password stringValue] copy] autorelease]];
[request retryWithAuthentication];
} else {
[request cancelLoad];
... ...
... ... @@ -27,6 +27,9 @@ ASIHTTPRequest is partly based on code from Apple's ImageClient code samples, so
ASIHTTPRequest is my first open source Objective-C code. I hope to expand the class and example application further (unit tests, maybe even iphone examples...) in the coming months. If you find it helpful, please do get in touch!
To do:
NTLM Authentication?
Digest Authentication?
More unit tests
Cookie support
Split up class - move form request stuff into subclass, and have simple implementation for main that sets request body according to data supplied by the subclass - this will allow other types of HTTP request (eg soap)
Add SOAP example
See if Digest Authentication works
PUT / DELETE /GET?
... ...
This diff is collapsed. Click to expand it.
This diff could not be displayed because it is too large.
This diff was suppressed by a .gitattributes entry.