Ben Copsey

Merge branch 'master' into s3

@@ -26,7 +26,8 @@ typedef enum _ASINetworkErrorType { @@ -26,7 +26,8 @@ typedef enum _ASINetworkErrorType {
26 ASIUnableToCreateRequestErrorType = 5, 26 ASIUnableToCreateRequestErrorType = 5,
27 ASIInternalErrorWhileBuildingRequestType = 6, 27 ASIInternalErrorWhileBuildingRequestType = 6,
28 ASIInternalErrorWhileApplyingCredentialsType = 7, 28 ASIInternalErrorWhileApplyingCredentialsType = 7,
29 - ASIFileManagementError = 8 29 + ASIFileManagementError = 8,
  30 + ASITooMuchRedirectionErrorType = 9
30 31
31 } ASINetworkErrorType; 32 } ASINetworkErrorType;
32 33
@@ -218,6 +219,12 @@ extern NSString* const NetworkRequestErrorDomain; @@ -218,6 +219,12 @@ extern NSString* const NetworkRequestErrorDomain;
218 // When YES, requests will automatically redirect when they get a HTTP 30x header (defaults to YES) 219 // When YES, requests will automatically redirect when they get a HTTP 30x header (defaults to YES)
219 BOOL shouldRedirect; 220 BOOL shouldRedirect;
220 221
  222 + // Used internally to tell the main loop we need to stop and retry with a new url
  223 + BOOL needsRedirect;
  224 +
  225 + // Incremented every time this request redirects. When it reaches 5, we give up
  226 + int redirectCount;
  227 +
221 // When NO, requests will not check the secure certificate is valid (use for self-signed cerficates during development, DO NOT USE IN PRODUCTION) Default is YES 228 // When NO, requests will not check the secure certificate is valid (use for self-signed cerficates during development, DO NOT USE IN PRODUCTION) Default is YES
222 BOOL validatesSecureCertificate; 229 BOOL validatesSecureCertificate;
223 230
@@ -297,7 +304,9 @@ extern NSString* const NetworkRequestErrorDomain; @@ -297,7 +304,9 @@ extern NSString* const NetworkRequestErrorDomain;
297 304
298 #pragma mark http authentication stuff 305 #pragma mark http authentication stuff
299 306
300 -// Reads the response headers to find the content length, and returns true if the request needs a username and password (or if those supplied were incorrect) 307 +// Reads the response headers to find the content length, encoding, cookies for the session
  308 +// Also initiates request redirection when shouldRedirect is true
  309 +// Returns true if the request needs a username and password (or if those supplied were incorrect)
301 - (BOOL)readResponseHeadersReturningAuthenticationFailure; 310 - (BOOL)readResponseHeadersReturningAuthenticationFailure;
302 311
303 // Apply credentials to this request 312 // Apply credentials to this request
@@ -345,6 +354,9 @@ extern NSString* const NetworkRequestErrorDomain; @@ -345,6 +354,9 @@ extern NSString* const NetworkRequestErrorDomain;
345 + (void)setSessionCookies:(NSMutableArray *)newSessionCookies; 354 + (void)setSessionCookies:(NSMutableArray *)newSessionCookies;
346 + (NSMutableArray *)sessionCookies; 355 + (NSMutableArray *)sessionCookies;
347 356
  357 +// Adds a cookie to our list of cookies we've accepted, checking first for an old version of the same cookie and removing that
  358 ++ (void)addSessionCookie:(NSHTTPCookie *)newCookie;
  359 +
348 // Dump all session data (authentication and cookies) 360 // Dump all session data (authentication and cookies)
349 + (void)clearSession; 361 + (void)clearSession;
350 362
@@ -27,6 +27,8 @@ static CFHTTPAuthenticationRef sessionAuthentication = NULL; @@ -27,6 +27,8 @@ static CFHTTPAuthenticationRef sessionAuthentication = NULL;
27 static NSMutableDictionary *sessionCredentials = nil; 27 static NSMutableDictionary *sessionCredentials = nil;
28 static NSMutableArray *sessionCookies = nil; 28 static NSMutableArray *sessionCookies = nil;
29 29
  30 +// The number of times we will allow requests to redirect before we fail with a redirection error
  31 +const int RedirectionLimit = 5;
