aboutsummaryrefslogtreecommitdiff
path: root/Foundation/GTMHTTPFetcher.m
diff options
context:
space:
mode:
Diffstat (limited to 'Foundation/GTMHTTPFetcher.m')
-rw-r--r--Foundation/GTMHTTPFetcher.m1889
1 files changed, 1889 insertions, 0 deletions
diff --git a/Foundation/GTMHTTPFetcher.m b/Foundation/GTMHTTPFetcher.m
new file mode 100644
index 0000000..9853d0d
--- /dev/null
+++ b/Foundation/GTMHTTPFetcher.m
@@ -0,0 +1,1889 @@
+//
+// GTMHTTPFetcher.m
+//
+// Copyright 2007-2008 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy
+// of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+//
+
+#define GTMHTTPFETCHER_DEFINE_GLOBALS 1
+
+#import "GTMHTTPFetcher.h"
+#import "GTMDebugSelectorValidation.h"
+
+@interface GTMHTTPFetcher (GTMHTTPFetcherLoggingInternal)
+- (void)logFetchWithError:(NSError *)error;
+- (void)logCapturePostStream;
+@end
+
+// Make sure that if logging is disabled, the InputStream logging is also
+// diabled.
+#if !GTM_HTTPFETCHER_ENABLE_LOGGING
+# undef GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING
+# define GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING 0
+#endif // GTM_HTTPFETCHER_ENABLE_LOGGING
+
+#if GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING
+#import "GTMProgressMonitorInputStream.h"
+@interface GTMInputStreamLogger : GTMProgressMonitorInputStream
+// GTMInputStreamLogger wraps any NSInputStream used for uploading so we can
+// capture a copy of the data for the log
+@end
+#endif // !GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING
+
+#if MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_4
+@interface NSURLConnection (LeopardMethodsOnTigerBuilds)
+- (id)initWithRequest:(NSURLRequest *)request delegate:(id)delegate startImmediately:(BOOL)startImmediately;
+- (void)start;
+- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode;
+@end
+#endif
+
+NSString* const kGTMLastModifiedHeader = @"Last-Modified";
+NSString* const kGTMIfModifiedSinceHeader = @"If-Modified-Since";
+
+
+NSMutableArray* gGTMFetcherStaticCookies = nil;
+Class gGTMFetcherConnectionClass = nil;
+NSArray *gGTMFetcherDefaultRunLoopModes = nil;
+
+const NSTimeInterval kDefaultMaxRetryInterval = 60. * 10.; // 10 minutes
+
+@interface GTMHTTPFetcher (PrivateMethods)
+- (void)setCookies:(NSArray *)newCookies
+ inArray:(NSMutableArray *)cookieStorageArray;
+- (NSArray *)cookiesForURL:(NSURL *)theURL inArray:(NSMutableArray *)cookieStorageArray;
+- (void)handleCookiesForResponse:(NSURLResponse *)response;
+- (BOOL)shouldRetryNowForStatus:(NSInteger)status error:(NSError *)error;
+- (void)destroyRetryTimer;
+- (void)beginRetryTimer;
+- (void)primeTimerWithNewTimeInterval:(NSTimeInterval)secs;
+- (void)retryFetch;
+@end
+
+@implementation GTMHTTPFetcher
+
++ (GTMHTTPFetcher *)httpFetcherWithRequest:(NSURLRequest *)request {
+ return [[[GTMHTTPFetcher alloc] initWithRequest:request] autorelease];
+}
+
++ (void)initialize {
+ if (!gGTMFetcherStaticCookies) {
+ gGTMFetcherStaticCookies = [[NSMutableArray alloc] init];
+ }
+}
+
+- (id)init {
+ return [self initWithRequest:nil];
+}
+
+- (id)initWithRequest:(NSURLRequest *)request {
+ if ((self = [super init]) != nil) {
+
+ request_ = [request mutableCopy];
+
+ [self setCookieStorageMethod:kGTMHTTPFetcherCookieStorageMethodStatic];
+ }
+ return self;
+}
+
+// TODO: do we need finalize to call stopFetching?
+
+- (void)dealloc {
+ [self stopFetching]; // releases connection_
+
+ [request_ release];
+ [downloadedData_ release];
+ [credential_ release];
+ [proxyCredential_ release];
+ [postData_ release];
+ [postStream_ release];
+ [loggedStreamData_ release];
+ [response_ release];
+ [userData_ release];
+ [runLoopModes_ release];
+ [fetchHistory_ release];
+ [self destroyRetryTimer];
+
+ [super dealloc];
+}
+
+#pragma mark -
+
+// Begin fetching the URL. |delegate| is not retained
+// The delegate must provide and implement the finished and failed selectors.
+//
+// finishedSEL has a signature like:
+// - (void)fetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data
+// failedSEL has a signature like:
+// - (void)fetcher:(GTMHTTPFetcher *)fetcher failedWithError:(NSError *)error
+
+- (BOOL)beginFetchWithDelegate:(id)delegate
+ didFinishSelector:(SEL)finishedSEL
+ didFailSelector:(SEL)failedSEL {
+
+ GTMAssertSelectorNilOrImplementedWithArguments(delegate, finishedSEL, @encode(GTMHTTPFetcher *), @encode(NSData *), NULL);
+ GTMAssertSelectorNilOrImplementedWithArguments(delegate, failedSEL, @encode(GTMHTTPFetcher *), @encode(NSError *), NULL);
+ GTMAssertSelectorNilOrImplementedWithArguments(delegate, receivedDataSEL_, @encode(GTMHTTPFetcher *), @encode(NSData *), NULL);
+ GTMAssertSelectorNilOrImplementedWithArguments(delegate, retrySEL_, @encode(GTMHTTPFetcher *), @encode(BOOL), @encode(NSError *), NULL);
+
+ if (connection_ != nil) {
+ _GTMDevAssert(connection_ != nil,
+ @"fetch object %@ being reused; this should never happen",
+ self);
+ goto CannotBeginFetch;
+ }
+
+ if (request_ == nil) {
+ _GTMDevAssert(request_ != nil, @"beginFetchWithDelegate requires a request");
+ goto CannotBeginFetch;
+ }
+
+ [downloadedData_ release];
+ downloadedData_ = nil;
+
+ [self setDelegate:delegate];
+ finishedSEL_ = finishedSEL;
+ failedSEL_ = failedSEL;
+
+ if (postData_ || postStream_) {
+ if ([request_ HTTPMethod] == nil || [[request_ HTTPMethod] isEqual:@"GET"]) {
+ [request_ setHTTPMethod:@"POST"];
+ }
+
+ if (postData_) {
+ [request_ setHTTPBody:postData_];
+ } else {
+
+ // if logging is enabled, it needs a buffer to accumulate data from any
+ // NSInputStream used for uploading. Logging will wrap the input
+ // stream with a stream that lets us keep a copy the data being read.
+ if ([GTMHTTPFetcher isLoggingEnabled] && postStream_ != nil) {
+ loggedStreamData_ = [[NSMutableData alloc] init];
+ [self logCapturePostStream];
+ }
+
+ [request_ setHTTPBodyStream:postStream_];
+ }
+ }
+
+ if (fetchHistory_) {
+
+ // If this URL is in the history, set the Last-Modified header field
+
+ // if we have a history, we're tracking across fetches, so we don't
+ // want to pull results from a cache
+ [request_ setCachePolicy:NSURLRequestReloadIgnoringCacheData];
+
+ NSDictionary* lastModifiedDict = [fetchHistory_ objectForKey:kGTMHTTPFetcherHistoryLastModifiedKey];
+ NSString* urlString = [[request_ URL] absoluteString];
+ NSString* lastModifiedStr = [lastModifiedDict objectForKey:urlString];
+
+ // servers don't want last-modified-ifs on POSTs, so check for a body
+ if (lastModifiedStr
+ && [request_ HTTPBody] == nil
+ && [request_ HTTPBodyStream] == nil) {
+
+ [request_ addValue:lastModifiedStr forHTTPHeaderField:kGTMIfModifiedSinceHeader];
+ }
+ }
+
+ // get cookies for this URL from our storage array, if
+ // we have a storage array
+ if (cookieStorageMethod_ != kGTMHTTPFetcherCookieStorageMethodSystemDefault) {
+
+ NSArray *cookies = [self cookiesForURL:[request_ URL]];
+
+ if ([cookies count]) {
+
+ NSDictionary *headerFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
+ NSString *cookieHeader = [headerFields objectForKey:@"Cookie"]; // key used in header dictionary
+ if (cookieHeader) {
+ [request_ addValue:cookieHeader forHTTPHeaderField:@"Cookie"]; // header name
+ }
+ }
+ }
+
+ // finally, start the connection
+
+ Class connectionClass = [[self class] connectionClass];
+
+ NSArray *runLoopModes = nil;
+
+ if ([[self class] doesSupportRunLoopModes]) {
+
+ // use the connection-specific run loop modes, if they were provided,
+ // or else use the GTMHTTPFetcher default run loop modes, if any
+ if (runLoopModes_) {
+ runLoopModes = runLoopModes_;
+ } else {
+ runLoopModes = gGTMFetcherDefaultRunLoopModes;
+ }
+ }
+
+ if ([runLoopModes count] == 0) {
+
+ // if no run loop modes were specified, then we'll start the connection
+ // on the current run loop in the current mode
+ connection_ = [[connectionClass connectionWithRequest:request_
+ delegate:self] retain];
+ } else {
+
+ // schedule on current run loop in the specified modes
+ connection_ = [[connectionClass alloc] initWithRequest:request_
+ delegate:self
+ startImmediately:NO];
+
+ for (int idx = 0; idx < [runLoopModes count]; idx++) {
+ NSString *mode = [runLoopModes objectAtIndex:idx];
+ [connection_ scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:mode];
+ }
+ [connection_ start];
+ }
+
+ if (!connection_) {
+ _GTMDevAssert(connection_ != nil,
+ @"beginFetchWithDelegate could not create a connection");
+ goto CannotBeginFetch;
+ }
+
+ // we'll retain the delegate only during the outstanding connection (similar
+ // to what Cocoa does with performSelectorOnMainThread:) since we'd crash
+ // if the delegate was released in the interim. We don't retain the selector
+ // at other times, to avoid vicious retain loops. This retain is balanced in
+ // the -stopFetch method.
+ [delegate_ retain];
+
+ downloadedData_ = [[NSMutableData alloc] init];
+ return YES;
+
+CannotBeginFetch:
+
+ if (failedSEL) {
+
+ NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherErrorDomain
+ code:kGTMHTTPFetcherErrorDownloadFailed
+ userInfo:nil];
+
+ [[self retain] autorelease]; // in case the callback releases us
+
+ [delegate performSelector:failedSEL_
+ withObject:self
+ withObject:error];
+ }
+
+ return NO;
+}
+
+// Returns YES if this is in the process of fetching a URL, or waiting to
+// retry
+- (BOOL)isFetching {
+ return (connection_ != nil || retryTimer_ != nil);
+}
+
+// Returns the status code set in connection:didReceiveResponse:
+- (NSInteger)statusCode {
+
+ NSInteger statusCode;
+
+ if (response_ != nil
+ && [response_ respondsToSelector:@selector(statusCode)]) {
+
+ statusCode = [(NSHTTPURLResponse *)response_ statusCode];
+ } else {
+ // Default to zero, in hopes of hinting "Unknown" (we can't be
+ // sure that things are OK enough to use 200).
+ statusCode = 0;
+ }
+ return statusCode;
+}
+
+// Cancel the fetch of the URL that's currently in progress.
+- (void)stopFetching {
+ [self destroyRetryTimer];
+
+ if (connection_) {
+ // in case cancelling the connection calls this recursively, we want
+ // to ensure that we'll only release the connection and delegate once,
+ // so first set connection_ to nil
+
+ NSURLConnection* oldConnection = connection_;
+ connection_ = nil;
+
+ // this may be called in a callback from the connection, so use autorelease
+ [oldConnection cancel];
+ [oldConnection autorelease];
+
+ // balance the retain done when the connection was opened
+ [delegate_ release];
+ }
+}
+
+- (void)retryFetch {
+
+ id holdDelegate = [[delegate_ retain] autorelease];
+
+ [self stopFetching];
+
+ [self beginFetchWithDelegate:holdDelegate
+ didFinishSelector:finishedSEL_
+ didFailSelector:failedSEL_];
+}
+
+#pragma mark NSURLConnection Delegate Methods
+
+//
+// NSURLConnection Delegate Methods
+//
+
+// This method just says "follow all redirects", which _should_ be the default behavior,
+// According to file:///Developer/ADC%20Reference%20Library/documentation/Cocoa/Conceptual/URLLoadingSystem
+// but the redirects were not being followed until I added this method. May be
+// a bug in the NSURLConnection code, or the documentation.
+//
+// In OS X 10.4.8 and earlier, the redirect request doesn't
+// get the original's headers and body. This causes POSTs to fail.
+// So we construct a new request, a copy of the original, with overrides from the
+// redirect.
+//
+// Docs say that if redirectResponse is nil, just return the redirectRequest.
+
+- (NSURLRequest *)connection:(NSURLConnection *)connection
+ willSendRequest:(NSURLRequest *)redirectRequest
+ redirectResponse:(NSURLResponse *)redirectResponse {
+
+ if (redirectRequest && redirectResponse) {
+ NSMutableURLRequest *newRequest = [[request_ mutableCopy] autorelease];
+ // copy the URL
+ NSURL *redirectURL = [redirectRequest URL];
+ NSURL *url = [newRequest URL];
+
+ // disallow scheme changes (say, from https to http)
+ NSString *redirectScheme = [url scheme];
+ NSString *newScheme = [redirectURL scheme];
+ NSString *newResourceSpecifier = [redirectURL resourceSpecifier];
+
+ if ([redirectScheme caseInsensitiveCompare:@"http"] == NSOrderedSame
+ && newScheme != nil
+ && [newScheme caseInsensitiveCompare:@"https"] == NSOrderedSame) {
+
+ // allow the change from http to https
+ redirectScheme = newScheme;
+ }
+
+ NSString *newUrlString = [NSString stringWithFormat:@"%@:%@",
+ redirectScheme, newResourceSpecifier];
+
+ NSURL *newURL = [NSURL URLWithString:newUrlString];
+ [newRequest setURL:newURL];
+
+ // any headers in the redirect override headers in the original.
+ NSDictionary *redirectHeaders = [redirectRequest allHTTPHeaderFields];
+ if (redirectHeaders) {
+ NSEnumerator *enumerator = [redirectHeaders keyEnumerator];
+ NSString *key;
+ while (nil != (key = [enumerator nextObject])) {
+ NSString *value = [redirectHeaders objectForKey:key];
+ [newRequest setValue:value forHTTPHeaderField:key];
+ }
+ }
+ redirectRequest = newRequest;
+
+ // save cookies from the response
+ [self handleCookiesForResponse:redirectResponse];
+
+ // log the response we just received
+ [self setResponse:redirectResponse];
+ [self logFetchWithError:nil];
+
+ // update the request for future logging
+ [self setRequest:redirectRequest];
+}
+ return redirectRequest;
+}
+
+- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
+
+ // this method is called when the server has determined that it
+ // has enough information to create the NSURLResponse
+ // it can be called multiple times, for example in the case of a
+ // redirect, so each time we reset the data.
+ [downloadedData_ setLength:0];
+
+ [self setResponse:response];
+
+ // save cookies from the response
+ [self handleCookiesForResponse:response];
+}
+
+
+// handleCookiesForResponse: handles storage of cookies for responses passed to
+// connection:willSendRequest:redirectResponse: and connection:didReceiveResponse:
+- (void)handleCookiesForResponse:(NSURLResponse *)response {
+
+ if (cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodSystemDefault) {
+
+ // do nothing special for NSURLConnection's default storage mechanism
+
+ } else if ([response respondsToSelector:@selector(allHeaderFields)]) {
+
+ // grab the cookies from the header as NSHTTPCookies and store them either
+ // into our static array or into the fetchHistory
+
+ NSDictionary *responseHeaderFields = [(NSHTTPURLResponse *)response allHeaderFields];
+ if (responseHeaderFields) {
+
+ NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:responseHeaderFields
+ forURL:[response URL]];
+ if ([cookies count] > 0) {
+
+ NSMutableArray *cookieArray = nil;
+
+ // static cookies are stored in gGTMFetcherStaticCookies; fetchHistory
+ // cookies are stored in fetchHistory_'s kGTMHTTPFetcherHistoryCookiesKey
+
+ if (cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodStatic) {
+
+ cookieArray = gGTMFetcherStaticCookies;
+
+ } else if (cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodFetchHistory
+ && fetchHistory_ != nil) {
+
+ cookieArray = [fetchHistory_ objectForKey:kGTMHTTPFetcherHistoryCookiesKey];
+ if (cookieArray == nil) {
+ cookieArray = [NSMutableArray array];
+ [fetchHistory_ setObject:cookieArray forKey:kGTMHTTPFetcherHistoryCookiesKey];
+ }
+ }
+
+ if (cookieArray) {
+ @synchronized(cookieArray) {
+ [self setCookies:cookies inArray:cookieArray];
+ }
+ }
+ }
+ }
+ }
+}
+
+-(void)connection:(NSURLConnection *)connection
+ didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
+
+ if ([challenge previousFailureCount] <= 2) {
+
+ NSURLCredential *credential = credential_;
+
+ if ([[challenge protectionSpace] isProxy] && proxyCredential_ != nil) {
+ credential = proxyCredential_;
+ }
+
+ // Here, if credential is still nil, then we *could* try to get it from
+ // NSURLCredentialStorage's defaultCredentialForProtectionSpace:.
+ // We don't, because we're assuming:
+ //
+ // - for server credentials, we only want ones supplied by the program
+ // calling http fetcher
+ // - for proxy credentials, if one were necessary and available in the
+ // keychain, it would've been found automatically by NSURLConnection
+ // and this challenge delegate method never would've been called
+ // anyway
+
+ if (credential) {
+ // try the credential
+ [[challenge sender] useCredential:credential
+ forAuthenticationChallenge:challenge];
+ return;
+ }
+ }
+
+ // If we don't have credentials, or we've already failed auth 3x...
+ [[challenge sender] cancelAuthenticationChallenge:challenge];
+
+ // report the error, putting the challenge as a value in the userInfo
+ // dictionary
+ NSDictionary *userInfo = [NSDictionary dictionaryWithObject:challenge
+ forKey:kGTMHTTPFetcherErrorChallengeKey];
+
+ NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherErrorDomain
+ code:kGTMHTTPFetcherErrorAuthenticationChallengeFailed
+ userInfo:userInfo];
+
+ [self connection:connection didFailWithError:error];
+}
+
+
+
+- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
+ [downloadedData_ appendData:data];
+
+ if (receivedDataSEL_) {
+ [delegate_ performSelector:receivedDataSEL_
+ withObject:self
+ withObject:downloadedData_];
+ }
+}
+
+- (void)updateFetchHistory {
+
+ if (fetchHistory_) {
+
+ NSString* urlString = [[request_ URL] absoluteString];
+ if ([response_ respondsToSelector:@selector(allHeaderFields)]) {
+ NSDictionary *headers = [(NSHTTPURLResponse *)response_ allHeaderFields];
+ NSString* lastModifiedStr = [headers objectForKey:kGTMLastModifiedHeader];
+
+ // get the dictionary mapping URLs to last-modified dates
+ NSMutableDictionary* lastModifiedDict = [fetchHistory_ objectForKey:kGTMHTTPFetcherHistoryLastModifiedKey];
+ if (!lastModifiedDict) {
+ lastModifiedDict = [NSMutableDictionary dictionary];
+ [fetchHistory_ setObject:lastModifiedDict forKey:kGTMHTTPFetcherHistoryLastModifiedKey];
+ }
+
+ NSMutableDictionary* datedDataCache = nil;
+ if (shouldCacheDatedData_) {
+ // get the dictionary mapping URLs to cached, dated data
+ datedDataCache = [fetchHistory_ objectForKey:kGTMHTTPFetcherHistoryDatedDataKey];
+ if (!datedDataCache) {
+ datedDataCache = [NSMutableDictionary dictionary];
+ [fetchHistory_ setObject:datedDataCache forKey:kGTMHTTPFetcherHistoryDatedDataKey];
+ }
+ }
+
+ NSInteger statusCode = [self statusCode];
+ if (statusCode != kGTMHTTPFetcherStatusNotModified) {
+
+ // save this last modified date string for successful results (<300)
+ // If there's no last modified string, clear the dictionary
+ // entry for this URL. Also cache or delete the data, if appropriate
+ // (when datedDataCache is non-nil.)
+ if (lastModifiedStr && statusCode < 300) {
+ [lastModifiedDict setValue:lastModifiedStr forKey:urlString];
+ [datedDataCache setValue:downloadedData_ forKey:urlString];
+ } else {
+ [lastModifiedDict removeObjectForKey:urlString];
+ [datedDataCache removeObjectForKey:urlString];
+ }
+ }
+ }
+ }
+}
+
+// for error 304's ("Not Modified") where we've cached the data, return status
+// 200 ("OK") to the caller (but leave the fetcher status as 304)
+// and copy the cached data to downloadedData_.
+// For other errors or if there's no cached data, just return the actual status.
+- (NSInteger)statusAfterHandlingNotModifiedError {
+
+ NSInteger status = [self statusCode];
+ if (status == kGTMHTTPFetcherStatusNotModified && shouldCacheDatedData_) {
+
+ // get the dictionary of URLs and data
+ NSString* urlString = [[request_ URL] absoluteString];
+
+ NSDictionary* datedDataCache = [fetchHistory_ objectForKey:kGTMHTTPFetcherHistoryDatedDataKey];
+ NSData* cachedData = [datedDataCache objectForKey:urlString];
+
+ if (cachedData) {
+ // copy our stored data, and forge the status to pass on to the delegate
+ [downloadedData_ setData:cachedData];
+ status = 200;
+ }
+ }
+ return status;
+}
+
+- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
+
+ [self updateFetchHistory];
+
+ [[self retain] autorelease]; // in case the callback releases us
+
+ [self logFetchWithError:nil];
+
+ NSInteger status = [self statusAfterHandlingNotModifiedError];
+
+ if (status >= 300) {
+
+ if ([self shouldRetryNowForStatus:status error:nil]) {
+
+ [self beginRetryTimer];
+
+ } else {
+ // not retrying
+
+ // did they want failure notifications?
+ if (failedSEL_) {
+
+ NSDictionary *userInfo =
+ [NSDictionary dictionaryWithObject:downloadedData_
+ forKey:kGTMHTTPFetcherStatusDataKey];
+ NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain
+ code:status
+ userInfo:userInfo];
+
+ [delegate_ performSelector:failedSEL_
+ withObject:self
+ withObject:error];
+ }
+ // we're done fetching
+ [self stopFetching];
+ }
+
+ } else if (finishedSEL_) {
+
+ // successful http status (under 300)
+ [delegate_ performSelector:finishedSEL_
+ withObject:self
+ withObject:downloadedData_];
+ [self stopFetching];
+ }
+
+}
+
+- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
+
+ [self logFetchWithError:error];
+
+ if ([self shouldRetryNowForStatus:0 error:error]) {
+
+ [self beginRetryTimer];
+
+ } else {
+
+ if (failedSEL_) {
+ [[self retain] autorelease]; // in case the callback releases us
+
+ [delegate_ performSelector:failedSEL_
+ withObject:self
+ withObject:error];
+ }
+
+ [self stopFetching];
+ }
+}
+
+#pragma mark Retries
+
+- (BOOL)isRetryError:(NSError *)error {
+
+ struct retryRecord {
+ NSString *const domain;
+ int code;
+ };
+
+ struct retryRecord retries[] = {
+ { kGTMHTTPFetcherStatusDomain, 408 }, // request timeout
+ { kGTMHTTPFetcherStatusDomain, 503 }, // service unavailable
+ { kGTMHTTPFetcherStatusDomain, 504 }, // request timeout
+ { NSURLErrorDomain, NSURLErrorTimedOut },
+ { NSURLErrorDomain, NSURLErrorNetworkConnectionLost },
+ { nil, 0 }
+ };
+
+ // NSError's isEqual always returns false for equal but distinct instances
+ // of NSError, so we have to compare the domain and code values explicitly
+
+ for (int idx = 0; retries[idx].domain != nil; idx++) {
+
+ if ([[error domain] isEqual:retries[idx].domain]
+ && [error code] == retries[idx].code) {
+
+ return YES;
+ }
+ }
+ return NO;
+}
+
+
+// shouldRetryNowForStatus:error: returns YES if the user has enabled retries
+// and the status or error is one that is suitable for retrying. "Suitable"
+// means either the isRetryError:'s list contains the status or error, or the
+// user's retrySelector: is present and returns YES when called.
+- (BOOL)shouldRetryNowForStatus:(NSInteger)status
+ error:(NSError *)error {
+
+ if ([self isRetryEnabled]) {
+
+ if ([self nextRetryInterval] < [self maxRetryInterval]) {
+
+ if (error == nil) {
+ // make an error for the status
+ error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain
+ code:status
+ userInfo:nil];
+ }
+
+ BOOL willRetry = [self isRetryError:error];
+
+ if (retrySEL_) {
+ NSMethodSignature *signature = [delegate_ methodSignatureForSelector:retrySEL_];
+ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
+ [invocation setSelector:retrySEL_];
+ [invocation setTarget:delegate_];
+ [invocation setArgument:&self atIndex:2];
+ [invocation setArgument:&willRetry atIndex:3];
+ [invocation setArgument:&error atIndex:4];
+ [invocation invoke];
+
+ [invocation getReturnValue:&willRetry];
+ }
+
+ return willRetry;
+ }
+ }
+
+ return NO;
+}
+
+- (void)beginRetryTimer {
+
+ NSTimeInterval nextInterval = [self nextRetryInterval];
+ NSTimeInterval maxInterval = [self maxRetryInterval];
+
+ NSTimeInterval newInterval = MIN(nextInterval, maxInterval);
+
+ [self primeTimerWithNewTimeInterval:newInterval];
+}
+
+- (void)primeTimerWithNewTimeInterval:(NSTimeInterval)secs {
+
+ [self destroyRetryTimer];
+
+ lastRetryInterval_ = secs;
+
+ retryTimer_ = [NSTimer scheduledTimerWithTimeInterval:secs
+ target:self
+ selector:@selector(retryTimerFired:)
+ userInfo:nil
+ repeats:NO];
+ [retryTimer_ retain];
+}
+
+- (void)retryTimerFired:(NSTimer *)timer {
+
+ [self destroyRetryTimer];
+
+ retryCount_++;
+
+ [self retryFetch];
+}
+
+- (void)destroyRetryTimer {
+
+ [retryTimer_ invalidate];
+ [retryTimer_ autorelease];
+ retryTimer_ = nil;
+}
+
+- (unsigned int)retryCount {
+ return retryCount_;
+}
+
+- (NSTimeInterval)nextRetryInterval {
+ // the next wait interval is the factor (2.0) times the last interval,
+ // but never less than the minimum interval
+ NSTimeInterval secs = lastRetryInterval_ * retryFactor_;
+ secs = MIN(secs, maxRetryInterval_);
+ secs = MAX(secs, minRetryInterval_);
+
+ return secs;
+}
+
+- (BOOL)isRetryEnabled {
+ return isRetryEnabled_;
+}
+
+- (void)setIsRetryEnabled:(BOOL)flag {
+
+ if (flag && !isRetryEnabled_) {
+ // We defer initializing these until the user calls setIsRetryEnabled
+ // to avoid seeding the random number generator if it's not needed.
+ // However, it means min and max intervals for this fetcher are reset
+ // as a side effect of calling setIsRetryEnabled.
+ //
+ // seed the random value, and make an initial retry interval
+ // random between 1.0 and 2.0 seconds
+ srandomdev();
+ [self setMinRetryInterval:0.0];
+ [self setMaxRetryInterval:kDefaultMaxRetryInterval];
+ [self setRetryFactor:2.0];
+ lastRetryInterval_ = 0.0;
+ }
+ isRetryEnabled_ = flag;
+};
+
+- (SEL)retrySelector {
+ return retrySEL_;
+}
+
+- (void)setRetrySelector:(SEL)theSelector {
+ retrySEL_ = theSelector;
+}
+
+- (NSTimeInterval)maxRetryInterval {
+ return maxRetryInterval_;
+}
+
+- (void)setMaxRetryInterval:(NSTimeInterval)secs {
+ if (secs > 0) {
+ maxRetryInterval_ = secs;
+ } else {
+ maxRetryInterval_ = kDefaultMaxRetryInterval;
+ }
+}
+
+- (double)minRetryInterval {
+ return minRetryInterval_;
+}
+
+- (void)setMinRetryInterval:(NSTimeInterval)secs {
+ if (secs > 0) {
+ minRetryInterval_ = secs;
+ } else {
+ // set min interval to a random value between 1.0 and 2.0 seconds
+ // so that if multiple clients start retrying at the same time, they'll
+ // repeat at different times and avoid overloading the server
+ minRetryInterval_ = 1.0 + ((double)(random() & 0x0FFFF) / (double) 0x0FFFF);
+ }
+}
+
+- (double)retryFactor {
+ return retryFactor_;
+}
+
+- (void)setRetryFactor:(double)multiplier {
+ retryFactor_ = multiplier;
+}
+
+#pragma mark Getters and Setters
+
+- (NSMutableURLRequest *)request {
+ return request_;
+}
+
+- (void)setRequest:(NSURLRequest *)theRequest {
+ [request_ autorelease];
+ request_ = [theRequest mutableCopy];
+}
+
+- (NSURLCredential *)credential {
+ return credential_;
+}
+
+- (void)setCredential:(NSURLCredential *)theCredential {
+ [credential_ autorelease];
+ credential_ = [theCredential retain];
+}
+
+- (NSURLCredential *)proxyCredential {
+ return proxyCredential_;
+}
+
+- (void)setProxyCredential:(NSURLCredential *)theCredential {
+ [proxyCredential_ autorelease];
+ proxyCredential_ = [theCredential retain];
+}
+
+- (NSData *)postData {
+ return postData_;
+}
+
+- (void)setPostData:(NSData *)theData {
+ [postData_ autorelease];
+ postData_ = [theData retain];
+}
+
+- (NSInputStream *)postStream {
+ return postStream_;
+}
+
+- (void)setPostStream:(NSInputStream *)theStream {
+ [postStream_ autorelease];
+ postStream_ = [theStream retain];
+}
+
+- (GTMHTTPFetcherCookieStorageMethod)cookieStorageMethod {
+ return cookieStorageMethod_;
+}
+
+- (void)setCookieStorageMethod:(GTMHTTPFetcherCookieStorageMethod)method {
+
+ cookieStorageMethod_ = method;
+
+ if (method == kGTMHTTPFetcherCookieStorageMethodSystemDefault) {
+ [request_ setHTTPShouldHandleCookies:YES];
+ } else {
+ [request_ setHTTPShouldHandleCookies:NO];
+ }
+}
+
+- (id)delegate {
+ return delegate_;
+}
+
+- (void)setDelegate:(id)theDelegate {
+
+ // we retain delegate_ only during the life of the connection
+ if (connection_) {
+ [delegate_ autorelease];
+ delegate_ = [theDelegate retain];
+ } else {
+ delegate_ = theDelegate;
+ }
+}
+
+- (SEL)receivedDataSelector {
+ return receivedDataSEL_;
+}
+
+- (void)setReceivedDataSelector:(SEL)theSelector {
+ receivedDataSEL_ = theSelector;
+}
+
+- (NSURLResponse *)response {
+ return response_;
+}
+
+- (void)setResponse:(NSURLResponse *)response {
+ [response_ autorelease];
+ response_ = [response retain];
+}
+
+- (NSMutableDictionary *)fetchHistory {
+ return fetchHistory_;
+}
+
+- (void)setFetchHistory:(NSMutableDictionary *)fetchHistory {
+ [fetchHistory_ autorelease];
+ fetchHistory_ = [fetchHistory retain];
+
+ if (fetchHistory_ != nil) {
+ [self setCookieStorageMethod:kGTMHTTPFetcherCookieStorageMethodFetchHistory];
+ } else {
+ [self setCookieStorageMethod:kGTMHTTPFetcherCookieStorageMethodStatic];
+ }
+}
+
+- (void)setShouldCacheDatedData:(BOOL)flag {
+ shouldCacheDatedData_ = flag;
+ if (!flag) {
+ [self clearDatedDataHistory];
+ }
+}
+
+- (BOOL)shouldCacheDatedData {
+ return shouldCacheDatedData_;
+}
+
+// delete last-modified dates and cached data from the fetch history
+- (void)clearDatedDataHistory {
+ [fetchHistory_ removeObjectForKey:kGTMHTTPFetcherHistoryLastModifiedKey];
+ [fetchHistory_ removeObjectForKey:kGTMHTTPFetcherHistoryDatedDataKey];
+}
+
+- (id)userData {
+ return userData_;
+}
+
+- (void)setUserData:(id)theObj {
+ [userData_ autorelease];
+ userData_ = [theObj retain];
+}
+
+- (NSArray *)runLoopModes {
+ return runLoopModes_;
+}
+
+- (void)setRunLoopModes:(NSArray *)modes {
+ [runLoopModes_ autorelease];
+ runLoopModes_ = [modes retain];
+}
+
++ (BOOL)doesSupportRunLoopModes {
+ SEL sel = @selector(initWithRequest:delegate:startImmediately:);
+ return [NSURLConnection instancesRespondToSelector:sel];
+}
+
++ (NSArray *)defaultRunLoopModes {
+ return gGTMFetcherDefaultRunLoopModes;
+}
+
++ (void)setDefaultRunLoopModes:(NSArray *)modes {
+ [gGTMFetcherDefaultRunLoopModes autorelease];
+ gGTMFetcherDefaultRunLoopModes = [modes retain];
+}
+
++ (Class)connectionClass {
+ if (gGTMFetcherConnectionClass == nil) {
+ gGTMFetcherConnectionClass = [NSURLConnection class];
+ }
+ return gGTMFetcherConnectionClass;
+}
+
++ (void)setConnectionClass:(Class)theClass {
+ gGTMFetcherConnectionClass = theClass;
+}
+
+#pragma mark Cookies
+
+// return a cookie from the array with the same name, domain, and path as the
+// given cookie, or else return nil if none found
+//
+// Both the cookie being tested and all cookies in cookieStorageArray should
+// be valid (non-nil name, domains, paths)
+- (NSHTTPCookie *)cookieMatchingCookie:(NSHTTPCookie *)cookie
+ inArray:(NSArray *)cookieStorageArray {
+
+ NSUInteger numberOfCookies = [cookieStorageArray count];
+ NSString *name = [cookie name];
+ NSString *domain = [cookie domain];
+ NSString *path = [cookie path];
+
+ _GTMDevAssert(name && domain && path,
+ @"Invalid cookie (name:%@ domain:%@ path:%@)",
+ name, domain, path);
+
+ for (NSUInteger idx = 0; idx < numberOfCookies; idx++) {
+
+ NSHTTPCookie *storedCookie = [cookieStorageArray objectAtIndex:idx];
+
+ if ([[storedCookie name] isEqual:name]
+ && [[storedCookie domain] isEqual:domain]
+ && [[storedCookie path] isEqual:path]) {
+
+ return storedCookie;
+ }
+ }
+ return nil;
+}
+
+// remove any expired cookies from the array, excluding cookies with nil
+// expirations
+- (void)removeExpiredCookiesInArray:(NSMutableArray *)cookieStorageArray {
+
+ // count backwards since we're deleting items from the array
+ for (NSInteger idx = [cookieStorageArray count] - 1; idx >= 0; idx--) {
+
+ NSHTTPCookie *storedCookie = [cookieStorageArray objectAtIndex:idx];
+
+ NSDate *expiresDate = [storedCookie expiresDate];
+ if (expiresDate && [expiresDate timeIntervalSinceNow] < 0) {
+ [cookieStorageArray removeObjectAtIndex:idx];
+ }
+ }
+}
+
+
+// retrieve all cookies appropriate for the given URL, considering
+// domain, path, cookie name, expiration, security setting.
+// Side effect: removed expired cookies from the storage array
+- (NSArray *)cookiesForURL:(NSURL *)theURL inArray:(NSMutableArray *)cookieStorageArray {
+
+ [self removeExpiredCookiesInArray:cookieStorageArray];
+
+ NSMutableArray *foundCookies = [NSMutableArray array];
+
+ // we'll prepend "." to the desired domain, since we want the
+ // actual domain "nytimes.com" to still match the cookie domain ".nytimes.com"
+ // when we check it below with hasSuffix
+ NSString *host = [theURL host];
+ NSString *path = [theURL path];
+ NSString *scheme = [theURL scheme];
+
+ NSString *domain = nil;
+ if ([host isEqual:@"localhost"]) {
+ // the domain stored into NSHTTPCookies for localhost is "localhost.local"
+ domain = @"localhost.local";
+ } else {
+ if (host) {
+ domain = [@"." stringByAppendingString:host];
+ }
+ }
+
+ NSUInteger numberOfCookies = [cookieStorageArray count];
+ for (NSUInteger idx = 0; idx < numberOfCookies; idx++) {
+
+ NSHTTPCookie *storedCookie = [cookieStorageArray objectAtIndex:idx];
+
+ NSString *cookieDomain = [storedCookie domain];
+ NSString *cookiePath = [storedCookie path];
+ BOOL cookieIsSecure = [storedCookie isSecure];
+
+ BOOL domainIsOK = [domain hasSuffix:cookieDomain];
+ BOOL pathIsOK = [cookiePath isEqual:@"/"] || [path hasPrefix:cookiePath];
+ BOOL secureIsOK = (!cookieIsSecure) || [scheme isEqual:@"https"];
+
+ if (domainIsOK && pathIsOK && secureIsOK) {
+ [foundCookies addObject:storedCookie];
+ }
+ }
+ return foundCookies;
+}
+
+// return cookies for the given URL using the current cookie storage method
+- (NSArray *)cookiesForURL:(NSURL *)theURL {
+
+ NSArray *cookies = nil;
+ NSMutableArray *cookieStorageArray = nil;
+
+ if (cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodStatic) {
+ cookieStorageArray = gGTMFetcherStaticCookies;
+ } else if (cookieStorageMethod_ == kGTMHTTPFetcherCookieStorageMethodFetchHistory) {
+ cookieStorageArray = [fetchHistory_ objectForKey:kGTMHTTPFetcherHistoryCookiesKey];
+ } else {
+ // kGTMHTTPFetcherCookieStorageMethodSystemDefault
+ cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:theURL];
+ }
+
+ if (cookieStorageArray) {
+
+ @synchronized(cookieStorageArray) {
+
+ // cookiesForURL returns a new array of immutable NSCookie objects
+ // from cookieStorageArray
+ cookies = [self cookiesForURL:theURL
+ inArray:cookieStorageArray];
+ }
+ }
+ return cookies;
+}
+
+
+// add all cookies in the array |newCookies| to the storage array,
+// replacing cookies in the storage array as appropriate
+// Side effect: removes expired cookies from the storage array
+- (void)setCookies:(NSArray *)newCookies
+ inArray:(NSMutableArray *)cookieStorageArray {
+
+ [self removeExpiredCookiesInArray:cookieStorageArray];
+
+ NSEnumerator *newCookieEnum = [newCookies objectEnumerator];
+ NSHTTPCookie *newCookie;
+
+ while ((newCookie = [newCookieEnum nextObject]) != nil) {
+
+ if ([[newCookie name] length] > 0
+ && [[newCookie domain] length] > 0
+ && [[newCookie path] length] > 0) {
+
+ // remove the cookie if it's currently in the array
+ NSHTTPCookie *oldCookie = [self cookieMatchingCookie:newCookie
+ inArray:cookieStorageArray];
+ if (oldCookie) {
+ [cookieStorageArray removeObject:oldCookie];
+ }
+
+ // make sure the cookie hasn't already expired
+ NSDate *expiresDate = [newCookie expiresDate];
+ if ((!expiresDate) || [expiresDate timeIntervalSinceNow] > 0) {
+ [cookieStorageArray addObject:newCookie];
+ }
+
+ } else {
+ _GTMDevAssert(NO, @"Cookie incomplete: %@", newCookie);
+ }
+ }
+}
+@end
+
+#pragma mark Logging
+
+// NOTE: Threads and Logging
+//
+// All the NSURLConnection callbacks happen on one thread, so we don't have
+// to put any synchronization into the logging code. Yes, the state around
+// logging (it's directory, etc.) could use it, but for now, that's punted.
+
+
+// We don't invoke Leopard methods on 10.4, because we check if the methods are
+// implemented before invoking it, but we need to be able to compile without
+// warnings.
+// This declaration means if you target <=10.4, this method will compile
+// without complaint in this source, so you must test with
+// -respondsToSelector:, too.
+#if MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_4
+@interface NSFileManager (LeopardMethodsOnTigerBuilds)
+- (BOOL)removeItemAtPath:(NSString *)path error:(NSError **)error;
+@end
+#endif
+// The iPhone Foundation removes the deprecated removeFileAtPath:handler:
+#if GTM_IPHONE_SDK
+@interface NSFileManager (TigerMethodsOniPhoneBuilds)
+- (BOOL)removeFileAtPath:(NSString *)path handler:(id)handler;
+@end
+#endif
+
+@implementation GTMHTTPFetcher (GTMHTTPFetcherLogging)
+
+// if GTM_HTTPFETCHER_ENABLE_LOGGING is defined by the user's project then
+// logging code will be compiled into the framework
+
+#if !GTM_HTTPFETCHER_ENABLE_LOGGING
+- (void)logFetchWithError:(NSError *)error {}
+
++ (void)setLoggingDirectory:(NSString *)path {}
++ (NSString *)loggingDirectory {return nil;}
+
++ (void)setIsLoggingEnabled:(BOOL)flag {}
++ (BOOL)isLoggingEnabled {return NO;}
+
++ (void)setLoggingProcessName:(NSString *)str {}
++ (NSString *)loggingProcessName {return nil;}
+
++ (void)setLoggingDateStamp:(NSString *)str {}
++ (NSString *)loggingDateStamp {return nil;}
+
+- (void)appendLoggedStreamData:(NSData *)newData {}
+- (void)logCapturePostStream {}
+#else // GTM_HTTPFETCHER_ENABLE_LOGGING
+
+// fetchers come and fetchers go, but statics are forever
+static BOOL gIsLoggingEnabled = NO;
+static NSString *gLoggingDirectoryPath = nil;
+static NSString *gLoggingDateStamp = nil;
+static NSString* gLoggingProcessName = nil;
+
++ (void)setLoggingDirectory:(NSString *)path {
+ [gLoggingDirectoryPath autorelease];
+ gLoggingDirectoryPath = [path copy];
+}
+
++ (NSString *)loggingDirectory {
+
+ if (!gLoggingDirectoryPath) {
+
+#if GTM_IPHONE_SDK
+ // default to a directory called GTMHTTPDebugLogs into a sandbox-safe
+ // directory that a devloper can find easily, the application home
+ NSArray *arr = [NSArray arrayWithObject:NSHomeDirectory()];
+#else
+ // default to a directory called GTMHTTPDebugLogs in the desktop folder
+ NSArray *arr = NSSearchPathForDirectoriesInDomains(NSDesktopDirectory,
+ NSUserDomainMask, YES);
+#endif
+
+ if ([arr count] > 0) {
+ NSString *const kGTMLogFolderName = @"GTMHTTPDebugLogs";
+
+ NSString *desktopPath = [arr objectAtIndex:0];
+ NSString *logsFolderPath = [desktopPath stringByAppendingPathComponent:kGTMLogFolderName];
+
+ BOOL doesFolderExist;
+ BOOL isDir = NO;
+ NSFileManager *fileManager = [NSFileManager defaultManager];
+ doesFolderExist = [fileManager fileExistsAtPath:logsFolderPath
+ isDirectory:&isDir];
+
+ if (!doesFolderExist) {
+ // make the directory
+ doesFolderExist = [fileManager createDirectoryAtPath:logsFolderPath
+ attributes:nil];
+ }
+
+ if (doesFolderExist) {
+ // it's there; store it in the global
+ gLoggingDirectoryPath = [logsFolderPath copy];
+ }
+ }
+ }
+ return gLoggingDirectoryPath;
+}
+
++ (void)setIsLoggingEnabled:(BOOL)flag {
+ gIsLoggingEnabled = flag;
+}
+
++ (BOOL)isLoggingEnabled {
+ return gIsLoggingEnabled;
+}
+
++ (void)setLoggingProcessName:(NSString *)str {
+ [gLoggingProcessName release];
+ gLoggingProcessName = [str copy];
+}
+
++ (NSString *)loggingProcessName {
+
+ // get the process name (once per run) replacing spaces with underscores
+ if (!gLoggingProcessName) {
+
+ NSString *procName = [[NSProcessInfo processInfo] processName];
+ NSMutableString *loggingProcessName;
+ loggingProcessName = [[NSMutableString alloc] initWithString:procName];
+
+ [loggingProcessName replaceOccurrencesOfString:@" "
+ withString:@"_"
+ options:0
+ range:NSMakeRange(0, [gLoggingProcessName length])];
+ gLoggingProcessName = loggingProcessName;
+ }
+ return gLoggingProcessName;
+}
+
++ (void)setLoggingDateStamp:(NSString *)str {
+ [gLoggingDateStamp release];
+ gLoggingDateStamp = [str copy];
+}
+
++ (NSString *)loggingDateStamp {
+ // we'll pick one date stamp per run, so a run that starts at a later second
+ // will get a unique results html file
+ if (!gLoggingDateStamp) {
+ // produce a string like 08-21_01-41-23PM
+
+ NSDateFormatter *formatter = [[[NSDateFormatter alloc] init] autorelease];
+ [formatter setFormatterBehavior:NSDateFormatterBehavior10_4];
+ [formatter setDateFormat:@"M-dd_hh-mm-ssa"];
+
+ gLoggingDateStamp = [[formatter stringFromDate:[NSDate date]] retain] ;
+ }
+ return gLoggingDateStamp;
+}
+
+- (NSString *)cleanParameterFollowing:(NSString *)paramName
+ fromString:(NSString *)originalStr {
+ // We don't want the password written to disk
+ //
+ // find "&Passwd=" in the string, and replace it and the stuff that
+ // follows it with "Passwd=_snip_"
+
+ NSRange passwdRange = [originalStr rangeOfString:@"&Passwd="];
+ if (passwdRange.location != NSNotFound) {
+
+ // we found Passwd=; find the & that follows the parameter
+ NSUInteger origLength = [originalStr length];
+ NSRange restOfString = NSMakeRange(passwdRange.location+1,
+ origLength - passwdRange.location - 1);
+ NSRange rangeOfFollowingAmp = [originalStr rangeOfString:@"&"
+ options:0
+ range:restOfString];
+ NSRange replaceRange;
+ if (rangeOfFollowingAmp.location == NSNotFound) {
+ // found no other & so replace to end of string
+ replaceRange = NSMakeRange(passwdRange.location,
+ rangeOfFollowingAmp.location - passwdRange.location);
+ } else {
+ // another parameter after &Passwd=foo
+ replaceRange = NSMakeRange(passwdRange.location,
+ rangeOfFollowingAmp.location - passwdRange.location);
+ }
+
+ NSMutableString *result = [NSMutableString stringWithString:originalStr];
+ NSString *replacement = [NSString stringWithFormat:@"%@_snip_", paramName];
+
+ [result replaceCharactersInRange:replaceRange withString:replacement];
+ return result;
+ }
+ return originalStr;
+}
+
+// stringFromStreamData creates a string given the supplied data
+//
+// If NSString can create a UTF-8 string from the data, then that is returned.
+//
+// Otherwise, this routine tries to find a MIME boundary at the beginning of
+// the data block, and uses that to break up the data into parts. Each part
+// will be used to try to make a UTF-8 string. For parts that fail, a
+// replacement string showing the part header and <<n bytes>> is supplied
+// in place of the binary data.
+
+- (NSString *)stringFromStreamData:(NSData *)data {
+
+ if (data == nil) return nil;
+
+ // optimistically, see if the whole data block is UTF-8
+ NSString *streamDataStr = [[[NSString alloc] initWithData:data
+ encoding:NSUTF8StringEncoding] autorelease];
+ if (streamDataStr) return streamDataStr;
+
+ // Munge a buffer by replacing non-ASCII bytes with underscores,
+ // and turn that munged buffer an NSString. That gives us a string
+ // we can use with NSScanner.
+ NSMutableData *mutableData = [NSMutableData dataWithData:data];
+ unsigned char *bytes = [mutableData mutableBytes];
+
+ for (int idx = 0; idx < [mutableData length]; idx++) {
+ if (bytes[idx] > 0x7F || bytes[idx] == 0) {
+ bytes[idx] = '_';
+ }
+ }
+
+ NSString *mungedStr = [[[NSString alloc] initWithData:mutableData
+ encoding:NSUTF8StringEncoding] autorelease];
+ if (mungedStr != nil) {
+
+ // scan for the boundary string
+ NSString *boundary = nil;
+ NSScanner *scanner = [NSScanner scannerWithString:mungedStr];
+
+ if ([scanner scanUpToString:@"\r\n" intoString:&boundary]
+ && [boundary hasPrefix:@"--"]) {
+
+ // we found a boundary string; use it to divide the string into parts
+ NSArray *mungedParts = [mungedStr componentsSeparatedByString:boundary];
+
+ // look at each of the munged parts in the original string, and try to
+ // convert those into UTF-8
+ NSMutableArray *origParts = [NSMutableArray array];
+ NSUInteger offset = 0;
+ for (int partIdx = 0; partIdx < [mungedParts count]; partIdx++) {
+
+ NSString *mungedPart = [mungedParts objectAtIndex:partIdx];
+ NSUInteger partSize = [mungedPart length];
+
+ NSRange range = NSMakeRange(offset, partSize);
+ NSData *origPartData = [data subdataWithRange:range];
+
+ NSString *origPartStr = [[[NSString alloc] initWithData:origPartData
+ encoding:NSUTF8StringEncoding] autorelease];
+ if (origPartStr) {
+ // we could make this original part into UTF-8; use the string
+ [origParts addObject:origPartStr];
+ } else {
+ // this part can't be made into UTF-8; scan the header, if we can
+ NSString *header = nil;
+ NSScanner *headerScanner = [NSScanner scannerWithString:mungedPart];
+ if (![headerScanner scanUpToString:@"\r\n\r\n" intoString:&header]) {
+ // we couldn't find a header
+ header = @"";
+ }
+
+ // make a part string with the header and <<n bytes>>
+ NSString *binStr = [NSString stringWithFormat:@"\r%@\r<<%u bytes>>\r",
+ header, partSize - [header length]];
+ [origParts addObject:binStr];
+ }
+ offset += partSize + [boundary length];
+ }
+
+ // rejoin the original parts
+ streamDataStr = [origParts componentsJoinedByString:boundary];
+ }
+ }
+
+ if (!streamDataStr) {
+ // give up; just make a string showing the uploaded bytes
+ streamDataStr = [NSString stringWithFormat:@"<<%u bytes>>", [data length]];
+ }
+ return streamDataStr;
+}
+
+// logFetchWithError is called following a successful or failed fetch attempt
+//
+// This method does all the work for appending to and creating log files
+
+- (void)logFetchWithError:(NSError *)error {
+
+ if (![[self class] isLoggingEnabled]) return;
+
+ NSFileManager *fileManager = [NSFileManager defaultManager];
+
+ // TODO: add Javascript to display response data formatted in hex
+
+ NSString *logDirectory = [[self class] loggingDirectory];
+ NSString *processName = [[self class] loggingProcessName];
+ NSString *dateStamp = [[self class] loggingDateStamp];
+
+ // each response's NSData goes into its own xml or txt file, though all
+ // responses for this run of the app share a main html file. This
+ // counter tracks all fetch responses for this run of the app.
+ static int zResponseCounter = 0;
+ zResponseCounter++;
+
+ // file name for the html file containing plain text in a <textarea>
+ NSString *responseDataUnformattedFileName = nil;
+
+ // file name for the "formatted" (raw) data file
+ NSString *responseDataFormattedFileName = nil;
+ NSUInteger responseDataLength = [downloadedData_ length];
+
+ NSURLResponse *response = [self response];
+ NSString *responseBaseName = nil;
+
+ // if there's response data, decide what kind of file to put it in based
+ // on the first bytes of the file or on the mime type supplied by the server
+ if (responseDataLength) {
+ NSString *responseDataExtn = nil;
+
+ // generate a response file base name like
+ // SyncProto_http_response_10-16_01-56-58PM_3
+ responseBaseName = [NSString stringWithFormat:@"%@_http_response_%@_%d",
+ processName, dateStamp, zResponseCounter];
+
+ NSString *dataStr = [[[NSString alloc] initWithData:downloadedData_
+ encoding:NSUTF8StringEncoding] autorelease];
+ if (dataStr) {
+ // we were able to make a UTF-8 string from the response data
+
+ NSCharacterSet *whitespaceSet = [NSCharacterSet whitespaceCharacterSet];
+ dataStr = [dataStr stringByTrimmingCharactersInSet:whitespaceSet];
+
+ // save a plain-text version of the response data in an html cile
+ // containing a wrapped, scrollable <textarea>
+ //
+ // we'll use <textarea rows="33" cols="108" readonly=true wrap=soft>
+ // </textarea> to fit inside our iframe
+ responseDataUnformattedFileName = [responseBaseName stringByAppendingPathExtension:@"html"];
+ NSString *textFilePath = [logDirectory stringByAppendingPathComponent:responseDataUnformattedFileName];
+
+ NSString* wrapFmt = @"<textarea rows=\"33\" cols=\"108\" readonly=true"
+ " wrap=soft>\n%@\n</textarea>";
+ NSString* wrappedStr = [NSString stringWithFormat:wrapFmt, dataStr];
+ [wrappedStr writeToFile:textFilePath
+ atomically:NO
+ encoding:NSUTF8StringEncoding
+ error:nil];
+
+ // now determine the extension for the "formatted" file, which is really
+ // the raw data written with an appropriate extension
+
+ // for known file types, we'll write the data to a file with the
+ // appropriate extension
+ if ([dataStr hasPrefix:@"<?xml"]) {
+ responseDataExtn = @"xml";
+ } else if ([dataStr hasPrefix:@"<html"]) {
+ responseDataExtn = @"html";
+ } else {
+ // add more types of identifiable text here
+ }
+
+ } else if ([[response MIMEType] isEqual:@"image/jpeg"]) {
+ responseDataExtn = @"jpg";
+ } else if ([[response MIMEType] isEqual:@"image/gif"]) {
+ responseDataExtn = @"gif";
+ } else if ([[response MIMEType] isEqual:@"image/png"]) {
+ responseDataExtn = @"png";
+ } else {
+ // add more non-text types here
+ }
+
+ // if we have an extension, save the raw data in a file with that
+ // extension to be our "formatted" display file
+ if (responseDataExtn) {
+ responseDataFormattedFileName = [responseBaseName stringByAppendingPathExtension:responseDataExtn];
+ NSString *formattedFilePath = [logDirectory stringByAppendingPathComponent:responseDataFormattedFileName];
+
+ [downloadedData_ writeToFile:formattedFilePath atomically:NO];
+ }
+ }
+
+ // we'll have one main html file per run of the app
+ NSString *htmlName = [NSString stringWithFormat:@"%@_http_log_%@.html",
+ processName, dateStamp];
+ NSString *htmlPath =[logDirectory stringByAppendingPathComponent:htmlName];
+
+ // if the html file exists (from logging previous fetches) we don't need
+ // to re-write the header or the scripts
+ BOOL didFileExist = [fileManager fileExistsAtPath:htmlPath];
+
+ NSMutableString* outputHTML = [NSMutableString string];
+ NSURLRequest *request = [self request];
+
+ // we need file names for the various div's that we're going to show and hide,
+ // names unique to this response's bundle of data, so we format our div
+ // names with the counter that we incremented earlier
+ NSString *requestHeadersName = [NSString stringWithFormat:@"RequestHeaders%d", zResponseCounter];
+ NSString *postDataName = [NSString stringWithFormat:@"PostData%d", zResponseCounter];
+
+ NSString *responseHeadersName = [NSString stringWithFormat:@"ResponseHeaders%d", zResponseCounter];
+ NSString *responseDataDivName = [NSString stringWithFormat:@"ResponseData%d", zResponseCounter];
+ NSString *dataIFrameID = [NSString stringWithFormat:@"DataIFrame%d", zResponseCounter];
+
+ // we need a header to say we'll have UTF-8 text
+ if (!didFileExist) {
+ [outputHTML appendFormat:@"<html><head><meta http-equiv=\"content-type\" "
+ "content=\"text/html; charset=UTF-8\"><title>%@ HTTP fetch log %@</title>",
+ processName, dateStamp];
+ }
+
+ // write style sheets for each hideable element; each style sheet is
+ // customized with our current response number, since they'll share
+ // the html page with other responses
+ NSString *styleFormat = @"<style type=\"text/css\">div#%@ "
+ "{ margin: 0px 20px 0px 20px; display: none; }</style>\n";
+
+ [outputHTML appendFormat:styleFormat, requestHeadersName];
+ [outputHTML appendFormat:styleFormat, postDataName];
+ [outputHTML appendFormat:styleFormat, responseHeadersName];
+ [outputHTML appendFormat:styleFormat, responseDataDivName];
+
+ if (!didFileExist) {
+ // write javascript functions. The first one shows/hides the layer
+ // containing the iframe.
+ NSString *scriptFormat = @"<script type=\"text/javascript\"> "
+ "function toggleLayer(whichLayer){ var style2 = document.getElementById(whichLayer).style; "
+ "style2.display = style2.display ? \"\":\"block\";}</script>\n";
+ [outputHTML appendFormat:scriptFormat];
+
+ // the second function is passed the src file; if it's what's shown, it
+ // toggles the iframe's visibility. If some other src is shown, it shows
+ // the iframe and loads the new source. Note we want to load the source
+ // whenever we show the iframe too since Firefox seems to format it wrong
+ // when showing it if we don't reload it.
+ NSString *toggleIFScriptFormat = @"<script type=\"text/javascript\"> "
+ "function toggleIFrame(whichLayer,iFrameID,newsrc)"
+ "{ \n var iFrameElem=document.getElementById(iFrameID); "
+ "if (iFrameElem.src.indexOf(newsrc) != -1) { toggleLayer(whichLayer); } "
+ "else { document.getElementById(whichLayer).style.display=\"block\"; } "
+ "iFrameElem.src=newsrc; }</script>\n</head>\n<body>\n";
+ [outputHTML appendFormat:toggleIFScriptFormat];
+ }
+
+ // now write the visible html elements
+
+ // write the date & time
+ [outputHTML appendFormat:@"<b>%@</b><br>", [[NSDate date] description]];
+
+ // write the request URL
+ [outputHTML appendFormat:@"<b>request:</b> %@ <i>URL:</i> <code>%@</code><br>\n",
+ [request HTTPMethod], [request URL]];
+
+ // write the request headers, toggleable
+ NSDictionary *requestHeaders = [request allHTTPHeaderFields];
+ if ([requestHeaders count]) {
+ NSString *requestHeadersFormat = @"<a href=\"javascript:toggleLayer('%@');\">"
+ "request headers (%d)</a><div id=\"%@\"><pre>%@</pre></div><br>\n";
+ [outputHTML appendFormat:requestHeadersFormat,
+ requestHeadersName, // layer name
+ [requestHeaders count],
+ requestHeadersName,
+ [requestHeaders description]]; // description gives a human-readable dump
+ } else {
+ [outputHTML appendString:@"<i>Request headers: none</i><br>"];
+ }
+
+ // write the request post data, toggleable
+ NSData *postData = postData_;
+ if (loggedStreamData_) {
+ postData = loggedStreamData_;
+ }
+
+ if ([postData length]) {
+ NSString *postDataFormat = @"<a href=\"javascript:toggleLayer('%@');\">"
+ "posted data (%d bytes)</a><div id=\"%@\">%@</div><br>\n";
+ NSString *postDataStr = [self stringFromStreamData:postData];
+ if (postDataStr) {
+ NSString *postDataTextAreaFmt = @"<pre>%@</pre>";
+ if ([postDataStr rangeOfString:@"<"].location != NSNotFound) {
+ postDataTextAreaFmt = @"<textarea rows=\"15\" cols=\"100\""
+ " readonly=true wrap=soft>\n%@\n</textarea>";
+ }
+ NSString *cleanedPostData = [self cleanParameterFollowing:@"&Passwd="
+ fromString:postDataStr];
+ NSString *postDataTextArea = [NSString stringWithFormat:
+ postDataTextAreaFmt, cleanedPostData];
+
+ [outputHTML appendFormat:postDataFormat,
+ postDataName, // layer name
+ [postData length],
+ postDataName,
+ postDataTextArea];
+ }
+ } else {
+ // no post data
+ }
+
+ // write the response status, MIME type, URL
+ if (response) {
+ NSString *statusString = @"";
+ if ([response respondsToSelector:@selector(statusCode)]) {
+ NSInteger status = [(NSHTTPURLResponse *)response statusCode];
+ statusString = @"200";
+ if (status != 200) {
+ // purple for errors
+ statusString = [NSString stringWithFormat:@"<FONT COLOR=\"#FF00FF\">%d</FONT>",
+ status];
+ }
+ }
+
+ // show the response URL only if it's different from the request URL
+ NSString *responseURLStr = @"";
+ NSURL *responseURL = [response URL];
+
+ if (responseURL && ![responseURL isEqual:[request URL]]) {
+ NSString *responseURLFormat = @"<br><FONT COLOR=\"#FF00FF\">response URL:"
+ "</FONT> <code>%@</code>";
+ responseURLStr = [NSString stringWithFormat:responseURLFormat,
+ [responseURL absoluteString]];
+ }
+
+ NSDictionary *responseHeaders = nil;
+ if ([response respondsToSelector:@selector(allHeaderFields)]) {
+ responseHeaders = [(NSHTTPURLResponse *)response allHeaderFields];
+ }
+ [outputHTML appendFormat:@"<b>response:</b> <i>status:</i> %@ <i> "
+ "&nbsp;&nbsp;&nbsp;MIMEType:</i><code> %@</code>%@<br>\n",
+ statusString,
+ [response MIMEType],
+ responseURLStr,
+ responseHeaders ? [responseHeaders description] : @""];
+
+ // write the response headers, toggleable
+ if ([responseHeaders count]) {
+
+ NSString *cookiesSet = [responseHeaders objectForKey:@"Set-Cookie"];
+
+ NSString *responseHeadersFormat = @"<a href=\"javascript:toggleLayer("
+ "'%@');\">response headers (%d) %@</a><div id=\"%@\"><pre>%@</pre>"
+ "</div><br>\n";
+ [outputHTML appendFormat:responseHeadersFormat,
+ responseHeadersName,
+ [responseHeaders count],
+ (cookiesSet ? @"<i>sets cookies</i>" : @""),
+ responseHeadersName,
+ [responseHeaders description]];
+
+ } else {
+ [outputHTML appendString:@"<i>Response headers: none</i><br>\n"];
+ }
+ }
+
+ // error
+ if (error) {
+ [outputHTML appendFormat:@"<b>error:</b> %@ <br>\n", [error description]];
+ }
+
+ // write the response data. We have links to show formatted and text
+ // versions, but they both show it in the same iframe, and both
+ // links also toggle visible/hidden
+ if (responseDataFormattedFileName || responseDataUnformattedFileName) {
+
+ // response data, toggleable links -- formatted and text versions
+ if (responseDataFormattedFileName) {
+ [outputHTML appendFormat:@"response data (%d bytes) formatted <b>%@</b> ",
+ responseDataLength,
+ [responseDataFormattedFileName pathExtension]];
+
+ // inline (iframe) link
+ NSString *responseInlineFormattedDataNameFormat = @"&nbsp;&nbsp;<a "
+ "href=\"javascript:toggleIFrame('%@','%@','%@');\">inline</a>\n";
+ [outputHTML appendFormat:responseInlineFormattedDataNameFormat,
+ responseDataDivName, // div ID
+ dataIFrameID, // iframe ID (for reloading)
+ responseDataFormattedFileName]; // src to reload
+
+ // plain link (so the user can command-click it into another tab)
+ [outputHTML appendFormat:@"&nbsp;&nbsp;<a href=\"%@\">stand-alone</a><br>\n",
+ [responseDataFormattedFileName
+ stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
+ }
+ if (responseDataUnformattedFileName) {
+ [outputHTML appendFormat:@"response data (%d bytes) plain text ",
+ responseDataLength];
+
+ // inline (iframe) link
+ NSString *responseInlineDataNameFormat = @"&nbsp;&nbsp;<a href=\""
+ "javascript:toggleIFrame('%@','%@','%@');\">inline</a> \n";
+ [outputHTML appendFormat:responseInlineDataNameFormat,
+ responseDataDivName, // div ID
+ dataIFrameID, // iframe ID (for reloading)
+ responseDataUnformattedFileName]; // src to reload
+
+ // plain link (so the user can command-click it into another tab)
+ [outputHTML appendFormat:@"&nbsp;&nbsp;<a href=\"%@\">stand-alone</a><br>\n",
+ [responseDataUnformattedFileName
+ stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
+ }
+
+ // make the iframe
+ NSString *divHTMLFormat = @"<div id=\"%@\">%@</div><br>\n";
+ NSString *src = responseDataFormattedFileName ?
+ responseDataFormattedFileName : responseDataUnformattedFileName;
+ NSString *escapedSrc = [src stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
+ NSString *iframeFmt = @" <iframe src=\"%@\" id=\"%@\" width=800 height=400>"
+ "\n<a href=\"%@\">%@</a>\n </iframe>\n";
+ NSString *dataIFrameHTML = [NSString stringWithFormat:iframeFmt,
+ escapedSrc, dataIFrameID, escapedSrc, src];
+ [outputHTML appendFormat:divHTMLFormat,
+ responseDataDivName, dataIFrameHTML];
+ } else {
+ // could not parse response data; just show the length of it
+ [outputHTML appendFormat:@"<i>Response data: %d bytes </i>\n",
+ responseDataLength];
+ }
+
+ [outputHTML appendString:@"<br><hr><p>"];
+
+ // append the HTML to the main output file
+ const char* htmlBytes = [outputHTML UTF8String];
+ NSOutputStream *stream = [NSOutputStream outputStreamToFileAtPath:htmlPath
+ append:YES];
+ [stream open];
+ [stream write:(const uint8_t *) htmlBytes maxLength:strlen(htmlBytes)];
+ [stream close];
+
+ // make a symlink to the latest html
+ NSString *symlinkName = [NSString stringWithFormat:@"%@_http_log_newest.html",
+ processName];
+ NSString *symlinkPath = [logDirectory stringByAppendingPathComponent:symlinkName];
+
+ // removeFileAtPath might be going away, but removeItemAtPath does not exist
+ // in 10.4
+ if ([fileManager respondsToSelector:@selector(removeFileAtPath:handler:)]) {
+ [fileManager removeFileAtPath:symlinkPath handler:nil];
+ } else if ([fileManager respondsToSelector:@selector(removeItemAtPath:error:)]) {
+ // To make the next line compile when targeting 10.4, we declare
+ // removeItemAtPath:error: in an @interface above
+ [fileManager removeItemAtPath:symlinkPath error:NULL];
+ }
+
+ [fileManager createSymbolicLinkAtPath:symlinkPath pathContent:htmlPath];
+}
+
+- (void)logCapturePostStream {
+
+#if GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING
+ // This is called when beginning a fetch. The caller should have already
+ // verified that logging is enabled, and should have allocated
+ // loggedStreamData_ as a mutable object.
+
+ // If we're logging, we need to wrap the upload stream with our monitor
+ // stream subclass that will call us back with the bytes being read from the
+ // stream
+
+ // our wrapper will retain the old post stream
+ [postStream_ autorelease];
+
+ // length can be
+ postStream_ = [GTMInputStreamLogger inputStreamWithStream:postStream_
+ length:0];
+ [postStream_ retain];
+
+ // we don't really want monitoring callbacks; our subclass will be
+ // calling our appendLoggedStreamData: method at every read instead
+ [(GTMInputStreamLogger *)postStream_ setMonitorDelegate:self
+ selector:nil];
+#endif // GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING
+}
+
+- (void)appendLoggedStreamData:(NSData *)newData {
+ [loggedStreamData_ appendData:newData];
+}
+
+#endif // GTM_HTTPFETCHER_ENABLE_LOGGING
+@end
+
+#if GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING
+@implementation GTMInputStreamLogger
+- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len {
+
+ // capture the read stream data, and pass it to the delegate to append to
+ NSInteger result = [super read:buffer maxLength:len];
+ if (result >= 0) {
+ NSData *data = [NSData dataWithBytes:buffer length:result];
+ [monitorDelegate_ appendLoggedStreamData:data];
+ }
+ return result;
+}
+@end
+#endif // GTM_HTTPFETCHER_ENABLE_INPUTSTREAM_LOGGING