/* Copyright 2014 Google Inc. All rights reserved. * * 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. */ #if !defined(__has_feature) || !__has_feature(objc_arc) #error "This file requires ARC support." #endif #import "GTMSessionFetcher.h" #import #ifndef STRIP_GTM_FETCH_LOGGING #error GTMSessionFetcher headers should have defaulted this if it wasn't already defined. #endif GTM_ASSUME_NONNULL_BEGIN NSString *const kGTMSessionFetcherStartedNotification = @"kGTMSessionFetcherStartedNotification"; NSString *const kGTMSessionFetcherStoppedNotification = @"kGTMSessionFetcherStoppedNotification"; NSString *const kGTMSessionFetcherRetryDelayStartedNotification = @"kGTMSessionFetcherRetryDelayStartedNotification"; NSString *const kGTMSessionFetcherRetryDelayStoppedNotification = @"kGTMSessionFetcherRetryDelayStoppedNotification"; NSString *const kGTMSessionFetcherCompletionInvokedNotification = @"kGTMSessionFetcherCompletionInvokedNotification"; NSString *const kGTMSessionFetcherCompletionDataKey = @"data"; NSString *const kGTMSessionFetcherCompletionErrorKey = @"error"; NSString *const kGTMSessionFetcherErrorDomain = @"com.google.GTMSessionFetcher"; NSString *const kGTMSessionFetcherStatusDomain = @"com.google.HTTPStatus"; NSString *const kGTMSessionFetcherStatusDataKey = @"data"; // data returned with a kGTMSessionFetcherStatusDomain error NSString *const kGTMSessionFetcherNumberOfRetriesDoneKey = @"kGTMSessionFetcherNumberOfRetriesDoneKey"; NSString *const kGTMSessionFetcherElapsedIntervalWithRetriesKey = @"kGTMSessionFetcherElapsedIntervalWithRetriesKey"; static NSString *const kGTMSessionIdentifierPrefix = @"com.google.GTMSessionFetcher"; static NSString *const kGTMSessionIdentifierDestinationFileURLMetadataKey = @"_destURL"; static NSString *const kGTMSessionIdentifierBodyFileURLMetadataKey = @"_bodyURL"; // The default max retry interview is 10 minutes for uploads (POST/PUT/PATCH), // 1 minute for downloads. static const NSTimeInterval kUnsetMaxRetryInterval = -1.0; static const NSTimeInterval kDefaultMaxDownloadRetryInterval = 60.0; static const NSTimeInterval kDefaultMaxUploadRetryInterval = 60.0 * 10.; #ifdef GTMSESSION_PERSISTED_DESTINATION_KEY // Projects using unique class names should also define a unique persisted destination key. static NSString * const kGTMSessionFetcherPersistedDestinationKey = GTMSESSION_PERSISTED_DESTINATION_KEY; #else static NSString * const kGTMSessionFetcherPersistedDestinationKey = @"com.google.GTMSessionFetcher.downloads"; #endif GTM_ASSUME_NONNULL_END // // GTMSessionFetcher // #if 0 #define GTM_LOG_BACKGROUND_SESSION(...) GTMSESSION_LOG_DEBUG(__VA_ARGS__) #else #define GTM_LOG_BACKGROUND_SESSION(...) #endif #ifndef GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY #if (TARGET_OS_TV \ || TARGET_OS_WATCH \ || (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_11) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_11) \ || (TARGET_OS_IPHONE && defined(__IPHONE_9_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_9_0)) #define GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY 1 #endif #endif @interface GTMSessionFetcher () @property(atomic, strong, readwrite, GTM_NULLABLE) NSData *downloadedData; @property(atomic, strong, readwrite, GTM_NULLABLE) NSData *downloadResumeData; #if GTM_BACKGROUND_TASK_FETCHING // Should always be accessed within an @synchronized(self). @property(assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskIdentifier; #endif @property(atomic, readwrite, getter=isUsingBackgroundSession) BOOL usingBackgroundSession; @end #if !GTMSESSION_BUILD_COMBINED_SOURCES @interface GTMSessionFetcher (GTMSessionFetcherLoggingInternal) - (void)logFetchWithError:(NSError *)error; - (void)logNowWithError:(GTM_NULLABLE NSError *)error; - (NSInputStream *)loggedInputStreamForInputStream:(NSInputStream *)inputStream; - (GTMSessionFetcherBodyStreamProvider)loggedStreamProviderForStreamProvider: (GTMSessionFetcherBodyStreamProvider)streamProvider; @end #endif // !GTMSESSION_BUILD_COMBINED_SOURCES GTM_ASSUME_NONNULL_BEGIN static NSTimeInterval InitialMinRetryInterval(void) { return 1.0 + ((double)(arc4random_uniform(0x0FFFF)) / (double) 0x0FFFF); } static BOOL IsLocalhost(NSString * GTM_NULLABLE_TYPE host) { // We check if there's host, and then make the comparisons. if (host == nil) return NO; return ([host caseInsensitiveCompare:@"localhost"] == NSOrderedSame || [host isEqual:@"::1"] || [host isEqual:@"127.0.0.1"]); } static GTMSessionFetcherTestBlock GTM_NULLABLE_TYPE gGlobalTestBlock; @implementation GTMSessionFetcher { NSMutableURLRequest *_request; // after beginFetch, changed only in delegate callbacks BOOL _useUploadTask; // immutable after beginFetch NSURL *_bodyFileURL; // immutable after beginFetch GTMSessionFetcherBodyStreamProvider _bodyStreamProvider; // immutable after beginFetch NSURLSession *_session; BOOL _shouldInvalidateSession; // immutable after beginFetch NSURLSession *_sessionNeedingInvalidation; NSURLSessionConfiguration *_configuration; NSURLSessionTask *_sessionTask; NSString *_taskDescription; float _taskPriority; NSURLResponse *_response; NSString *_sessionIdentifier; BOOL _wasCreatedFromBackgroundSession; BOOL _didCreateSessionIdentifier; NSString *_sessionIdentifierUUID; BOOL _userRequestedBackgroundSession; BOOL _usingBackgroundSession; NSMutableData * GTM_NULLABLE_TYPE _downloadedData; NSError *_downloadFinishedError; NSData *_downloadResumeData; // immutable after construction NSURL *_destinationFileURL; int64_t _downloadedLength; NSURLCredential *_credential; // username & password NSURLCredential *_proxyCredential; // credential supplied to proxy servers BOOL _isStopNotificationNeeded; // set when start notification has been sent BOOL _isUsingTestBlock; // set when a test block was provided (remains set when the block is released) id _userData; // retained, if set by caller NSMutableDictionary *_properties; // more data retained for caller dispatch_queue_t _callbackQueue; dispatch_group_t _callbackGroup; // read-only after creation NSOperationQueue *_delegateQueue; // immutable after beginFetch id _authorizer; // immutable after beginFetch // The service object that created and monitors this fetcher, if any. id _service; // immutable; set by the fetcher service upon creation NSString *_serviceHost; NSInteger _servicePriority; // immutable after beginFetch BOOL _hasStoppedFetching; // counterpart to _initialBeginFetchDate BOOL _userStoppedFetching; BOOL _isRetryEnabled; // user wants auto-retry NSTimer *_retryTimer; NSUInteger _retryCount; NSTimeInterval _maxRetryInterval; // default 60 (download) or 600 (upload) seconds NSTimeInterval _minRetryInterval; // random between 1 and 2 seconds NSTimeInterval _retryFactor; // default interval multiplier is 2 NSTimeInterval _lastRetryInterval; NSDate *_initialBeginFetchDate; // date that beginFetch was first invoked; immutable after initial beginFetch NSDate *_initialRequestDate; // date of first request to the target server (ignoring auth) BOOL _hasAttemptedAuthRefresh; // accessed only in shouldRetryNowForStatus: NSString *_comment; // comment for log NSString *_log; #if !STRIP_GTM_FETCH_LOGGING NSMutableData *_loggedStreamData; NSURL *_redirectedFromURL; NSString *_logRequestBody; NSString *_logResponseBody; BOOL _hasLoggedError; BOOL _deferResponseBodyLogging; #endif } #if !GTMSESSION_UNIT_TESTING + (void)load { [self fetchersForBackgroundSessions]; } #endif + (instancetype)fetcherWithRequest:(GTM_NULLABLE NSURLRequest *)request { return [[self alloc] initWithRequest:request configuration:nil]; } + (instancetype)fetcherWithURL:(NSURL *)requestURL { return [self fetcherWithRequest:[NSURLRequest requestWithURL:requestURL]]; } + (instancetype)fetcherWithURLString:(NSString *)requestURLString { return [self fetcherWithURL:(NSURL *)[NSURL URLWithString:requestURLString]]; } + (instancetype)fetcherWithDownloadResumeData:(NSData *)resumeData { GTMSessionFetcher *fetcher = [self fetcherWithRequest:nil]; fetcher.comment = @"Resuming download"; fetcher.downloadResumeData = resumeData; return fetcher; } + (GTM_NULLABLE instancetype)fetcherWithSessionIdentifier:(NSString *)sessionIdentifier { GTMSESSION_ASSERT_DEBUG(sessionIdentifier != nil, @"Invalid session identifier"); NSMapTable *sessionIdentifierToFetcherMap = [self sessionIdentifierToFetcherMap]; GTMSessionFetcher *fetcher = [sessionIdentifierToFetcherMap objectForKey:sessionIdentifier]; if (!fetcher && [sessionIdentifier hasPrefix:kGTMSessionIdentifierPrefix]) { fetcher = [self fetcherWithRequest:nil]; [fetcher setSessionIdentifier:sessionIdentifier]; [sessionIdentifierToFetcherMap setObject:fetcher forKey:sessionIdentifier]; fetcher->_wasCreatedFromBackgroundSession = YES; [fetcher setCommentWithFormat:@"Resuming %@", fetcher && fetcher->_sessionIdentifierUUID ? fetcher->_sessionIdentifierUUID : @"?"]; } return fetcher; } + (NSMapTable *)sessionIdentifierToFetcherMap { // TODO: What if a service is involved in creating the fetcher? Currently, when re-creating // fetchers, if a service was involved, it is not re-created. Should the service maintain a map? static NSMapTable *gSessionIdentifierToFetcherMap = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ gSessionIdentifierToFetcherMap = [NSMapTable strongToWeakObjectsMapTable]; }); return gSessionIdentifierToFetcherMap; } #if !GTM_ALLOW_INSECURE_REQUESTS + (BOOL)appAllowsInsecureRequests { // If the main bundle Info.plist key NSAppTransportSecurity is present, and it specifies // NSAllowsArbitraryLoads, then we need to explicitly enforce secure schemes. #if GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY static BOOL allowsInsecureRequests; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSBundle *mainBundle = [NSBundle mainBundle]; NSDictionary *appTransportSecurity = [mainBundle objectForInfoDictionaryKey:@"NSAppTransportSecurity"]; allowsInsecureRequests = [[appTransportSecurity objectForKey:@"NSAllowsArbitraryLoads"] boolValue]; }); return allowsInsecureRequests; #else // For builds targeting iOS 8 or 10.10 and earlier, we want to require fetcher // security checks. return YES; #endif // GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY } #else // GTM_ALLOW_INSECURE_REQUESTS + (BOOL)appAllowsInsecureRequests { return YES; } #endif // !GTM_ALLOW_INSECURE_REQUESTS - (instancetype)init { return [self initWithRequest:nil configuration:nil]; } - (instancetype)initWithRequest:(NSURLRequest *)request { return [self initWithRequest:request configuration:nil]; } - (instancetype)initWithRequest:(GTM_NULLABLE NSURLRequest *)request configuration:(GTM_NULLABLE NSURLSessionConfiguration *)configuration { self = [super init]; if (self) { if (![NSURLSession class]) { Class oldFetcherClass = NSClassFromString(@"GTMHTTPFetcher"); if (oldFetcherClass && request) { self = [[oldFetcherClass alloc] initWithRequest:(NSURLRequest *)request]; } else { self = nil; } return self; } #if GTM_BACKGROUND_TASK_FETCHING _backgroundTaskIdentifier = UIBackgroundTaskInvalid; #endif _request = [request mutableCopy]; _configuration = configuration; NSData *bodyData = request.HTTPBody; if (bodyData) { _bodyLength = (int64_t)bodyData.length; } else { _bodyLength = NSURLSessionTransferSizeUnknown; } _callbackQueue = dispatch_get_main_queue(); _callbackGroup = dispatch_group_create(); _delegateQueue = [NSOperationQueue mainQueue]; _minRetryInterval = InitialMinRetryInterval(); _maxRetryInterval = kUnsetMaxRetryInterval; _taskPriority = -1.0f; // Valid values if set are 0.0...1.0. _testBlockAccumulateDataChunkCount = 1; #if !STRIP_GTM_FETCH_LOGGING // Encourage developers to set the comment property or use // setCommentWithFormat: by providing a default string. _comment = @"(No fetcher comment set)"; #endif } return self; } - (id)copyWithZone:(NSZone *)zone { // disallow use of fetchers in a copy property [self doesNotRecognizeSelector:_cmd]; return nil; } - (NSString *)description { NSString *requestStr = self.request.URL.description; if (requestStr.length == 0) { if (self.downloadResumeData.length > 0) { requestStr = @""; } else if (_wasCreatedFromBackgroundSession) { requestStr = @""; } else { requestStr = @""; } } return [NSString stringWithFormat:@"%@ %p (%@)", [self class], self, requestStr]; } - (void)dealloc { GTMSESSION_ASSERT_DEBUG(!_isStopNotificationNeeded, @"unbalanced fetcher notification for %@", _request.URL); [self forgetSessionIdentifierForFetcherWithoutSyncCheck]; // Note: if a session task or a retry timer was pending, then this instance // would be retained by those so it wouldn't be getting dealloc'd, // hence we don't need to stopFetch here } #pragma mark - // Begin fetching the URL (or begin a retry fetch). The delegate is retained // for the duration of the fetch connection. - (void)beginFetchWithCompletionHandler:(GTM_NULLABLE GTMSessionFetcherCompletionHandler)handler { GTMSessionCheckNotSynchronized(self); _completionHandler = [handler copy]; // The user may have called setDelegate: earlier if they want to use other // delegate-style callbacks during the fetch; otherwise, the delegate is nil, // which is fine. [self beginFetchMayDelay:YES mayAuthorize:YES]; } // Begin fetching the URL for a retry fetch. The delegate and completion handler // are already provided, and do not need to be copied. - (void)beginFetchForRetry { GTMSessionCheckNotSynchronized(self); [self beginFetchMayDelay:YES mayAuthorize:YES]; } - (GTMSessionFetcherCompletionHandler)completionHandlerWithTarget:(GTM_NULLABLE_TYPE id)target didFinishSelector:(GTM_NULLABLE_TYPE SEL)finishedSelector { GTMSessionFetcherAssertValidSelector(target, finishedSelector, @encode(GTMSessionFetcher *), @encode(NSData *), @encode(NSError *), 0); GTMSessionFetcherCompletionHandler completionHandler = ^(NSData *data, NSError *error) { if (target && finishedSelector) { id selfArg = self; // Placate ARC. NSMethodSignature *sig = [target methodSignatureForSelector:finishedSelector]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig]; [invocation setSelector:(SEL)finishedSelector]; [invocation setTarget:target]; [invocation setArgument:&selfArg atIndex:2]; [invocation setArgument:&data atIndex:3]; [invocation setArgument:&error atIndex:4]; [invocation invoke]; } }; return completionHandler; } - (void)beginFetchWithDelegate:(GTM_NULLABLE_TYPE id)target didFinishSelector:(GTM_NULLABLE_TYPE SEL)finishedSelector { GTMSessionCheckNotSynchronized(self); GTMSessionFetcherCompletionHandler handler = [self completionHandlerWithTarget:target didFinishSelector:finishedSelector]; [self beginFetchWithCompletionHandler:handler]; } - (void)beginFetchMayDelay:(BOOL)mayDelay mayAuthorize:(BOOL)mayAuthorize { // This is the internal entry point for re-starting fetches. GTMSessionCheckNotSynchronized(self); NSMutableURLRequest *fetchRequest = _request; // The request property is now externally immutable. NSURL *fetchRequestURL = fetchRequest.URL; NSString *priorSessionIdentifier = self.sessionIdentifier; // A utility block for creating error objects when we fail to start the fetch. NSError *(^beginFailureError)(NSInteger) = ^(NSInteger code){ NSString *urlString = fetchRequestURL.absoluteString; NSDictionary *userInfo = @{ NSURLErrorFailingURLStringErrorKey : (urlString ? urlString : @"(missing URL)") }; return [NSError errorWithDomain:kGTMSessionFetcherErrorDomain code:code userInfo:userInfo]; }; // Catch delegate queue maxConcurrentOperationCount values other than 1, particularly // NSOperationQueueDefaultMaxConcurrentOperationCount (-1), to avoid the additional complexity // of simultaneous or out-of-order delegate callbacks. GTMSESSION_ASSERT_DEBUG(_delegateQueue.maxConcurrentOperationCount == 1, @"delegate queue %@ should support one concurrent operation, not %ld", _delegateQueue.name, (long)_delegateQueue.maxConcurrentOperationCount); if (!_initialBeginFetchDate) { // This ivar is set only here on the initial beginFetch so need not be synchronized. _initialBeginFetchDate = [[NSDate alloc] init]; } if (self.sessionTask != nil) { // If cached fetcher returned through fetcherWithSessionIdentifier:, then it's // already begun, but don't consider this a failure, since the user need not know this. if (self.sessionIdentifier != nil) { return; } GTMSESSION_ASSERT_DEBUG(NO, @"Fetch object %@ being reused; this should never happen", self); [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorDownloadFailed)]; return; } if (fetchRequestURL == nil && !_downloadResumeData && !priorSessionIdentifier) { GTMSESSION_ASSERT_DEBUG(NO, @"Beginning a fetch requires a request with a URL"); [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorDownloadFailed)]; return; } // We'll respect the user's request for a background session (unless this is // an upload fetcher, which does its initial request foreground.) self.usingBackgroundSession = self.useBackgroundSession && [self canFetchWithBackgroundSession]; NSURL *bodyFileURL = self.bodyFileURL; if (bodyFileURL) { NSError *fileCheckError; if (![bodyFileURL checkResourceIsReachableAndReturnError:&fileCheckError]) { // This assert fires when the file being uploaded no longer exists once // the fetcher is ready to start the upload. GTMSESSION_ASSERT_DEBUG_OR_LOG(0, @"Body file is unreachable: %@\n %@", bodyFileURL.path, fileCheckError); [self failToBeginFetchWithError:fileCheckError]; return; } } NSString *requestScheme = fetchRequestURL.scheme; BOOL isDataRequest = [requestScheme isEqual:@"data"]; if (isDataRequest) { // NSURLSession does not support data URLs in background sessions. #if DEBUG if (priorSessionIdentifier || self.sessionIdentifier) { GTMSESSION_LOG_DEBUG(@"Converting background to foreground session for %@", fetchRequest); } #endif [self setSessionIdentifierInternal:nil]; self.useBackgroundSession = NO; } #if GTM_ALLOW_INSECURE_REQUESTS BOOL shouldCheckSecurity = NO; #else BOOL shouldCheckSecurity = (fetchRequestURL != nil && !isDataRequest && [[self class] appAllowsInsecureRequests]); #endif if (shouldCheckSecurity) { // Allow https only for requests, unless overridden by the client. // // Non-https requests may too easily be snooped, so we disallow them by default. // // file: and data: schemes are usually safe if they are hardcoded in the client or provided // by a trusted source, but since it's fairly rare to need them, it's safest to make clients // explicitly whitelist them. BOOL isSecure = requestScheme != nil && [requestScheme caseInsensitiveCompare:@"https"] == NSOrderedSame; if (!isSecure) { BOOL allowRequest = NO; NSString *host = fetchRequestURL.host; // Check schemes first. A file scheme request may be allowed here, or as a localhost request. for (NSString *allowedScheme in _allowedInsecureSchemes) { if (requestScheme != nil && [requestScheme caseInsensitiveCompare:allowedScheme] == NSOrderedSame) { allowRequest = YES; break; } } if (!allowRequest) { // Check for localhost requests. Security checks only occur for non-https requests, so // this check won't happen for an https request to localhost. BOOL isLocalhostRequest = (host.length == 0 && [fetchRequestURL isFileURL]) || IsLocalhost(host); if (isLocalhostRequest) { if (self.allowLocalhostRequest) { allowRequest = YES; } else { GTMSESSION_ASSERT_DEBUG(NO, @"Fetch request for localhost but fetcher" @" allowLocalhostRequest is not set: %@", fetchRequestURL); } } else { GTMSESSION_ASSERT_DEBUG(NO, @"Insecure fetch request has a scheme (%@)" @" not found in fetcher allowedInsecureSchemes (%@): %@", requestScheme, _allowedInsecureSchemes ?: @" @[] ", fetchRequestURL); } } if (!allowRequest) { #if !DEBUG NSLog(@"Insecure fetch disallowed for %@", fetchRequestURL.description ?: @"nil request URL"); #endif [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorInsecureRequest)]; return; } } // !isSecure } // (requestURL != nil) && !isDataRequest if (self.cookieStorage == nil) { self.cookieStorage = [[self class] staticCookieStorage]; } BOOL isRecreatingSession = (self.sessionIdentifier != nil) && (fetchRequest == nil); self.canShareSession = !isRecreatingSession && !self.usingBackgroundSession; if (!self.session && self.canShareSession) { self.session = [_service sessionForFetcherCreation]; // If _session is nil, then the service's session creation semaphore will block // until this fetcher invokes fetcherDidCreateSession: below, so this *must* invoke // that method, even if the session fails to be created. } if (!self.session) { // Create a session. if (!_configuration) { if (priorSessionIdentifier || self.usingBackgroundSession) { NSString *sessionIdentifier = priorSessionIdentifier; if (!sessionIdentifier) { sessionIdentifier = [self createSessionIdentifierWithMetadata:nil]; } NSMapTable *sessionIdentifierToFetcherMap = [[self class] sessionIdentifierToFetcherMap]; [sessionIdentifierToFetcherMap setObject:self forKey:self.sessionIdentifier]; #if (TARGET_OS_TV \ || TARGET_OS_WATCH \ || (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_10) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_10) \ || (TARGET_OS_IPHONE && defined(__IPHONE_8_0) && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0)) // iOS 8/10.10 builds require the new backgroundSessionConfiguration method name. _configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessionIdentifier]; #elif (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_10) && MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_10) \ || (TARGET_OS_IPHONE && defined(__IPHONE_8_0) && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0) // Do a runtime check to avoid a deprecation warning about using // +backgroundSessionConfiguration: on iOS 8. if ([NSURLSessionConfiguration respondsToSelector:@selector(backgroundSessionConfigurationWithIdentifier:)]) { // Running on iOS 8+/OS X 10.10+. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunguarded-availability" // Disable unguarded availability warning as we can't use the @availability macro until we require // all clients to build with Xcode 9 or above. _configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessionIdentifier]; #pragma clang diagnostic pop } else { // Running on iOS 7/OS X 10.9. _configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:sessionIdentifier]; } #else // Building with an SDK earlier than iOS 8/OS X 10.10. _configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:sessionIdentifier]; #endif self.usingBackgroundSession = YES; self.canShareSession = NO; } else { _configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration]; } #if !GTM_ALLOW_INSECURE_REQUESTS _configuration.TLSMinimumSupportedProtocol = kTLSProtocol12; #endif } // !_configuration _configuration.HTTPCookieStorage = self.cookieStorage; if (_configurationBlock) { _configurationBlock(self, _configuration); } id delegate = [_service sessionDelegate]; if (!delegate || !self.canShareSession) { delegate = self; } self.session = [NSURLSession sessionWithConfiguration:_configuration delegate:delegate delegateQueue:self.sessionDelegateQueue]; GTMSESSION_ASSERT_DEBUG(self.session, @"Couldn't create session"); // Tell the service about the session created by this fetcher. This also signals the // service's semaphore to allow other fetchers to request this session. [_service fetcherDidCreateSession:self]; // If this assertion fires, the client probably tried to use a session identifier that was // already used. The solution is to make the client use a unique identifier (or better yet let // the session fetcher assign the identifier). GTMSESSION_ASSERT_DEBUG(self.session.delegate == delegate, @"Couldn't assign delegate."); if (self.session) { BOOL isUsingSharedDelegate = (delegate != self); if (!isUsingSharedDelegate) { _shouldInvalidateSession = YES; } } } if (isRecreatingSession) { _shouldInvalidateSession = YES; // Let's make sure there are tasks still running or if not that we get a callback from a // completed one; otherwise, we assume the tasks failed. // This is the observed behavior perhaps 25% of the time within the Simulator running 7.0.3 on // exiting the app after starting an upload and relaunching the app if we manage to relaunch // after the task has completed, but before the system relaunches us in the background. [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) { if (dataTasks.count == 0 && uploadTasks.count == 0 && downloadTasks.count == 0) { double const kDelayInSeconds = 1.0; // We should get progress indication or completion soon dispatch_time_t checkForFeedbackDelay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kDelayInSeconds * NSEC_PER_SEC)); dispatch_after(checkForFeedbackDelay, dispatch_get_main_queue(), ^{ if (!self.sessionTask && !fetchRequest) { // If our task and/or request haven't been restored, then we assume task feedback lost. [self removePersistedBackgroundSessionFromDefaults]; NSError *sessionError = [NSError errorWithDomain:kGTMSessionFetcherErrorDomain code:GTMSessionFetcherErrorBackgroundFetchFailed userInfo:nil]; [self failToBeginFetchWithError:sessionError]; } }); } }]; return; } self.downloadedData = nil; self.downloadedLength = 0; if (_servicePriority == NSIntegerMin) { mayDelay = NO; } if (mayDelay && _service) { BOOL shouldFetchNow = [_service fetcherShouldBeginFetching:self]; if (!shouldFetchNow) { // The fetch is deferred, but will happen later. // // If this session is held by the fetcher service, clear the session now so that we don't // assume it's still valid after the fetcher is restarted. if (self.canShareSession) { self.session = nil; } return; } } NSString *effectiveHTTPMethod = [fetchRequest valueForHTTPHeaderField:@"X-HTTP-Method-Override"]; if (effectiveHTTPMethod == nil) { effectiveHTTPMethod = fetchRequest.HTTPMethod; } BOOL isEffectiveHTTPGet = (effectiveHTTPMethod == nil || [effectiveHTTPMethod isEqual:@"GET"]); BOOL needsUploadTask = (self.useUploadTask || self.bodyFileURL || self.bodyStreamProvider); if (_bodyData || self.bodyStreamProvider || fetchRequest.HTTPBodyStream) { if (isEffectiveHTTPGet) { fetchRequest.HTTPMethod = @"POST"; isEffectiveHTTPGet = NO; } if (_bodyData) { if (!needsUploadTask) { fetchRequest.HTTPBody = _bodyData; } #if !STRIP_GTM_FETCH_LOGGING } else if (fetchRequest.HTTPBodyStream) { if ([self respondsToSelector:@selector(loggedInputStreamForInputStream:)]) { fetchRequest.HTTPBodyStream = [self performSelector:@selector(loggedInputStreamForInputStream:) withObject:fetchRequest.HTTPBodyStream]; } #endif } } // We authorize after setting up the http method and body in the request // because OAuth 1 may need to sign the request body if (mayAuthorize && _authorizer && !isDataRequest) { BOOL isAuthorized = [_authorizer isAuthorizedRequest:fetchRequest]; if (!isAuthorized) { // Authorization needed. // // If this session is held by the fetcher service, clear the session now so that we don't // assume it's still valid after authorization completes. if (self.canShareSession) { self.session = nil; } // Authorizing the request will recursively call this beginFetch:mayDelay: // or failToBeginFetchWithError:. [self authorizeRequest]; return; } } // set the default upload or download retry interval, if necessary if ([self isRetryEnabled] && self.maxRetryInterval <= 0) { if (isEffectiveHTTPGet || [effectiveHTTPMethod isEqual:@"HEAD"]) { [self setMaxRetryInterval:kDefaultMaxDownloadRetryInterval]; } else { [self setMaxRetryInterval:kDefaultMaxUploadRetryInterval]; } } // finally, start the connection NSURLSessionTask *newSessionTask; BOOL needsDataAccumulator = NO; if (_downloadResumeData) { newSessionTask = [_session downloadTaskWithResumeData:_downloadResumeData]; GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask, @"Failed downloadTaskWithResumeData for %@, resume data %tu bytes", _session, _downloadResumeData.length); } else if (_destinationFileURL && !isDataRequest) { newSessionTask = [_session downloadTaskWithRequest:fetchRequest]; GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask, @"Failed downloadTaskWithRequest for %@, %@", _session, fetchRequest); } else if (needsUploadTask) { if (bodyFileURL) { newSessionTask = [_session uploadTaskWithRequest:fetchRequest fromFile:bodyFileURL]; GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask, @"Failed uploadTaskWithRequest for %@, %@, file %@", _session, fetchRequest, bodyFileURL.path); } else if (self.bodyStreamProvider) { newSessionTask = [_session uploadTaskWithStreamedRequest:fetchRequest]; GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask, @"Failed uploadTaskWithStreamedRequest for %@, %@", _session, fetchRequest); } else { GTMSESSION_ASSERT_DEBUG_OR_LOG(_bodyData != nil, @"Upload task needs body data, %@", fetchRequest); newSessionTask = [_session uploadTaskWithRequest:fetchRequest fromData:(NSData * GTM_NONNULL_TYPE)_bodyData]; GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask, @"Failed uploadTaskWithRequest for %@, %@, body data %tu bytes", _session, fetchRequest, _bodyData.length); } needsDataAccumulator = YES; } else { newSessionTask = [_session dataTaskWithRequest:fetchRequest]; needsDataAccumulator = YES; GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask, @"Failed dataTaskWithRequest for %@, %@", _session, fetchRequest); } self.sessionTask = newSessionTask; if (!newSessionTask) { // We shouldn't get here; if we're here, an earlier assertion should have fired to explain // which session task creation failed. [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorTaskCreationFailed)]; return; } if (needsDataAccumulator && _accumulateDataBlock == nil) { self.downloadedData = [NSMutableData data]; } if (_taskDescription) { newSessionTask.taskDescription = _taskDescription; } if (_taskPriority >= 0) { #if TARGET_OS_TV || TARGET_OS_WATCH BOOL hasTaskPriority = YES; #elif (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_10) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_10) \ || (TARGET_OS_IPHONE && defined(__IPHONE_8_0) && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0) BOOL hasTaskPriority = YES; #else BOOL hasTaskPriority = [newSessionTask respondsToSelector:@selector(setPriority:)]; #endif if (hasTaskPriority) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunguarded-availability" // Disable unguarded availability warning as we can't use the @availability macro until we require // all clients to build with Xcode 9 or above. newSessionTask.priority = _taskPriority; #pragma clang diagnostic pop } } #if GTM_DISABLE_FETCHER_TEST_BLOCK GTMSESSION_ASSERT_DEBUG(_testBlock == nil && gGlobalTestBlock == nil, @"test blocks disabled"); _testBlock = nil; #else if (!_testBlock) { if (gGlobalTestBlock) { // Note that the test block may pass nil for all of its response parameters, // indicating that the fetch should actually proceed. This is useful when the // global test block has been set, and the app is only testing a specific // fetcher. The block simulation code will then resume the task. _testBlock = gGlobalTestBlock; } } _isUsingTestBlock = (_testBlock != nil); #endif // GTM_DISABLE_FETCHER_TEST_BLOCK #if GTM_BACKGROUND_TASK_FETCHING id app = [[self class] fetcherUIApplication]; // Background tasks seem to interfere with out-of-process uploads and downloads. if (app && !self.skipBackgroundTask && !self.useBackgroundSession) { // Tell UIApplication that we want to continue even when the app is in the // background. #if DEBUG NSString *bgTaskName = [NSString stringWithFormat:@"%@-%@", [self class], fetchRequest.URL.host]; #else NSString *bgTaskName = @"GTMSessionFetcher"; #endif __block UIBackgroundTaskIdentifier bgTaskID = [app beginBackgroundTaskWithName:bgTaskName expirationHandler:^{ // Background task expiration callback - this block is always invoked by // UIApplication on the main thread. if (bgTaskID != UIBackgroundTaskInvalid) { @synchronized(self) { if (bgTaskID == self.backgroundTaskIdentifier) { self.backgroundTaskIdentifier = UIBackgroundTaskInvalid; } } [app endBackgroundTask:bgTaskID]; } }]; @synchronized(self) { self.backgroundTaskIdentifier = bgTaskID; } } #endif if (!_initialRequestDate) { _initialRequestDate = [[NSDate alloc] init]; } // We don't expect to reach here even on retry or auth until a stop notification has been sent // for the previous task, but we should ensure that we don't unbalance that. GTMSESSION_ASSERT_DEBUG(!_isStopNotificationNeeded, @"Start notification without a prior stop"); [self sendStopNotificationIfNeeded]; [self addPersistedBackgroundSessionToDefaults]; [self setStopNotificationNeeded:YES]; [self postNotificationOnMainThreadWithName:kGTMSessionFetcherStartedNotification userInfo:nil requireAsync:NO]; // The service needs to know our task if it is serving as NSURLSession delegate. [_service fetcherDidBeginFetching:self]; if (_testBlock) { #if !GTM_DISABLE_FETCHER_TEST_BLOCK [self simulateFetchForTestBlock]; #endif } else { // We resume the session task after posting the notification since the // delegate callbacks may happen immediately if the fetch is started off // the main thread or the session delegate queue is on a background thread, // and we don't want to post a start notification after a premature finish // of the session task. [newSessionTask resume]; } } NSData * GTM_NULLABLE_TYPE GTMDataFromInputStream(NSInputStream *inputStream, NSError **outError) { NSMutableData *data = [NSMutableData data]; [inputStream open]; NSInteger numberOfBytesRead = 0; while ([inputStream hasBytesAvailable]) { uint8_t buffer[512]; numberOfBytesRead = [inputStream read:buffer maxLength:sizeof(buffer)]; if (numberOfBytesRead > 0) { [data appendBytes:buffer length:(NSUInteger)numberOfBytesRead]; } else { break; } } [inputStream close]; NSError *streamError = inputStream.streamError; if (streamError) { data = nil; } if (outError) { *outError = streamError; } return data; } #if !GTM_DISABLE_FETCHER_TEST_BLOCK - (void)simulateFetchForTestBlock { // This is invoked on the same thread as the beginFetch method was. // // Callbacks will all occur on the callback queue. _testBlock(self, ^(NSURLResponse *response, NSData *responseData, NSError *error) { // Callback from test block. if (response == nil && responseData == nil && error == nil) { // Assume the fetcher should execute rather than be tested. self->_testBlock = nil; self->_isUsingTestBlock = NO; [self->_sessionTask resume]; return; } GTMSessionFetcherBodyStreamProvider bodyStreamProvider = self.bodyStreamProvider; if (bodyStreamProvider) { bodyStreamProvider(^(NSInputStream *bodyStream){ // Read from the input stream into an NSData buffer. We'll drain the stream // explicitly on a background queue. [self invokeOnCallbackQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0) afterUserStopped:NO block:^{ NSError *streamError; NSData *streamedData = GTMDataFromInputStream(bodyStream, &streamError); dispatch_async(dispatch_get_main_queue(), ^{ // Continue callbacks on the main thread, since serial behavior // is more reliable for tests. [self simulateDataCallbacksForTestBlockWithBodyData:streamedData response:response responseData:responseData error:(error ?: streamError)]; }); }]; }); } else { // No input stream; use the supplied data or file URL. NSURL *bodyFileURL = self.bodyFileURL; if (bodyFileURL) { NSError *readError; self->_bodyData = [NSData dataWithContentsOfURL:bodyFileURL options:NSDataReadingMappedIfSafe error:&readError]; error = readError; } // No stream provider. // In real fetches, nothing happens until the run loop spins, so apps have leeway to // set callbacks after they call beginFetch. We'll mirror that fetcher behavior by // delaying callbacks here at least to the next spin of the run loop. That keeps // immediate, synchronous setting of callback blocks after beginFetch working in tests. dispatch_async(dispatch_get_main_queue(), ^{ [self simulateDataCallbacksForTestBlockWithBodyData:self->_bodyData response:response responseData:responseData error:error]; }); } }); } - (void)simulateByteTransferReportWithDataLength:(int64_t)totalDataLength block:(GTMSessionFetcherSendProgressBlock)block { // This utility method simulates transfer progress with up to three callbacks. // It is used to call back to any of the progress blocks. int64_t sendReportSize = totalDataLength / 3 + 1; int64_t totalSent = 0; while (totalSent < totalDataLength) { int64_t bytesRemaining = totalDataLength - totalSent; sendReportSize = MIN(sendReportSize, bytesRemaining); totalSent += sendReportSize; [self invokeOnCallbackQueueUnlessStopped:^{ block(sendReportSize, totalSent, totalDataLength); }]; } } - (void)simulateDataCallbacksForTestBlockWithBodyData:(NSData * GTM_NULLABLE_TYPE)bodyData response:(NSURLResponse *)response responseData:(NSData *)suppliedData error:(NSError *)suppliedError { __block NSData *responseData = suppliedData; __block NSError *responseError = suppliedError; // This method does the test simulation of callbacks once the upload // and download data are known. @synchronized(self) { GTMSessionMonitorSynchronized(self); // Get copies of ivars we'll access in async invocations. This simulation assumes // they won't change during fetcher execution. NSURL *destinationFileURL = _destinationFileURL; GTMSessionFetcherWillRedirectBlock willRedirectBlock = _willRedirectBlock; GTMSessionFetcherDidReceiveResponseBlock didReceiveResponseBlock = _didReceiveResponseBlock; GTMSessionFetcherSendProgressBlock sendProgressBlock = _sendProgressBlock; GTMSessionFetcherDownloadProgressBlock downloadProgressBlock = _downloadProgressBlock; GTMSessionFetcherAccumulateDataBlock accumulateDataBlock = _accumulateDataBlock; GTMSessionFetcherReceivedProgressBlock receivedProgressBlock = _receivedProgressBlock; GTMSessionFetcherWillCacheURLResponseBlock willCacheURLResponseBlock = _willCacheURLResponseBlock; // Simulate receipt of redirection. if (willRedirectBlock) { [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES block:^{ willRedirectBlock((NSHTTPURLResponse *)response, self->_request, ^(NSURLRequest *redirectRequest) { // For simulation, we'll assume the app will just continue. }); }]; } // If the fetcher has a challenge block, simulate a challenge. // // It might be nice to eventually let the user determine which testBlock // fetches get challenged rather than always executing the supplied // challenge block. if (_challengeBlock) { [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES block:^{ if (self->_challengeBlock) { NSURL *requestURL = self->_request.URL; NSString *host = requestURL.host; NSURLProtectionSpace *pspace = [[NSURLProtectionSpace alloc] initWithHost:host port:requestURL.port.integerValue protocol:requestURL.scheme realm:nil authenticationMethod:NSURLAuthenticationMethodHTTPBasic]; id unusedSender = (id)[NSNull null]; NSURLAuthenticationChallenge *challenge = [[NSURLAuthenticationChallenge alloc] initWithProtectionSpace:pspace proposedCredential:nil previousFailureCount:0 failureResponse:nil error:nil sender:unusedSender]; self->_challengeBlock(self, challenge, ^(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * GTM_NULLABLE_TYPE credential){ // We could change the responseData and responseError based on the disposition, // but it's easier for apps to just supply the expected data and error // directly to the test block. So this simulation ignores the disposition. }); } }]; } // Simulate receipt of an initial response. if (response && didReceiveResponseBlock) { [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES block:^{ didReceiveResponseBlock(response, ^(NSURLSessionResponseDisposition desiredDisposition) { // For simulation, we'll assume the disposition is to continue. }); }]; } // Simulate reporting send progress. if (sendProgressBlock) { [self simulateByteTransferReportWithDataLength:(int64_t)bodyData.length block:^(int64_t bytesSent, int64_t totalBytesSent, int64_t totalBytesExpectedToSend) { // This is invoked on the callback queue unless stopped. sendProgressBlock(bytesSent, totalBytesSent, totalBytesExpectedToSend); }]; } if (destinationFileURL) { // Simulate download to file progress. if (downloadProgressBlock) { [self simulateByteTransferReportWithDataLength:(int64_t)responseData.length block:^(int64_t bytesDownloaded, int64_t totalBytesDownloaded, int64_t totalBytesExpectedToDownload) { // This is invoked on the callback queue unless stopped. downloadProgressBlock(bytesDownloaded, totalBytesDownloaded, totalBytesExpectedToDownload); }]; } NSError *writeError; [responseData writeToURL:destinationFileURL options:NSDataWritingAtomic error:&writeError]; if (writeError) { // Tell the test code that writing failed. responseError = writeError; } } else { // Simulate download to NSData progress. if ((accumulateDataBlock || receivedProgressBlock) && responseData) { [self simulateByteTransferWithData:responseData block:^(NSData *data, int64_t bytesReceived, int64_t totalBytesReceived, int64_t totalBytesExpectedToReceive) { // This is invoked on the callback queue unless stopped. if (accumulateDataBlock) { accumulateDataBlock(data); } if (receivedProgressBlock) { receivedProgressBlock(bytesReceived, totalBytesReceived); } }]; } if (!accumulateDataBlock) { _downloadedData = [responseData mutableCopy]; } if (willCacheURLResponseBlock) { // Simulate letting the client inspect and alter the cached response. NSData *cachedData = responseData ?: [[NSData alloc] init]; // Always have non-nil data. NSCachedURLResponse *cachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:cachedData]; [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES block:^{ willCacheURLResponseBlock(cachedResponse, ^(NSCachedURLResponse *responseToCache){ // The app may provide an alternative response, or nil to defeat caching. }); }]; } } _response = response; } // @synchronized(self) NSOperationQueue *queue = self.sessionDelegateQueue; [queue addOperationWithBlock:^{ // Rather than invoke failToBeginFetchWithError: we want to simulate completion of // a connection that started and ended, so we'll call down to finishWithError: NSInteger status = responseError ? responseError.code : 200; if (status >= 200 && status <= 399) { [self finishWithError:nil shouldRetry:NO]; } else { [self shouldRetryNowForStatus:status error:responseError forceAssumeRetry:NO response:^(BOOL shouldRetry) { [self finishWithError:responseError shouldRetry:shouldRetry]; }]; } }]; } - (void)simulateByteTransferWithData:(NSData *)responseData block:(GTMSessionFetcherSimulateByteTransferBlock)transferBlock { // This utility method simulates transfering data to the client. It divides the data into at most // "chunkCount" chunks and then passes each chunk along with a progress update to transferBlock. // This function can be used with accumulateDataBlock or receivedProgressBlock. NSUInteger chunkCount = MAX(self.testBlockAccumulateDataChunkCount, (NSUInteger) 1); NSUInteger totalDataLength = responseData.length; NSUInteger sendDataSize = totalDataLength / chunkCount + 1; NSUInteger totalSent = 0; while (totalSent < totalDataLength) { NSUInteger bytesRemaining = totalDataLength - totalSent; sendDataSize = MIN(sendDataSize, bytesRemaining); NSData *chunkData = [responseData subdataWithRange:NSMakeRange(totalSent, sendDataSize)]; totalSent += sendDataSize; [self invokeOnCallbackQueueUnlessStopped:^{ transferBlock(chunkData, (int64_t)sendDataSize, (int64_t)totalSent, (int64_t)totalDataLength); }]; } } #endif // !GTM_DISABLE_FETCHER_TEST_BLOCK - (void)setSessionTask:(NSURLSessionTask *)sessionTask { @synchronized(self) { GTMSessionMonitorSynchronized(self); if (_sessionTask != sessionTask) { _sessionTask = sessionTask; if (_sessionTask) { // Request could be nil on restoring this fetcher from a background session. if (!_request) { _request = [_sessionTask.originalRequest mutableCopy]; } } } } // @synchronized(self) } - (NSURLSessionTask * GTM_NULLABLE_TYPE)sessionTask { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _sessionTask; } // @synchronized(self) } + (NSUserDefaults *)fetcherUserDefaults { static NSUserDefaults *gFetcherUserDefaults = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class fetcherUserDefaultsClass = NSClassFromString(@"GTMSessionFetcherUserDefaultsFactory"); if (fetcherUserDefaultsClass) { gFetcherUserDefaults = [fetcherUserDefaultsClass fetcherUserDefaults]; } else { gFetcherUserDefaults = [NSUserDefaults standardUserDefaults]; } }); return gFetcherUserDefaults; } - (void)addPersistedBackgroundSessionToDefaults { NSString *sessionIdentifier = self.sessionIdentifier; if (!sessionIdentifier) { return; } NSArray *oldBackgroundSessions = [[self class] activePersistedBackgroundSessions]; if ([oldBackgroundSessions containsObject:_sessionIdentifier]) { return; } NSMutableArray *newBackgroundSessions = [NSMutableArray arrayWithArray:oldBackgroundSessions]; [newBackgroundSessions addObject:sessionIdentifier]; GTM_LOG_BACKGROUND_SESSION(@"Add to background sessions: %@", newBackgroundSessions); NSUserDefaults *userDefaults = [[self class] fetcherUserDefaults]; [userDefaults setObject:newBackgroundSessions forKey:kGTMSessionFetcherPersistedDestinationKey]; [userDefaults synchronize]; } - (void)removePersistedBackgroundSessionFromDefaults { NSString *sessionIdentifier = self.sessionIdentifier; if (!sessionIdentifier) return; NSArray *oldBackgroundSessions = [[self class] activePersistedBackgroundSessions]; if (!oldBackgroundSessions) { return; } NSMutableArray *newBackgroundSessions = [NSMutableArray arrayWithArray:oldBackgroundSessions]; NSUInteger sessionIndex = [newBackgroundSessions indexOfObject:sessionIdentifier]; if (sessionIndex == NSNotFound) { return; } [newBackgroundSessions removeObjectAtIndex:sessionIndex]; GTM_LOG_BACKGROUND_SESSION(@"Remove from background sessions: %@", newBackgroundSessions); NSUserDefaults *userDefaults = [[self class] fetcherUserDefaults]; if (newBackgroundSessions.count == 0) { [userDefaults removeObjectForKey:kGTMSessionFetcherPersistedDestinationKey]; } else { [userDefaults setObject:newBackgroundSessions forKey:kGTMSessionFetcherPersistedDestinationKey]; } [userDefaults synchronize]; } + (GTM_NULLABLE NSArray *)activePersistedBackgroundSessions { NSUserDefaults *userDefaults = [[self class] fetcherUserDefaults]; NSArray *oldBackgroundSessions = [userDefaults arrayForKey:kGTMSessionFetcherPersistedDestinationKey]; if (oldBackgroundSessions.count == 0) { return nil; } NSMutableArray *activeBackgroundSessions = nil; NSMapTable *sessionIdentifierToFetcherMap = [self sessionIdentifierToFetcherMap]; for (NSString *sessionIdentifier in oldBackgroundSessions) { GTMSessionFetcher *fetcher = [sessionIdentifierToFetcherMap objectForKey:sessionIdentifier]; if (fetcher) { if (!activeBackgroundSessions) { activeBackgroundSessions = [[NSMutableArray alloc] init]; } [activeBackgroundSessions addObject:sessionIdentifier]; } } return activeBackgroundSessions; } + (NSArray *)fetchersForBackgroundSessions { NSUserDefaults *userDefaults = [[self class] fetcherUserDefaults]; NSArray *backgroundSessions = [userDefaults arrayForKey:kGTMSessionFetcherPersistedDestinationKey]; NSMapTable *sessionIdentifierToFetcherMap = [self sessionIdentifierToFetcherMap]; NSMutableArray *fetchers = [NSMutableArray array]; for (NSString *sessionIdentifier in backgroundSessions) { GTMSessionFetcher *fetcher = [sessionIdentifierToFetcherMap objectForKey:sessionIdentifier]; if (!fetcher) { fetcher = [self fetcherWithSessionIdentifier:sessionIdentifier]; GTMSESSION_ASSERT_DEBUG(fetcher != nil, @"Unexpected invalid session identifier: %@", sessionIdentifier); [fetcher beginFetchWithCompletionHandler:nil]; } GTM_LOG_BACKGROUND_SESSION(@"%@ restoring session %@ by creating fetcher %@ %p", [self class], sessionIdentifier, fetcher, fetcher); if (fetcher != nil) { [fetchers addObject:fetcher]; } } return fetchers; } #if TARGET_OS_IPHONE && !TARGET_OS_WATCH + (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(GTMSessionFetcherSystemCompletionHandler)completionHandler { GTMSessionFetcher *fetcher = [self fetcherWithSessionIdentifier:identifier]; if (fetcher != nil) { fetcher.systemCompletionHandler = completionHandler; } else { GTM_LOG_BACKGROUND_SESSION(@"%@ did not create background session identifier: %@", [self class], identifier); } } #endif - (NSString * GTM_NULLABLE_TYPE)sessionIdentifier { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _sessionIdentifier; } // @synchronized(self) } - (void)setSessionIdentifier:(NSString *)sessionIdentifier { GTMSESSION_ASSERT_DEBUG(sessionIdentifier != nil, @"Invalid session identifier"); @synchronized(self) { GTMSessionMonitorSynchronized(self); GTMSESSION_ASSERT_DEBUG(!_session, @"Unable to set session identifier after session created"); _sessionIdentifier = [sessionIdentifier copy]; _usingBackgroundSession = YES; _canShareSession = NO; [self restoreDefaultStateForSessionIdentifierMetadata]; } // @synchronized(self) } - (void)setSessionIdentifierInternal:(GTM_NULLABLE NSString *)sessionIdentifier { // This internal method only does a synchronized set of the session identifier. // It does not have side effects on the background session, shared session, or // session identifier metadata. @synchronized(self) { GTMSessionMonitorSynchronized(self); _sessionIdentifier = [sessionIdentifier copy]; } // @synchronized(self) } - (NSDictionary * GTM_NULLABLE_TYPE)sessionUserInfo { @synchronized(self) { GTMSessionMonitorSynchronized(self); if (_sessionUserInfo == nil) { // We'll return the metadata dictionary with internal keys removed. This avoids the user // re-using the userInfo dictionary later and accidentally including the internal keys. NSMutableDictionary *metadata = [[self sessionIdentifierMetadataUnsynchronized] mutableCopy]; NSSet *keysToRemove = [metadata keysOfEntriesPassingTest:^BOOL(id key, id obj, BOOL *stop) { return [key hasPrefix:@"_"]; }]; [metadata removeObjectsForKeys:[keysToRemove allObjects]]; if (metadata.count > 0) { _sessionUserInfo = metadata; } } return _sessionUserInfo; } // @synchronized(self) } - (void)setSessionUserInfo:(NSDictionary * GTM_NULLABLE_TYPE)dictionary { @synchronized(self) { GTMSessionMonitorSynchronized(self); GTMSESSION_ASSERT_DEBUG(_sessionIdentifier == nil, @"Too late to assign userInfo"); _sessionUserInfo = dictionary; } // @synchronized(self) } - (GTM_NULLABLE NSDictionary *)sessionIdentifierDefaultMetadata { GTMSessionCheckSynchronized(self); NSMutableDictionary *defaultUserInfo = [[NSMutableDictionary alloc] init]; if (_destinationFileURL) { defaultUserInfo[kGTMSessionIdentifierDestinationFileURLMetadataKey] = [_destinationFileURL absoluteString]; } if (_bodyFileURL) { defaultUserInfo[kGTMSessionIdentifierBodyFileURLMetadataKey] = [_bodyFileURL absoluteString]; } return (defaultUserInfo.count > 0) ? defaultUserInfo : nil; } - (void)restoreDefaultStateForSessionIdentifierMetadata { GTMSessionCheckSynchronized(self); NSDictionary *metadata = [self sessionIdentifierMetadataUnsynchronized]; NSString *destinationFileURLString = metadata[kGTMSessionIdentifierDestinationFileURLMetadataKey]; if (destinationFileURLString) { _destinationFileURL = [NSURL URLWithString:destinationFileURLString]; GTM_LOG_BACKGROUND_SESSION(@"Restoring destination file URL: %@", _destinationFileURL); } NSString *bodyFileURLString = metadata[kGTMSessionIdentifierBodyFileURLMetadataKey]; if (bodyFileURLString) { _bodyFileURL = [NSURL URLWithString:bodyFileURLString]; GTM_LOG_BACKGROUND_SESSION(@"Restoring body file URL: %@", _bodyFileURL); } } - (NSDictionary * GTM_NULLABLE_TYPE)sessionIdentifierMetadata { @synchronized(self) { GTMSessionMonitorSynchronized(self); return [self sessionIdentifierMetadataUnsynchronized]; } } - (NSDictionary * GTM_NULLABLE_TYPE)sessionIdentifierMetadataUnsynchronized { GTMSessionCheckSynchronized(self); // Session Identifier format: "com.google.__ if (!_sessionIdentifier) { return nil; } NSScanner *metadataScanner = [NSScanner scannerWithString:_sessionIdentifier]; [metadataScanner setCharactersToBeSkipped:nil]; NSString *metadataString; NSString *uuid; if ([metadataScanner scanUpToString:@"_" intoString:NULL] && [metadataScanner scanString:@"_" intoString:NULL] && [metadataScanner scanUpToString:@"_" intoString:&uuid] && [metadataScanner scanString:@"_" intoString:NULL] && [metadataScanner scanUpToString:@"\n" intoString:&metadataString]) { _sessionIdentifierUUID = uuid; NSData *metadataData = [metadataString dataUsingEncoding:NSUTF8StringEncoding]; NSError *error; NSDictionary *metadataDict = [NSJSONSerialization JSONObjectWithData:metadataData options:0 error:&error]; GTM_LOG_BACKGROUND_SESSION(@"User Info from session identifier: %@ %@", metadataDict, error ? error : @""); return metadataDict; } return nil; } - (NSString *)createSessionIdentifierWithMetadata:(NSDictionary * GTM_NULLABLE_TYPE)metadataToInclude { NSString *result; @synchronized(self) { GTMSessionMonitorSynchronized(self); // Session Identifier format: "com.google.__ GTMSESSION_ASSERT_DEBUG(!_sessionIdentifier, @"Session identifier already created"); _sessionIdentifierUUID = [[NSUUID UUID] UUIDString]; _sessionIdentifier = [NSString stringWithFormat:@"%@_%@", kGTMSessionIdentifierPrefix, _sessionIdentifierUUID]; // Start with user-supplied keys so they cannot accidentally override the fetcher's keys. NSMutableDictionary *metadataDict = [NSMutableDictionary dictionaryWithDictionary:(NSDictionary * GTM_NONNULL_TYPE)_sessionUserInfo]; if (metadataToInclude) { [metadataDict addEntriesFromDictionary:(NSDictionary *)metadataToInclude]; } NSDictionary *defaultMetadataDict = [self sessionIdentifierDefaultMetadata]; if (defaultMetadataDict) { [metadataDict addEntriesFromDictionary:defaultMetadataDict]; } if (metadataDict.count > 0) { NSData *metadataData = [NSJSONSerialization dataWithJSONObject:metadataDict options:0 error:NULL]; GTMSESSION_ASSERT_DEBUG(metadataData != nil, @"Session identifier user info failed to convert to JSON"); if (metadataData.length > 0) { NSString *metadataString = [[NSString alloc] initWithData:metadataData encoding:NSUTF8StringEncoding]; _sessionIdentifier = [_sessionIdentifier stringByAppendingFormat:@"_%@", metadataString]; } } _didCreateSessionIdentifier = YES; result = _sessionIdentifier; } // @synchronized(self) return result; } - (void)failToBeginFetchWithError:(NSError *)error { @synchronized(self) { GTMSessionMonitorSynchronized(self); _hasStoppedFetching = YES; } if (error == nil) { error = [NSError errorWithDomain:kGTMSessionFetcherErrorDomain code:GTMSessionFetcherErrorDownloadFailed userInfo:nil]; } [self invokeFetchCallbacksOnCallbackQueueWithData:nil error:error]; [self releaseCallbacks]; [_service fetcherDidStop:self]; self.authorizer = nil; } + (GTMSessionCookieStorage *)staticCookieStorage { static GTMSessionCookieStorage *gCookieStorage = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ gCookieStorage = [[GTMSessionCookieStorage alloc] init]; }); return gCookieStorage; } #if GTM_BACKGROUND_TASK_FETCHING - (void)endBackgroundTask { // Whenever the connection stops or background execution expires, // we need to tell UIApplication we're done. UIBackgroundTaskIdentifier bgTaskID; @synchronized(self) { bgTaskID = self.backgroundTaskIdentifier; if (bgTaskID != UIBackgroundTaskInvalid) { self.backgroundTaskIdentifier = UIBackgroundTaskInvalid; } } if (bgTaskID != UIBackgroundTaskInvalid) { id app = [[self class] fetcherUIApplication]; [app endBackgroundTask:bgTaskID]; } } #endif // GTM_BACKGROUND_TASK_FETCHING - (void)authorizeRequest { GTMSessionCheckNotSynchronized(self); id authorizer = self.authorizer; SEL asyncAuthSel = @selector(authorizeRequest:delegate:didFinishSelector:); if ([authorizer respondsToSelector:asyncAuthSel]) { SEL callbackSel = @selector(authorizer:request:finishedWithError:); NSMutableURLRequest *mutableRequest = [self.request mutableCopy]; [authorizer authorizeRequest:mutableRequest delegate:self didFinishSelector:callbackSel]; } else { GTMSESSION_ASSERT_DEBUG(authorizer == nil, @"invalid authorizer for fetch"); // No authorizing possible, and authorizing happens only after any delay; // just begin fetching [self beginFetchMayDelay:NO mayAuthorize:NO]; } } - (void)authorizer:(id)auth request:(NSMutableURLRequest *)authorizedRequest finishedWithError:(NSError *)error { GTMSessionCheckNotSynchronized(self); if (error != nil) { // We can't fetch without authorization [self failToBeginFetchWithError:error]; } else { @synchronized(self) { _request = authorizedRequest; } [self beginFetchMayDelay:NO mayAuthorize:NO]; } } - (BOOL)canFetchWithBackgroundSession { // Subclasses may override. return YES; } // Returns YES if the fetcher has been started and has not yet stopped. // // Fetching includes waiting for authorization or for retry, waiting to be allowed by the // service object to start the request, and actually fetching the request. - (BOOL)isFetching { @synchronized(self) { GTMSessionMonitorSynchronized(self); return [self isFetchingUnsynchronized]; } } - (BOOL)isFetchingUnsynchronized { GTMSessionCheckSynchronized(self); BOOL hasBegun = (_initialBeginFetchDate != nil); return hasBegun && !_hasStoppedFetching; } - (NSURLResponse * GTM_NULLABLE_TYPE)response { @synchronized(self) { GTMSessionMonitorSynchronized(self); NSURLResponse *response = [self responseUnsynchronized]; return response; } // @synchronized(self) } - (NSURLResponse * GTM_NULLABLE_TYPE)responseUnsynchronized { GTMSessionCheckSynchronized(self); NSURLResponse *response = _sessionTask.response; if (!response) response = _response; return response; } - (NSInteger)statusCode { @synchronized(self) { GTMSessionMonitorSynchronized(self); NSInteger statusCode = [self statusCodeUnsynchronized]; return statusCode; } // @synchronized(self) } - (NSInteger)statusCodeUnsynchronized { GTMSessionCheckSynchronized(self); NSURLResponse *response = [self responseUnsynchronized]; NSInteger statusCode; if ([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; } - (NSDictionary * GTM_NULLABLE_TYPE)responseHeaders { GTMSessionCheckNotSynchronized(self); NSURLResponse *response = self.response; if ([response respondsToSelector:@selector(allHeaderFields)]) { NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields]; return headers; } return nil; } - (NSDictionary * GTM_NULLABLE_TYPE)responseHeadersUnsynchronized { GTMSessionCheckSynchronized(self); NSURLResponse *response = [self responseUnsynchronized]; if ([response respondsToSelector:@selector(allHeaderFields)]) { NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields]; return headers; } return nil; } - (void)releaseCallbacks { // Avoid releasing blocks in the sync section since objects dealloc'd by // the blocks being released may call back into the fetcher or fetcher // service. dispatch_queue_t NS_VALID_UNTIL_END_OF_SCOPE holdCallbackQueue; GTMSessionFetcherCompletionHandler NS_VALID_UNTIL_END_OF_SCOPE holdCompletionHandler; @synchronized(self) { GTMSessionMonitorSynchronized(self); holdCallbackQueue = _callbackQueue; holdCompletionHandler = _completionHandler; _callbackQueue = nil; _completionHandler = nil; // Setter overridden in upload. Setter assumed to be used externally. } // Set local callback pointers to nil here rather than let them release at the end of the scope // to make any problems due to the blocks being released be a bit more obvious in a stack trace. holdCallbackQueue = nil; holdCompletionHandler = nil; self.configurationBlock = nil; self.didReceiveResponseBlock = nil; self.challengeBlock = nil; self.willRedirectBlock = nil; self.sendProgressBlock = nil; self.receivedProgressBlock = nil; self.downloadProgressBlock = nil; self.accumulateDataBlock = nil; self.willCacheURLResponseBlock = nil; self.retryBlock = nil; self.testBlock = nil; self.resumeDataBlock = nil; } - (void)forgetSessionIdentifierForFetcher { GTMSessionCheckSynchronized(self); [self forgetSessionIdentifierForFetcherWithoutSyncCheck]; } - (void)forgetSessionIdentifierForFetcherWithoutSyncCheck { // This should be called inside a @synchronized block (except during dealloc.) if (_sessionIdentifier) { NSMapTable *sessionIdentifierToFetcherMap = [[self class] sessionIdentifierToFetcherMap]; [sessionIdentifierToFetcherMap removeObjectForKey:_sessionIdentifier]; _sessionIdentifier = nil; _didCreateSessionIdentifier = NO; } } // External stop method - (void)stopFetching { @synchronized(self) { GTMSessionMonitorSynchronized(self); // Prevent enqueued callbacks from executing. _userStoppedFetching = YES; } // @synchronized(self) [self stopFetchReleasingCallbacks:YES]; } // Cancel the fetch of the URL that's currently in progress. // // If shouldReleaseCallbacks is NO then the fetch will be retried so the callbacks // need to still be retained. - (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks { [self removePersistedBackgroundSessionFromDefaults]; id service; NSMutableURLRequest *request; // If the task or the retry timer is all that's retaining the fetcher, // we want to be sure this instance survives stopping at least long enough for // the stack to unwind. __autoreleasing GTMSessionFetcher *holdSelf = self; BOOL hasCanceledTask = NO; [holdSelf destroyRetryTimer]; @synchronized(self) { GTMSessionMonitorSynchronized(self); _hasStoppedFetching = YES; service = _service; request = _request; if (_sessionTask) { // In case cancelling the task or session calls this recursively, we want // to ensure that we'll only release the task and delegate once, // so first set _sessionTask to nil // // This may be called in a callback from the task, so use autorelease to avoid // releasing the task in its own callback. __autoreleasing NSURLSessionTask *oldTask = _sessionTask; if (!_isUsingTestBlock) { _response = _sessionTask.response; } _sessionTask = nil; if ([oldTask state] != NSURLSessionTaskStateCompleted) { // For download tasks, when the fetch is stopped, we may provide resume data that can // be used to create a new session. BOOL mayResume = (_resumeDataBlock && [oldTask respondsToSelector:@selector(cancelByProducingResumeData:)]); if (!mayResume) { [oldTask cancel]; // A side effect of stopping the task is that URLSession:task:didCompleteWithError: // will be invoked asynchronously on the delegate queue. } else { void (^resumeBlock)(NSData *) = _resumeDataBlock; _resumeDataBlock = nil; // Save callbackQueue since releaseCallbacks clears it. dispatch_queue_t callbackQueue = _callbackQueue; dispatch_group_enter(_callbackGroup); [(NSURLSessionDownloadTask *)oldTask cancelByProducingResumeData:^(NSData *resumeData) { [self invokeOnCallbackQueue:callbackQueue afterUserStopped:YES block:^{ resumeBlock(resumeData); dispatch_group_leave(self->_callbackGroup); }]; }]; } hasCanceledTask = YES; } } // If the task was canceled, wait until the URLSession:task:didCompleteWithError: to call // finishTasksAndInvalidate, since calling it immediately tends to crash, see radar 18471901. if (_session) { BOOL shouldInvalidate = _shouldInvalidateSession; #if TARGET_OS_IPHONE // Don't invalidate if we've got a systemCompletionHandler, since // URLSessionDidFinishEventsForBackgroundURLSession: won't be called if invalidated. shouldInvalidate = shouldInvalidate && !self.systemCompletionHandler; #endif if (shouldInvalidate) { __autoreleasing NSURLSession *oldSession = _session; _session = nil; if (!hasCanceledTask) { [oldSession finishTasksAndInvalidate]; } else { _sessionNeedingInvalidation = oldSession; } } } } // @synchronized(self) // send the stopped notification [self sendStopNotificationIfNeeded]; [_authorizer stopAuthorizationForRequest:request]; if (shouldReleaseCallbacks) { [self releaseCallbacks]; self.authorizer = nil; } [service fetcherDidStop:self]; #if GTM_BACKGROUND_TASK_FETCHING [self endBackgroundTask]; #endif } - (void)setStopNotificationNeeded:(BOOL)flag { @synchronized(self) { GTMSessionMonitorSynchronized(self); _isStopNotificationNeeded = flag; } // @synchronized(self) } - (void)sendStopNotificationIfNeeded { BOOL sendNow = NO; @synchronized(self) { GTMSessionMonitorSynchronized(self); if (_isStopNotificationNeeded) { _isStopNotificationNeeded = NO; sendNow = YES; } } // @synchronized(self) if (sendNow) { [self postNotificationOnMainThreadWithName:kGTMSessionFetcherStoppedNotification userInfo:nil requireAsync:NO]; } } - (void)retryFetch { [self stopFetchReleasingCallbacks:NO]; // A retry will need a configuration with a fresh session identifier. @synchronized(self) { GTMSessionMonitorSynchronized(self); if (_sessionIdentifier && _didCreateSessionIdentifier) { [self forgetSessionIdentifierForFetcher]; _configuration = nil; } if (_canShareSession) { // Force a grab of the current session from the fetcher service in case // the service's old one has become invalid. _session = nil; } } // @synchronized(self) [self beginFetchForRetry]; } - (BOOL)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds { // Uncovered in upload fetcher testing, because the chunk fetcher is being waited on, and gets // released by the upload code. The uploader just holds onto it with an ivar, and that gets // nilled in the chunk fetcher callback. // Used once in while loop just to avoid unused variable compiler warning. __autoreleasing GTMSessionFetcher *holdSelf = self; NSDate *giveUpDate = [NSDate dateWithTimeIntervalSinceNow:timeoutInSeconds]; BOOL shouldSpinRunLoop = ([NSThread isMainThread] && (!self.callbackQueue || self.callbackQueue == dispatch_get_main_queue())); BOOL expired = NO; // Loop until the callbacks have been called and released, and until // the connection is no longer pending, until there are no callback dispatches // in flight, or until the timeout has expired. int64_t delta = (int64_t)(100 * NSEC_PER_MSEC); // 100 ms while (1) { BOOL isTaskInProgress = (holdSelf->_sessionTask && [_sessionTask state] != NSURLSessionTaskStateCompleted); BOOL needsToCallCompletion = (_completionHandler != nil); BOOL isCallbackInProgress = (_callbackGroup && dispatch_group_wait(_callbackGroup, dispatch_time(DISPATCH_TIME_NOW, delta))); if (!isTaskInProgress && !needsToCallCompletion && !isCallbackInProgress) break; expired = ([giveUpDate timeIntervalSinceNow] < 0); if (expired) { GTMSESSION_LOG_DEBUG(@"GTMSessionFetcher waitForCompletionWithTimeout:%0.1f expired -- " @"%@%@%@", timeoutInSeconds, isTaskInProgress ? @"taskInProgress " : @"", needsToCallCompletion ? @"needsToCallCompletion " : @"", isCallbackInProgress ? @"isCallbackInProgress" : @""); break; } // Run the current run loop 1/1000 of a second to give the networking // code a chance to work const NSTimeInterval kSpinInterval = 0.001; if (shouldSpinRunLoop) { NSDate *stopDate = [NSDate dateWithTimeIntervalSinceNow:kSpinInterval]; [[NSRunLoop currentRunLoop] runUntilDate:stopDate]; } else { [NSThread sleepForTimeInterval:kSpinInterval]; } } return !expired; } + (void)setGlobalTestBlock:(GTMSessionFetcherTestBlock GTM_NULLABLE_TYPE)block { #if GTM_DISABLE_FETCHER_TEST_BLOCK GTMSESSION_ASSERT_DEBUG(block == nil, @"test blocks disabled"); #endif gGlobalTestBlock = [block copy]; } #if GTM_BACKGROUND_TASK_FETCHING static GTM_NULLABLE_TYPE id gSubstituteUIApp; + (void)setSubstituteUIApplication:(nullable id)app { gSubstituteUIApp = app; } + (nullable id)substituteUIApplication { return gSubstituteUIApp; } + (nullable id)fetcherUIApplication { id app = gSubstituteUIApp; if (app) return app; // iOS App extensions should not call [UIApplication sharedApplication], even // if UIApplication responds to it. static Class applicationClass = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ BOOL isAppExtension = [[[NSBundle mainBundle] bundlePath] hasSuffix:@".appex"]; if (!isAppExtension) { Class cls = NSClassFromString(@"UIApplication"); if (cls && [cls respondsToSelector:NSSelectorFromString(@"sharedApplication")]) { applicationClass = cls; } } }); if (applicationClass) { app = (id)[applicationClass sharedApplication]; } return app; } #endif // GTM_BACKGROUND_TASK_FETCHING #pragma mark NSURLSession Delegate Methods // NSURLSession documentation indicates that redirectRequest can be passed to the handler // but empirically redirectRequest lacks the HTTP body, so passing it will break POSTs. // Instead, we construct a new request, a copy of the original, with overrides from the // redirect. - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)redirectResponse newRequest:(NSURLRequest *)redirectRequest completionHandler:(void (^)(NSURLRequest * GTM_NULLABLE_TYPE))handler { [self setSessionTask:task]; GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ task:%@ willPerformHTTPRedirection:%@ newRequest:%@", [self class], self, session, task, redirectResponse, redirectRequest); if ([self userStoppedFetching]) { handler(nil); return; } if (redirectRequest && redirectResponse) { // Copy the original request, including the body. NSURLRequest *originalRequest = self.request; NSMutableURLRequest *newRequest = [originalRequest mutableCopy]; // Disallow scheme changes (say, from https to http). NSURL *originalRequestURL = originalRequest.URL; NSURL *redirectRequestURL = redirectRequest.URL; NSString *originalScheme = originalRequestURL.scheme; NSString *redirectScheme = redirectRequestURL.scheme; if (originalScheme != nil && [originalScheme caseInsensitiveCompare:@"http"] == NSOrderedSame && redirectScheme != nil && [redirectScheme caseInsensitiveCompare:@"https"] == NSOrderedSame) { // Allow the change from http to https. } else { // Disallow any other scheme changes. redirectScheme = originalScheme; } // The new requests's URL overrides the original's URL. NSURLComponents *components = [NSURLComponents componentsWithURL:redirectRequestURL resolvingAgainstBaseURL:NO]; components.scheme = redirectScheme; NSURL *newURL = components.URL; [newRequest setURL:newURL]; // Any headers in the redirect override headers in the original. NSDictionary *redirectHeaders = redirectRequest.allHTTPHeaderFields; for (NSString *key in redirectHeaders) { NSString *value = [redirectHeaders objectForKey:key]; [newRequest setValue:value forHTTPHeaderField:key]; } redirectRequest = newRequest; // Log the response we just received [self setResponse:redirectResponse]; [self logNowWithError:nil]; GTMSessionFetcherWillRedirectBlock willRedirectBlock = self.willRedirectBlock; if (willRedirectBlock) { @synchronized(self) { GTMSessionMonitorSynchronized(self); [self invokeOnCallbackQueueAfterUserStopped:YES block:^{ willRedirectBlock(redirectResponse, redirectRequest, ^(NSURLRequest *clientRequest) { // Update the request for future logging. [self updateMutableRequest:[clientRequest mutableCopy]]; handler(clientRequest); }); }]; } // @synchronized(self) return; } // Continues here if the client did not provide a redirect block. // Update the request for future logging. [self updateMutableRequest:[redirectRequest mutableCopy]]; } handler(redirectRequest); } - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))handler { [self setSessionTask:dataTask]; GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ dataTask:%@ didReceiveResponse:%@", [self class], self, session, dataTask, response); void (^accumulateAndFinish)(NSURLSessionResponseDisposition) = ^(NSURLSessionResponseDisposition dispositionValue) { // 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. @synchronized(self) { GTMSessionMonitorSynchronized(self); BOOL hadPreviousData = self->_downloadedLength > 0; [self->_downloadedData setLength:0]; self->_downloadedLength = 0; if (hadPreviousData && (dispositionValue != NSURLSessionResponseCancel)) { // Tell the accumulate block to discard prior data. GTMSessionFetcherAccumulateDataBlock accumulateBlock = self->_accumulateDataBlock; if (accumulateBlock) { [self invokeOnCallbackQueueUnlessStopped:^{ accumulateBlock(nil); }]; } } } // @synchronized(self) handler(dispositionValue); }; GTMSessionFetcherDidReceiveResponseBlock receivedResponseBlock; @synchronized(self) { GTMSessionMonitorSynchronized(self); receivedResponseBlock = _didReceiveResponseBlock; if (receivedResponseBlock) { // We will ultimately need to call back to NSURLSession's handler with the disposition value // for this delegate method even if the user has stopped the fetcher. [self invokeOnCallbackQueueAfterUserStopped:YES block:^{ receivedResponseBlock(response, ^(NSURLSessionResponseDisposition desiredDisposition) { accumulateAndFinish(desiredDisposition); }); }]; } } // @synchronized(self) if (receivedResponseBlock == nil) { accumulateAndFinish(NSURLSessionResponseAllow); } } - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask { GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ dataTask:%@ didBecomeDownloadTask:%@", [self class], self, session, dataTask, downloadTask); [self setSessionTask:downloadTask]; } - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * GTM_NULLABLE_TYPE credential))handler { [self setSessionTask:task]; GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ task:%@ didReceiveChallenge:%@", [self class], self, session, task, challenge); GTMSessionFetcherChallengeBlock challengeBlock = self.challengeBlock; if (challengeBlock) { // The fetcher user has provided custom challenge handling. // // We will ultimately need to call back to NSURLSession's handler with the disposition value // for this delegate method even if the user has stopped the fetcher. @synchronized(self) { GTMSessionMonitorSynchronized(self); [self invokeOnCallbackQueueAfterUserStopped:YES block:^{ challengeBlock(self, challenge, handler); }]; } } else { // No challenge block was provided by the client. [self respondToChallenge:challenge completionHandler:handler]; } } - (void)respondToChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * GTM_NULLABLE_TYPE credential))handler { @synchronized(self) { GTMSessionMonitorSynchronized(self); NSInteger previousFailureCount = [challenge previousFailureCount]; if (previousFailureCount <= 2) { NSURLProtectionSpace *protectionSpace = [challenge protectionSpace]; NSString *authenticationMethod = [protectionSpace authenticationMethod]; if ([authenticationMethod isEqual:NSURLAuthenticationMethodServerTrust]) { // SSL. // // Background sessions seem to require an explicit check of the server trust object // rather than default handling. SecTrustRef serverTrust = challenge.protectionSpace.serverTrust; if (serverTrust == NULL) { // No server trust information is available. handler(NSURLSessionAuthChallengePerformDefaultHandling, nil); } else { // Server trust information is available. void (^callback)(SecTrustRef, BOOL) = ^(SecTrustRef trustRef, BOOL allow){ if (allow) { NSURLCredential *trustCredential = [NSURLCredential credentialForTrust:trustRef]; handler(NSURLSessionAuthChallengeUseCredential, trustCredential); } else { GTMSESSION_LOG_DEBUG(@"Cancelling authentication challenge for %@", self->_request.URL); handler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); } }; if (_allowInvalidServerCertificates) { callback(serverTrust, YES); } else { [[self class] evaluateServerTrust:serverTrust forRequest:_request completionHandler:callback]; } } return; } NSURLCredential *credential = _credential; if ([[challenge protectionSpace] isProxy] && _proxyCredential != nil) { credential = _proxyCredential; } if (credential) { handler(NSURLSessionAuthChallengeUseCredential, credential); } else { // The credential is still nil; tell the OS to use the default handling. This is needed // for things that can come out of the keychain (proxies, client certificates, etc.). // // Note: Looking up a credential with NSURLCredentialStorage's // defaultCredentialForProtectionSpace: is *not* the same invoking the handler with // NSURLSessionAuthChallengePerformDefaultHandling. In the case of // NSURLAuthenticationMethodClientCertificate, you can get nil back from // NSURLCredentialStorage, while using this code path instead works. handler(NSURLSessionAuthChallengePerformDefaultHandling, nil); } } else { // We've failed auth 3 times. The completion handler will be called with code // NSURLErrorCancelled. handler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); } } // @synchronized(self) } // Validate the certificate chain. // // This may become a public method if it appears to be useful to users. + (void)evaluateServerTrust:(SecTrustRef)serverTrust forRequest:(NSURLRequest *)request completionHandler:(void (^)(SecTrustRef trustRef, BOOL allow))handler { // Retain the trust object to avoid a SecTrustEvaluate() crash on iOS 7. CFRetain(serverTrust); // Evaluate the certificate chain. // // The delegate queue may be the main thread. Trust evaluation could cause some // blocking network activity, so we must evaluate async, as documented at // https://developer.apple.com/library/ios/technotes/tn2232/ // // We must also avoid multiple uses of the trust object, per docs: // "It is not safe to call this function concurrently with any other function that uses // the same trust management object, or to re-enter this function for the same trust // management object." // // SecTrustEvaluateAsync both does sync execution of Evaluate and calls back on the // queue passed to it, according to at sources in // http://www.opensource.apple.com/source/libsecurity_keychain/libsecurity_keychain-55050.9/lib/SecTrust.cpp // It would require a global serial queue to ensure the evaluate happens only on a // single thread at a time, so we'll stick with using SecTrustEvaluate on a background // thread. dispatch_queue_t evaluateBackgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(evaluateBackgroundQueue, ^{ // It looks like the implementation of SecTrustEvaluate() on Mac grabs a global lock, // so it may be redundant for us to also lock, but it's easy to synchronize here // anyway. SecTrustResultType trustEval = kSecTrustResultInvalid; BOOL shouldAllow; OSStatus trustError; @synchronized([GTMSessionFetcher class]) { GTMSessionMonitorSynchronized([GTMSessionFetcher class]); trustError = SecTrustEvaluate(serverTrust, &trustEval); } if (trustError != errSecSuccess) { GTMSESSION_LOG_DEBUG(@"Error %d evaluating trust for %@", (int)trustError, request); shouldAllow = NO; } else { // Having a trust level "unspecified" by the user is the usual result, described at // https://developer.apple.com/library/mac/qa/qa1360 if (trustEval == kSecTrustResultUnspecified || trustEval == kSecTrustResultProceed) { shouldAllow = YES; } else { shouldAllow = NO; GTMSESSION_LOG_DEBUG(@"Challenge SecTrustResultType %u for %@, properties: %@", trustEval, request.URL.host, CFBridgingRelease(SecTrustCopyProperties(serverTrust))); } } handler(serverTrust, shouldAllow); CFRelease(serverTrust); }); } - (void)invokeOnCallbackQueueUnlessStopped:(void (^)(void))block { [self invokeOnCallbackQueueAfterUserStopped:NO block:block]; } - (void)invokeOnCallbackQueueAfterUserStopped:(BOOL)afterStopped block:(void (^)(void))block { GTMSessionCheckSynchronized(self); [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:afterStopped block:block]; } - (void)invokeOnCallbackUnsynchronizedQueueAfterUserStopped:(BOOL)afterStopped block:(void (^)(void))block { // testBlock simulation code may not be synchronizing when this is invoked. [self invokeOnCallbackQueue:_callbackQueue afterUserStopped:afterStopped block:block]; } - (void)invokeOnCallbackQueue:(dispatch_queue_t)callbackQueue afterUserStopped:(BOOL)afterStopped block:(void (^)(void))block { if (callbackQueue) { dispatch_group_async(_callbackGroup, callbackQueue, ^{ if (!afterStopped) { NSDate *serviceStoppedAllDate = [self->_service stoppedAllFetchersDate]; @synchronized(self) { GTMSessionMonitorSynchronized(self); // Avoid a race between stopFetching and the callback. if (self->_userStoppedFetching) { return; } // Also avoid calling back if the service has stopped all fetchers // since this one was created. The fetcher may have stopped before // stopAllFetchers was invoked, so _userStoppedFetching wasn't set, // but the app still won't expect the callback to fire after // the service's stopAllFetchers was invoked. if (serviceStoppedAllDate && [self->_initialBeginFetchDate compare:serviceStoppedAllDate] != NSOrderedDescending) { // stopAllFetchers was called after this fetcher began. return; } } // @synchronized(self) } block(); }); } } - (void)invokeFetchCallbacksOnCallbackQueueWithData:(GTM_NULLABLE NSData *)data error:(GTM_NULLABLE NSError *)error { // Callbacks will be released in the method stopFetchReleasingCallbacks: GTMSessionFetcherCompletionHandler handler; @synchronized(self) { GTMSessionMonitorSynchronized(self); handler = _completionHandler; if (handler) { [self invokeOnCallbackQueueUnlessStopped:^{ handler(data, error); // Post a notification, primarily to allow code to collect responses for // testing. // // The observing code is not likely on the fetcher's callback // queue, so this posts explicitly to the main queue. NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; if (data) { userInfo[kGTMSessionFetcherCompletionDataKey] = data; } if (error) { userInfo[kGTMSessionFetcherCompletionErrorKey] = error; } [self postNotificationOnMainThreadWithName:kGTMSessionFetcherCompletionInvokedNotification userInfo:userInfo requireAsync:NO]; }]; } } // @synchronized(self) } - (void)postNotificationOnMainThreadWithName:(NSString *)noteName userInfo:(GTM_NULLABLE NSDictionary *)userInfo requireAsync:(BOOL)requireAsync { dispatch_block_t postBlock = ^{ [[NSNotificationCenter defaultCenter] postNotificationName:noteName object:self userInfo:userInfo]; }; if ([NSThread isMainThread] && !requireAsync) { // Post synchronously for compatibility with older code using the fetcher. // Avoid calling out to other code from inside a sync block to avoid risk // of a deadlock or of recursive sync. GTMSessionCheckNotSynchronized(self); postBlock(); } else { dispatch_async(dispatch_get_main_queue(), postBlock); } } - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)uploadTask needNewBodyStream:(void (^)(NSInputStream * GTM_NULLABLE_TYPE bodyStream))completionHandler { [self setSessionTask:uploadTask]; GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ task:%@ needNewBodyStream:", [self class], self, session, uploadTask); @synchronized(self) { GTMSessionMonitorSynchronized(self); GTMSessionFetcherBodyStreamProvider provider = _bodyStreamProvider; #if !STRIP_GTM_FETCH_LOGGING if ([self respondsToSelector:@selector(loggedStreamProviderForStreamProvider:)]) { provider = [self performSelector:@selector(loggedStreamProviderForStreamProvider:) withObject:provider]; } #endif if (provider) { [self invokeOnCallbackQueueUnlessStopped:^{ provider(completionHandler); }]; } else { GTMSESSION_ASSERT_DEBUG(NO, @"NSURLSession expects a stream provider"); completionHandler(nil); } } // @synchronized(self) } - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend { [self setSessionTask:task]; GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ task:%@ didSendBodyData:%lld" @" totalBytesSent:%lld totalBytesExpectedToSend:%lld", [self class], self, session, task, bytesSent, totalBytesSent, totalBytesExpectedToSend); @synchronized(self) { GTMSessionMonitorSynchronized(self); if (!_sendProgressBlock) { return; } // We won't hold on to send progress block; it's ok to not send it if the upload finishes. [self invokeOnCallbackQueueUnlessStopped:^{ GTMSessionFetcherSendProgressBlock progressBlock; @synchronized(self) { GTMSessionMonitorSynchronized(self); progressBlock = self->_sendProgressBlock; } if (progressBlock) { progressBlock(bytesSent, totalBytesSent, totalBytesExpectedToSend); } }]; } // @synchronized(self) } - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { [self setSessionTask:dataTask]; NSUInteger bufferLength = data.length; GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ dataTask:%@ didReceiveData:%p (%llu bytes)", [self class], self, session, dataTask, data, (unsigned long long)bufferLength); if (bufferLength == 0) { // Observed on completing an out-of-process upload. return; } @synchronized(self) { GTMSessionMonitorSynchronized(self); GTMSessionFetcherAccumulateDataBlock accumulateBlock = _accumulateDataBlock; if (accumulateBlock) { // Let the client accumulate the data. _downloadedLength += bufferLength; [self invokeOnCallbackQueueUnlessStopped:^{ accumulateBlock(data); }]; } else if (!_userStoppedFetching) { // Append to the mutable data buffer unless the fetch has been cancelled. // Resumed upload tasks may not yet have a data buffer. if (_downloadedData == nil) { // Using NSClassFromString for iOS 6 compatibility. GTMSESSION_ASSERT_DEBUG( ![dataTask isKindOfClass:NSClassFromString(@"NSURLSessionDownloadTask")], @"Resumed download tasks should not receive data bytes"); _downloadedData = [[NSMutableData alloc] init]; } [_downloadedData appendData:data]; _downloadedLength = (int64_t)_downloadedData.length; // We won't hold on to receivedProgressBlock here; it's ok to not send // it if the transfer finishes. if (_receivedProgressBlock) { [self invokeOnCallbackQueueUnlessStopped:^{ GTMSessionFetcherReceivedProgressBlock progressBlock; @synchronized(self) { GTMSessionMonitorSynchronized(self); progressBlock = self->_receivedProgressBlock; } if (progressBlock) { progressBlock((int64_t)bufferLength, self->_downloadedLength); } }]; } } } // @synchronized(self) } - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler { GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ dataTask:%@ willCacheResponse:%@ %@", [self class], self, session, dataTask, proposedResponse, proposedResponse.response); GTMSessionFetcherWillCacheURLResponseBlock callback; @synchronized(self) { GTMSessionMonitorSynchronized(self); callback = _willCacheURLResponseBlock; if (callback) { [self invokeOnCallbackQueueAfterUserStopped:YES block:^{ callback(proposedResponse, completionHandler); }]; } } // @synchronized(self) if (!callback) { completionHandler(proposedResponse); } } - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ downloadTask:%@ didWriteData:%lld" @" bytesWritten:%lld totalBytesExpectedToWrite:%lld", [self class], self, session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite); [self setSessionTask:downloadTask]; @synchronized(self) { GTMSessionMonitorSynchronized(self); if ((totalBytesExpectedToWrite != NSURLSessionTransferSizeUnknown) && (totalBytesExpectedToWrite < totalBytesWritten)) { // Have observed cases were bytesWritten == totalBytesExpectedToWrite, // but totalBytesWritten > totalBytesExpectedToWrite, so setting to unkown in these cases. totalBytesExpectedToWrite = NSURLSessionTransferSizeUnknown; } // We won't hold on to download progress block during the enqueue; // it's ok to not send it if the upload finishes. [self invokeOnCallbackQueueUnlessStopped:^{ GTMSessionFetcherDownloadProgressBlock progressBlock; @synchronized(self) { GTMSessionMonitorSynchronized(self); progressBlock = self->_downloadProgressBlock; } if (progressBlock) { progressBlock(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite); } }]; } // @synchronized(self) } - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes { GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ downloadTask:%@ didResumeAtOffset:%lld" @" expectedTotalBytes:%lld", [self class], self, session, downloadTask, fileOffset, expectedTotalBytes); [self setSessionTask:downloadTask]; } - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)downloadLocationURL { // Download may have relaunched app, so update _sessionTask. [self setSessionTask:downloadTask]; GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ downloadTask:%@ didFinishDownloadingToURL:%@", [self class], self, session, downloadTask, downloadLocationURL); NSNumber *fileSizeNum; [downloadLocationURL getResourceValue:&fileSizeNum forKey:NSURLFileSizeKey error:NULL]; @synchronized(self) { GTMSessionMonitorSynchronized(self); NSURL *destinationURL = _destinationFileURL; _downloadedLength = fileSizeNum.longLongValue; // Overwrite any previous file at the destination URL. NSFileManager *fileMgr = [NSFileManager defaultManager]; NSError *removeError; if (![fileMgr removeItemAtURL:destinationURL error:&removeError] && removeError.code != NSFileNoSuchFileError) { GTMSESSION_LOG_DEBUG(@"Could not remove previous file at %@ due to %@", downloadLocationURL.path, removeError); } NSInteger statusCode = [self statusCodeUnsynchronized]; if (statusCode < 200 || statusCode > 399) { // In OS X 10.11, the response body is written to a file even on a server // status error. For convenience of the fetcher client, we'll skip saving the // downloaded body to the destination URL so that clients do not need to know // to delete the file following fetch errors. A downside of this is that // the server may have included error details in the response body, and // abandoning the downloaded file here means that the details from the // body are not available to the fetcher client. GTMSESSION_LOG_DEBUG(@"Abandoning download due to status %ld, file %@", (long)statusCode, downloadLocationURL.path); } else { NSError *moveError; NSURL *destinationFolderURL = [destinationURL URLByDeletingLastPathComponent]; BOOL didMoveDownload = NO; if ([fileMgr createDirectoryAtURL:destinationFolderURL withIntermediateDirectories:YES attributes:nil error:&moveError]) { didMoveDownload = [fileMgr moveItemAtURL:downloadLocationURL toURL:destinationURL error:&moveError]; } if (!didMoveDownload) { _downloadFinishedError = moveError; } GTM_LOG_BACKGROUND_SESSION(@"%@ %p Moved download from \"%@\" to \"%@\" %@", [self class], self, downloadLocationURL.path, destinationURL.path, error ? error : @""); } } // @synchronized(self) } /* Sent as the last message related to a specific task. Error may be * nil, which implies that no error occurred and this task is complete. */ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { [self setSessionTask:task]; GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ task:%@ didCompleteWithError:%@", [self class], self, session, task, error); NSInteger status = self.statusCode; BOOL forceAssumeRetry = NO; BOOL succeeded = NO; @synchronized(self) { GTMSessionMonitorSynchronized(self); #if !GTM_DISABLE_FETCHER_TEST_BLOCK // The task is never resumed when a testBlock is used. When the session is destroyed, // we should ignore the callback, since the testBlock support code itself invokes // shouldRetryNowForStatus: and finishWithError:shouldRetry: if (_isUsingTestBlock) return; #endif if (error == nil) { error = _downloadFinishedError; } succeeded = (error == nil && status >= 0 && status < 300); if (succeeded) { // Succeeded. _bodyLength = task.countOfBytesSent; } } // @synchronized(self) if (succeeded) { [self finishWithError:nil shouldRetry:NO]; return; } // For background redirects, no delegate method is called, so we cannot restore a stripped // Authorization header, so if a 403 ("Forbidden") was generated due to a missing OAuth 2 header, // set the current request's URL to the redirected URL, so we in effect restore the Authorization // header. if ((status == 403) && self.usingBackgroundSession) { NSURL *redirectURL = self.response.URL; NSURLRequest *request = self.request; if (![request.URL isEqual:redirectURL]) { NSString *authorizationHeader = [request.allHTTPHeaderFields objectForKey:@"Authorization"]; if (authorizationHeader != nil) { NSMutableURLRequest *mutableRequest = [request mutableCopy]; mutableRequest.URL = redirectURL; [self updateMutableRequest:mutableRequest]; // Avoid assuming the session is still valid. self.session = nil; forceAssumeRetry = YES; } } } // If invalidating the session was deferred in stopFetchReleasingCallbacks: then do it now. NSURLSession *oldSession = self.sessionNeedingInvalidation; if (oldSession) { [self setSessionNeedingInvalidation:NULL]; [oldSession finishTasksAndInvalidate]; } // Failed. [self shouldRetryNowForStatus:status error:error forceAssumeRetry:forceAssumeRetry response:^(BOOL shouldRetry) { [self finishWithError:error shouldRetry:shouldRetry]; }]; } #if TARGET_OS_IPHONE - (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session { GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSessionDidFinishEventsForBackgroundURLSession:%@", [self class], self, session); [self removePersistedBackgroundSessionFromDefaults]; GTMSessionFetcherSystemCompletionHandler handler; @synchronized(self) { GTMSessionMonitorSynchronized(self); handler = self.systemCompletionHandler; self.systemCompletionHandler = nil; } // @synchronized(self) if (handler) { GTM_LOG_BACKGROUND_SESSION(@"%@ %p Calling system completionHandler", [self class], self); handler(); @synchronized(self) { GTMSessionMonitorSynchronized(self); NSURLSession *oldSession = _session; _session = nil; if (_shouldInvalidateSession) { [oldSession finishTasksAndInvalidate]; } } // @synchronized(self) } } #endif - (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(GTM_NULLABLE NSError *)error { // This may happen repeatedly for retries. On authentication callbacks, the retry // may begin before the prior session sends the didBecomeInvalid delegate message. GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ didBecomeInvalidWithError:%@", [self class], self, session, error); if (session == (NSURLSession *)self.session) { GTM_LOG_SESSION_DELEGATE(@" Unexpected retained invalid session: %@", session); self.session = nil; } } - (void)finishWithError:(GTM_NULLABLE NSError *)error shouldRetry:(BOOL)shouldRetry { [self removePersistedBackgroundSessionFromDefaults]; BOOL shouldStopFetching = YES; NSData *downloadedData = nil; #if !STRIP_GTM_FETCH_LOGGING BOOL shouldDeferLogging = NO; #endif BOOL shouldBeginRetryTimer = NO; NSInteger status = [self statusCode]; NSURL *destinationURL = self.destinationFileURL; BOOL fetchSucceeded = (error == nil && status >= 0 && status < 300); #if !STRIP_GTM_FETCH_LOGGING if (!fetchSucceeded) { if (!shouldDeferLogging && !self.hasLoggedError) { [self logNowWithError:error]; self.hasLoggedError = YES; } } #endif // !STRIP_GTM_FETCH_LOGGING @synchronized(self) { GTMSessionMonitorSynchronized(self); #if !STRIP_GTM_FETCH_LOGGING shouldDeferLogging = _deferResponseBodyLogging; #endif if (fetchSucceeded) { // Success if ((_downloadedData.length > 0) && (destinationURL != nil)) { // Overwrite any previous file at the destination URL. NSFileManager *fileMgr = [NSFileManager defaultManager]; [fileMgr removeItemAtURL:destinationURL error:NULL]; NSURL *destinationFolderURL = [destinationURL URLByDeletingLastPathComponent]; BOOL didMoveDownload = NO; if ([fileMgr createDirectoryAtURL:destinationFolderURL withIntermediateDirectories:YES attributes:nil error:&error]) { didMoveDownload = [_downloadedData writeToURL:destinationURL options:NSDataWritingAtomic error:&error]; } if (didMoveDownload) { _downloadedData = nil; } else { _downloadFinishedError = error; } } downloadedData = _downloadedData; } else { // Unsuccessful with error or status over 300. Retry or notify the delegate of failure if (shouldRetry) { // Retrying. shouldBeginRetryTimer = YES; shouldStopFetching = NO; } else { if (error == nil) { // Create an error. NSDictionary *userInfo = nil; if (_downloadedData.length > 0) { NSMutableData *data = _downloadedData; userInfo = @{ kGTMSessionFetcherStatusDataKey : data }; } error = [NSError errorWithDomain:kGTMSessionFetcherStatusDomain code:status userInfo:userInfo]; } else { // If the error had resume data, and the client supplied a resume block, pass the // data to the client. void (^resumeBlock)(NSData *) = _resumeDataBlock; _resumeDataBlock = nil; if (resumeBlock) { NSData *resumeData = [error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData]; if (resumeData) { [self invokeOnCallbackQueueAfterUserStopped:YES block:^{ resumeBlock(resumeData); }]; } } } if (_downloadedData.length > 0) { downloadedData = _downloadedData; } // If the error occurred after retries, report the number and duration of the // retries. This provides a clue to a developer looking at the error description // that the fetcher did retry before failing with this error. if (_retryCount > 0) { NSMutableDictionary *userInfoWithRetries = [NSMutableDictionary dictionaryWithDictionary:(NSDictionary *)error.userInfo]; NSTimeInterval timeSinceInitialRequest = -[_initialRequestDate timeIntervalSinceNow]; [userInfoWithRetries setObject:@(timeSinceInitialRequest) forKey:kGTMSessionFetcherElapsedIntervalWithRetriesKey]; [userInfoWithRetries setObject:@(_retryCount) forKey:kGTMSessionFetcherNumberOfRetriesDoneKey]; error = [NSError errorWithDomain:(NSString *)error.domain code:error.code userInfo:userInfoWithRetries]; } } } } // @synchronized(self) if (shouldBeginRetryTimer) { [self beginRetryTimer]; } // We want to send the stop notification before calling the delegate's // callback selector, since the callback selector may release all of // the fetcher properties that the client is using to track the fetches. // // We'll also stop now so that, to any observers watching the notifications, // it doesn't look like our wait for a retry (which may be long, // 30 seconds or more) is part of the network activity. [self sendStopNotificationIfNeeded]; if (shouldStopFetching) { [self invokeFetchCallbacksOnCallbackQueueWithData:downloadedData error:error]; // The upload subclass doesn't want to release callbacks until upload chunks have completed. BOOL shouldRelease = [self shouldReleaseCallbacksUponCompletion]; [self stopFetchReleasingCallbacks:shouldRelease]; } #if !STRIP_GTM_FETCH_LOGGING // _hasLoggedError is only set by this method if (!shouldDeferLogging && !_hasLoggedError) { [self logNowWithError:error]; } #endif } - (BOOL)shouldReleaseCallbacksUponCompletion { // A subclass can override this to keep callbacks around after the // connection has finished successfully return YES; } - (void)logNowWithError:(GTM_NULLABLE NSError *)error { GTMSessionCheckNotSynchronized(self); // If the logging category is available, then log the current request, // response, data, and error if ([self respondsToSelector:@selector(logFetchWithError:)]) { [self performSelector:@selector(logFetchWithError:) withObject:error]; } } #pragma mark Retries - (BOOL)isRetryError:(NSError *)error { struct RetryRecord { __unsafe_unretained NSString *const domain; NSInteger code; }; struct RetryRecord retries[] = { { kGTMSessionFetcherStatusDomain, 408 }, // request timeout { kGTMSessionFetcherStatusDomain, 502 }, // failure gatewaying to another server { kGTMSessionFetcherStatusDomain, 503 }, // service unavailable { kGTMSessionFetcherStatusDomain, 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 NSString *domain = error.domain; NSInteger code = error.code; for (int idx = 0; retries[idx].domain != nil; idx++) { if (code == retries[idx].code && [domain isEqual:retries[idx].domain]) { return YES; } } return NO; } // shouldRetryNowForStatus:error: responds with 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 retry block is present and returns YES when called, or the // authorizer may be able to fix. - (void)shouldRetryNowForStatus:(NSInteger)status error:(NSError *)error forceAssumeRetry:(BOOL)forceAssumeRetry response:(GTMSessionFetcherRetryResponse)response { // Determine if a refreshed authorizer may avoid an authorization error BOOL willRetry = NO; // We assume _authorizer is immutable after beginFetch, and _hasAttemptedAuthRefresh is modified // only in this method, and this method is invoked on the serial delegate queue. // // We want to avoid calling the authorizer from inside a sync block. BOOL isFirstAuthError = (_authorizer != nil && !_hasAttemptedAuthRefresh && status == GTMSessionFetcherStatusUnauthorized); // 401 BOOL hasPrimed = NO; if (isFirstAuthError) { if ([_authorizer respondsToSelector:@selector(primeForRefresh)]) { hasPrimed = [_authorizer primeForRefresh]; } } BOOL shouldRetryForAuthRefresh = NO; if (hasPrimed) { shouldRetryForAuthRefresh = YES; _hasAttemptedAuthRefresh = YES; [self updateRequestValue:nil forHTTPHeaderField:@"Authorization"]; } @synchronized(self) { GTMSessionMonitorSynchronized(self); BOOL shouldDoRetry = [self isRetryEnabledUnsynchronized]; if (shouldDoRetry && ![self hasRetryAfterInterval]) { // Determine if we're doing exponential backoff retries shouldDoRetry = [self nextRetryIntervalUnsynchronized] < _maxRetryInterval; if (shouldDoRetry) { // If an explicit max retry interval was set, we expect repeated backoffs to take // up to roughly twice that for repeated fast failures. If the initial attempt is // already more than 3 times the max retry interval, then failures have taken a long time // (such as from network timeouts) so don't retry again to avoid the app becoming // unexpectedly unresponsive. if (_maxRetryInterval > 0) { NSTimeInterval maxAllowedIntervalBeforeRetry = _maxRetryInterval * 3; NSTimeInterval timeSinceInitialRequest = -[_initialRequestDate timeIntervalSinceNow]; if (timeSinceInitialRequest > maxAllowedIntervalBeforeRetry) { shouldDoRetry = NO; } } } } BOOL canRetry = shouldRetryForAuthRefresh || forceAssumeRetry || shouldDoRetry; if (canRetry) { NSDictionary *userInfo = nil; if (_downloadedData.length > 0) { NSMutableData *data = _downloadedData; userInfo = @{ kGTMSessionFetcherStatusDataKey : data }; } NSError *statusError = [NSError errorWithDomain:kGTMSessionFetcherStatusDomain code:status userInfo:userInfo]; if (error == nil) { error = statusError; } willRetry = shouldRetryForAuthRefresh || forceAssumeRetry || [self isRetryError:error] || ((error != statusError) && [self isRetryError:statusError]); // If the user has installed a retry callback, consult that. GTMSessionFetcherRetryBlock retryBlock = _retryBlock; if (retryBlock) { [self invokeOnCallbackQueueUnlessStopped:^{ retryBlock(willRetry, error, response); }]; return; } } } // @synchronized(self) response(willRetry); } - (BOOL)hasRetryAfterInterval { GTMSessionCheckSynchronized(self); NSDictionary *responseHeaders = [self responseHeadersUnsynchronized]; NSString *retryAfterValue = [responseHeaders valueForKey:@"Retry-After"]; return (retryAfterValue != nil); } - (NSTimeInterval)retryAfterInterval { GTMSessionCheckSynchronized(self); NSDictionary *responseHeaders = [self responseHeadersUnsynchronized]; NSString *retryAfterValue = [responseHeaders valueForKey:@"Retry-After"]; if (retryAfterValue == nil) { return 0; } // Retry-After formatted as HTTP-date | delta-seconds // Reference: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html NSDateFormatter *rfc1123DateFormatter = [[NSDateFormatter alloc] init]; rfc1123DateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]; rfc1123DateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"]; rfc1123DateFormatter.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss z"; NSDate *retryAfterDate = [rfc1123DateFormatter dateFromString:retryAfterValue]; NSTimeInterval retryAfterInterval = (retryAfterDate != nil) ? retryAfterDate.timeIntervalSinceNow : retryAfterValue.intValue; retryAfterInterval = MAX(0, retryAfterInterval); return retryAfterInterval; } - (void)beginRetryTimer { if (![NSThread isMainThread]) { // Defer creating and starting the timer until we're on the main thread to ensure it has // a run loop. dispatch_group_async(_callbackGroup, dispatch_get_main_queue(), ^{ [self beginRetryTimer]; }); return; } [self destroyRetryTimer]; @synchronized(self) { GTMSessionMonitorSynchronized(self); NSTimeInterval nextInterval = [self nextRetryIntervalUnsynchronized]; NSTimeInterval maxInterval = _maxRetryInterval; NSTimeInterval newInterval = MIN(nextInterval, (maxInterval > 0 ? maxInterval : DBL_MAX)); NSTimeInterval newIntervalTolerance = (newInterval / 10) > 1.0 ?: 1.0; _lastRetryInterval = newInterval; _retryTimer = [NSTimer timerWithTimeInterval:newInterval target:self selector:@selector(retryTimerFired:) userInfo:nil repeats:NO]; _retryTimer.tolerance = newIntervalTolerance; [[NSRunLoop mainRunLoop] addTimer:_retryTimer forMode:NSDefaultRunLoopMode]; } // @synchronized(self) [self postNotificationOnMainThreadWithName:kGTMSessionFetcherRetryDelayStartedNotification userInfo:nil requireAsync:NO]; } - (void)retryTimerFired:(NSTimer *)timer { [self destroyRetryTimer]; @synchronized(self) { GTMSessionMonitorSynchronized(self); _retryCount++; } // @synchronized(self) NSOperationQueue *queue = self.sessionDelegateQueue; [queue addOperationWithBlock:^{ [self retryFetch]; }]; } - (void)destroyRetryTimer { BOOL shouldNotify = NO; @synchronized(self) { GTMSessionMonitorSynchronized(self); if (_retryTimer) { [_retryTimer invalidate]; _retryTimer = nil; shouldNotify = YES; } } if (shouldNotify) { [self postNotificationOnMainThreadWithName:kGTMSessionFetcherRetryDelayStoppedNotification userInfo:nil requireAsync:NO]; } } - (NSUInteger)retryCount { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _retryCount; } // @synchronized(self) } - (NSTimeInterval)nextRetryInterval { @synchronized(self) { GTMSessionMonitorSynchronized(self); NSTimeInterval interval = [self nextRetryIntervalUnsynchronized]; return interval; } // @synchronized(self) } - (NSTimeInterval)nextRetryIntervalUnsynchronized { GTMSessionCheckSynchronized(self); NSInteger statusCode = [self statusCodeUnsynchronized]; if ((statusCode == 503) && [self hasRetryAfterInterval]) { NSTimeInterval secs = [self retryAfterInterval]; return secs; } // The next wait interval is the factor (2.0) times the last interval, // but never less than the minimum interval. NSTimeInterval secs = _lastRetryInterval * _retryFactor; if (_maxRetryInterval > 0) { secs = MIN(secs, _maxRetryInterval); } secs = MAX(secs, _minRetryInterval); return secs; } - (NSTimer *)retryTimer { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _retryTimer; } // @synchronized(self) } - (BOOL)isRetryEnabled { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _isRetryEnabled; } // @synchronized(self) } - (BOOL)isRetryEnabledUnsynchronized { GTMSessionCheckSynchronized(self); return _isRetryEnabled; } - (void)setRetryEnabled:(BOOL)flag { @synchronized(self) { GTMSessionMonitorSynchronized(self); if (flag && !_isRetryEnabled) { // We defer initializing these until the user calls setRetryEnabled // to avoid using the random number generator if it's not needed. // However, this means min and max intervals for this fetcher are reset // as a side effect of calling setRetryEnabled. // // Make an initial retry interval random between 1.0 and 2.0 seconds _minRetryInterval = InitialMinRetryInterval(); _maxRetryInterval = kUnsetMaxRetryInterval; _retryFactor = 2.0; _lastRetryInterval = 0.0; } _isRetryEnabled = flag; } // @synchronized(self) }; - (NSTimeInterval)maxRetryInterval { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _maxRetryInterval; } // @synchronized(self) } - (void)setMaxRetryInterval:(NSTimeInterval)secs { @synchronized(self) { GTMSessionMonitorSynchronized(self); if (secs > 0) { _maxRetryInterval = secs; } else { _maxRetryInterval = kUnsetMaxRetryInterval; } } // @synchronized(self) } - (double)minRetryInterval { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _minRetryInterval; } // @synchronized(self) } - (void)setMinRetryInterval:(NSTimeInterval)secs { @synchronized(self) { GTMSessionMonitorSynchronized(self); 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 = InitialMinRetryInterval(); } } // @synchronized(self) } #pragma mark iOS System Completion Handlers #if TARGET_OS_IPHONE static NSMutableDictionary *gSystemCompletionHandlers = nil; - (GTM_NULLABLE GTMSessionFetcherSystemCompletionHandler)systemCompletionHandler { return [[self class] systemCompletionHandlerForSessionIdentifier:_sessionIdentifier]; } - (void)setSystemCompletionHandler:(GTM_NULLABLE GTMSessionFetcherSystemCompletionHandler)systemCompletionHandler { [[self class] setSystemCompletionHandler:systemCompletionHandler forSessionIdentifier:_sessionIdentifier]; } + (void)setSystemCompletionHandler:(GTM_NULLABLE GTMSessionFetcherSystemCompletionHandler)systemCompletionHandler forSessionIdentifier:(NSString *)sessionIdentifier { if (!sessionIdentifier) { NSLog(@"%s with nil identifier", __PRETTY_FUNCTION__); return; } @synchronized([GTMSessionFetcher class]) { if (gSystemCompletionHandlers == nil && systemCompletionHandler != nil) { gSystemCompletionHandlers = [[NSMutableDictionary alloc] init]; } // Use setValue: to remove the object if completionHandler is nil. [gSystemCompletionHandlers setValue:systemCompletionHandler forKey:sessionIdentifier]; } } + (GTM_NULLABLE GTMSessionFetcherSystemCompletionHandler)systemCompletionHandlerForSessionIdentifier:(NSString *)sessionIdentifier { if (!sessionIdentifier) { return nil; } @synchronized([GTMSessionFetcher class]) { return [gSystemCompletionHandlers objectForKey:sessionIdentifier]; } } #endif // TARGET_OS_IPHONE #pragma mark Getters and Setters @synthesize downloadResumeData = _downloadResumeData, configuration = _configuration, configurationBlock = _configurationBlock, sessionTask = _sessionTask, wasCreatedFromBackgroundSession = _wasCreatedFromBackgroundSession, sessionUserInfo = _sessionUserInfo, taskDescription = _taskDescription, taskPriority = _taskPriority, usingBackgroundSession = _usingBackgroundSession, canShareSession = _canShareSession, completionHandler = _completionHandler, credential = _credential, proxyCredential = _proxyCredential, bodyData = _bodyData, bodyLength = _bodyLength, service = _service, serviceHost = _serviceHost, accumulateDataBlock = _accumulateDataBlock, receivedProgressBlock = _receivedProgressBlock, downloadProgressBlock = _downloadProgressBlock, resumeDataBlock = _resumeDataBlock, didReceiveResponseBlock = _didReceiveResponseBlock, challengeBlock = _challengeBlock, willRedirectBlock = _willRedirectBlock, sendProgressBlock = _sendProgressBlock, willCacheURLResponseBlock = _willCacheURLResponseBlock, retryBlock = _retryBlock, retryFactor = _retryFactor, allowedInsecureSchemes = _allowedInsecureSchemes, allowLocalhostRequest = _allowLocalhostRequest, allowInvalidServerCertificates = _allowInvalidServerCertificates, cookieStorage = _cookieStorage, callbackQueue = _callbackQueue, initialBeginFetchDate = _initialBeginFetchDate, testBlock = _testBlock, testBlockAccumulateDataChunkCount = _testBlockAccumulateDataChunkCount, comment = _comment, log = _log; #if !STRIP_GTM_FETCH_LOGGING @synthesize redirectedFromURL = _redirectedFromURL, logRequestBody = _logRequestBody, logResponseBody = _logResponseBody, hasLoggedError = _hasLoggedError; #endif #if GTM_BACKGROUND_TASK_FETCHING @synthesize backgroundTaskIdentifier = _backgroundTaskIdentifier, skipBackgroundTask = _skipBackgroundTask; #endif - (GTM_NULLABLE NSURLRequest *)request { @synchronized(self) { GTMSessionMonitorSynchronized(self); return [_request copy]; } // @synchronized(self) } - (void)setRequest:(GTM_NULLABLE NSURLRequest *)request { @synchronized(self) { GTMSessionMonitorSynchronized(self); if (![self isFetchingUnsynchronized]) { _request = [request mutableCopy]; } else { GTMSESSION_ASSERT_DEBUG(0, @"request may not be set after beginFetch has been invoked"); } } // @synchronized(self) } - (GTM_NULLABLE NSMutableURLRequest *)mutableRequestForTesting { // Allow tests only to modify the request, useful during retries. return _request; } - (GTM_NULLABLE NSMutableURLRequest *)mutableRequest { @synchronized(self) { GTMSessionMonitorSynchronized(self); GTMSESSION_LOG_DEBUG(@"[GTMSessionFetcher mutableRequest] is deprecated; use -request or" @" -setRequestValue:forHTTPHeaderField:"); return _request; } // @synchronized(self) } - (void)setMutableRequest:(GTM_NULLABLE NSMutableURLRequest *)request { GTMSESSION_LOG_DEBUG(@"[GTMSessionFetcher setMutableRequest:] is deprecated; use -request or" @" -setRequestValue:forHTTPHeaderField:"); GTMSESSION_ASSERT_DEBUG(![self isFetching], @"mutableRequest should not change after beginFetch has been invoked"); [self updateMutableRequest:request]; } // Internal method for updating the request property such as on redirects. - (void)updateMutableRequest:(GTM_NULLABLE NSMutableURLRequest *)request { @synchronized(self) { GTMSessionMonitorSynchronized(self); _request = request; } // @synchronized(self) } // Set a header field value on the request. Header field value changes will not // affect a fetch after the fetch has begun. - (void)setRequestValue:(GTM_NULLABLE NSString *)value forHTTPHeaderField:(NSString *)field { if (![self isFetching]) { [self updateRequestValue:value forHTTPHeaderField:field]; } else { GTMSESSION_ASSERT_DEBUG(0, @"request may not be set after beginFetch has been invoked"); } } // Internal method for updating request headers. - (void)updateRequestValue:(GTM_NULLABLE NSString *)value forHTTPHeaderField:(NSString *)field { @synchronized(self) { GTMSessionMonitorSynchronized(self); [_request setValue:value forHTTPHeaderField:field]; } // @synchronized(self) } - (void)setResponse:(GTM_NULLABLE NSURLResponse *)response { @synchronized(self) { GTMSessionMonitorSynchronized(self); _response = response; } // @synchronized(self) } - (int64_t)bodyLength { @synchronized(self) { GTMSessionMonitorSynchronized(self); if (_bodyLength == NSURLSessionTransferSizeUnknown) { if (_bodyData) { _bodyLength = (int64_t)_bodyData.length; } else if (_bodyFileURL) { NSNumber *fileSizeNum = nil; NSError *fileSizeError = nil; if ([_bodyFileURL getResourceValue:&fileSizeNum forKey:NSURLFileSizeKey error:&fileSizeError]) { _bodyLength = [fileSizeNum longLongValue]; } } } return _bodyLength; } // @synchronized(self) } - (BOOL)useUploadTask { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _useUploadTask; } // @synchronized(self) } - (void)setUseUploadTask:(BOOL)flag { @synchronized(self) { GTMSessionMonitorSynchronized(self); if (flag != _useUploadTask) { GTMSESSION_ASSERT_DEBUG(![self isFetchingUnsynchronized], @"useUploadTask should not change after beginFetch has been invoked"); _useUploadTask = flag; } } // @synchronized(self) } - (GTM_NULLABLE NSURL *)bodyFileURL { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _bodyFileURL; } // @synchronized(self) } - (void)setBodyFileURL:(GTM_NULLABLE NSURL *)fileURL { @synchronized(self) { GTMSessionMonitorSynchronized(self); // The comparison here is a trivial optimization and forgiveness for any client that // repeatedly sets the property, so it just uses pointer comparison rather than isEqual:. if (fileURL != _bodyFileURL) { GTMSESSION_ASSERT_DEBUG(![self isFetchingUnsynchronized], @"fileURL should not change after beginFetch has been invoked"); _bodyFileURL = fileURL; } } // @synchronized(self) } - (GTM_NULLABLE GTMSessionFetcherBodyStreamProvider)bodyStreamProvider { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _bodyStreamProvider; } // @synchronized(self) } - (void)setBodyStreamProvider:(GTM_NULLABLE GTMSessionFetcherBodyStreamProvider)block { @synchronized(self) { GTMSessionMonitorSynchronized(self); GTMSESSION_ASSERT_DEBUG(![self isFetchingUnsynchronized], @"stream provider should not change after beginFetch has been invoked"); _bodyStreamProvider = [block copy]; } // @synchronized(self) } - (GTM_NULLABLE id)authorizer { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _authorizer; } // @synchronized(self) } - (void)setAuthorizer:(GTM_NULLABLE id)authorizer { @synchronized(self) { GTMSessionMonitorSynchronized(self); if (authorizer != _authorizer) { if ([self isFetchingUnsynchronized]) { GTMSESSION_ASSERT_DEBUG(0, @"authorizer should not change after beginFetch has been invoked"); } else { _authorizer = authorizer; } } } // @synchronized(self) } - (GTM_NULLABLE NSData *)downloadedData { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _downloadedData; } // @synchronized(self) } - (void)setDownloadedData:(GTM_NULLABLE NSData *)data { @synchronized(self) { GTMSessionMonitorSynchronized(self); _downloadedData = [data mutableCopy]; } // @synchronized(self) } - (int64_t)downloadedLength { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _downloadedLength; } // @synchronized(self) } - (void)setDownloadedLength:(int64_t)length { @synchronized(self) { GTMSessionMonitorSynchronized(self); _downloadedLength = length; } // @synchronized(self) } - (dispatch_queue_t GTM_NONNULL_TYPE)callbackQueue { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _callbackQueue; } // @synchronized(self) } - (void)setCallbackQueue:(dispatch_queue_t GTM_NULLABLE_TYPE)queue { @synchronized(self) { GTMSessionMonitorSynchronized(self); _callbackQueue = queue ?: dispatch_get_main_queue(); } // @synchronized(self) } - (GTM_NULLABLE NSURLSession *)session { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _session; } // @synchronized(self) } - (NSInteger)servicePriority { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _servicePriority; } // @synchronized(self) } - (void)setServicePriority:(NSInteger)value { @synchronized(self) { GTMSessionMonitorSynchronized(self); if (value != _servicePriority) { GTMSESSION_ASSERT_DEBUG(![self isFetchingUnsynchronized], @"servicePriority should not change after beginFetch has been invoked"); _servicePriority = value; } } // @synchronized(self) } - (void)setSession:(GTM_NULLABLE NSURLSession *)session { @synchronized(self) { GTMSessionMonitorSynchronized(self); _session = session; } // @synchronized(self) } - (BOOL)canShareSession { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _canShareSession; } // @synchronized(self) } - (void)setCanShareSession:(BOOL)flag { @synchronized(self) { GTMSessionMonitorSynchronized(self); _canShareSession = flag; } // @synchronized(self) } - (BOOL)useBackgroundSession { // This reflects if the user requested a background session, not necessarily // if one was created. That is tracked with _usingBackgroundSession. @synchronized(self) { GTMSessionMonitorSynchronized(self); return _userRequestedBackgroundSession; } // @synchronized(self) } - (void)setUseBackgroundSession:(BOOL)flag { @synchronized(self) { GTMSessionMonitorSynchronized(self); if (flag != _userRequestedBackgroundSession) { GTMSESSION_ASSERT_DEBUG(![self isFetchingUnsynchronized], @"useBackgroundSession should not change after beginFetch has been invoked"); _userRequestedBackgroundSession = flag; } } // @synchronized(self) } - (BOOL)isUsingBackgroundSession { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _usingBackgroundSession; } // @synchronized(self) } - (void)setUsingBackgroundSession:(BOOL)flag { @synchronized(self) { GTMSessionMonitorSynchronized(self); _usingBackgroundSession = flag; } // @synchronized(self) } - (GTM_NULLABLE NSURLSession *)sessionNeedingInvalidation { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _sessionNeedingInvalidation; } // @synchronized(self) } - (void)setSessionNeedingInvalidation:(GTM_NULLABLE NSURLSession *)session { @synchronized(self) { GTMSessionMonitorSynchronized(self); _sessionNeedingInvalidation = session; } // @synchronized(self) } - (NSOperationQueue * GTM_NONNULL_TYPE)sessionDelegateQueue { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _delegateQueue; } // @synchronized(self) } - (void)setSessionDelegateQueue:(NSOperationQueue * GTM_NULLABLE_TYPE)queue { @synchronized(self) { GTMSessionMonitorSynchronized(self); if (queue != _delegateQueue) { if ([self isFetchingUnsynchronized]) { GTMSESSION_ASSERT_DEBUG(0, @"sessionDelegateQueue should not change after fetch begins"); } else { _delegateQueue = queue ?: [NSOperationQueue mainQueue]; } } } // @synchronized(self) } - (BOOL)userStoppedFetching { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _userStoppedFetching; } // @synchronized(self) } - (GTM_NULLABLE id)userData { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _userData; } // @synchronized(self) } - (void)setUserData:(GTM_NULLABLE id)theObj { @synchronized(self) { GTMSessionMonitorSynchronized(self); _userData = theObj; } // @synchronized(self) } - (GTM_NULLABLE NSURL *)destinationFileURL { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _destinationFileURL; } // @synchronized(self) } - (void)setDestinationFileURL:(GTM_NULLABLE NSURL *)destinationFileURL { @synchronized(self) { GTMSessionMonitorSynchronized(self); if (((_destinationFileURL == nil) && (destinationFileURL == nil)) || [_destinationFileURL isEqual:destinationFileURL]) { return; } if (_sessionIdentifier) { // This is something we don't expect to happen in production. // However if it ever happen, leave a system log. NSLog(@"%@: Destination File URL changed from (%@) to (%@) after session identifier has " @"been created.", [self class], _destinationFileURL, destinationFileURL); #if DEBUG // On both the simulator and devices, the path can change to the download file, but the name // shouldn't change. Technically, this isn't supported in the fetcher, but the change of // URL is expected to happen only across development runs through Xcode. NSString *oldFilename = [_destinationFileURL lastPathComponent]; NSString *newFilename = [destinationFileURL lastPathComponent]; #pragma unused(oldFilename) #pragma unused(newFilename) GTMSESSION_ASSERT_DEBUG([oldFilename isEqualToString:newFilename], @"Destination File URL cannot be changed after session identifier has been created"); #endif } _destinationFileURL = destinationFileURL; } // @synchronized(self) } - (void)setProperties:(GTM_NULLABLE NSDictionary *)dict { @synchronized(self) { GTMSessionMonitorSynchronized(self); _properties = [dict mutableCopy]; } // @synchronized(self) } - (GTM_NULLABLE NSDictionary *)properties { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _properties; } // @synchronized(self) } - (void)setProperty:(GTM_NULLABLE id)obj forKey:(NSString *)key { @synchronized(self) { GTMSessionMonitorSynchronized(self); if (_properties == nil && obj != nil) { _properties = [[NSMutableDictionary alloc] init]; } [_properties setValue:obj forKey:key]; } // @synchronized(self) } - (GTM_NULLABLE id)propertyForKey:(NSString *)key { @synchronized(self) { GTMSessionMonitorSynchronized(self); return [_properties objectForKey:key]; } // @synchronized(self) } - (void)addPropertiesFromDictionary:(NSDictionary *)dict { @synchronized(self) { GTMSessionMonitorSynchronized(self); if (_properties == nil && dict != nil) { [self setProperties:[dict mutableCopy]]; } else { [_properties addEntriesFromDictionary:dict]; } } // @synchronized(self) } - (void)setCommentWithFormat:(id)format, ... { #if !STRIP_GTM_FETCH_LOGGING NSString *result = format; if (format) { va_list argList; va_start(argList, format); result = [[NSString alloc] initWithFormat:format arguments:argList]; va_end(argList); } [self setComment:result]; #endif } #if !STRIP_GTM_FETCH_LOGGING - (NSData *)loggedStreamData { return _loggedStreamData; } - (void)appendLoggedStreamData:dataToAdd { if (!_loggedStreamData) { _loggedStreamData = [NSMutableData data]; } [_loggedStreamData appendData:dataToAdd]; } - (void)clearLoggedStreamData { _loggedStreamData = nil; } - (void)setDeferResponseBodyLogging:(BOOL)deferResponseBodyLogging { @synchronized(self) { GTMSessionMonitorSynchronized(self); if (deferResponseBodyLogging != _deferResponseBodyLogging) { _deferResponseBodyLogging = deferResponseBodyLogging; if (!deferResponseBodyLogging && !self.hasLoggedError) { [_delegateQueue addOperationWithBlock:^{ [self logNowWithError:nil]; }]; } } } // @synchronized(self) } - (BOOL)deferResponseBodyLogging { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _deferResponseBodyLogging; } // @synchronized(self) } #else + (void)setLoggingEnabled:(BOOL)flag { } + (BOOL)isLoggingEnabled { return NO; } #endif // STRIP_GTM_FETCH_LOGGING @end @implementation GTMSessionFetcher (BackwardsCompatibilityOnly) - (void)setCookieStorageMethod:(NSInteger)method { // For backwards compatibility with the old fetcher, we'll support the old constants. // // Clients using the GTMSessionFetcher class should set the cookie storage explicitly // themselves. NSHTTPCookieStorage *storage = nil; switch(method) { case 0: // kGTMHTTPFetcherCookieStorageMethodStatic // nil storage will use [[self class] staticCookieStorage] when the fetch begins. break; case 1: // kGTMHTTPFetcherCookieStorageMethodFetchHistory // Do nothing; use whatever was set by the fetcher service. return; case 2: // kGTMHTTPFetcherCookieStorageMethodSystemDefault storage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; break; case 3: // kGTMHTTPFetcherCookieStorageMethodNone // Create temporary storage for this fetcher only. storage = [[GTMSessionCookieStorage alloc] init]; break; default: GTMSESSION_ASSERT_DEBUG(0, @"Invalid cookie storage method: %d", (int)method); } self.cookieStorage = storage; } @end @implementation GTMSessionCookieStorage { NSMutableArray *_cookies; NSHTTPCookieAcceptPolicy _policy; } - (id)init { self = [super init]; if (self != nil) { _cookies = [[NSMutableArray alloc] init]; } return self; } - (GTM_NULLABLE NSArray *)cookies { @synchronized(self) { GTMSessionMonitorSynchronized(self); return [_cookies copy]; } // @synchronized(self) } - (void)setCookie:(NSHTTPCookie *)cookie { if (!cookie) return; if (_policy == NSHTTPCookieAcceptPolicyNever) return; @synchronized(self) { GTMSessionMonitorSynchronized(self); [self internalSetCookie:cookie]; } // @synchronized(self) } // Note: this should only be called from inside a @synchronized(self) block. - (void)internalSetCookie:(NSHTTPCookie *)newCookie { GTMSessionCheckSynchronized(self); if (_policy == NSHTTPCookieAcceptPolicyNever) return; BOOL isValidCookie = (newCookie.name.length > 0 && newCookie.domain.length > 0 && newCookie.path.length > 0); GTMSESSION_ASSERT_DEBUG(isValidCookie, @"invalid cookie: %@", newCookie); if (isValidCookie) { // Remove the cookie if it's currently in the array. NSHTTPCookie *oldCookie = [self cookieMatchingCookie:newCookie]; if (oldCookie) { [_cookies removeObjectIdenticalTo:oldCookie]; } if (![[self class] hasCookieExpired:newCookie]) { [_cookies addObject:newCookie]; } } } // Add all cookies in the new cookie array to the storage, // replacing stored cookies as appropriate. // // Side effect: removes expired cookies from the storage array. - (void)setCookies:(GTM_NULLABLE NSArray *)newCookies { @synchronized(self) { GTMSessionMonitorSynchronized(self); [self removeExpiredCookies]; for (NSHTTPCookie *newCookie in newCookies) { [self internalSetCookie:newCookie]; } } // @synchronized(self) } - (void)setCookies:(NSArray *)cookies forURL:(GTM_NULLABLE NSURL *)URL mainDocumentURL:(GTM_NULLABLE NSURL *)mainDocumentURL { @synchronized(self) { GTMSessionMonitorSynchronized(self); if (_policy == NSHTTPCookieAcceptPolicyNever) { return; } if (_policy == NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain) { NSString *mainHost = mainDocumentURL.host; NSString *associatedHost = URL.host; if (!mainHost || ![associatedHost hasSuffix:mainHost]) { return; } } } // @synchronized(self) [self setCookies:cookies]; } - (void)deleteCookie:(NSHTTPCookie *)cookie { if (!cookie) return; @synchronized(self) { GTMSessionMonitorSynchronized(self); NSHTTPCookie *foundCookie = [self cookieMatchingCookie:cookie]; if (foundCookie) { [_cookies removeObjectIdenticalTo:foundCookie]; } } // @synchronized(self) } // 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. - (GTM_NULLABLE NSArray *)cookiesForURL:(NSURL *)theURL { NSMutableArray *foundCookies = nil; @synchronized(self) { GTMSessionMonitorSynchronized(self); [self removeExpiredCookies]; // 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.lowercaseString; NSString *path = theURL.path; NSString *scheme = [theURL scheme]; NSString *requestingDomain = nil; BOOL isLocalhostRetrieval = NO; if (IsLocalhost(host)) { isLocalhostRetrieval = YES; } else { if (host.length > 0) { requestingDomain = [@"." stringByAppendingString:host]; } } for (NSHTTPCookie *storedCookie in _cookies) { NSString *cookieDomain = storedCookie.domain.lowercaseString; NSString *cookiePath = storedCookie.path; BOOL cookieIsSecure = [storedCookie isSecure]; BOOL isDomainOK; if (isLocalhostRetrieval) { // Prior to 10.5.6, the domain stored into NSHTTPCookies for localhost // is "localhost.local" isDomainOK = (IsLocalhost(cookieDomain) || [cookieDomain isEqual:@"localhost.local"]); } else { // Ensure we're matching exact domain names. We prepended a dot to the // requesting domain, so we can also prepend one here if needed before // checking if the request contains the cookie domain. if (![cookieDomain hasPrefix:@"."]) { cookieDomain = [@"." stringByAppendingString:cookieDomain]; } isDomainOK = [requestingDomain hasSuffix:cookieDomain]; } BOOL isPathOK = [cookiePath isEqual:@"/"] || [path hasPrefix:cookiePath]; BOOL isSecureOK = (!cookieIsSecure || [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame); if (isDomainOK && isPathOK && isSecureOK) { if (foundCookies == nil) { foundCookies = [NSMutableArray array]; } [foundCookies addObject:storedCookie]; } } } // @synchronized(self) return foundCookies; } // Override methods from the NSHTTPCookieStorage (NSURLSessionTaskAdditions) category. - (void)storeCookies:(NSArray *)cookies forTask:(NSURLSessionTask *)task { NSURLRequest *currentRequest = task.currentRequest; [self setCookies:cookies forURL:currentRequest.URL mainDocumentURL:nil]; } - (void)getCookiesForTask:(NSURLSessionTask *)task completionHandler:(void (^)(GTM_NSArrayOf(NSHTTPCookie *) *))completionHandler { if (completionHandler) { NSURLRequest *currentRequest = task.currentRequest; NSURL *currentRequestURL = currentRequest.URL; NSArray *cookies = [self cookiesForURL:currentRequestURL]; completionHandler(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 the storage array should // be valid (non-nil name, domains, paths). // // Note: this should only be called from inside a @synchronized(self) block - (GTM_NULLABLE NSHTTPCookie *)cookieMatchingCookie:(NSHTTPCookie *)cookie { GTMSessionCheckSynchronized(self); NSString *name = cookie.name; NSString *domain = cookie.domain; NSString *path = cookie.path; GTMSESSION_ASSERT_DEBUG(name && domain && path, @"Invalid stored cookie (name:%@ domain:%@ path:%@)", name, domain, path); for (NSHTTPCookie *storedCookie in _cookies) { if ([storedCookie.name isEqual:name] && [storedCookie.domain isEqual:domain] && [storedCookie.path isEqual:path]) { return storedCookie; } } return nil; } // Internal routine to remove any expired cookies from the array, excluding // cookies with nil expirations. // // Note: this should only be called from inside a @synchronized(self) block - (void)removeExpiredCookies { GTMSessionCheckSynchronized(self); // Count backwards since we're deleting items from the array for (NSInteger idx = (NSInteger)_cookies.count - 1; idx >= 0; idx--) { NSHTTPCookie *storedCookie = [_cookies objectAtIndex:(NSUInteger)idx]; if ([[self class] hasCookieExpired:storedCookie]) { [_cookies removeObjectAtIndex:(NSUInteger)idx]; } } } + (BOOL)hasCookieExpired:(NSHTTPCookie *)cookie { NSDate *expiresDate = [cookie expiresDate]; if (expiresDate == nil) { // Cookies seem to have a Expires property even when the expiresDate method returns nil. id expiresVal = [[cookie properties] objectForKey:NSHTTPCookieExpires]; if ([expiresVal isKindOfClass:[NSDate class]]) { expiresDate = expiresVal; } } BOOL hasExpired = (expiresDate != nil && [expiresDate timeIntervalSinceNow] < 0); return hasExpired; } - (void)removeAllCookies { @synchronized(self) { GTMSessionMonitorSynchronized(self); [_cookies removeAllObjects]; } // @synchronized(self) } - (NSHTTPCookieAcceptPolicy)cookieAcceptPolicy { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _policy; } // @synchronized(self) } - (void)setCookieAcceptPolicy:(NSHTTPCookieAcceptPolicy)cookieAcceptPolicy { @synchronized(self) { GTMSessionMonitorSynchronized(self); _policy = cookieAcceptPolicy; } // @synchronized(self) } @end void GTMSessionFetcherAssertValidSelector(id GTM_NULLABLE_TYPE obj, SEL GTM_NULLABLE_TYPE sel, ...) { // Verify that the object's selector is implemented with the proper // number and type of arguments #if DEBUG va_list argList; va_start(argList, sel); if (obj && sel) { // Check that the selector is implemented if (![obj respondsToSelector:sel]) { NSLog(@"\"%@\" selector \"%@\" is unimplemented or misnamed", NSStringFromClass([(id)obj class]), NSStringFromSelector((SEL)sel)); NSCAssert(0, @"callback selector unimplemented or misnamed"); } else { const char *expectedArgType; unsigned int argCount = 2; // skip self and _cmd NSMethodSignature *sig = [obj methodSignatureForSelector:sel]; // Check that each expected argument is present and of the correct type while ((expectedArgType = va_arg(argList, const char*)) != 0) { if ([sig numberOfArguments] > argCount) { const char *foundArgType = [sig getArgumentTypeAtIndex:argCount]; if (0 != strncmp(foundArgType, expectedArgType, strlen(expectedArgType))) { NSLog(@"\"%@\" selector \"%@\" argument %d should be type %s", NSStringFromClass([(id)obj class]), NSStringFromSelector((SEL)sel), (argCount - 2), expectedArgType); NSCAssert(0, @"callback selector argument type mistake"); } } argCount++; } // Check that the proper number of arguments are present in the selector if (argCount != [sig numberOfArguments]) { NSLog(@"\"%@\" selector \"%@\" should have %d arguments", NSStringFromClass([(id)obj class]), NSStringFromSelector((SEL)sel), (argCount - 2)); NSCAssert(0, @"callback selector arguments incorrect"); } } } va_end(argList); #endif } NSString *GTMFetcherCleanedUserAgentString(NSString *str) { // Reference http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html // and http://www-archive.mozilla.org/build/user-agent-strings.html if (str == nil) return @""; NSMutableString *result = [NSMutableString stringWithString:str]; // Replace spaces and commas with underscores [result replaceOccurrencesOfString:@" " withString:@"_" options:0 range:NSMakeRange(0, result.length)]; [result replaceOccurrencesOfString:@"," withString:@"_" options:0 range:NSMakeRange(0, result.length)]; // Delete http token separators and remaining whitespace static NSCharacterSet *charsToDelete = nil; if (charsToDelete == nil) { // Make a set of unwanted characters NSString *const kSeparators = @"()<>@;:\\\"/[]?={}"; NSMutableCharacterSet *mutableChars = [[NSCharacterSet whitespaceAndNewlineCharacterSet] mutableCopy]; [mutableChars addCharactersInString:kSeparators]; charsToDelete = [mutableChars copy]; // hang on to an immutable copy } while (1) { NSRange separatorRange = [result rangeOfCharacterFromSet:charsToDelete]; if (separatorRange.location == NSNotFound) break; [result deleteCharactersInRange:separatorRange]; }; return result; } NSString *GTMFetcherSystemVersionString(void) { static NSString *sSavedSystemString; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // The Xcode 8 SDKs finally cleaned up this mess by providing TARGET_OS_OSX // and TARGET_OS_IOS, but to build with older SDKs, those don't exist and // instead one has to rely on TARGET_OS_MAC (which is true for iOS, watchOS, // and tvOS) and TARGET_OS_IPHONE (which is true for iOS, watchOS, tvOS). So // one has to order these carefully so you pick off the specific things // first. // If the code can ever assume Xcode 8 or higher (even when building for // older OSes), then // TARGET_OS_MAC -> TARGET_OS_OSX // TARGET_OS_IPHONE -> TARGET_OS_IOS // TARGET_IPHONE_SIMULATOR -> TARGET_OS_SIMULATOR #if TARGET_OS_WATCH // watchOS - WKInterfaceDevice WKInterfaceDevice *currentDevice = [WKInterfaceDevice currentDevice]; NSString *rawModel = [currentDevice model]; NSString *model = GTMFetcherCleanedUserAgentString(rawModel); NSString *systemVersion = [currentDevice systemVersion]; #if TARGET_OS_SIMULATOR NSString *hardwareModel = @"sim"; #else NSString *hardwareModel; struct utsname unameRecord; if (uname(&unameRecord) == 0) { NSString *machineName = @(unameRecord.machine); hardwareModel = GTMFetcherCleanedUserAgentString(machineName); } if (hardwareModel.length == 0) { hardwareModel = @"unk"; } #endif sSavedSystemString = [[NSString alloc] initWithFormat:@"%@/%@ hw/%@", model, systemVersion, hardwareModel]; // Example: Apple_Watch/3.0 hw/Watch1_2 #elif TARGET_OS_TV || TARGET_OS_IPHONE // iOS and tvOS have UIDevice, use that. UIDevice *currentDevice = [UIDevice currentDevice]; NSString *rawModel = [currentDevice model]; NSString *model = GTMFetcherCleanedUserAgentString(rawModel); NSString *systemVersion = [currentDevice systemVersion]; #if TARGET_IPHONE_SIMULATOR || TARGET_OS_SIMULATOR NSString *hardwareModel = @"sim"; #else NSString *hardwareModel; struct utsname unameRecord; if (uname(&unameRecord) == 0) { NSString *machineName = @(unameRecord.machine); hardwareModel = GTMFetcherCleanedUserAgentString(machineName); } if (hardwareModel.length == 0) { hardwareModel = @"unk"; } #endif sSavedSystemString = [[NSString alloc] initWithFormat:@"%@/%@ hw/%@", model, systemVersion, hardwareModel]; // Example: iPod_Touch/2.2 hw/iPod1_1 // Example: Apple_TV/9.2 hw/AppleTV5,3 #elif TARGET_OS_MAC // Mac build NSProcessInfo *procInfo = [NSProcessInfo processInfo]; #if !defined(MAC_OS_X_VERSION_10_10) BOOL hasOperatingSystemVersion = NO; #elif MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_10 BOOL hasOperatingSystemVersion = [procInfo respondsToSelector:@selector(operatingSystemVersion)]; #else BOOL hasOperatingSystemVersion = YES; #endif NSString *versString; if (hasOperatingSystemVersion) { #if defined(MAC_OS_X_VERSION_10_10) // A reference to NSOperatingSystemVersion requires the 10.10 SDK. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunguarded-availability" // Disable unguarded availability warning as we can't use the @availability macro until we require // all clients to build with Xcode 9 or above. NSOperatingSystemVersion version = procInfo.operatingSystemVersion; #pragma clang diagnostic pop versString = [NSString stringWithFormat:@"%zd.%zd.%zd", version.majorVersion, version.minorVersion, version.patchVersion]; #else #pragma unused(procInfo) #endif } else { // With Gestalt inexplicably deprecated in 10.8, we're reduced to reading // the system plist file. NSString *const kPath = @"/System/Library/CoreServices/SystemVersion.plist"; NSDictionary *plist = [NSDictionary dictionaryWithContentsOfFile:kPath]; versString = [plist objectForKey:@"ProductVersion"]; if (versString.length == 0) { versString = @"10.?.?"; } } sSavedSystemString = [[NSString alloc] initWithFormat:@"MacOSX/%@", versString]; #elif defined(_SYS_UTSNAME_H) // Foundation-only build struct utsname unameRecord; uname(&unameRecord); sSavedSystemString = [NSString stringWithFormat:@"%s/%s", unameRecord.sysname, unameRecord.release]; // "Darwin/8.11.1" #else #error No branch taken for a default user agent #endif }); return sSavedSystemString; } NSString *GTMFetcherStandardUserAgentString(NSBundle * GTM_NULLABLE_TYPE bundle) { NSString *result = [NSString stringWithFormat:@"%@ %@", GTMFetcherApplicationIdentifier(bundle), GTMFetcherSystemVersionString()]; return result; } NSString *GTMFetcherApplicationIdentifier(NSBundle * GTM_NULLABLE_TYPE bundle) { @synchronized([GTMSessionFetcher class]) { static NSMutableDictionary *sAppIDMap = nil; // If there's a bundle ID, use that; otherwise, use the process name if (bundle == nil) { bundle = [NSBundle mainBundle]; } NSString *bundleID = [bundle bundleIdentifier]; if (bundleID == nil) { bundleID = @""; } NSString *identifier = [sAppIDMap objectForKey:bundleID]; if (identifier) return identifier; // Apps may add a string to the info.plist to uniquely identify different builds. identifier = [bundle objectForInfoDictionaryKey:@"GTMUserAgentID"]; if (identifier.length == 0) { if (bundleID.length > 0) { identifier = bundleID; } else { // Fall back on the procname, prefixed by "proc" to flag that it's // autogenerated and perhaps unreliable NSString *procName = [[NSProcessInfo processInfo] processName]; identifier = [NSString stringWithFormat:@"proc_%@", procName]; } } // Clean up whitespace and special characters identifier = GTMFetcherCleanedUserAgentString(identifier); // If there's a version number, append that NSString *version = [bundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; if (version.length == 0) { version = [bundle objectForInfoDictionaryKey:@"CFBundleVersion"]; } // Clean up whitespace and special characters version = GTMFetcherCleanedUserAgentString(version); // Glue the two together (cleanup done above or else cleanup would strip the // slash) if (version.length > 0) { identifier = [identifier stringByAppendingFormat:@"/%@", version]; } if (sAppIDMap == nil) { sAppIDMap = [[NSMutableDictionary alloc] init]; } [sAppIDMap setObject:identifier forKey:bundleID]; return identifier; } } #if DEBUG @implementation GTMSessionSyncMonitorInternal { NSValue *_objectKey; // The synchronize target object. const char *_functionName; // The function containing the monitored sync block. } - (instancetype)initWithSynchronizationObject:(id)object allowRecursive:(BOOL)allowRecursive functionName:(const char *)functionName { self = [super init]; if (self) { Class threadKey = [GTMSessionSyncMonitorInternal class]; _objectKey = [NSValue valueWithNonretainedObject:object]; _functionName = functionName; NSMutableDictionary *threadDict = [NSThread currentThread].threadDictionary; NSMutableDictionary *counters = threadDict[threadKey]; if (counters == nil) { counters = [NSMutableDictionary dictionary]; threadDict[(id)threadKey] = counters; } NSCountedSet *functionNamesCounter = counters[_objectKey]; NSUInteger numberOfSyncingFunctions = functionNamesCounter.count; if (!allowRecursive) { BOOL isTopLevelSyncScope = (numberOfSyncingFunctions == 0); NSArray *stack = [NSThread callStackSymbols]; GTMSESSION_ASSERT_DEBUG(isTopLevelSyncScope, @"*** Recursive sync on %@ at %s; previous sync at %@\n%@", [object class], functionName, functionNamesCounter.allObjects, [stack subarrayWithRange:NSMakeRange(1, stack.count - 1)]); } if (!functionNamesCounter) { functionNamesCounter = [NSCountedSet set]; counters[_objectKey] = functionNamesCounter; } [functionNamesCounter addObject:@(functionName)]; } return self; } - (void)dealloc { Class threadKey = [GTMSessionSyncMonitorInternal class]; NSMutableDictionary *threadDict = [NSThread currentThread].threadDictionary; NSMutableDictionary *counters = threadDict[threadKey]; NSCountedSet *functionNamesCounter = counters[_objectKey]; NSString *functionNameStr = @(_functionName); NSUInteger numberOfSyncsByThisFunction = [functionNamesCounter countForObject:functionNameStr]; NSArray *stack = [NSThread callStackSymbols]; GTMSESSION_ASSERT_DEBUG(numberOfSyncsByThisFunction > 0, @"Sync not found on %@ at %s\n%@", [_objectKey.nonretainedObjectValue class], _functionName, [stack subarrayWithRange:NSMakeRange(1, stack.count - 1)]); [functionNamesCounter removeObject:functionNameStr]; if (functionNamesCounter.count == 0) { [counters removeObjectForKey:_objectKey]; } } + (NSArray *)functionsHoldingSynchronizationOnObject:(id)object { Class threadKey = [GTMSessionSyncMonitorInternal class]; NSValue *localObjectKey = [NSValue valueWithNonretainedObject:object]; NSMutableDictionary *threadDict = [NSThread currentThread].threadDictionary; NSMutableDictionary *counters = threadDict[threadKey]; NSCountedSet *functionNamesCounter = counters[localObjectKey]; return functionNamesCounter.count > 0 ? functionNamesCounter.allObjects : nil; } @end #endif // DEBUG GTM_ASSUME_NONNULL_END