30 32
31 static void ReadStreamClientCallBack(CFReadStreamRef readStream, CFStreamEventType type, void *clientCallBackInfo) { 33 static void ReadStreamClientCallBack(CFReadStreamRef readStream, CFStreamEventType type, void *clientCallBackInfo) {
32 [((ASIHTTPRequest*)clientCallBackInfo) handleNetworkEvent: type]; 34 [((ASIHTTPRequest*)clientCallBackInfo) handleNetworkEvent: type];
@@ -39,6 +41,8 @@ static NSError *ASIRequestCancelledError; @@ -39,6 +41,8 @@ static NSError *ASIRequestCancelledError;
39 static NSError *ASIRequestTimedOutError; 41 static NSError *ASIRequestTimedOutError;
40 static NSError *ASIAuthenticationError; 42 static NSError *ASIAuthenticationError;
41 static NSError *ASIUnableToCreateRequestError; 43 static NSError *ASIUnableToCreateRequestError;
  44 +static NSError *ASITooMuchRedirectionError;
  45 +
42 46
43 // Private stuff 47 // Private stuff
44 @interface ASIHTTPRequest () 48 @interface ASIHTTPRequest ()
@@ -64,6 +68,8 @@ static NSError *ASIUnableToCreateRequestError; @@ -64,6 +68,8 @@ static NSError *ASIUnableToCreateRequestError;
64 @property (retain, nonatomic) NSOutputStream *fileDownloadOutputStream; 68 @property (retain, nonatomic) NSOutputStream *fileDownloadOutputStream;
65 @property (assign, nonatomic) int authenticationRetryCount; 69 @property (assign, nonatomic) int authenticationRetryCount;
66 @property (assign, nonatomic) BOOL updatedProgress; 70 @property (assign, nonatomic) BOOL updatedProgress;
  71 + @property (assign, nonatomic) BOOL needsRedirect;
  72 + @property (assign, nonatomic) int redirectCount;
67 @end 73 @end
68 74
69 @implementation ASIHTTPRequest 75 @implementation ASIHTTPRequest
@@ -80,6 +86,8 @@ static NSError *ASIUnableToCreateRequestError; @@ -80,6 +86,8 @@ static NSError *ASIUnableToCreateRequestError;
80 ASIAuthenticationError = [[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIAuthenticationErrorType userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Authentication needed",NSLocalizedDescriptionKey,nil]] retain]; 86 ASIAuthenticationError = [[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIAuthenticationErrorType userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Authentication needed",NSLocalizedDescriptionKey,nil]] retain];
81 ASIRequestCancelledError = [[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIRequestCancelledErrorType userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"The request was cancelled",NSLocalizedDescriptionKey,nil]] retain]; 87 ASIRequestCancelledError = [[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIRequestCancelledErrorType userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"The request was cancelled",NSLocalizedDescriptionKey,nil]] retain];
82 ASIUnableToCreateRequestError = [[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIUnableToCreateRequestErrorType userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Unable to create request (bad url?)",NSLocalizedDescriptionKey,nil]] retain]; 88 ASIUnableToCreateRequestError = [[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIUnableToCreateRequestErrorType userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Unable to create request (bad url?)",NSLocalizedDescriptionKey,nil]] retain];
  89 + ASITooMuchRedirectionError = [[NSError errorWithDomain:NetworkRequestErrorDomain code:ASITooMuchRedirectionErrorType userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"The request failed because it redirected too many times",NSLocalizedDescriptionKey,nil]] retain];
  90 +
