Skip to content
Browse files

Stop using kCFStreamPropertyHTTPShouldAutoredirect and handle 30x red…

…irection ourselves

This ensures cookies presented as part of the initial response can be applied to the redirected request
sessionCookies should now hang on to all cookies, and cookies will replace old versions of the same cookie
  • Loading branch information...
1 parent 583963c commit ab5f503c78981b7b5e9621751fe9406d1ea5311d @pokeb committed Jul 13, 2009
Showing with 102 additions and 19 deletions.
  1. +14 −2 Classes/ASIHTTPRequest.h
  2. +66 −17 Classes/ASIHTTPRequest.m
  3. +2 −0 Classes/Tests/ASIHTTPRequestTests.h
  4. +20 −0 Classes/Tests/ASIHTTPRequestTests.m
View
16 Classes/ASIHTTPRequest.h
@@ -26,7 +26,8 @@ typedef enum _ASINetworkErrorType {
ASIUnableToCreateRequestErrorType = 5,
ASIInternalErrorWhileBuildingRequestType = 6,
ASIInternalErrorWhileApplyingCredentialsType = 7,
- ASIFileManagementError = 8
+ ASIFileManagementError = 8,
+ ASITooMuchRedirectionErrorType = 9
} ASINetworkErrorType;
@@ -218,6 +219,12 @@ extern NSString* const NetworkRequestErrorDomain;
// When YES, requests will automatically redirect when they get a HTTP 30x header (defaults to YES)
BOOL shouldRedirect;
+ // Used internally to tell the main loop we need to stop and retry with a new url
+ BOOL needsRedirect;
+
+ // Incremented every time this request redirects. When it reaches 5, we give up
+ int redirectCount;
+
// 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
BOOL validatesSecureCertificate;
@@ -297,7 +304,9 @@ extern NSString* const NetworkRequestErrorDomain;
#pragma mark http authentication stuff
-// 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)
+// Reads the response headers to find the content length, encoding, cookies for the session
+// Also initiates request redirection when shouldRedirect is true
+// Returns true if the request needs a username and password (or if those supplied were incorrect)
- (BOOL)readResponseHeadersReturningAuthenticationFailure;
// Apply credentials to this request
@@ -345,6 +354,9 @@ extern NSString* const NetworkRequestErrorDomain;
+ (void)setSessionCookies:(NSMutableArray *)newSessionCookies;
+ (NSMutableArray *)sessionCookies;
+// Adds a cookie to our list of cookies we've accepted, checking first for an old version of the same cookie and removing that
++ (void)addSessionCookie:(NSHTTPCookie *)newCookie;
+
// Dump all session data (authentication and cookies)
+ (void)clearSession;
View
83 Classes/ASIHTTPRequest.m
@@ -27,6 +27,8 @@
static NSMutableDictionary *sessionCredentials = nil;
static NSMutableArray *sessionCookies = nil;
+// The number of times we will allow requests to redirect before we fail with a redirection error
+const int RedirectionLimit = 5;
static void ReadStreamClientCallBack(CFReadStreamRef readStream, CFStreamEventType type, void *clientCallBackInfo) {
[((ASIHTTPRequest*)clientCallBackInfo) handleNetworkEvent: type];
@@ -39,6 +41,8 @@ static void ReadStreamClientCallBack(CFReadStreamRef readStream, CFStreamEventTy
static NSError *ASIRequestTimedOutError;
static NSError *ASIAuthenticationError;
static NSError *ASIUnableToCreateRequestError;
+static NSError *ASITooMuchRedirectionError;
+
// Private stuff
@interface ASIHTTPRequest ()
@@ -64,6 +68,8 @@ @interface ASIHTTPRequest ()
@property (retain, nonatomic) NSOutputStream *fileDownloadOutputStream;
@property (assign, nonatomic) int authenticationRetryCount;
@property (assign, nonatomic) BOOL updatedProgress;
+ @property (assign, nonatomic) BOOL needsRedirect;
+ @property (assign, nonatomic) int redirectCount;
@end
@implementation ASIHTTPRequest
@@ -80,6 +86,8 @@ + (void)initialize
ASIAuthenticationError = [[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIAuthenticationErrorType userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Authentication needed",NSLocalizedDescriptionKey,nil]] retain];
ASIRequestCancelledError = [[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIRequestCancelledErrorType userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"The request was cancelled",NSLocalizedDescriptionKey,nil]] retain];
ASIUnableToCreateRequestError = [[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIUnableToCreateRequestErrorType userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Unable to create request (bad url?)",NSLocalizedDescriptionKey,nil]] retain];
+ ASITooMuchRedirectionError = [[NSError errorWithDomain:NetworkRequestErrorDomain code:ASITooMuchRedirectionErrorType userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"The request failed because it redirected too many times",NSLocalizedDescriptionKey,nil]] retain];
+
}
[super initialize];
}
@@ -436,10 +444,7 @@ - (void)startRequest
[self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIInternalErrorWhileBuildingRequestType userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Unable to create read stream",NSLocalizedDescriptionKey,nil]]];
return;
}
-
- // Tell CFNetwork to automatically redirect for 30x status codes
- CFReadStreamSetProperty(readStream, kCFStreamPropertyHTTPShouldAutoredirect, [self shouldRedirect] ? kCFBooleanTrue : kCFBooleanFalse);
-
+
// Tell CFNetwork not to validate SSL certificates
if (!validatesSecureCertificate) {
CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, [NSMutableDictionary dictionaryWithObject:(NSString *)kCFBooleanFalse forKey:(NSString *)kCFStreamSSLValidatesCertificateChain]);
@@ -517,7 +522,7 @@ - (void)loadRequest
// See if we need to timeout
if (lastActivityTime && timeOutSeconds > 0 && [now timeIntervalSinceDate:lastActivityTime] > timeOutSeconds) {
- // Prevent timeouts before 128KB has been sent when the size of data to upload is greater than 128KB
+ // Prevent timeouts before 128KB* has been sent when the size of data to upload is greater than 128KB* (*32KB on iPhone 3.0 SDK)
// This is to workaround the fact that kCFStreamPropertyHTTPRequestBytesWrittenCount is the amount written to the buffer, not the amount actually sent
// This workaround prevents erroneous timeouts in low bandwidth situations (eg iPhone)
if (contentLength <= uploadBufferSize || (uploadBufferSize > 0 && totalBytesSent > uploadBufferSize)) {
@@ -528,7 +533,23 @@ - (void)loadRequest
}
}
- // See if our NSOperationQueue told us to cancel
+ // Do we need to redirect?
+ if ([self needsRedirect]) {
+ [self cancelLoad];
+ [self setNeedsRedirect:NO];
+ [self setRedirectCount:[self redirectCount]+1];
+ if ([self redirectCount] > RedirectionLimit) {
+ // Some naughty / badly coded website is trying to force us into a redirection loop. This is not cool.
+ [self failWithError:ASITooMuchRedirectionError];
+ [self setComplete:YES];
+ } else {
+ // Go all the way back to the beginning and build the request again, so that we can apply any new cookies
+ [self main];
+ }
+ break;
+ }
+
+ // See if our NSOperationQueue told us to cancel or we need to redirect
if ([self isCancelled]) {
break;
}
@@ -944,7 +965,7 @@ - (BOOL)readResponseHeadersReturningAuthenticationFailure
[self setResponseStatusCode:CFHTTPMessageGetResponseStatusCode(headers)];
// Is the server response a challenge for credentials?
- isAuthenticationChallenge = (responseStatusCode == 401);
+ isAuthenticationChallenge = ([self responseStatusCode] == 401);
// We won't reset the download progress delegate if we got an authentication challenge
if (!isAuthenticationChallenge) {
@@ -990,18 +1011,22 @@ - (BOOL)readResponseHeadersReturningAuthenticationFailure
NSArray *newCookies = [NSHTTPCookie cookiesWithResponseHeaderFields:responseHeaders forURL:url];
[self setResponseCookies:newCookies];
- if (useCookiePersistance) {
+ if ([self useCookiePersistance]) {
// Store cookies in global persistent store
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookies:newCookies forURL:url mainDocumentURL:nil];
// We also keep any cookies in the sessionCookies array, so that we have a reference to them if we need to remove them later
- if (!sessionCookies) {
- [ASIHTTPRequest setSessionCookies:[[[NSMutableArray alloc] init] autorelease]];
- NSHTTPCookie *cookie;
- for (cookie in newCookies) {
- [[ASIHTTPRequest sessionCookies] addObject:cookie];
- }
+ NSHTTPCookie *cookie;
+ for (cookie in newCookies) {
+ [ASIHTTPRequest addSessionCookie:cookie];
+ }
+ }
+ // Do we need to redirect?
+ if ([self shouldRedirect]) {
+ if ([self responseStatusCode] > 300 && [self responseStatusCode] < 308 && [self responseStatusCode] != 304) {
+ [self setURL:[[NSURL URLWithString:[responseHeaders valueForKey:@"Location"] relativeToURL:[self url]] absoluteURL]];
+ [self setNeedsRedirect:YES];
}
}
@@ -1256,6 +1281,9 @@ - (void)handleBytesAvailable
return;
}
}
+ if ([self needsRedirect]) {
+ return;
+ }
int bufferSize = 2048;
if (contentLength > 262144) {
bufferSize = 65536;
@@ -1307,6 +1335,9 @@ - (void)handleStreamComplete
return;
}
}
+ if ([self needsRedirect]) {
+ return;
+ }
[progressLock lock];
[self setComplete:YES];
[self updateProgressIndicators];
@@ -1371,8 +1402,6 @@ - (void)handleStreamError
{
NSError *underlyingError = [(NSError *)CFReadStreamCopyError(readStream) autorelease];
-
-
[self cancelLoad];
[self setComplete:YES];
@@ -1459,19 +1488,37 @@ + (void)removeCredentialsForHost:(NSString *)host port:(int)port protocol:(NSStr
+ (NSMutableArray *)sessionCookies
{
+ if (!sessionCookies) {
+ [ASIHTTPRequest setSessionCookies:[[[NSMutableArray alloc] init] autorelease]];
+ }
return sessionCookies;
}
+ (void)setSessionCookies:(NSMutableArray *)newSessionCookies
{
// Remove existing cookies from the persistent store
- for (NSHTTPCookie *cookie in [ASIHTTPRequest sessionCookies]) {
+ for (NSHTTPCookie *cookie in sessionCookies) {
[[NSHTTPCookieStorage sharedHTTPCookieStorage] deleteCookie:cookie];
}
[sessionCookies release];
sessionCookies = [newSessionCookies retain];
}
++ (void)addSessionCookie:(NSHTTPCookie *)newCookie
+{
+ NSHTTPCookie *cookie;
+ int i;
+ int max = [[ASIHTTPRequest sessionCookies] count];
+ for (i=0; i<max; i++) {
+ cookie = [[ASIHTTPRequest sessionCookies] objectAtIndex:i];
+ if ([[cookie domain] isEqualToString:[newCookie domain]] && [[cookie path] isEqualToString:[newCookie path]] && [[cookie name] isEqualToString:[newCookie name]]) {
+ [[ASIHTTPRequest sessionCookies] removeObjectAtIndex:i];
+ break;
+ }
+ }
+ [[ASIHTTPRequest sessionCookies] addObject:newCookie];
+}
+
// Dump all session data (authentication and cookies)
+ (void)clearSession
{
@@ -1676,4 +1723,6 @@ + (int)uncompressZippedDataFromSource:(FILE *)source toDestination:(FILE *)dest
@synthesize updatedProgress;
@synthesize shouldRedirect;
@synthesize validatesSecureCertificate;
+@synthesize needsRedirect;
+@synthesize redirectCount;
@end
View
2 Classes/Tests/ASIHTTPRequestTests.h
@@ -33,4 +33,6 @@
- (void)testCompressedResponse;
- (void)testCompressedResponseDownloadToFile;
- (void)testSSL;
+- (void)testRedirectPreservesSession;
+- (void)testTooMuchRedirection;
@end
View
20 Classes/Tests/ASIHTTPRequestTests.m
@@ -639,4 +639,24 @@ - (void)testSSL
GHAssertNil([request error],@"Failed to accept a self-signed certificate");
}
+- (void)testRedirectPreservesSession
+{
+ // Remove any old session cookies
+ [ASIHTTPRequest clearSession];
+ ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:@"http://asi/ASIHTTPRequest/tests/session_redirect"]];
+ [request start];
+ BOOL success = [[request responseString] isEqualToString:@"Take me to your leader"];
+ GHAssertTrue(success,@"Failed to redirect preserving session cookies");
+}
+
+- (void)testTooMuchRedirection
+{
+ // This url will simply send a 302 redirect back to itself
+ ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:@"http://asi/ASIHTTPRequest/tests/one_infinite_loop"]];
+ [request start];
+ GHAssertNotNil([request error],@"Failed to generate an error when redirection occurs too many times");
+ BOOL success = ([[request error] code] == ASITooMuchRedirectionErrorType);
+ GHAssertTrue(success,@"Generated the wrong error for a redirection loop");
+}
+
@end

0 comments on commit ab5f503

Please sign in to comment.
Something went wrong with that request. Please try again.