Ben Copsey

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

…ession persistant cookies
One or two small bug fixes
  1 +//
  2 +// ASIHTTPCookie.h
  3 +// asi-http-request
  4 +//
  5 +// Created by Ben Copsey on 25/08/2008.
  6 +// Copyright 2008 All-Seeing Interactive. All rights reserved.
  7 +//
  8 +
  9 +#import <Cocoa/Cocoa.h>
  10 +
  11 +
  12 +@interface ASIHTTPCookie : NSObject {
  13 + NSString *name;
  14 + NSString *value;
  15 + NSDate *expires;
  16 + NSString *path;
  17 + NSString *domain;
  18 + BOOL requiresHTTPS;
  19 +}
  20 +
  21 +- (void)setValue:(NSString *)newValue forProperty:(NSString *)property;
  22 +
  23 ++ (NSMutableArray *)cookiesFromHeader:(NSString *)header;
  24 ++ (NSString *)urlEncodedValue:(NSString *)string;
  25 ++ (NSString *)urlDecodedValue:(NSString *)string;
  26 +
  27 +@property (retain) NSString *name;
  28 +@property (retain) NSString *value;
  29 +@property (retain) NSDate *expires;
  30 +@property (retain) NSString *path;
  31 +@property (retain) NSString *domain;
  32 +@property (assign) BOOL requiresHTTPS;
  33 +
  34 +@end
  1 +//
  2 +// ASIHTTPCookie.m
  3 +// asi-http-request
  4 +//
  5 +// Created by Ben Copsey on 25/08/2008.
  6 +// Copyright 2008 All-Seeing Interactive. All rights reserved.
  7 +//
  8 +
  9 +#import "ASIHTTPCookie.h"
  10 +
  11 +@implementation ASIHTTPCookie
  12 +
  13 +- (void)setValue:(NSString *)newValue forProperty:(NSString *)property
  14 +{
  15 + NSString *prop = [property lowercaseString];
  16 + if ([prop isEqualToString:@"expires"]) {
  17 + //[self setExpires:[NSDate dateFrom
  18 + return;
  19 + } else if ([prop isEqualToString:@"domain"]) {
  20 + [self setDomain:newValue];
  21 + return;
  22 + } else if ([prop isEqualToString:@"path"]) {
  23 + [self setPath:newValue];
  24 + return;
  25 + } else if ([prop isEqualToString:@"secure"]) {
  26 + [self setRequiresHTTPS:[newValue isEqualToString:@"1"]];
  27 + return;
  28 + }
  29 + if (![self name] && ![self value]) {
  30 + [self setName:property];
  31 + [self setValue:newValue];
  32 + }
  33 +}
  34 +
  35 +
  36 +// 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!
  37 +// 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
  38 +
  39 +
  40 ++ (NSMutableArray *)cookiesFromHeader:(NSString *)header
  41 +{
  42 + NSMutableArray *cookies = [[[NSMutableArray alloc] init] autorelease];
  43 + ASIHTTPCookie *cookie = [[[ASIHTTPCookie alloc] init] autorelease];
  44 +
  45 + NSArray *parts = [header componentsSeparatedByString:@"="];
  46 + int i;
  47 + NSString *name;
  48 + NSString *value;
  49 + NSArray *components;
  50 + NSString *newKey;
  51 + NSString *terminator;
  52 + for (i=0; i<[parts count]; i++) {
  53 + NSString *part = [parts objectAtIndex:i];
  54 + if (i == 0) {
  55 + name = part;
  56 + continue;
  57 + } else if (i == [parts count]-1) {
  58 + [cookie setValue:[ASIHTTPCookie urlDecodedValue:part] forProperty:name];
  59 + [cookies addObject:cookie];
  60 + continue;
  61 + }
  62 + components = [part componentsSeparatedByString:@" "];
  63 + newKey = [components lastObject];
  64 + value = [part substringWithRange:NSMakeRange(0,[part length]-[newKey length]-2)];
  65 + [cookie setValue:[ASIHTTPCookie urlDecodedValue:value] forProperty:name];
  66 +
  67 + terminator = [part substringWithRange:NSMakeRange([part length]-[newKey length]-2,1)];
  68 + if ([terminator isEqualToString:@","]) {
  69 + [cookies addObject:cookie];
  70 + cookie = [[[ASIHTTPCookie alloc] init] autorelease];
  71 + }
  72 + name = newKey;
  73 + }
  74 +
  75 + return cookies;
  76 +
  77 +}
  78 +
  79 ++ (NSString *)urlDecodedValue:(NSString *)string
  80 +{
  81 + NSMutableString *s = [NSMutableString stringWithString:[string stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
  82 + //Also swap plus signs for spaces
  83 + [s replaceOccurrencesOfString:@"+" withString:@" " options:NSLiteralSearch range:NSMakeRange(0, [s length])];
  84 + return [NSString stringWithString:s];
  85 +}
  86 +
  87 ++ (NSString *)urlEncodedValue:(NSString *)string
  88 +{
  89 + return [string stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
  90 +}
  91 +
  92 +@synthesize name;
  93 +@synthesize value;
  94 +@synthesize expires;
  95 +@synthesize path;
  96 +@synthesize domain;
  97 +@synthesize requiresHTTPS;
  98 +@end
  99 +
  100 +
@@ -32,9 +32,18 @@ @@ -32,9 +32,18 @@
32 //Dictionary for custom HTTP request headers 32 //Dictionary for custom HTTP request headers
33 NSMutableDictionary *requestHeaders; 33 NSMutableDictionary *requestHeaders;
34 34
35 - //Will be populate with HTTP response headers from the server 35 + //Will be populated with HTTP response headers from the server
36 NSDictionary *responseHeaders; 36 NSDictionary *responseHeaders;
37 37
  38 + //Can be used to manually insert cookie headers to a request, but it's more likely that sessionCookies will do this for you
  39 + NSMutableArray *requestCookies;
  40 +
  41 + //Will be populated with Cookies
  42 + NSMutableArray *responseCookies;
  43 +
  44 + //If use cokie persistance is true, network requests will present valid cookies from previous requests
  45 + BOOL useCookiePersistance;
  46 +
38 //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 47 //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
39 BOOL useKeychainPersistance; 48 BOOL useKeychainPersistance;
40 49
@@ -215,6 +224,17 @@ @@ -215,6 +224,17 @@
215 // Remove credentials from the keychain 224 // Remove credentials from the keychain
216 + (void)removeCredentialsForHost:(NSString *)host port:(int)port protocol:(NSString *)protocol realm:(NSString *)realm; 225 + (void)removeCredentialsForHost:(NSString *)host port:(int)port protocol:(NSString *)protocol realm:(NSString *)realm;
217 226
  227 +// Store cookies for a particular request in the session
  228 ++ (void)recordCookiesInSessionForRequest:(ASIHTTPRequest *)request;
  229 +
  230 ++ (void)setSessionCookies:(NSMutableArray *)newSessionCookies;
  231 ++ (NSMutableArray *)sessionCookies;
  232 +
  233 +// Dump all session data (authentication and cookies)
  234 ++ (void)clearSession;
  235 +
  236 +
  237 +
218 @property (retain) NSString *username; 238 @property (retain) NSString *username;
219 @property (retain) NSString *password; 239 @property (retain) NSString *password;
220 @property (retain) NSString *domain; 240 @property (retain) NSString *domain;
@@ -232,6 +252,9 @@ @@ -232,6 +252,9 @@
232 @property (retain) NSError *error; 252 @property (retain) NSError *error;
233 @property (assign,readonly) BOOL complete; 253 @property (assign,readonly) BOOL complete;
234 @property (retain) NSDictionary *responseHeaders; 254 @property (retain) NSDictionary *responseHeaders;
  255 +@property (retain) NSMutableArray *requestCookies;
  256 +@property (retain) NSMutableArray *responseCookies;
  257 +@property (assign) BOOL useCookiePersistance;
235 @property (retain) NSDictionary *requestCredentials; 258 @property (retain) NSDictionary *requestCredentials;
236 @property (assign) int responseStatusCode; 259 @property (assign) int responseStatusCode;
237 @property (retain) NSMutableData *receivedData; 260 @property (retain) NSMutableData *receivedData;
@@ -11,6 +11,7 @@ @@ -11,6 +11,7 @@
11 // See: http://developer.apple.com/samplecode/ImageClient/listing37.html 11 // See: http://developer.apple.com/samplecode/ImageClient/listing37.html
12 12
13 #import "ASIHTTPRequest.h" 13 #import "ASIHTTPRequest.h"
  14 +#import "ASIHTTPCookie.h"
14 15
15 static NSString *NetworkRequestErrorDomain = @"com.Your-Company.Your-Product.NetworkError."; 16 static NSString *NetworkRequestErrorDomain = @"com.Your-Company.Your-Product.NetworkError.";
16 17
@@ -21,6 +22,7 @@ static const CFOptionFlags kNetworkEvents = kCFStreamEventOpenCompleted | @@ -21,6 +22,7 @@ static const CFOptionFlags kNetworkEvents = kCFStreamEventOpenCompleted |
21 22
22 static CFHTTPAuthenticationRef sessionAuthentication = NULL; 23 static CFHTTPAuthenticationRef sessionAuthentication = NULL;
23 static NSMutableDictionary *sessionCredentials = nil; 24 static NSMutableDictionary *sessionCredentials = nil;
  25 +static NSMutableArray *sessionCookies = nil;
24 26
25 27
26 static void ReadStreamClientCallBack(CFReadStreamRef readStream, CFStreamEventType type, void *clientCallBackInfo) { 28 static void ReadStreamClientCallBack(CFReadStreamRef readStream, CFStreamEventType type, void *clientCallBackInfo) {
@@ -51,6 +53,8 @@ static void ReadStreamClientCallBack(CFReadStreamRef readStream, CFStreamEventTy @@ -51,6 +53,8 @@ static void ReadStreamClientCallBack(CFReadStreamRef readStream, CFStreamEventTy
51 responseHeaders = nil; 53 responseHeaders = nil;
52 [self setUseKeychainPersistance:NO]; 54 [self setUseKeychainPersistance:NO];
53 [self setUseSessionPersistance:YES]; 55 [self setUseSessionPersistance:YES];
  56 + [self setUseCookiePersistance:YES];
  57 + [self setRequestCookies:[[[NSMutableArray alloc] init] autorelease]];
54 didFinishSelector = @selector(requestFinished:); 58 didFinishSelector = @selector(requestFinished:);
55 didFailSelector = @selector(requestFailed:); 59 didFailSelector = @selector(requestFailed:);
56 delegate = nil; 60 delegate = nil;
@@ -164,6 +168,55 @@ static void ReadStreamClientCallBack(CFReadStreamRef readStream, CFStreamEventTy @@ -164,6 +168,55 @@ static void ReadStreamClientCallBack(CFReadStreamRef readStream, CFStreamEventTy
164 //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. 168 //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.
165 NSString *stringBoundary = @"0xKhTmLbOuNdArY"; 169 NSString *stringBoundary = @"0xKhTmLbOuNdArY";
166 170
  171 + //Add cookies from session
  172 + if (useCookiePersistance && [[ASIHTTPRequest sessionCookies] count] > 0) {
  173 + ASIHTTPCookie *requestCookie;
  174 + ASIHTTPCookie *storedCookie;
  175 + for (storedCookie in sessionCookies) {
  176 + BOOL foundExistingCookie = NO;
  177 + //Look for existing cookies in the request - these will always take precedence over session stored cookies
  178 + for (requestCookie in requestCookies) {
  179 + if ([[requestCookie domain] isEqualToString:[storedCookie domain]]) {
  180 + if ([[requestCookie path] isEqualToString:[storedCookie path]] || (![requestCookie path] && ![storedCookie path])) {
  181 + if ([[requestCookie name] isEqualToString:[storedCookie name]]) {
  182 + foundExistingCookie = YES;
  183 + break;
  184 + }
  185 + }
  186 + }
  187 + }
  188 + if (!foundExistingCookie) {
  189 + [requestCookies addObject:storedCookie];
  190 + }
  191 + }
  192 + }
  193 +
  194 + //Apply request cookies
  195 + if ([requestCookies count] > 0) {
  196 + ASIHTTPCookie *cookie;
  197 + NSString *cookieHeader = nil;
  198 + for (cookie in requestCookies) {
  199 + //Ensure the cookie is valid for this request
  200 + if ([[[url host] substringWithRange:NSMakeRange([[url host] length]-[[cookie domain] length],[[cookie domain] length])] isEqualToString:[cookie domain]]) {
  201 + if ([[[url path] substringWithRange:NSMakeRange(0,[[cookie path] length])] isEqualToString:[cookie path]]) {
  202 + if (![cookie requiresHTTPS] || [[url port] intValue] == 443) {
  203 + if (![cookie expires] || [[cookie expires] timeIntervalSinceNow] > 0) {
  204 + if (!cookieHeader) {
  205 + cookieHeader = [NSString stringWithFormat: @"%@=%@",[cookie name],[ASIHTTPCookie urlEncodedValue:[cookie value]]];
  206 + } else {
  207 + cookieHeader = [NSString stringWithFormat: @"%@; %@=%@",cookieHeader,[cookie name],[ASIHTTPCookie urlEncodedValue:[cookie value]]];
  208 + }
  209 + }
  210 + }
  211 + }
  212 + }
  213 + }
  214 + if (cookieHeader) {
  215 + [self addRequestHeader:@"Cookie" value:cookieHeader];
  216 + }
  217 + }
  218 +
  219 +
167 //Add custom headers 220 //Add custom headers
168 NSString *header; 221 NSString *header;
169 for (header in requestHeaders) { 222 for (header in requestHeaders) {
@@ -207,8 +260,6 @@ static void ReadStreamClientCallBack(CFReadStreamRef readStream, CFStreamEventTy @@ -207,8 +260,6 @@ static void ReadStreamClientCallBack(CFReadStreamRef readStream, CFStreamEventTy
207 } 260 }
208 261
209 [postBody appendData:[[NSString stringWithFormat:@"\r\n--%@--\r\n",stringBoundary] dataUsingEncoding:NSUTF8StringEncoding]]; 262 [postBody appendData:[[NSString stringWithFormat:@"\r\n--%@--\r\n",stringBoundary] dataUsingEncoding:NSUTF8StringEncoding]];
210 -  
211 - NSString *foo = [[[NSString alloc] initWithBytes:[postBody bytes] length:[postBody length] encoding:NSUTF8StringEncoding] autorelease];  
212 263
213 // Set the body. 264 // Set the body.
214 CFHTTPMessageSetBody(request, (CFDataRef)postBody); 265 CFHTTPMessageSetBody(request, (CFDataRef)postBody);
@@ -422,6 +473,16 @@ static void ReadStreamClientCallBack(CFReadStreamRef readStream, CFStreamEventTy @@ -422,6 +473,16 @@ static void ReadStreamClientCallBack(CFReadStreamRef readStream, CFStreamEventTy
422 [self performSelectorOnMainThread:@selector(resetDownloadProgress:) withObject:[NSNumber numberWithDouble:contentLength] waitUntilDone:YES]; 473 [self performSelectorOnMainThread:@selector(resetDownloadProgress:) withObject:[NSNumber numberWithDouble:contentLength] waitUntilDone:YES];
423 } 474 }
424 } 475 }
  476 +
  477 + //Handle cookies
  478 + NSString *cookieHeader = [responseHeaders valueForKey:@"Set-Cookie"];
  479 + if (cookieHeader) {
  480 + [self setResponseCookies:[ASIHTTPCookie cookiesFromHeader:cookieHeader]];
  481 + if (useCookiePersistance) {
  482 + [ASIHTTPRequest recordCookiesInSessionForRequest:self];
  483 + }
  484 + }
  485 +
425 } 486 }
426 487
427 } 488 }
@@ -768,6 +829,57 @@ static void ReadStreamClientCallBack(CFReadStreamRef readStream, CFStreamEventTy @@ -768,6 +829,57 @@ static void ReadStreamClientCallBack(CFReadStreamRef readStream, CFStreamEventTy
768 829
769 } 830 }
770 831
  832 ++ (void)recordCookiesInSessionForRequest:(ASIHTTPRequest *)request
  833 +{
  834 + if (!sessionCookies) {
  835 + [self setSessionCookies:[[[NSMutableArray alloc] init] autorelease]];
  836 + }
  837 + ASIHTTPCookie *newCookie;
  838 + ASIHTTPCookie *storedCookie;
  839 + for (newCookie in [request responseCookies]) {
  840 + //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
  841 + if (![newCookie domain]) {
  842 + [newCookie setDomain:[[request url] host]];
  843 + }
  844 + int i = 0;
  845 + BOOL foundExistingCookie = NO;
  846 + for (storedCookie in sessionCookies) {
  847 + if ([[storedCookie domain] isEqualToString:[newCookie domain]]) {
  848 + if ([[storedCookie path] isEqualToString:[newCookie path]] || (![storedCookie path] && ![newCookie path])) {
  849 + if ([[storedCookie name] isEqualToString:[newCookie name]]) {
  850 + foundExistingCookie = YES;
  851 + [sessionCookies replaceObjectAtIndex:i withObject:newCookie];
  852 + break;
  853 + }
  854 + }
  855 + }
  856 + i++;
  857 + }
  858 + if (!foundExistingCookie) {
  859 + [sessionCookies addObject:newCookie];
  860 + }
  861 + }
  862 +}
  863 +
  864 ++ (NSMutableArray *)sessionCookies
  865 +{
  866 + return sessionCookies;
  867 +}
  868 +
  869 ++ (void)setSessionCookies:(NSMutableArray *)newSessionCookies
  870 +{
  871 + [sessionCookies release];
  872 + sessionCookies = [newSessionCookies retain];
  873 +}
  874 +
  875 +// Dump all session data (authentication and cookies)
  876 ++ (void)clearSession
  877 +{
  878 + [ASIHTTPRequest setSessionAuthentication:NULL];
  879 + [ASIHTTPRequest setSessionCredentials:nil];
  880 + [ASIHTTPRequest setSessionCookies:nil];
  881 +}
  882 +
771 883
772 @synthesize username; 884 @synthesize username;
773 @synthesize password; 885 @synthesize password;
@@ -778,6 +890,7 @@ static void ReadStreamClientCallBack(CFReadStreamRef readStream, CFStreamEventTy @@ -778,6 +890,7 @@ static void ReadStreamClientCallBack(CFReadStreamRef readStream, CFStreamEventTy
778 @synthesize downloadProgressDelegate; 890 @synthesize downloadProgressDelegate;
779 @synthesize useKeychainPersistance; 891 @synthesize useKeychainPersistance;
780 @synthesize useSessionPersistance; 892 @synthesize useSessionPersistance;
  893 +@synthesize useCookiePersistance;
781 @synthesize downloadDestinationPath; 894 @synthesize downloadDestinationPath;
782 @synthesize didFinishSelector; 895 @synthesize didFinishSelector;
783 @synthesize didFailSelector; 896 @synthesize didFailSelector;
@@ -785,6 +898,8 @@ static void ReadStreamClientCallBack(CFReadStreamRef readStream, CFStreamEventTy @@ -785,6 +898,8 @@ static void ReadStreamClientCallBack(CFReadStreamRef readStream, CFStreamEventTy
785 @synthesize error; 898 @synthesize error;
786 @synthesize complete; 899 @synthesize complete;
787 @synthesize responseHeaders; 900 @synthesize responseHeaders;
  901 +@synthesize responseCookies;
  902 +@synthesize requestCookies;
788 @synthesize requestCredentials; 903 @synthesize requestCredentials;
789 @synthesize responseStatusCode; 904 @synthesize responseStatusCode;
790 @synthesize receivedData; 905 @synthesize receivedData;
@@ -14,5 +14,5 @@ @@ -14,5 +14,5 @@
14 14
15 - (void)testBasicDownload; 15 - (void)testBasicDownload;
16 - (void)testOperationQueue; 16 - (void)testOperationQueue;
17 - 17 +- (void)testCookies;
18 @end 18 @end
This diff is collapsed. Click to expand it.
@@ -147,7 +147,8 @@ @@ -147,7 +147,8 @@
147 - (void)authSheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo { 147 - (void)authSheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
148 ASIHTTPRequest *request = (ASIHTTPRequest *)contextInfo; 148 ASIHTTPRequest *request = (ASIHTTPRequest *)contextInfo;
149 if (returnCode == NSOKButton) { 149 if (returnCode == NSOKButton) {
150 - [request setUsername:[[[username stringValue] copy] autorelease] andPassword:[[[password stringValue] copy] autorelease]]; 150 + [request setUsername:[[[username stringValue] copy] autorelease]];
  151 + [request setPassword:[[[password stringValue] copy] autorelease]];
151 [request retryWithAuthentication]; 152 [request retryWithAuthentication];
152 } else { 153 } else {
153 [request cancelLoad]; 154 [request cancelLoad];
@@ -27,6 +27,9 @@ ASIHTTPRequest is partly based on code from Apple's ImageClient code samples, so @@ -27,6 +27,9 @@ ASIHTTPRequest is partly based on code from Apple's ImageClient code samples, so
27 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! 27 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!
28 28
29 To do: 29 To do:
30 -NTLM Authentication? 30 +More unit tests
31 -Digest Authentication? 31 +Cookie support
  32 +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)
  33 +Add SOAP example
  34 +See if Digest Authentication works
32 PUT / DELETE /GET? 35 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.