83 } 91 }
84 [super initialize]; 92 [super initialize];
85 } 93 }
@@ -437,9 +445,6 @@ static NSError *ASIUnableToCreateRequestError; @@ -437,9 +445,6 @@ static NSError *ASIUnableToCreateRequestError;
437 return; 445 return;
438 } 446 }
439 447
440 - // Tell CFNetwork to automatically redirect for 30x status codes  
441 - CFReadStreamSetProperty(readStream, kCFStreamPropertyHTTPShouldAutoredirect, [self shouldRedirect] ? kCFBooleanTrue : kCFBooleanFalse);  
442 -  
443 // Tell CFNetwork not to validate SSL certificates 448 // Tell CFNetwork not to validate SSL certificates
444 if (!validatesSecureCertificate) { 449 if (!validatesSecureCertificate) {
445 CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, [NSMutableDictionary dictionaryWithObject:(NSString *)kCFBooleanFalse forKey:(NSString *)kCFStreamSSLValidatesCertificateChain]); 450 CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, [NSMutableDictionary dictionaryWithObject:(NSString *)kCFBooleanFalse forKey:(NSString *)kCFStreamSSLValidatesCertificateChain]);
@@ -517,7 +522,7 @@ static NSError *ASIUnableToCreateRequestError; @@ -517,7 +522,7 @@ static NSError *ASIUnableToCreateRequestError;
517 // See if we need to timeout 522 // See if we need to timeout
518 if (lastActivityTime && timeOutSeconds > 0 && [now timeIntervalSinceDate:lastActivityTime] > timeOutSeconds) { 523 if (lastActivityTime && timeOutSeconds > 0 && [now timeIntervalSinceDate:lastActivityTime] > timeOutSeconds) {
519 524
520 - // Prevent timeouts before 128KB has been sent when the size of data to upload is greater than 128KB 525 + // Prevent timeouts before 128KB* has been sent when the size of data to upload is greater than 128KB* (*32KB on iPhone 3.0 SDK)
521 // This is to workaround the fact that kCFStreamPropertyHTTPRequestBytesWrittenCount is the amount written to the buffer, not the amount actually sent 526 // This is to workaround the fact that kCFStreamPropertyHTTPRequestBytesWrittenCount is the amount written to the buffer, not the amount actually sent
522 // This workaround prevents erroneous timeouts in low bandwidth situations (eg iPhone) 527 // This workaround prevents erroneous timeouts in low bandwidth situations (eg iPhone)
523 if (contentLength <= uploadBufferSize || (uploadBufferSize > 0 && totalBytesSent > uploadBufferSize)) { 528 if (contentLength <= uploadBufferSize || (uploadBufferSize > 0 && totalBytesSent > uploadBufferSize)) {
@@ -528,6 +533,22 @@ static NSError *ASIUnableToCreateRequestError; @@ -528,6 +533,22 @@ static NSError *ASIUnableToCreateRequestError;
528 } 533 }
529 } 534 }
530 535
  536 + // Do we need to redirect?
  537 + if ([self needsRedirect]) {
  538 + [self cancelLoad];
  539 + [self setNeedsRedirect:NO];
  540 + [self setRedirectCount:[self redirectCount]+1];
  541 + if ([self redirectCount] > RedirectionLimit) {
  542 + // Some naughty / badly coded website is trying to force us into a redirection loop. This is not cool.
  543 + [self failWithError:ASITooMuchRedirectionError];
  544 + [self setComplete:YES];
  545 + } else {
  546 + // Go all the way back to the beginning and build the request again, so that we can apply any new cookies
  547 + [self main];
  548 + }
  549 + break;
  550 + }
  551 +
531 // See if our NSOperationQueue told us to cancel 552 // See if our NSOperationQueue told us to cancel
532 if ([self isCancelled]) { 553 if ([self isCancelled]) {
533 break; 554 break;
@@ -944,7 +965,7 @@ static NSError *ASIUnableToCreateRequestError; @@ -944,7 +965,7 @@ static NSError *ASIUnableToCreateRequestError;
944 [self setResponseStatusCode:CFHTTPMessageGetResponseStatusCode(headers)]; 965 [self setResponseStatusCode:CFHTTPMessageGetResponseStatusCode(headers)];
945 966
946 // Is the server response a challenge for credentials? 967 // Is the server response a challenge for credentials?
947 - isAuthenticationChallenge = (responseStatusCode == 401); 968 + isAuthenticationChallenge = ([self responseStatusCode] == 401);
948 969
949 // We won't reset the download progress delegate if we got an authentication challenge 970 // We won't reset the download progress delegate if we got an authentication challenge
950 if (!isAuthenticationChallenge) { 971 if (!isAuthenticationChallenge) {
@@ -990,18 +1011,22 @@ static NSError *ASIUnableToCreateRequestError; @@ -990,18 +1011,22 @@ static NSError *ASIUnableToCreateRequestError;
990 NSArray *newCookies = [NSHTTPCookie cookiesWithResponseHeaderFields:responseHeaders forURL:url]; 1011 NSArray *newCookies = [NSHTTPCookie cookiesWithResponseHeaderFields:responseHeaders forURL:url];
991 [self setResponseCookies:newCookies]; 1012 [self setResponseCookies:newCookies];
992 1013
993 - if (useCookiePersistance) { 1014 + if ([self useCookiePersistance]) {
994 1015
995 // Store cookies in global persistent store 1016 // Store cookies in global persistent store
996 [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookies:newCookies forURL:url mainDocumentURL:nil]; 1017 [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookies:newCookies forURL:url mainDocumentURL:nil];
997 1018
998 // We also keep any cookies in the sessionCookies array, so that we have a reference to them if we need to remove them later 1019 // We also keep any cookies in the sessionCookies array, so that we have a reference to them if we need to remove them later
999 - if (!sessionCookies) {  
1000 - [ASIHTTPRequest setSessionCookies:[[[NSMutableArray alloc] init] autorelease]];  
1001 NSHTTPCookie *cookie; 1020 NSHTTPCookie *cookie;
1002 for (cookie in newCookies) { 1021 for (cookie in newCookies) {
1003 - [[ASIHTTPRequest sessionCookies] addObject:cookie]; 1022 + [ASIHTTPRequest addSessionCookie:cookie];
  1023 + }
1004 } 1024 }
  1025 + // Do we need to redirect?
  1026 + if ([self shouldRedirect]) {
  1027 + if ([self responseStatusCode] > 300 && [self responseStatusCode] < 308 && [self responseStatusCode] != 304) {
  1028 + [self setURL:[[NSURL URLWithString:[responseHeaders valueForKey:@"Location"] relativeToURL:[self url]] absoluteURL]];
  1029 + [self setNeedsRedirect:YES];
1005 } 1030 }
1006 } 1031 }
1007 1032
@@ -1256,6 +1281,9 @@ static NSError *ASIUnableToCreateRequestError; @@ -1256,6 +1281,9 @@ static NSError *ASIUnableToCreateRequestError;
1256 return; 1281 return;
1257 } 1282 }
1258 } 1283 }
  1284 + if ([self needsRedirect]) {
  1285 + return;
  1286 + }
