/* 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/GTMSessionFetcher.h" #import "GTMSessionFetcher/GTMSessionFetcherService.h" #import "GTMSessionFetcherService+Internal.h" #if TARGET_OS_OSX && GTMSESSION_RECONNECT_BACKGROUND_SESSIONS_ON_LAUNCH // To reconnect background sessions on Mac outside +load requires importing and linking // AppKit to access the NSApplicationDidFinishLaunching symbol. #import <AppKit/AppKit.h> #endif #include <sys/utsname.h> #ifndef STRIP_GTM_FETCH_LOGGING #error GTMSessionFetcher headers should have defaulted this if it wasn't already defined. #endif NS_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 kGTMSessionFetcherStatusDataContentTypeKey = @"data_content_type"; 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"; static NSString *const kGTMSessionIdentifierClientReconnectMetadataKey = @"_clientWillReconnect"; // 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.; // The maximum data length that can be loaded to the error userInfo static const int64_t kMaximumDownloadErrorDataLength = 20000; #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 NS_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 #define GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY 1 #endif #if ((defined(TARGET_OS_MACCATALYST) && TARGET_OS_MACCATALYST) || \ (TARGET_OS_OSX && defined(__MAC_10_15) && __MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_15) || \ (TARGET_OS_IOS && defined(__IPHONE_13_0) && \ __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_13_0) || \ (TARGET_OS_WATCH && defined(__WATCHOS_6_0) && \ __WATCH_OS_VERSION_MIN_REQUIRED >= __WATCHOS_6_0) || \ (TARGET_OS_TV && defined(__TVOS_13_0) && __TVOS_VERSION_MIN_REQUIRED >= __TVOS_13_0)) #define GTM_SDK_REQUIRES_TLSMINIMUMSUPPORTEDPROTOCOLVERSION 1 #define GTM_SDK_SUPPORTS_TLSMINIMUMSUPPORTEDPROTOCOLVERSION 1 #elif ((TARGET_OS_OSX && defined(__MAC_10_15) && __MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_10_15) || \ (TARGET_OS_IOS && defined(__IPHONE_13_0) && \ __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0) || \ (TARGET_OS_WATCH && defined(__WATCHOS_6_0) && \ __WATCH_OS_VERSION_MAX_ALLOWED >= __WATCHOS_6_0) || \ (TARGET_OS_TV && defined(__TVOS_13_0) && __TVOS_VERSION_MAX_ALLOWED >= __TVOS_13_0)) #define GTM_SDK_REQUIRES_TLSMINIMUMSUPPORTEDPROTOCOLVERSION 0 #define GTM_SDK_SUPPORTS_TLSMINIMUMSUPPORTEDPROTOCOLVERSION 1 #else #define GTM_SDK_REQUIRES_TLSMINIMUMSUPPORTEDPROTOCOLVERSION 0 #define GTM_SDK_SUPPORTS_TLSMINIMUMSUPPORTEDPROTOCOLVERSION 0 #endif #if ((defined(TARGET_OS_MACCATALYST) && TARGET_OS_MACCATALYST) || \ (TARGET_OS_OSX && defined(__MAC_10_15) && __MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_15) || \ (TARGET_OS_IOS && defined(__IPHONE_13_0) && \ __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_13_0) || \ (TARGET_OS_WATCH && defined(__WATCHOS_6_0) && \ __WATCH_OS_VERSION_MIN_REQUIRED >= __WATCHOS_6_0) || \ (TARGET_OS_TV && defined(__TVOS_13_0) && __TVOS_VERSION_MIN_REQUIRED >= __TVOS_13_0)) #define GTM_SDK_REQUIRES_SECTRUSTEVALUATEWITHERROR 1 #else #define GTM_SDK_REQUIRES_SECTRUSTEVALUATEWITHERROR 0 #endif #if __has_attribute(swift_async) // Once Clang 13/Xcode 13 can be assumed, can switch to NS_SWIFT_DISABLE_ASYNC. #define GTM_SWIFT_DISABLE_ASYNC __attribute__((swift_async(none))) #else #define GTM_SWIFT_DISABLE_ASYNC #endif @interface GTMSessionFetcher () @property(atomic, strong, readwrite, nullable) NSData *downloadedData; @property(atomic, strong, readwrite, 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:(nullable NSError *)error; - (NSInputStream *)loggedInputStreamForInputStream:(NSInputStream *)inputStream; - (GTMSessionFetcherBodyStreamProvider)loggedStreamProviderForStreamProvider: (GTMSessionFetcherBodyStreamProvider)streamProvider; @end #endif // !GTMSESSION_BUILD_COMBINED_SOURCES NS_ASSUME_NONNULL_BEGIN static NSTimeInterval InitialMinRetryInterval(void) { return 1.0 + ((double)(arc4random_uniform(0x0FFFF)) / (double)0x0FFFF); } static BOOL IsLocalhost(NSString *_Nullable 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 NSDictionary *_Nullable GTMErrorUserInfoForData(NSData *_Nullable data, NSDictionary *_Nullable responseHeaders) { NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; if (data.length > 0) { userInfo[kGTMSessionFetcherStatusDataKey] = data; NSString *contentType = responseHeaders[@"Content-Type"]; if (contentType) { userInfo[kGTMSessionFetcherStatusDataContentTypeKey] = contentType; } } return userInfo.count > 0 ? userInfo : nil; } static GTMSessionFetcherTestBlock _Nullable 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 _clientWillReconnectBackgroundSession; BOOL _didCreateSessionIdentifier; NSString *_sessionIdentifierUUID; BOOL _userRequestedBackgroundSession; BOOL _usingBackgroundSession; NSMutableData *_Nullable _downloadedData; NSError *_downloadFinishedError; NSData *_downloadResumeData; // immutable after construction NSData *_Nullable _downloadTaskErrorData; // Data for when download task fails 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 #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated" id<GTMFetcherAuthorizationProtocol> _authorizer; // immutable after beginFetch #pragma clang diagnostic pop // The service object that created and monitors this fetcher, if any. GTMSessionFetcherService *_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 { #if GTMSESSION_RECONNECT_BACKGROUND_SESSIONS_ON_LAUNCH && TARGET_OS_IPHONE NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; [nc addObserver:self selector:@selector(reconnectFetchersForBackgroundSessionsOnAppLaunch:) name:UIApplicationDidFinishLaunchingNotification object:nil]; #elif GTMSESSION_RECONNECT_BACKGROUND_SESSIONS_ON_LAUNCH && TARGET_OS_OSX NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; [nc addObserver:self selector:@selector(reconnectFetchersForBackgroundSessionsOnAppLaunch:) name:NSApplicationDidFinishLaunchingNotification object:nil]; #else [self fetchersForBackgroundSessions]; #endif } + (void)reconnectFetchersForBackgroundSessionsOnAppLaunch:(NSNotification *)notification { // Give all other app-did-launch handlers a chance to complete before // reconnecting the fetchers. Not doing this may lead to reconnecting // before the app delegate has a chance to run. dispatch_async(dispatch_get_main_queue(), ^{ [self fetchersForBackgroundSessions]; }); } #endif // !GTMSESSION_UNIT_TESTING + (instancetype)fetcherWithRequest:(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; } + (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:(nullable NSURLRequest *)request configuration:(nullable NSURLSessionConfiguration *)configuration { self = [super init]; if (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 = @"<download resume data>"; } else if (_wasCreatedFromBackgroundSession) { requestStr = @"<from bg session>"; } else { requestStr = @"<no request>"; } } 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:(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 mayDecorate: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 mayDecorate:YES]; } - (GTMSessionFetcherCompletionHandler)completionHandlerWithTarget:(nullable id)target didFinishSelector:(nullable 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:(nullable id)target didFinishSelector:(nullable SEL)finishedSelector { GTMSessionCheckNotSynchronized(self); GTMSessionFetcherCompletionHandler handler = [self completionHandlerWithTarget:target didFinishSelector:finishedSelector]; [self beginFetchWithCompletionHandler:handler]; } - (void)beginFetchMayDelay:(BOOL)mayDelay mayAuthorize:(BOOL)mayAuthorize mayDecorate:(BOOL)mayDecorate { // 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; GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URL:%@ beginFetchMayDelay:%d mayAuthorize:%d mayDecorate:%d", [self class], self, _request.URL, mayDelay, mayAuthorize, mayDecorate); // 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 // If priorSessionIdentifier is allowed to stay non-nil, a background session can // still be created. priorSessionIdentifier = nil; [self setSessionIdentifierInternal:nil]; self.usingBackgroundSession = 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 allow 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 = (_service != nil) && !isRecreatingSession && !self.usingBackgroundSession; if (!self.session) { if (self.canShareSession) { self.session = [_service sessionWithCreationBlock:^NSURLSession *(id<NSURLSessionDelegate> sessionDelegate) { return [self createSessionWithDelegate:sessionDelegate sessionIdentifier:priorSessionIdentifier]; }]; } else { self.session = [self createSessionWithDelegate:self sessionIdentifier:priorSessionIdentifier]; } } 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; } } if (mayDecorate && [_service respondsToSelector:@selector(decorators)]) { NSArray<id<GTMFetcherDecoratorProtocol>> *decorators = _service.decorators; if (decorators.count) { // If this session is held by the fetcher service, clear the session now so that we don't // assume it's still valid after decoration completes. // // The service will still hold on to the session, so as long as decoration doesn't take more // than 30 seconds since the last request, the service's session will be re-used when the // fetch actually starts. if (self.canShareSession) { self.session = nil; } [self applyDecoratorsAtRequestWillStart:decorators startingAtIndex:0]; 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 %lu bytes", _session, (unsigned long)_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 *_Nonnull)_bodyData]; GTMSESSION_ASSERT_DEBUG_OR_LOG( newSessionTask, @"Failed uploadTaskWithRequest for %@, %@, body data %lu bytes", _session, fetchRequest, (unsigned long)_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) { newSessionTask.priority = _taskPriority; } #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<GTMUIApplicationProtocol> app = [[self class] fetcherUIApplication]; // Background tasks seem to interfere with out-of-process uploads and downloads. if (app && !self.skipBackgroundTask && !self.usingBackgroundSession) { // 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 // Since a request can be started from any thread, we also have to ensure the // variable for accessing it is safe across the initial thread and the handler // (incase it gets failed immediately from the app already heading into the // background). __block UIBackgroundTaskIdentifier guardedTaskID = UIBackgroundTaskInvalid; UIBackgroundTaskIdentifier returnedTaskID = [app beginBackgroundTaskWithName:bgTaskName expirationHandler:^{ // Background task expiration callback - this block is always invoked by // UIApplication on the main thread. UIBackgroundTaskIdentifier localTaskID; @synchronized(self) { localTaskID = guardedTaskID; } if (localTaskID != UIBackgroundTaskInvalid) { @synchronized(self) { if (localTaskID == self.backgroundTaskIdentifier) { self.backgroundTaskIdentifier = UIBackgroundTaskInvalid; } } [app endBackgroundTask:localTaskID]; } }]; @synchronized(self) { guardedTaskID = returnedTaskID; self.backgroundTaskIdentifier = returnedTaskID; } } #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]; } } // Helper method to create a new NSURLSession for this fetcher. Because the original // implementation had this code inline, marking direct to avoid any danger of subclasses // overriding the behavior. - (NSURLSession *)createSessionWithDelegate:(id<NSURLSessionDelegate>)sessionDelegate sessionIdentifier:(nullable NSString *)priorSessionIdentifier __attribute__((objc_direct)) { // 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]; _configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessionIdentifier]; self.usingBackgroundSession = YES; self.canShareSession = NO; } else { _configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration]; } #if !GTM_ALLOW_INSECURE_REQUESTS #if GTM_SDK_REQUIRES_TLSMINIMUMSUPPORTEDPROTOCOLVERSION _configuration.TLSMinimumSupportedProtocolVersion = tls_protocol_version_TLSv12; #elif GTM_SDK_SUPPORTS_TLSMINIMUMSUPPORTEDPROTOCOLVERSION if (@available(iOS 13, tvOS 13, macOS 10.15, *)) { _configuration.TLSMinimumSupportedProtocolVersion = tls_protocol_version_TLSv12; } else { _configuration.TLSMinimumSupportedProtocol = kTLSProtocol12; } #else _configuration.TLSMinimumSupportedProtocol = kTLSProtocol12; #endif // GTM_SDK_REQUIRES_TLSMINIMUMSUPPORTEDPROTOCOLVERSION #endif } // !_configuration _configuration.HTTPCookieStorage = self.cookieStorage; if (_configurationBlock) { _configurationBlock(self, _configuration); } id<NSURLSessionDelegate> delegate = sessionDelegate; if (!delegate || !self.canShareSession) { delegate = self; } NSURLSession *session = [NSURLSession sessionWithConfiguration:_configuration delegate:delegate delegateQueue:self.sessionDelegateQueue]; GTMSESSION_ASSERT_DEBUG(session, @"Couldn't create session"); // 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(session.delegate == delegate, @"Couldn't assign delegate."); if (session) { BOOL isUsingSharedDelegate = (delegate != self); if (!isUsingSharedDelegate) { _shouldInvalidateSession = YES; } } return session; } NSData *_Nullable 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:(nullable NSData *)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; GTMSessionFetcherChallengeBlock challengeBlock = _challengeBlock; // Simulate receipt of redirection. if (willRedirectBlock) { __auto_type block = ^{ willRedirectBlock((NSHTTPURLResponse *)response, self->_request, ^(NSURLRequest *redirectRequest){ // For simulation, we'll assume // the app will just continue. }); }; [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES block:block]; } // 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) { __auto_type block = ^{ 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<NSURLAuthenticationChallengeSender> unusedSender = (id<NSURLAuthenticationChallengeSender>)[NSNull null]; NSURLAuthenticationChallenge *challenge = [[NSURLAuthenticationChallenge alloc] initWithProtectionSpace:pspace proposedCredential:nil previousFailureCount:0 failureResponse:nil error:nil sender:unusedSender]; challengeBlock(self, challenge, ^(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *_Nullable 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. }); }; [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES block:block]; } // Simulate receipt of an initial response. if (response && didReceiveResponseBlock) { __auto_type block = ^{ didReceiveResponseBlock(response, ^(NSURLSessionResponseDisposition desiredDisposition){ // For simulation, we'll assume // the disposition is to continue. }); }; [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES block:block]; } // Simulate reporting send progress. if (sendProgressBlock) { __auto_type block = ^(int64_t bytesSent, int64_t totalBytesSent, int64_t totalBytesExpectedToSend) { // This is invoked on the callback queue unless // stopped. sendProgressBlock(bytesSent, totalBytesSent, totalBytesExpectedToSend); }; [self simulateByteTransferReportWithDataLength:(int64_t)bodyData.length block:block]; } if (destinationFileURL) { // Simulate download to file progress. if (downloadProgressBlock) { __auto_type block = ^(int64_t bytesDownloaded, int64_t totalBytesDownloaded, int64_t totalBytesExpectedToDownload) { // This is invoked on the callback queue unless // stopped. downloadProgressBlock(bytesDownloaded, totalBytesDownloaded, totalBytesExpectedToDownload); }; [self simulateByteTransferReportWithDataLength:(int64_t)responseData.length block:block]; } 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) { __auto_type 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); } }; [self simulateByteTransferWithData:responseData block:block]; } 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]; __auto_type block = ^{ willCacheURLResponseBlock(cachedResponse, ^(NSCachedURLResponse *responseToCache){ // The app may provide an // alternative response, or // nil to defeat caching. }); }; [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES block:block]; } } _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) } - (nullable NSURLSessionTask *)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]; } + (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); if (!fetcher.clientWillReconnectBackgroundSession) { [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 GTM_SWIFT_DISABLE_ASYNC { 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 - (nullable NSString *)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:(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) } - (nullable NSDictionary *)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:(nullable NSDictionary *)dictionary { @synchronized(self) { GTMSessionMonitorSynchronized(self); GTMSESSION_ASSERT_DEBUG(_sessionIdentifier == nil, @"Too late to assign userInfo"); _sessionUserInfo = dictionary; } // @synchronized(self) } - (nullable NSDictionary *)sessionIdentifierDefaultMetadata { GTMSessionCheckSynchronized(self); NSMutableDictionary *defaultUserInfo = [[NSMutableDictionary alloc] init]; if (_destinationFileURL) { defaultUserInfo[kGTMSessionIdentifierDestinationFileURLMetadataKey] = [_destinationFileURL absoluteString]; } if (_bodyFileURL) { defaultUserInfo[kGTMSessionIdentifierBodyFileURLMetadataKey] = [_bodyFileURL absoluteString]; } if (_clientWillReconnectBackgroundSession) { defaultUserInfo[kGTMSessionIdentifierClientReconnectMetadataKey] = @"YES"; } 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); } NSString *clientReconnectString = metadata[kGTMSessionIdentifierClientReconnectMetadataKey]; if (clientReconnectString) { _clientWillReconnectBackgroundSession = [clientReconnectString boolValue]; GTM_LOG_BACKGROUND_SESSION(@"Restoring clientWillReconnectBackgroundSession: %@", (_clientWillReconnectBackgroundSession ? @"YES" : @"NO")); } } - (nullable NSDictionary *)sessionIdentifierMetadata { @synchronized(self) { GTMSessionMonitorSynchronized(self); return [self sessionIdentifierMetadataUnsynchronized]; } } - (nullable NSDictionary *)sessionIdentifierMetadataUnsynchronized { GTMSessionCheckSynchronized(self); // Session Identifier format: "com.google.<ClassName>_<UUID>_<Metadata in JSON format> 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:(nullable NSDictionary *)metadataToInclude { NSString *result; @synchronized(self) { GTMSessionMonitorSynchronized(self); // Session Identifier format: "com.google.<ClassName>_<UUID>_<Metadata in JSON format> 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 *_Nonnull)_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 mayDecorate:YES shouldReleaseCallbacks:YES]; [_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<GTMUIApplicationProtocol> app = [[self class] fetcherUIApplication]; [app endBackgroundTask:bgTaskID]; } } #endif // GTM_BACKGROUND_TASK_FETCHING - (void)authorizeRequest { GTMSessionCheckNotSynchronized(self); id authorizer = self.authorizer; // Prefer the block-based implementation. This *is* a change in behavior, but if authorizers // previously provided this method they would presumably assume they can be used for the same // requests as before. if ([authorizer respondsToSelector:@selector(authorizeRequest:completionHandler:)]) { // It's unknown how long an authorizer maintains ownership of the provided block, so // avoid potential retain cycles on self and the authorizer. __weak __typeof__(self) weakSelf = self; NSMutableURLRequest *mutableRequest = [self.request mutableCopy]; [authorizer authorizeRequest:mutableRequest completionHandler:^(NSError *_Nullable error) { [weakSelf authorizer:nil request:mutableRequest finishedWithError:error]; }]; } else if ([authorizer respondsToSelector:@selector(authorizeRequest: delegate:didFinishSelector:)]) { 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 mayDecorate:YES]; } } // The authorizer parameter is unused, and the block-based callback will never pass // non-nil; the field is only for the deprecated selector-based implementation for // legacy reasons. - (void)authorizer:(nullable id __unused)auth request:(nullable NSMutableURLRequest *)authorizedRequest finishedWithError:(nullable NSError *)error { GTMSessionCheckNotSynchronized(self); if (error != nil) { // We can't fetch without authorization [self failToBeginFetchWithError:(NSError *_Nonnull)error]; } else { @synchronized(self) { _request = authorizedRequest; } [self beginFetchMayDelay:NO mayAuthorize:NO mayDecorate:YES]; } } - (void)applyDecoratorsAtRequestWillStart:(NSArray<id<GTMFetcherDecoratorProtocol>> *)decorators startingAtIndex:(NSUInteger)index { GTMSessionCheckNotSynchronized(self); if (index >= decorators.count) { GTMSESSION_LOG_DEBUG(@"GTMSessionFetcher decorate requestWillStart %zu decorators complete", decorators.count); [self beginFetchMayDelay:NO mayAuthorize:NO mayDecorate:NO]; return; } __weak __typeof__(self) weakSelf = self; id<GTMFetcherDecoratorProtocol> decorator = decorators[index]; GTMSESSION_LOG_DEBUG(@"GTMSessionFetcher decorate requestWillStart %zu decorators, index %zu, " @"retry count %zu, decorator %@", decorators.count, index, self.retryCount, decorator); [decorator fetcherWillStart:self completionHandler:^(NSURLRequest *_Nullable newRequest, NSError *_Nullable error) { GTMSESSION_LOG_DEBUG(@"GTMSessionFetcher decorator requestWillStart index %zu " @"complete, newRequest %@, error %@", index, newRequest, error); __strong __typeof__(self) strongSelf = weakSelf; if (!strongSelf) { GTMSESSION_LOG_DEBUG(@"GTMSessionFetcher destroyed before requestWillStart " @"decorators completed, ignoring."); return; } if (error) { [self failToBeginFetchWithError:(NSError *_Nonnull)error]; return; } if (newRequest) { // Copying `NSURLRequest` should be cheap, but in case profiling shows this // operation is prohibitively expensive, this API might need to be changed to allow // clients to manipulate `self.request` directly. [strongSelf updateMutableRequest:[newRequest mutableCopy]]; } [strongSelf applyDecoratorsAtRequestWillStart:decorators startingAtIndex:index + 1]; }]; } - (void)applyDecoratorsAtRequestDidFinish:(NSArray<id<GTMFetcherDecoratorProtocol>> *)decorators withData:(nullable NSData *)data error:(nullable NSError *)error startingAtIndex:(NSUInteger)index shouldReleaseCallbacks:(BOOL)shouldReleaseCallbacks { GTMSessionCheckNotSynchronized(self); if (index >= decorators.count) { GTMSESSION_LOG_DEBUG(@"GTMSessionFetcher decorate requestDidFinish %zu decorators complete", decorators.count); [self invokeFetchCallbacksOnCallbackQueueWithData:data error:error mayDecorate:NO shouldReleaseCallbacks:shouldReleaseCallbacks]; return; } __weak __typeof__(self) weakSelf = self; id<GTMFetcherDecoratorProtocol> decorator = decorators[index]; GTMSESSION_LOG_DEBUG(@"GTMSessionFetcher decorate requestDidFinish %zu decorators, index %zu, " @"retry count %zu, decorator %@", decorators.count, index, self.retryCount, decorator); [decorator fetcherDidFinish:self withData:data error:error completionHandler:^{ GTMSESSION_LOG_DEBUG( @"GTMSessionFetcher decorator requestDidFinish index %zu complete", index); __strong __typeof__(self) strongSelf = weakSelf; if (!strongSelf) { GTMSESSION_LOG_DEBUG(@"GTMSessionFetcher destroyed before requestDidFinish " @"decorators completed, ignoring."); return; } [strongSelf applyDecoratorsAtRequestDidFinish:decorators withData:data error:error startingAtIndex:index + 1 shouldReleaseCallbacks:shouldReleaseCallbacks]; }]; } - (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; } - (nullable NSURLResponse *)response { @synchronized(self) { GTMSessionMonitorSynchronized(self); NSURLResponse *response = [self responseUnsynchronized]; return response; } // @synchronized(self) } - (nullable NSURLResponse *)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; } - (nullable NSDictionary *)responseHeaders { GTMSessionCheckNotSynchronized(self); NSURLResponse *response = self.response; if ([response respondsToSelector:@selector(allHeaderFields)]) { NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields]; return headers; } return nil; } - (nullable NSDictionary *)responseHeadersUnsynchronized { GTMSessionCheckSynchronized(self); NSURLResponse *response = [self responseUnsynchronized]; if ([response respondsToSelector:@selector(allHeaderFields)]) { NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields]; return headers; } return nil; } - (void)releaseCallbacks { // The clang included with Xcode 13.3 betas added a -Wunused-but-set-variable warning, // which doesn't (yet) skip variables annotated with objc_precie_lifetime. Since that // warning is not available in all Xcodes, turn off the -Wunused warning group entirely. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused" // 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; #pragma clang diagnostic pop @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; if (@available(iOS 10.0, *)) { self.metricsCollectionBlock = 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. The completion handler will still execute if // the property `stopFetchingTriggersCompletionHandler` is `YES`. _userStoppedFetching = YES; } // @synchronized(self) [self stopFetchReleasingCallbacks:!self.stopFetchingTriggersCompletionHandler]; } // 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 // still be retained or `stopFetching` was called and `stopFetchingTriggersCompletionHandler` is // `YES`. - (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks { [self removePersistedBackgroundSessionFromDefaults]; GTMSessionFetcherService *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]; BOOL sendStopNotification = YES; BOOL cancelStopFetcher = NO; @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 { sendStopNotification = NO; _sessionNeedingInvalidation = oldSession; } } } cancelStopFetcher = _stopFetchingTriggersCompletionHandler && _userStoppedFetching; } // @synchronized(self) // If the NSURLSession needs to be invalidated, but needs to wait until the delegate method // URLSession:task:didCompleteWithError: is called, delay sending the fetch stopped notification // until then; otherwise send it now. if (sendStopNotification) { [self sendStopNotificationIfNeeded]; } [_authorizer stopAuthorizationForRequest:request]; if (shouldReleaseCallbacks) { [self releaseCallbacks]; self.authorizer = nil; } if (!cancelStopFetcher) { [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:(nullable GTMSessionFetcherTestBlock)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 _Nullable id<GTMUIApplicationProtocol> gSubstituteUIApp; + (void)setSubstituteUIApplication:(nullable id<GTMUIApplicationProtocol>)app { gSubstituteUIApp = app; } + (nullable id<GTMUIApplicationProtocol>)substituteUIApplication { return gSubstituteUIApp; } + (nullable id<GTMUIApplicationProtocol>)fetcherUIApplication { id<GTMUIApplicationProtocol> 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<GTMUIApplicationProtocol>)[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 *_Nullable))handler { [self setSessionTask:task]; GTMSESSION_LOG_DEBUG_VERBOSE( @"%@ %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]; // The new requests's URL overrides the original's URL. [newRequest setURL:[GTMSessionFetcher redirectURLWithOriginalRequestURL:originalRequest.URL redirectRequestURL:redirectRequest.URL]]; // 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); __auto_type block = ^{ willRedirectBlock(redirectResponse, redirectRequest, ^(NSURLRequest *clientRequest) { // Update the request for future logging. [self updateMutableRequest:[clientRequest mutableCopy]]; handler(clientRequest); }); }; [self invokeOnCallbackQueueAfterUserStopped:YES block:block]; } // @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]; GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSession:%@ dataTask:%@ didReceiveResponse:%@", [self class], self, session, dataTask, response); __auto_type accumulateAndFinish = ^(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. __auto_type block = ^{ receivedResponseBlock(response, ^(NSURLSessionResponseDisposition desiredDisposition) { accumulateAndFinish(desiredDisposition); }); }; [self invokeOnCallbackQueueAfterUserStopped:YES block:block]; } } // @synchronized(self) if (receivedResponseBlock == nil) { accumulateAndFinish(NSURLSessionResponseAllow); } } - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask { GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %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 *_Nullable credential))handler { [self setSessionTask:task]; GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %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 *_Nullable 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. __auto_type callback = ^(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) } // Return redirect URL based on the original request URL and redirect request URL. // // Method disallows any scheme changes between the original request URL and redirect request URL // aside from "http" to "https". If a change in scheme is detected the redirect URL inherits the // scheme from the original request URL. + (nullable NSURL *)redirectURLWithOriginalRequestURL:(nullable NSURL *)originalRequestURL redirectRequestURL:(nullable NSURL *)redirectRequestURL { // In the case of an NSURLSession redirect, neither URL should ever be nil; as a sanity check // if either is nil return the other URL. if (!redirectRequestURL) return originalRequestURL; if (!originalRequestURL) return redirectRequestURL; NSString *originalScheme = originalRequestURL.scheme; NSString *redirectScheme = redirectRequestURL.scheme; BOOL insecureToSecureRedirect = (originalScheme != nil && [originalScheme caseInsensitiveCompare:@"http"] == NSOrderedSame && redirectScheme != nil && [redirectScheme caseInsensitiveCompare:@"https"] == NSOrderedSame); // This can't really be nil for the inputs, but to keep the analyzer happy // for the -caseInsensitiveCompare: call below, give it a value if it were. if (!originalScheme) originalScheme = @"https"; // Check for changes to the scheme and disallow any changes except for http to https. if (!insecureToSecureRedirect && (redirectScheme.length != originalScheme.length || [redirectScheme caseInsensitiveCompare:originalScheme] != NSOrderedSame)) { NSURLComponents *components = [NSURLComponents componentsWithURL:(NSURL *_Nonnull)redirectRequestURL resolvingAgainstBaseURL:NO]; components.scheme = originalScheme; return components.URL; } return redirectRequestURL; } // 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. BOOL shouldAllow; #if GTM_SDK_REQUIRES_SECTRUSTEVALUATEWITHERROR CFErrorRef errorRef = NULL; @synchronized([GTMSessionFetcher class]) { GTMSessionMonitorSynchronized([GTMSessionFetcher class]); // SecTrustEvaluateWithError handles both the "proceed" and "unspecified" cases, // so it is not necessary to check the trust result the evaluation returns true. shouldAllow = SecTrustEvaluateWithError(serverTrust, &errorRef); } if (errorRef) { GTMSESSION_LOG_DEBUG(@"Error %d evaluating trust for %@", (int)CFErrorGetCode(errorRef), request); CFRelease(errorRef); } #else SecTrustResultType trustEval = kSecTrustResultInvalid; 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))); } } #endif // GTM_SDK_REQUIRES_SECTRUSTEVALUATEWITHERROR 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 && !self->_stopFetchingTriggersCompletionHandler) { 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:(nullable NSData *)data error:(nullable NSError *)error mayDecorate:(BOOL)mayDecorate shouldReleaseCallbacks:(BOOL)shouldReleaseCallbacks { if (mayDecorate && [_service respondsToSelector:@selector(decorators)]) { NSArray<id<GTMFetcherDecoratorProtocol>> *decorators = _service.decorators; if (decorators.count) { [self applyDecoratorsAtRequestDidFinish:decorators withData:data error:error startingAtIndex:0 shouldReleaseCallbacks:shouldReleaseCallbacks]; return; } } GTMSESSION_LOG_DEBUG_VERBOSE( @"GTMSessionFetcher invoking fetch callbacks, data length %lu, error %@", (unsigned long)data.length, error); // Callbacks will be released in the method stopFetchReleasingCallbacks: GTMSessionFetcherCompletionHandler handler; dispatch_queue_t callbackQueue; @synchronized(self) { GTMSessionMonitorSynchronized(self); // Capture the completion handler and callback queue, and call them only // after releasing any callbacks to ensure the order of release vs // callback is deterministic given potential QoS differences between // callback queue and whatever queue this method eventually executes on. handler = _completionHandler; callbackQueue = _callbackQueue; } if (shouldReleaseCallbacks) { [self releaseCallbacks]; } if (handler) { [self invokeOnCallbackQueue:callbackQueue afterUserStopped:NO block:^{ 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]; }]; } } - (void)postNotificationOnMainThreadWithName:(NSString *)noteName userInfo:(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 *_Nullable bodyStream))completionHandler { [self setSessionTask:uploadTask]; GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %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]; GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %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; GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %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_SWIFT_DISABLE_ASYNC { GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %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 { GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %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; } GTMSessionFetcherDownloadProgressBlock progressBlock; progressBlock = self->_downloadProgressBlock; if (progressBlock) { [self invokeOnCallbackQueueUnlessStopped:^{ progressBlock(bytesWritten, totalBytesWritten, totalBytesExpectedToWrite); }]; } } // @synchronized(self) } - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes { GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %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]; GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %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. GTMSESSION_LOG_DEBUG(@"Abandoning download due to status %ld, file %@", (long)statusCode, downloadLocationURL.path); // On error code, add the contents of the temporary file to _downloadTaskErrorData // This way fetcher clients have access to error details possibly passed by the server. if (_downloadedLength > 0 && _downloadedLength <= kMaximumDownloadErrorDataLength) { _downloadTaskErrorData = [NSData dataWithContentsOfURL:downloadLocationURL]; } else if (_downloadedLength > kMaximumDownloadErrorDataLength) { GTMSESSION_LOG_DEBUG(@"Download error data for file %@ not passed to userInfo due to size " @"%lld", downloadLocationURL.path, _downloadedLength); } } 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]; GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSession:%@ task:%@ didCompleteWithError:%@", [self class], self, session, task, error); NSInteger status = self.statusCode; BOOL forceAssumeRetry = NO; BOOL succeeded = NO; BOOL userStoppedTriggerCompletion = 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 userStoppedTriggerCompletion = _userStoppedFetching && _stopFetchingTriggersCompletionHandler; if (error == nil) { error = _downloadFinishedError; } succeeded = (error == nil && status >= 0 && status < 300); if (succeeded && !userStoppedTriggerCompletion) { // Succeeded. _bodyLength = task.countOfBytesSent; } } // @synchronized(self) if (userStoppedTriggerCompletion) { NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; [userInfo setObject:@"Operation cancelled" forKey:NSLocalizedDescriptionKey]; if (error) { [userInfo setObject:error forKey:NSUnderlyingErrorKey]; } NSError *cancelError = [NSError errorWithDomain:kGTMSessionFetcherErrorDomain code:GTMSessionFetcherErrorUserCancelled userInfo:userInfo]; [self finishWithError:cancelError shouldRetry:NO]; return; } 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]; }]; } - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics API_AVAILABLE(ios(10.0), macosx(10.12), tvos(10.0), watchos(6.0)) { @synchronized(self) { GTMSessionMonitorSynchronized(self); GTMSessionFetcherMetricsCollectionBlock metricsCollectionBlock = _metricsCollectionBlock; if (metricsCollectionBlock) { [self invokeOnCallbackQueueUnlessStopped:^{ metricsCollectionBlock(metrics); }]; } } } #if TARGET_OS_IPHONE - (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session { GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %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:(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. GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSession:%@ didBecomeInvalidWithError:%@", [self class], self, session, error); if (session == (NSURLSession *)self.session) { GTMSESSION_LOG_DEBUG_VERBOSE(@" Unexpected retained invalid session: %@", session); self.session = nil; } } - (void)finishWithError:(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 = GTMErrorUserInfoForData( _downloadedData.length > 0 ? _downloadedData : _downloadTaskErrorData, [self responseHeadersUnsynchronized]); 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) { // The upload subclass doesn't want to release callbacks until upload chunks have completed. BOOL shouldRelease = [self shouldReleaseCallbacksUponCompletion]; [self invokeFetchCallbacksOnCallbackQueueWithData:downloadedData error:error mayDecorate:YES shouldReleaseCallbacks:shouldRelease]; [self stopFetchReleasingCallbacks:NO]; } #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:(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 = GTMErrorUserInfoForData(_downloadedData, [self responseHeadersUnsynchronized]); 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]; #if GTM_BACKGROUND_TASK_FETCHING // Don't keep a background task active while awaiting retry, which can lead to the // app exceeding the allotted time for keeping the background task open, causing the // system to terminate the app. When the retry starts, a new background task will // be created. [self endBackgroundTask]; #endif // GTM_BACKGROUND_TASK_FETCHING @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; - (nullable GTMSessionFetcherSystemCompletionHandler)systemCompletionHandler { return [[self class] systemCompletionHandlerForSessionIdentifier:_sessionIdentifier]; } - (void)setSystemCompletionHandler: (nullable GTMSessionFetcherSystemCompletionHandler)systemCompletionHandler { [[self class] setSystemCompletionHandler:systemCompletionHandler forSessionIdentifier:_sessionIdentifier]; } + (void)setSystemCompletionHandler: (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]; } } + (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 // clang-format off // Don't re-format the @synthesize blocks: @synthesize downloadResumeData = _downloadResumeData, configuration = _configuration, configurationBlock = _configurationBlock, sessionTask = _sessionTask, wasCreatedFromBackgroundSession = _wasCreatedFromBackgroundSession, clientWillReconnectBackgroundSession = _clientWillReconnectBackgroundSession, 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, metricsCollectionBlock = _metricsCollectionBlock, retryFactor = _retryFactor, allowedInsecureSchemes = _allowedInsecureSchemes, allowLocalhostRequest = _allowLocalhostRequest, allowInvalidServerCertificates = _allowInvalidServerCertificates, cookieStorage = _cookieStorage, callbackQueue = _callbackQueue, initialBeginFetchDate = _initialBeginFetchDate, testBlock = _testBlock, testBlockAccumulateDataChunkCount = _testBlockAccumulateDataChunkCount, comment = _comment, log = _log, stopFetchingTriggersCompletionHandler = _stopFetchingTriggersCompletionHandler; #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 // clang-format on - (nullable NSURLRequest *)request { @synchronized(self) { GTMSessionMonitorSynchronized(self); return [_request copy]; } // @synchronized(self) } - (void)setRequest:(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) } - (nullable NSMutableURLRequest *)mutableRequestForTesting { // Allow tests only to modify the request, useful during retries. return _request; } // Internal method for updating the request property such as on redirects. - (void)updateMutableRequest:(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:(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:(nullable NSString *)value forHTTPHeaderField:(NSString *)field { @synchronized(self) { GTMSessionMonitorSynchronized(self); [_request setValue:value forHTTPHeaderField:field]; } // @synchronized(self) } - (void)setResponse:(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) } - (nullable NSURL *)bodyFileURL { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _bodyFileURL; } // @synchronized(self) } - (void)setBodyFileURL:(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) } - (nullable GTMSessionFetcherBodyStreamProvider)bodyStreamProvider { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _bodyStreamProvider; } // @synchronized(self) } - (void)setBodyStreamProvider:(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) } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated" - (nullable id<GTMFetcherAuthorizationProtocol>)authorizer { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _authorizer; } // @synchronized(self) } - (void)setAuthorizer:(nullable id<GTMFetcherAuthorizationProtocol>)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) } #pragma clang diagnostic pop - (nullable NSData *)downloadedData { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _downloadedData; } // @synchronized(self) } - (void)setDownloadedData:(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) } - (nonnull dispatch_queue_t)callbackQueue { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _callbackQueue; } // @synchronized(self) } - (void)setCallbackQueue:(nullable dispatch_queue_t)queue { @synchronized(self) { GTMSessionMonitorSynchronized(self); _callbackQueue = queue ?: dispatch_get_main_queue(); } // @synchronized(self) } - (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:(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) } - (BOOL)stopFetchingTriggersCompletionHandler { return _stopFetchingTriggersCompletionHandler; } - (void)setStopFetchingTriggersCompletionHandler:(BOOL)flag { if (_initialBeginFetchDate == nil) { _stopFetchingTriggersCompletionHandler = flag; } else { GTMSESSION_ASSERT_DEBUG( 0, @"stopFetchingTriggersCompletionHandler should not change after fetcher starts"); } } - (nullable NSURLSession *)sessionNeedingInvalidation { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _sessionNeedingInvalidation; } // @synchronized(self) } - (void)setSessionNeedingInvalidation:(nullable NSURLSession *)session { @synchronized(self) { GTMSessionMonitorSynchronized(self); _sessionNeedingInvalidation = session; } // @synchronized(self) } - (nonnull NSOperationQueue *)sessionDelegateQueue { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _delegateQueue; } // @synchronized(self) } - (void)setSessionDelegateQueue:(nullable NSOperationQueue *)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) } - (nullable id)userData { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _userData; } // @synchronized(self) } - (void)setUserData:(nullable id)theObj { @synchronized(self) { GTMSessionMonitorSynchronized(self); _userData = theObj; } // @synchronized(self) } - (nullable NSURL *)destinationFileURL { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _destinationFileURL; } // @synchronized(self) } - (void)setDestinationFileURL:(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. __unused NSString *oldFilename = [_destinationFileURL lastPathComponent]; __unused NSString *newFilename = [destinationFileURL lastPathComponent]; 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:(nullable NSDictionary *)dict { @synchronized(self) { GTMSessionMonitorSynchronized(self); _properties = [dict mutableCopy]; } // @synchronized(self) } - (nullable NSDictionary *)properties { @synchronized(self) { GTMSessionMonitorSynchronized(self); return _properties; } // @synchronized(self) } - (void)setProperty:(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) } - (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 GTMSessionCookieStorage { NSMutableArray *_cookies; NSHTTPCookieAcceptPolicy _policy; } - (id)init { self = [super init]; if (self != nil) { _cookies = [[NSMutableArray alloc] init]; } return self; } - (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:(nullable NSArray *)newCookies { @synchronized(self) { GTMSessionMonitorSynchronized(self); [self removeExpiredCookies]; for (NSHTTPCookie *newCookie in newCookies) { [self internalSetCookie:newCookie]; } } // @synchronized(self) } - (void)setCookies:(NSArray *)cookies forURL:(nullable NSURL *)URL mainDocumentURL:(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. - (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 (^)(NSArray<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 - (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 _Nullable obj, SEL _Nullable 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, ^{ #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_IOS // 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_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_OSX // Mac build NSProcessInfo *procInfo = [NSProcessInfo processInfo]; NSString *versString; NSOperatingSystemVersion version = procInfo.operatingSystemVersion; versString = [NSString stringWithFormat:@"%ld.%ld.%ld", (long)version.majorVersion, (long)version.minorVersion, (long)version.patchVersion]; 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 *_Nullable bundle) { NSString *result = [NSString stringWithFormat:@"%@ %@", GTMFetcherApplicationIdentifier(bundle), GTMFetcherSystemVersionString()]; return result; } NSString *GTMFetcherApplicationIdentifier(NSBundle *_Nullable 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:@"GTMUserAgentVersion"]; if (version.length == 0) { 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 && (!defined(NS_BLOCK_ASSERTIONS) || GTMSESSION_ASSERT_AS_LOG) @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:(id _Nonnull) @(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]; } } + (nullable 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 && (!defined(NS_BLOCK_ASSERTIONS) || GTMSESSION_ASSERT_AS_LOG) NS_ASSUME_NONNULL_END