Ben Copsey

Merge branch 'master' into s3

... ... @@ -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;
... ...
... ... @@ -27,6 +27,8 @@ static CFHTTPAuthenticationRef sessionAuthentication = NULL;
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 NSError *ASIRequestCancelledError;
static NSError *ASIRequestTimedOutError;
static NSError *ASIAuthenticationError;
static NSError *ASIUnableToCreateRequestError;
static NSError *ASITooMuchRedirectionError;
// Private stuff
@interface ASIHTTPRequest ()
... ... @@ -64,6 +68,8 @@ static NSError *ASIUnableToCreateRequestError;
@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 @@ static NSError *ASIUnableToCreateRequestError;
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 @@ static NSError *ASIUnableToCreateRequestError;
[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 @@ static NSError *ASIUnableToCreateRequestError;
// 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,6 +533,22 @@ static NSError *ASIUnableToCreateRequestError;
}
}
// 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
if ([self isCancelled]) {
break;
... ... @@ -944,7 +965,7 @@ static NSError *ASIUnableToCreateRequestError;
[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 @@ static NSError *ASIUnableToCreateRequestError;
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 @@ static NSError *ASIUnableToCreateRequestError;
return;
}
}
if ([self needsRedirect]) {
return;
}
int bufferSize = 2048;
if (contentLength > 262144) {
bufferSize = 65536;
... ... @@ -1307,6 +1335,9 @@ static NSError *ASIUnableToCreateRequestError;
return;
}
}
if ([self needsRedirect]) {
return;
}
[progressLock lock];
[self setComplete:YES];
[self updateProgressIndicators];
... ... @@ -1371,8 +1402,6 @@ static NSError *ASIUnableToCreateRequestError;
{
NSError *underlyingError = [(NSError *)CFReadStreamCopyError(readStream) autorelease];
[self cancelLoad];
[self setComplete:YES];
... ... @@ -1459,19 +1488,37 @@ static NSError *ASIUnableToCreateRequestError;
+ (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 @@ static NSError *ASIUnableToCreateRequestError;
@synthesize updatedProgress;
@synthesize shouldRedirect;
@synthesize validatesSecureCertificate;
@synthesize needsRedirect;
@synthesize redirectCount;
@end
... ...
... ... @@ -33,4 +33,6 @@
- (void)testCompressedResponse;
- (void)testCompressedResponseDownloadToFile;
- (void)testSSL;
- (void)testRedirectPreservesSession;
- (void)testTooMuchRedirection;
@end
... ...
... ... @@ -639,4 +639,24 @@
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://allseeing-i.com/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://allseeing-i.com/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
... ...
... ... @@ -100,6 +100,7 @@
[imageView3 setImage:nil];
[networkQueue cancelAllOperations];
[networkQueue setRequestDidFinishSelector:NULL];
[networkQueue setDownloadProgressDelegate:progressIndicator];
[networkQueue setDelegate:self];
[networkQueue setShowAccurateProgress:([showAccurateProgress state] == NSOnState)];
... ...