1259 int bufferSize = 2048; 1287 int bufferSize = 2048;
1260 if (contentLength > 262144) { 1288 if (contentLength > 262144) {
1261 bufferSize = 65536; 1289 bufferSize = 65536;
@@ -1307,6 +1335,9 @@ static NSError *ASIUnableToCreateRequestError; @@ -1307,6 +1335,9 @@ static NSError *ASIUnableToCreateRequestError;
1307 return; 1335 return;
1308 } 1336 }
1309 } 1337 }
  1338 + if ([self needsRedirect]) {
  1339 + return;
  1340 + }
1310 [progressLock lock]; 1341 [progressLock lock];
1311 [self setComplete:YES]; 1342 [self setComplete:YES];
1312 [self updateProgressIndicators]; 1343 [self updateProgressIndicators];
@@ -1371,8 +1402,6 @@ static NSError *ASIUnableToCreateRequestError; @@ -1371,8 +1402,6 @@ static NSError *ASIUnableToCreateRequestError;
1371 { 1402 {
1372 NSError *underlyingError = [(NSError *)CFReadStreamCopyError(readStream) autorelease]; 1403 NSError *underlyingError = [(NSError *)CFReadStreamCopyError(readStream) autorelease];
1373 1404
1374 -  
1375 -  
1376 [self cancelLoad]; 1405 [self cancelLoad];
1377 [self setComplete:YES]; 1406 [self setComplete:YES];
1378 1407
@@ -1459,19 +1488,37 @@ static NSError *ASIUnableToCreateRequestError; @@ -1459,19 +1488,37 @@ static NSError *ASIUnableToCreateRequestError;
1459 1488
1460 + (NSMutableArray *)sessionCookies 1489 + (NSMutableArray *)sessionCookies
1461 { 1490 {
  1491 + if (!sessionCookies) {
  1492 + [ASIHTTPRequest setSessionCookies:[[[NSMutableArray alloc] init] autorelease]];
  1493 + }
1462 return sessionCookies; 1494 return sessionCookies;
1463 } 1495 }
1464 1496
1465 + (void)setSessionCookies:(NSMutableArray *)newSessionCookies 1497 + (void)setSessionCookies:(NSMutableArray *)newSessionCookies
1466 { 1498 {
1467 // Remove existing cookies from the persistent store 1499 // Remove existing cookies from the persistent store
1468 - for (NSHTTPCookie *cookie in [ASIHTTPRequest sessionCookies]) { 1500 + for (NSHTTPCookie *cookie in sessionCookies) {
1469 [[NSHTTPCookieStorage sharedHTTPCookieStorage] deleteCookie:cookie]; 1501 [[NSHTTPCookieStorage sharedHTTPCookieStorage] deleteCookie:cookie];
1470 } 1502 }
1471 [sessionCookies release]; 1503 [sessionCookies release];
1472 sessionCookies = [newSessionCookies retain]; 1504 sessionCookies = [newSessionCookies retain];
1473 } 1505 }
1474 1506
  1507 ++ (void)addSessionCookie:(NSHTTPCookie *)newCookie
  1508 +{
  1509 + NSHTTPCookie *cookie;
  1510 + int i;
  1511 + int max = [[ASIHTTPRequest sessionCookies] count];
  1512 + for (i=0; i<max; i++) {
  1513 + cookie = [[ASIHTTPRequest sessionCookies] objectAtIndex:i];
  1514 + if ([[cookie domain] isEqualToString:[newCookie domain]] && [[cookie path] isEqualToString:[newCookie path]] && [[cookie name] isEqualToString:[newCookie name]]) {
  1515 + [[ASIHTTPRequest sessionCookies] removeObjectAtIndex:i];
  1516 + break;
  1517 + }
  1518 + }
  1519 + [[ASIHTTPRequest sessionCookies] addObject:newCookie];
  1520 +}
  1521 +
1475 // Dump all session data (authentication and cookies) 1522 // Dump all session data (authentication and cookies)
1476 + (void)clearSession 1523 + (void)clearSession
1477 { 1524 {
@@ -1676,4 +1723,6 @@ static NSError *ASIUnableToCreateRequestError; @@ -1676,4 +1723,6 @@ static NSError *ASIUnableToCreateRequestError;
1676 @synthesize updatedProgress; 1723 @synthesize updatedProgress;
1677 @synthesize shouldRedirect; 1724 @synthesize shouldRedirect;
1678 @synthesize validatesSecureCertificate; 1725 @synthesize validatesSecureCertificate;
  1726 +@synthesize needsRedirect;
  1727 +@synthesize redirectCount;
1679 @end 1728 @end
@@ -33,4 +33,6 @@ @@ -33,4 +33,6 @@
33 - (void)testCompressedResponse; 33 - (void)testCompressedResponse;
34 - (void)testCompressedResponseDownloadToFile; 34 - (void)testCompressedResponseDownloadToFile;
35 - (void)testSSL; 35 - (void)testSSL;
  36 +- (void)testRedirectPreservesSession;
  37 +- (void)testTooMuchRedirection;
36 @end 38 @end
@@ -639,4 +639,24 @@ @@ -639,4 +639,24 @@
639 GHAssertNil([request error],@"Failed to accept a self-signed certificate"); 639 GHAssertNil([request error],@"Failed to accept a self-signed certificate");
640 } 640 }
641 641
  642 +- (void)testRedirectPreservesSession
  643 +{
  644 + // Remove any old session cookies
  645 + [ASIHTTPRequest clearSession];
  646 + ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:@"http://allseeing-i.com/ASIHTTPRequest/tests/session_redirect"]];
  647 + [request start];
  648 + BOOL success = [[request responseString] isEqualToString:@"Take me to your leader"];
  649 + GHAssertTrue(success,@"Failed to redirect preserving session cookies");
  650 +}
  651 +
  652 +- (void)testTooMuchRedirection
  653 +{
  654 + // This url will simply send a 302 redirect back to itself
  655 + ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:@"http://allseeing-i.com/ASIHTTPRequest/tests/one_infinite_loop"]];
  656 + [request start];
  657 + GHAssertNotNil([request error],@"Failed to generate an error when redirection occurs too many times");
  658 + BOOL success = ([[request error] code] == ASITooMuchRedirectionErrorType);
  659 + GHAssertTrue(success,@"Generated the wrong error for a redirection loop");
  660 +}
  661 +
642 @end 662 @end
@@ -100,6 +100,7 @@ @@ -100,6 +100,7 @@
100 [imageView3 setImage:nil]; 100 [imageView3 setImage:nil];
101 101
102 [networkQueue cancelAllOperations]; 102 [networkQueue cancelAllOperations];
  103 + [networkQueue setRequestDidFinishSelector:NULL];
103 [networkQueue setDownloadProgressDelegate:progressIndicator]; 104 [networkQueue setDownloadProgressDelegate:progressIndicator];
104 [networkQueue setDelegate:self]; 105 [networkQueue setDelegate:self];
105 [networkQueue setShowAccurateProgress:([showAccurateProgress state] == NSOnState)]; 106 [networkQueue setShowAccurateProgress:([showAccurateProgress state] == NSOnState)];