summaryrefslogtreecommitdiff
path: root/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcher.m
diff options
context:
space:
mode:
Diffstat (limited to 'StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcher.m')
-rw-r--r--StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcher.m4670
1 files changed, 4670 insertions, 0 deletions
diff --git a/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcher.m b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcher.m
new file mode 100644
index 00000000..3dc64597
--- /dev/null
+++ b/StoneIsland/platforms/ios/Pods/GTMSessionFetcher/Source/GTMSessionFetcher.m
@@ -0,0 +1,4670 @@
+/* Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#import "GTMSessionFetcher.h"
+#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
+
+#import <sys/utsname.h>
+
+#ifndef STRIP_GTM_FETCH_LOGGING
+ #error GTMSessionFetcher headers should have defaulted this if it wasn't already defined.
+#endif
+
+GTM_ASSUME_NONNULL_BEGIN
+
+NSString *const kGTMSessionFetcherStartedNotification = @"kGTMSessionFetcherStartedNotification";
+NSString *const kGTMSessionFetcherStoppedNotification = @"kGTMSessionFetcherStoppedNotification";
+NSString *const kGTMSessionFetcherRetryDelayStartedNotification = @"kGTMSessionFetcherRetryDelayStartedNotification";
+NSString *const kGTMSessionFetcherRetryDelayStoppedNotification = @"kGTMSessionFetcherRetryDelayStoppedNotification";
+
+NSString *const kGTMSessionFetcherCompletionInvokedNotification = @"kGTMSessionFetcherCompletionInvokedNotification";
+NSString *const kGTMSessionFetcherCompletionDataKey = @"data";
+NSString *const kGTMSessionFetcherCompletionErrorKey = @"error";
+
+NSString *const kGTMSessionFetcherErrorDomain = @"com.google.GTMSessionFetcher";
+NSString *const kGTMSessionFetcherStatusDomain = @"com.google.HTTPStatus";
+NSString *const kGTMSessionFetcherStatusDataKey = @"data"; // data returned with a kGTMSessionFetcherStatusDomain error
+NSString *const 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";
+
+// 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
+
+GTM_ASSUME_NONNULL_END
+
+//
+// GTMSessionFetcher
+//
+
+#if 0
+#define GTM_LOG_BACKGROUND_SESSION(...) GTMSESSION_LOG_DEBUG(__VA_ARGS__)
+#else
+#define GTM_LOG_BACKGROUND_SESSION(...)
+#endif
+
+#ifndef GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY
+ #if (TARGET_OS_TV \
+ || TARGET_OS_WATCH \
+ || (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_11) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_11) \
+ || (TARGET_OS_IPHONE && defined(__IPHONE_9_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_9_0))
+ #define GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY 1
+ #endif
+#endif
+
+#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) && __WATCHOS_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) && __WATCHOS_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) && __WATCHOS_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
+
+@interface GTMSessionFetcher ()
+
+@property(atomic, strong, readwrite, GTM_NULLABLE) NSData *downloadedData;
+@property(atomic, strong, readwrite, GTM_NULLABLE) NSData *downloadResumeData;
+
+#if GTM_BACKGROUND_TASK_FETCHING
+// Should always be accessed within an @synchronized(self).
+@property(assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskIdentifier;
+#endif
+
+@property(atomic, readwrite, getter=isUsingBackgroundSession) BOOL usingBackgroundSession;
+
+@end
+
+#if !GTMSESSION_BUILD_COMBINED_SOURCES
+@interface GTMSessionFetcher (GTMSessionFetcherLoggingInternal)
+- (void)logFetchWithError:(NSError *)error;
+- (void)logNowWithError:(GTM_NULLABLE NSError *)error;
+- (NSInputStream *)loggedInputStreamForInputStream:(NSInputStream *)inputStream;
+- (GTMSessionFetcherBodyStreamProvider)loggedStreamProviderForStreamProvider:
+ (GTMSessionFetcherBodyStreamProvider)streamProvider;
+@end
+#endif // !GTMSESSION_BUILD_COMBINED_SOURCES
+
+GTM_ASSUME_NONNULL_BEGIN
+
+static NSTimeInterval InitialMinRetryInterval(void) {
+ return 1.0 + ((double)(arc4random_uniform(0x0FFFF)) / (double) 0x0FFFF);
+}
+
+static BOOL IsLocalhost(NSString * GTM_NULLABLE_TYPE host) {
+ // We check if there's host, and then make the comparisons.
+ if (host == nil) return NO;
+ return ([host caseInsensitiveCompare:@"localhost"] == NSOrderedSame
+ || [host isEqual:@"::1"]
+ || [host isEqual:@"127.0.0.1"]);
+}
+
+static NSDictionary *GTM_NULLABLE_TYPE GTMErrorUserInfoForData(
+ NSData *GTM_NULLABLE_TYPE data, NSDictionary *GTM_NULLABLE_TYPE 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 GTM_NULLABLE_TYPE gGlobalTestBlock;
+
+@implementation GTMSessionFetcher {
+ NSMutableURLRequest *_request; // after beginFetch, changed only in delegate callbacks
+ BOOL _useUploadTask; // immutable after beginFetch
+ NSURL *_bodyFileURL; // immutable after beginFetch
+ GTMSessionFetcherBodyStreamProvider _bodyStreamProvider; // immutable after beginFetch
+ NSURLSession *_session;
+ BOOL _shouldInvalidateSession; // immutable after beginFetch
+ NSURLSession *_sessionNeedingInvalidation;
+ NSURLSessionConfiguration *_configuration;
+ NSURLSessionTask *_sessionTask;
+ NSString *_taskDescription;
+ float _taskPriority;
+ NSURLResponse *_response;
+ NSString *_sessionIdentifier;
+ BOOL _wasCreatedFromBackgroundSession;
+ BOOL _didCreateSessionIdentifier;
+ NSString *_sessionIdentifierUUID;
+ BOOL _userRequestedBackgroundSession;
+ BOOL _usingBackgroundSession;
+ NSMutableData * GTM_NULLABLE_TYPE _downloadedData;
+ NSError *_downloadFinishedError;
+ NSData *_downloadResumeData; // immutable after construction
+ NSData * GTM_NULLABLE_TYPE _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
+
+ id<GTMFetcherAuthorizationProtocol> _authorizer; // immutable after beginFetch
+
+ // The service object that created and monitors this fetcher, if any.
+ id<GTMSessionFetcherServiceProtocol> _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:(GTM_NULLABLE NSURLRequest *)request {
+ return [[self alloc] initWithRequest:request configuration:nil];
+}
+
++ (instancetype)fetcherWithURL:(NSURL *)requestURL {
+ return [self fetcherWithRequest:[NSURLRequest requestWithURL:requestURL]];
+}
+
++ (instancetype)fetcherWithURLString:(NSString *)requestURLString {
+ return [self fetcherWithURL:(NSURL *)[NSURL URLWithString:requestURLString]];
+}
+
++ (instancetype)fetcherWithDownloadResumeData:(NSData *)resumeData {
+ GTMSessionFetcher *fetcher = [self fetcherWithRequest:nil];
+ fetcher.comment = @"Resuming download";
+ fetcher.downloadResumeData = resumeData;
+ return fetcher;
+}
+
++ (GTM_NULLABLE instancetype)fetcherWithSessionIdentifier:(NSString *)sessionIdentifier {
+ GTMSESSION_ASSERT_DEBUG(sessionIdentifier != nil, @"Invalid session identifier");
+ NSMapTable *sessionIdentifierToFetcherMap = [self sessionIdentifierToFetcherMap];
+ GTMSessionFetcher *fetcher = [sessionIdentifierToFetcherMap objectForKey:sessionIdentifier];
+ if (!fetcher && [sessionIdentifier hasPrefix:kGTMSessionIdentifierPrefix]) {
+ fetcher = [self fetcherWithRequest:nil];
+ [fetcher setSessionIdentifier:sessionIdentifier];
+ [sessionIdentifierToFetcherMap setObject:fetcher forKey:sessionIdentifier];
+ fetcher->_wasCreatedFromBackgroundSession = YES;
+ [fetcher setCommentWithFormat:@"Resuming %@",
+ fetcher && fetcher->_sessionIdentifierUUID ? fetcher->_sessionIdentifierUUID : @"?"];
+ }
+ return fetcher;
+}
+
++ (NSMapTable *)sessionIdentifierToFetcherMap {
+ // TODO: What if a service is involved in creating the fetcher? Currently, when re-creating
+ // fetchers, if a service was involved, it is not re-created. Should the service maintain a map?
+ static NSMapTable *gSessionIdentifierToFetcherMap = nil;
+
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ gSessionIdentifierToFetcherMap = [NSMapTable strongToWeakObjectsMapTable];
+ });
+ return gSessionIdentifierToFetcherMap;
+}
+
+#if !GTM_ALLOW_INSECURE_REQUESTS
++ (BOOL)appAllowsInsecureRequests {
+ // If the main bundle Info.plist key NSAppTransportSecurity is present, and it specifies
+ // NSAllowsArbitraryLoads, then we need to explicitly enforce secure schemes.
+#if GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY
+ static BOOL allowsInsecureRequests;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ NSBundle *mainBundle = [NSBundle mainBundle];
+ NSDictionary *appTransportSecurity =
+ [mainBundle objectForInfoDictionaryKey:@"NSAppTransportSecurity"];
+ allowsInsecureRequests =
+ [[appTransportSecurity objectForKey:@"NSAllowsArbitraryLoads"] boolValue];
+ });
+ return allowsInsecureRequests;
+#else
+ // For builds targeting iOS 8 or 10.10 and earlier, we want to require fetcher
+ // security checks.
+ return YES;
+#endif // GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY
+}
+#else // GTM_ALLOW_INSECURE_REQUESTS
++ (BOOL)appAllowsInsecureRequests {
+ return YES;
+}
+#endif // !GTM_ALLOW_INSECURE_REQUESTS
+
+
+- (instancetype)init {
+ return [self initWithRequest:nil configuration:nil];
+}
+
+- (instancetype)initWithRequest:(NSURLRequest *)request {
+ return [self initWithRequest:request configuration:nil];
+}
+
+- (instancetype)initWithRequest:(GTM_NULLABLE NSURLRequest *)request
+ configuration:(GTM_NULLABLE NSURLSessionConfiguration *)configuration {
+ self = [super init];
+ if (self) {
+#if 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:(GTM_NULLABLE GTMSessionFetcherCompletionHandler)handler {
+ GTMSessionCheckNotSynchronized(self);
+
+ _completionHandler = [handler copy];
+
+ // The user may have called setDelegate: earlier if they want to use other
+ // delegate-style callbacks during the fetch; otherwise, the delegate is nil,
+ // which is fine.
+ [self beginFetchMayDelay:YES mayAuthorize:YES];
+}
+
+// Begin fetching the URL for a retry fetch. The delegate and completion handler
+// are already provided, and do not need to be copied.
+- (void)beginFetchForRetry {
+ GTMSessionCheckNotSynchronized(self);
+
+ [self beginFetchMayDelay:YES mayAuthorize:YES];
+}
+
+- (GTMSessionFetcherCompletionHandler)completionHandlerWithTarget:(GTM_NULLABLE_TYPE id)target
+ didFinishSelector:(GTM_NULLABLE_TYPE SEL)finishedSelector {
+ GTMSessionFetcherAssertValidSelector(target, finishedSelector, @encode(GTMSessionFetcher *),
+ @encode(NSData *), @encode(NSError *), 0);
+ GTMSessionFetcherCompletionHandler completionHandler = ^(NSData *data, NSError *error) {
+ if (target && finishedSelector) {
+ id selfArg = self; // Placate ARC.
+ NSMethodSignature *sig = [target methodSignatureForSelector:finishedSelector];
+ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
+ [invocation setSelector:(SEL)finishedSelector];
+ [invocation setTarget:target];
+ [invocation setArgument:&selfArg atIndex:2];
+ [invocation setArgument:&data atIndex:3];
+ [invocation setArgument:&error atIndex:4];
+ [invocation invoke];
+ }
+ };
+ return completionHandler;
+}
+
+- (void)beginFetchWithDelegate:(GTM_NULLABLE_TYPE id)target
+ didFinishSelector:(GTM_NULLABLE_TYPE SEL)finishedSelector {
+ GTMSessionCheckNotSynchronized(self);
+
+ GTMSessionFetcherCompletionHandler handler = [self completionHandlerWithTarget:target
+ didFinishSelector:finishedSelector];
+ [self beginFetchWithCompletionHandler:handler];
+}
+
+- (void)beginFetchMayDelay:(BOOL)mayDelay
+ mayAuthorize:(BOOL)mayAuthorize {
+ // This is the internal entry point for re-starting fetches.
+ GTMSessionCheckNotSynchronized(self);
+
+ NSMutableURLRequest *fetchRequest = _request; // The request property is now externally immutable.
+ NSURL *fetchRequestURL = fetchRequest.URL;
+ NSString *priorSessionIdentifier = self.sessionIdentifier;
+
+ // A utility block for creating error objects when we fail to start the fetch.
+ NSError *(^beginFailureError)(NSInteger) = ^(NSInteger code){
+ NSString *urlString = fetchRequestURL.absoluteString;
+ NSDictionary *userInfo = @{
+ NSURLErrorFailingURLStringErrorKey : (urlString ? urlString : @"(missing URL)")
+ };
+ return [NSError errorWithDomain:kGTMSessionFetcherErrorDomain
+ code:code
+ userInfo:userInfo];
+ };
+
+ // Catch delegate queue maxConcurrentOperationCount values other than 1, particularly
+ // NSOperationQueueDefaultMaxConcurrentOperationCount (-1), to avoid the additional complexity
+ // of simultaneous or out-of-order delegate callbacks.
+ GTMSESSION_ASSERT_DEBUG(_delegateQueue.maxConcurrentOperationCount == 1,
+ @"delegate queue %@ should support one concurrent operation, not %ld",
+ _delegateQueue.name,
+ (long)_delegateQueue.maxConcurrentOperationCount);
+
+ if (!_initialBeginFetchDate) {
+ // This ivar is set only here on the initial beginFetch so need not be synchronized.
+ _initialBeginFetchDate = [[NSDate alloc] init];
+ }
+
+ if (self.sessionTask != nil) {
+ // If cached fetcher returned through fetcherWithSessionIdentifier:, then it's
+ // already begun, but don't consider this a failure, since the user need not know this.
+ if (self.sessionIdentifier != nil) {
+ return;
+ }
+ GTMSESSION_ASSERT_DEBUG(NO, @"Fetch object %@ being reused; this should never happen", self);
+ [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorDownloadFailed)];
+ return;
+ }
+
+ if (fetchRequestURL == nil && !_downloadResumeData && !priorSessionIdentifier) {
+ GTMSESSION_ASSERT_DEBUG(NO, @"Beginning a fetch requires a request with a URL");
+ [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorDownloadFailed)];
+ return;
+ }
+
+ // We'll respect the user's request for a background session (unless this is
+ // an upload fetcher, which does its initial request foreground.)
+ self.usingBackgroundSession = self.useBackgroundSession && [self canFetchWithBackgroundSession];
+
+ NSURL *bodyFileURL = self.bodyFileURL;
+ if (bodyFileURL) {
+ NSError *fileCheckError;
+ if (![bodyFileURL checkResourceIsReachableAndReturnError:&fileCheckError]) {
+ // This assert fires when the file being uploaded no longer exists once
+ // the fetcher is ready to start the upload.
+ GTMSESSION_ASSERT_DEBUG_OR_LOG(0, @"Body file is unreachable: %@\n %@",
+ bodyFileURL.path, fileCheckError);
+ [self failToBeginFetchWithError:fileCheckError];
+ return;
+ }
+ }
+
+ NSString *requestScheme = fetchRequestURL.scheme;
+ BOOL isDataRequest = [requestScheme isEqual:@"data"];
+ if (isDataRequest) {
+ // NSURLSession does not support data URLs in background sessions.
+#if DEBUG
+ if (priorSessionIdentifier || self.sessionIdentifier) {
+ GTMSESSION_LOG_DEBUG(@"Converting background to foreground session for %@",
+ fetchRequest);
+ }
+#endif
+ // 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 whitelist them.
+ BOOL isSecure =
+ requestScheme != nil && [requestScheme caseInsensitiveCompare:@"https"] == NSOrderedSame;
+ if (!isSecure) {
+ BOOL allowRequest = NO;
+ NSString *host = fetchRequestURL.host;
+
+ // Check schemes first. A file scheme request may be allowed here, or as a localhost request.
+ for (NSString *allowedScheme in _allowedInsecureSchemes) {
+ if (requestScheme != nil &&
+ [requestScheme caseInsensitiveCompare:allowedScheme] == NSOrderedSame) {
+ allowRequest = YES;
+ break;
+ }
+ }
+ if (!allowRequest) {
+ // Check for localhost requests. Security checks only occur for non-https requests, so
+ // this check won't happen for an https request to localhost.
+ BOOL isLocalhostRequest = (host.length == 0 && [fetchRequestURL isFileURL]) || IsLocalhost(host);
+ if (isLocalhostRequest) {
+ if (self.allowLocalhostRequest) {
+ allowRequest = YES;
+ } else {
+ GTMSESSION_ASSERT_DEBUG(NO, @"Fetch request for localhost but fetcher"
+ @" allowLocalhostRequest is not set: %@", fetchRequestURL);
+ }
+ } else {
+ GTMSESSION_ASSERT_DEBUG(NO, @"Insecure fetch request has a scheme (%@)"
+ @" not found in fetcher allowedInsecureSchemes (%@): %@",
+ requestScheme, _allowedInsecureSchemes ?: @" @[] ", fetchRequestURL);
+ }
+ }
+
+ if (!allowRequest) {
+#if !DEBUG
+ NSLog(@"Insecure fetch disallowed for %@", fetchRequestURL.description ?: @"nil request URL");
+#endif
+ [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorInsecureRequest)];
+ return;
+ }
+ } // !isSecure
+ } // (requestURL != nil) && !isDataRequest
+
+ if (self.cookieStorage == nil) {
+ self.cookieStorage = [[self class] staticCookieStorage];
+ }
+
+ BOOL isRecreatingSession = (self.sessionIdentifier != nil) && (fetchRequest == nil);
+
+ self.canShareSession = !isRecreatingSession && !self.usingBackgroundSession;
+
+ if (!self.session && self.canShareSession) {
+ self.session = [_service sessionForFetcherCreation];
+ // If _session is nil, then the service's session creation semaphore will block
+ // until this fetcher invokes fetcherDidCreateSession: below, so this *must* invoke
+ // that method, even if the session fails to be created.
+ }
+
+ if (!self.session) {
+ // Create a session.
+ if (!_configuration) {
+ if (priorSessionIdentifier || self.usingBackgroundSession) {
+ NSString *sessionIdentifier = priorSessionIdentifier;
+ if (!sessionIdentifier) {
+ sessionIdentifier = [self createSessionIdentifierWithMetadata:nil];
+ }
+ NSMapTable *sessionIdentifierToFetcherMap = [[self class] sessionIdentifierToFetcherMap];
+ [sessionIdentifierToFetcherMap setObject:self forKey:self.sessionIdentifier];
+
+ if (@available(iOS 8.0, tvOS 9.0, watchOS 2.0, macOS 10.10, *)) {
+ _configuration =
+ [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessionIdentifier];
+ } else {
+#if ((!TARGET_OS_IPHONE && MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_10) \
+ || (TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0))
+ // If building with support for iOS 7 or < macOS 10.10, allow using the older
+ // -backgroundSessionConfiguration: method, otherwise leave it out to avoid deprecated
+ // API warnings/errors.
+ _configuration =
+ [NSURLSessionConfiguration backgroundSessionConfiguration:sessionIdentifier];
+#endif
+ }
+ 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, watchOS 6, macOS 10.15, *)) {
+#if TARGET_OS_IOS
+ // Early seeds of iOS 13 don't actually support the selector and several
+ // months later, those seeds are still in use, so validate if the selector
+ // is supported.
+ if ([_configuration respondsToSelector:@selector(setTLSMinimumSupportedProtocolVersion:)]) {
+ _configuration.TLSMinimumSupportedProtocolVersion = tls_protocol_version_TLSv12;
+ } else {
+ _configuration.TLSMinimumSupportedProtocol = kTLSProtocol12;
+ }
+#else
+ _configuration.TLSMinimumSupportedProtocolVersion = tls_protocol_version_TLSv12;
+#endif // TARGET_OS_IOS
+ } 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 = [_service sessionDelegate];
+ if (!delegate || !self.canShareSession) {
+ delegate = self;
+ }
+ self.session = [NSURLSession sessionWithConfiguration:_configuration
+ delegate:delegate
+ delegateQueue:self.sessionDelegateQueue];
+ GTMSESSION_ASSERT_DEBUG(self.session, @"Couldn't create session");
+
+ // Tell the service about the session created by this fetcher. This also signals the
+ // service's semaphore to allow other fetchers to request this session.
+ [_service fetcherDidCreateSession:self];
+
+ // If this assertion fires, the client probably tried to use a session identifier that was
+ // already used. The solution is to make the client use a unique identifier (or better yet let
+ // the session fetcher assign the identifier).
+ GTMSESSION_ASSERT_DEBUG(self.session.delegate == delegate, @"Couldn't assign delegate.");
+
+ if (self.session) {
+ BOOL isUsingSharedDelegate = (delegate != self);
+ if (!isUsingSharedDelegate) {
+ _shouldInvalidateSession = YES;
+ }
+ }
+ }
+
+ if (isRecreatingSession) {
+ _shouldInvalidateSession = YES;
+
+ // Let's make sure there are tasks still running or if not that we get a callback from a
+ // completed one; otherwise, we assume the tasks failed.
+ // This is the observed behavior perhaps 25% of the time within the Simulator running 7.0.3 on
+ // exiting the app after starting an upload and relaunching the app if we manage to relaunch
+ // after the task has completed, but before the system relaunches us in the background.
+ [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks,
+ NSArray *downloadTasks) {
+ if (dataTasks.count == 0 && uploadTasks.count == 0 && downloadTasks.count == 0) {
+ double const kDelayInSeconds = 1.0; // We should get progress indication or completion soon
+ dispatch_time_t checkForFeedbackDelay =
+ dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kDelayInSeconds * NSEC_PER_SEC));
+ dispatch_after(checkForFeedbackDelay, dispatch_get_main_queue(), ^{
+ if (!self.sessionTask && !fetchRequest) {
+ // If our task and/or request haven't been restored, then we assume task feedback lost.
+ [self removePersistedBackgroundSessionFromDefaults];
+ NSError *sessionError =
+ [NSError errorWithDomain:kGTMSessionFetcherErrorDomain
+ code:GTMSessionFetcherErrorBackgroundFetchFailed
+ userInfo:nil];
+ [self failToBeginFetchWithError:sessionError];
+ }
+ });
+ }
+ }];
+ return;
+ }
+
+ self.downloadedData = nil;
+ self.downloadedLength = 0;
+
+ if (_servicePriority == NSIntegerMin) {
+ mayDelay = NO;
+ }
+ if (mayDelay && _service) {
+ BOOL shouldFetchNow = [_service fetcherShouldBeginFetching:self];
+ if (!shouldFetchNow) {
+ // The fetch is deferred, but will happen later.
+ //
+ // If this session is held by the fetcher service, clear the session now so that we don't
+ // assume it's still valid after the fetcher is restarted.
+ if (self.canShareSession) {
+ self.session = nil;
+ }
+ return;
+ }
+ }
+
+ NSString *effectiveHTTPMethod = [fetchRequest valueForHTTPHeaderField:@"X-HTTP-Method-Override"];
+ if (effectiveHTTPMethod == nil) {
+ effectiveHTTPMethod = fetchRequest.HTTPMethod;
+ }
+ BOOL isEffectiveHTTPGet = (effectiveHTTPMethod == nil
+ || [effectiveHTTPMethod isEqual:@"GET"]);
+
+ BOOL needsUploadTask = (self.useUploadTask || self.bodyFileURL || self.bodyStreamProvider);
+ if (_bodyData || self.bodyStreamProvider || fetchRequest.HTTPBodyStream) {
+ if (isEffectiveHTTPGet) {
+ fetchRequest.HTTPMethod = @"POST";
+ isEffectiveHTTPGet = NO;
+ }
+
+ if (_bodyData) {
+ if (!needsUploadTask) {
+ fetchRequest.HTTPBody = _bodyData;
+ }
+#if !STRIP_GTM_FETCH_LOGGING
+ } else if (fetchRequest.HTTPBodyStream) {
+ if ([self respondsToSelector:@selector(loggedInputStreamForInputStream:)]) {
+ fetchRequest.HTTPBodyStream =
+ [self performSelector:@selector(loggedInputStreamForInputStream:)
+ withObject:fetchRequest.HTTPBodyStream];
+ }
+#endif
+ }
+ }
+
+ // We authorize after setting up the http method and body in the request
+ // because OAuth 1 may need to sign the request body
+ if (mayAuthorize && _authorizer && !isDataRequest) {
+ BOOL isAuthorized = [_authorizer isAuthorizedRequest:fetchRequest];
+ if (!isAuthorized) {
+ // Authorization needed.
+ //
+ // If this session is held by the fetcher service, clear the session now so that we don't
+ // assume it's still valid after authorization completes.
+ if (self.canShareSession) {
+ self.session = nil;
+ }
+
+ // Authorizing the request will recursively call this beginFetch:mayDelay:
+ // or failToBeginFetchWithError:.
+ [self authorizeRequest];
+ return;
+ }
+ }
+
+ // set the default upload or download retry interval, if necessary
+ if ([self isRetryEnabled] && self.maxRetryInterval <= 0) {
+ if (isEffectiveHTTPGet || [effectiveHTTPMethod isEqual:@"HEAD"]) {
+ [self setMaxRetryInterval:kDefaultMaxDownloadRetryInterval];
+ } else {
+ [self setMaxRetryInterval:kDefaultMaxUploadRetryInterval];
+ }
+ }
+
+ // finally, start the connection
+ NSURLSessionTask *newSessionTask;
+ BOOL needsDataAccumulator = NO;
+ if (_downloadResumeData) {
+ newSessionTask = [_session downloadTaskWithResumeData:_downloadResumeData];
+ GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask,
+ @"Failed downloadTaskWithResumeData for %@, resume data %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 * GTM_NONNULL_TYPE)_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) {
+ if (@available(iOS 8.0, macOS 10.10, *)) {
+ 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
+ __block UIBackgroundTaskIdentifier bgTaskID = [app beginBackgroundTaskWithName:bgTaskName
+ expirationHandler:^{
+ // Background task expiration callback - this block is always invoked by
+ // UIApplication on the main thread.
+ if (bgTaskID != UIBackgroundTaskInvalid) {
+ @synchronized(self) {
+ if (bgTaskID == self.backgroundTaskIdentifier) {
+ self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
+ }
+ }
+ [app endBackgroundTask:bgTaskID];
+ }
+ }];
+ @synchronized(self) {
+ self.backgroundTaskIdentifier = bgTaskID;
+ }
+ }
+#endif
+
+ if (!_initialRequestDate) {
+ _initialRequestDate = [[NSDate alloc] init];
+ }
+
+ // We don't expect to reach here even on retry or auth until a stop notification has been sent
+ // for the previous task, but we should ensure that we don't unbalance that.
+ GTMSESSION_ASSERT_DEBUG(!_isStopNotificationNeeded, @"Start notification without a prior stop");
+ [self sendStopNotificationIfNeeded];
+
+ [self addPersistedBackgroundSessionToDefaults];
+
+ [self setStopNotificationNeeded:YES];
+
+ [self postNotificationOnMainThreadWithName:kGTMSessionFetcherStartedNotification
+ userInfo:nil
+ requireAsync:NO];
+
+ // The service needs to know our task if it is serving as NSURLSession delegate.
+ [_service fetcherDidBeginFetching:self];
+
+ if (_testBlock) {
+#if !GTM_DISABLE_FETCHER_TEST_BLOCK
+ [self simulateFetchForTestBlock];
+#endif
+ } else {
+ // We resume the session task after posting the notification since the
+ // delegate callbacks may happen immediately if the fetch is started off
+ // the main thread or the session delegate queue is on a background thread,
+ // and we don't want to post a start notification after a premature finish
+ // of the session task.
+ [newSessionTask resume];
+ }
+}
+
+NSData * GTM_NULLABLE_TYPE GTMDataFromInputStream(NSInputStream *inputStream, NSError **outError) {
+ NSMutableData *data = [NSMutableData data];
+
+ [inputStream open];
+ NSInteger numberOfBytesRead = 0;
+ while ([inputStream hasBytesAvailable]) {
+ uint8_t buffer[512];
+ numberOfBytesRead = [inputStream read:buffer maxLength:sizeof(buffer)];
+ if (numberOfBytesRead > 0) {
+ [data appendBytes:buffer length:(NSUInteger)numberOfBytesRead];
+ } else {
+ break;
+ }
+ }
+ [inputStream close];
+ NSError *streamError = inputStream.streamError;
+
+ if (streamError) {
+ data = nil;
+ }
+ if (outError) {
+ *outError = streamError;
+ }
+ return data;
+}
+
+#if !GTM_DISABLE_FETCHER_TEST_BLOCK
+
+- (void)simulateFetchForTestBlock {
+ // This is invoked on the same thread as the beginFetch method was.
+ //
+ // Callbacks will all occur on the callback queue.
+ _testBlock(self, ^(NSURLResponse *response, NSData *responseData, NSError *error) {
+ // Callback from test block.
+ if (response == nil && responseData == nil && error == nil) {
+ // Assume the fetcher should execute rather than be tested.
+ self->_testBlock = nil;
+ self->_isUsingTestBlock = NO;
+ [self->_sessionTask resume];
+ return;
+ }
+
+ GTMSessionFetcherBodyStreamProvider bodyStreamProvider = self.bodyStreamProvider;
+ if (bodyStreamProvider) {
+ bodyStreamProvider(^(NSInputStream *bodyStream){
+ // Read from the input stream into an NSData buffer. We'll drain the stream
+ // explicitly on a background queue.
+ [self invokeOnCallbackQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)
+ afterUserStopped:NO
+ block:^{
+ NSError *streamError;
+ NSData *streamedData = GTMDataFromInputStream(bodyStream, &streamError);
+
+ dispatch_async(dispatch_get_main_queue(), ^{
+ // Continue callbacks on the main thread, since serial behavior
+ // is more reliable for tests.
+ [self simulateDataCallbacksForTestBlockWithBodyData:streamedData
+ response:response
+ responseData:responseData
+ error:(error ?: streamError)];
+ });
+ }];
+ });
+ } else {
+ // No input stream; use the supplied data or file URL.
+ NSURL *bodyFileURL = self.bodyFileURL;
+ if (bodyFileURL) {
+ NSError *readError;
+ self->_bodyData = [NSData dataWithContentsOfURL:bodyFileURL
+ options:NSDataReadingMappedIfSafe
+ error:&readError];
+ error = readError;
+ }
+
+ // No stream provider.
+
+ // In real fetches, nothing happens until the run loop spins, so apps have leeway to
+ // set callbacks after they call beginFetch. We'll mirror that fetcher behavior by
+ // delaying callbacks here at least to the next spin of the run loop. That keeps
+ // immediate, synchronous setting of callback blocks after beginFetch working in tests.
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [self simulateDataCallbacksForTestBlockWithBodyData:self->_bodyData
+ response:response
+ responseData:responseData
+ error:error];
+ });
+ }
+ });
+}
+
+- (void)simulateByteTransferReportWithDataLength:(int64_t)totalDataLength
+ block:(GTMSessionFetcherSendProgressBlock)block {
+ // This utility method simulates transfer progress with up to three callbacks.
+ // It is used to call back to any of the progress blocks.
+ int64_t sendReportSize = totalDataLength / 3 + 1;
+ int64_t totalSent = 0;
+ while (totalSent < totalDataLength) {
+ int64_t bytesRemaining = totalDataLength - totalSent;
+ sendReportSize = MIN(sendReportSize, bytesRemaining);
+ totalSent += sendReportSize;
+ [self invokeOnCallbackQueueUnlessStopped:^{
+ block(sendReportSize, totalSent, totalDataLength);
+ }];
+ }
+}
+
+- (void)simulateDataCallbacksForTestBlockWithBodyData:(NSData * GTM_NULLABLE_TYPE)bodyData
+ response:(NSURLResponse *)response
+ responseData:(NSData *)suppliedData
+ error:(NSError *)suppliedError {
+ __block NSData *responseData = suppliedData;
+ __block NSError *responseError = suppliedError;
+
+ // This method does the test simulation of callbacks once the upload
+ // and download data are known.
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ // Get copies of ivars we'll access in async invocations. This simulation assumes
+ // they won't change during fetcher execution.
+ NSURL *destinationFileURL = _destinationFileURL;
+ GTMSessionFetcherWillRedirectBlock willRedirectBlock = _willRedirectBlock;
+ GTMSessionFetcherDidReceiveResponseBlock didReceiveResponseBlock = _didReceiveResponseBlock;
+ GTMSessionFetcherSendProgressBlock sendProgressBlock = _sendProgressBlock;
+ GTMSessionFetcherDownloadProgressBlock downloadProgressBlock = _downloadProgressBlock;
+ GTMSessionFetcherAccumulateDataBlock accumulateDataBlock = _accumulateDataBlock;
+ GTMSessionFetcherReceivedProgressBlock receivedProgressBlock = _receivedProgressBlock;
+ GTMSessionFetcherWillCacheURLResponseBlock willCacheURLResponseBlock =
+ _willCacheURLResponseBlock;
+ GTMSessionFetcherChallengeBlock challengeBlock = _challengeBlock;
+
+ // Simulate receipt of redirection.
+ if (willRedirectBlock) {
+ [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES
+ block:^{
+ willRedirectBlock((NSHTTPURLResponse *)response, self->_request,
+ ^(NSURLRequest *redirectRequest) {
+ // For simulation, we'll assume the app will just continue.
+ });
+ }];
+ }
+
+ // If the fetcher has a challenge block, simulate a challenge.
+ //
+ // It might be nice to eventually let the user determine which testBlock
+ // fetches get challenged rather than always executing the supplied
+ // challenge block.
+ if (challengeBlock) {
+ [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES
+ block:^{
+ 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 * GTM_NULLABLE_TYPE credential){
+ // We could change the responseData and responseError based on the disposition,
+ // but it's easier for apps to just supply the expected data and error
+ // directly to the test block. So this simulation ignores the disposition.
+ });
+ }];
+ }
+
+ // Simulate receipt of an initial response.
+ if (response && didReceiveResponseBlock) {
+ [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES
+ block:^{
+ didReceiveResponseBlock(response, ^(NSURLSessionResponseDisposition desiredDisposition) {
+ // For simulation, we'll assume the disposition is to continue.
+ });
+ }];
+ }
+
+ // Simulate reporting send progress.
+ if (sendProgressBlock) {
+ [self simulateByteTransferReportWithDataLength:(int64_t)bodyData.length
+ block:^(int64_t bytesSent,
+ int64_t totalBytesSent,
+ int64_t totalBytesExpectedToSend) {
+ // This is invoked on the callback queue unless stopped.
+ sendProgressBlock(bytesSent, totalBytesSent, totalBytesExpectedToSend);
+ }];
+ }
+
+ if (destinationFileURL) {
+ // Simulate download to file progress.
+ if (downloadProgressBlock) {
+ [self simulateByteTransferReportWithDataLength:(int64_t)responseData.length
+ block:^(int64_t bytesDownloaded,
+ int64_t totalBytesDownloaded,
+ int64_t totalBytesExpectedToDownload) {
+ // This is invoked on the callback queue unless stopped.
+ downloadProgressBlock(bytesDownloaded, totalBytesDownloaded,
+ totalBytesExpectedToDownload);
+ }];
+ }
+
+ NSError *writeError;
+ [responseData writeToURL:destinationFileURL
+ options:NSDataWritingAtomic
+ error:&writeError];
+ if (writeError) {
+ // Tell the test code that writing failed.
+ responseError = writeError;
+ }
+ } else {
+ // Simulate download to NSData progress.
+ if ((accumulateDataBlock || receivedProgressBlock) && responseData) {
+ [self simulateByteTransferWithData:responseData
+ block:^(NSData *data,
+ int64_t bytesReceived,
+ int64_t totalBytesReceived,
+ int64_t totalBytesExpectedToReceive) {
+ // This is invoked on the callback queue unless stopped.
+ if (accumulateDataBlock) {
+ accumulateDataBlock(data);
+ }
+
+ if (receivedProgressBlock) {
+ receivedProgressBlock(bytesReceived, totalBytesReceived);
+ }
+ }];
+ }
+
+ if (!accumulateDataBlock) {
+ _downloadedData = [responseData mutableCopy];
+ }
+
+ if (willCacheURLResponseBlock) {
+ // Simulate letting the client inspect and alter the cached response.
+ NSData *cachedData = responseData ?: [[NSData alloc] init]; // Always have non-nil data.
+ NSCachedURLResponse *cachedResponse =
+ [[NSCachedURLResponse alloc] initWithResponse:response
+ data:cachedData];
+ [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES
+ block:^{
+ willCacheURLResponseBlock(cachedResponse, ^(NSCachedURLResponse *responseToCache){
+ // The app may provide an alternative response, or nil to defeat caching.
+ });
+ }];
+ }
+ }
+ _response = response;
+ } // @synchronized(self)
+
+ NSOperationQueue *queue = self.sessionDelegateQueue;
+ [queue addOperationWithBlock:^{
+ // Rather than invoke failToBeginFetchWithError: we want to simulate completion of
+ // a connection that started and ended, so we'll call down to finishWithError:
+ NSInteger status = responseError ? responseError.code : 200;
+ if (status >= 200 && status <= 399) {
+ [self finishWithError:nil shouldRetry:NO];
+ } else {
+ [self shouldRetryNowForStatus:status
+ error:responseError
+ forceAssumeRetry:NO
+ response:^(BOOL shouldRetry) {
+ [self finishWithError:responseError shouldRetry:shouldRetry];
+ }];
+ }
+ }];
+}
+
+- (void)simulateByteTransferWithData:(NSData *)responseData
+ block:(GTMSessionFetcherSimulateByteTransferBlock)transferBlock {
+ // This utility method simulates transfering data to the client. It divides the data into at most
+ // "chunkCount" chunks and then passes each chunk along with a progress update to transferBlock.
+ // This function can be used with accumulateDataBlock or receivedProgressBlock.
+
+ NSUInteger chunkCount = MAX(self.testBlockAccumulateDataChunkCount, (NSUInteger) 1);
+ NSUInteger totalDataLength = responseData.length;
+ NSUInteger sendDataSize = totalDataLength / chunkCount + 1;
+ NSUInteger totalSent = 0;
+ while (totalSent < totalDataLength) {
+ NSUInteger bytesRemaining = totalDataLength - totalSent;
+ sendDataSize = MIN(sendDataSize, bytesRemaining);
+ NSData *chunkData = [responseData subdataWithRange:NSMakeRange(totalSent, sendDataSize)];
+ totalSent += sendDataSize;
+ [self invokeOnCallbackQueueUnlessStopped:^{
+ transferBlock(chunkData,
+ (int64_t)sendDataSize,
+ (int64_t)totalSent,
+ (int64_t)totalDataLength);
+ }];
+ }
+}
+
+#endif // !GTM_DISABLE_FETCHER_TEST_BLOCK
+
+- (void)setSessionTask:(NSURLSessionTask *)sessionTask {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (_sessionTask != sessionTask) {
+ _sessionTask = sessionTask;
+ if (_sessionTask) {
+ // Request could be nil on restoring this fetcher from a background session.
+ if (!_request) {
+ _request = [_sessionTask.originalRequest mutableCopy];
+ }
+ }
+ }
+ } // @synchronized(self)
+}
+
+- (NSURLSessionTask * GTM_NULLABLE_TYPE)sessionTask {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _sessionTask;
+ } // @synchronized(self)
+}
+
++ (NSUserDefaults *)fetcherUserDefaults {
+ static NSUserDefaults *gFetcherUserDefaults = nil;
+
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ Class fetcherUserDefaultsClass = NSClassFromString(@"GTMSessionFetcherUserDefaultsFactory");
+ if (fetcherUserDefaultsClass) {
+ gFetcherUserDefaults = [fetcherUserDefaultsClass fetcherUserDefaults];
+ } else {
+ gFetcherUserDefaults = [NSUserDefaults standardUserDefaults];
+ }
+ });
+ return gFetcherUserDefaults;
+}
+
+- (void)addPersistedBackgroundSessionToDefaults {
+ NSString *sessionIdentifier = self.sessionIdentifier;
+ if (!sessionIdentifier) {
+ return;
+ }
+ NSArray *oldBackgroundSessions = [[self class] activePersistedBackgroundSessions];
+ if ([oldBackgroundSessions containsObject:_sessionIdentifier]) {
+ return;
+ }
+ NSMutableArray *newBackgroundSessions =
+ [NSMutableArray arrayWithArray:oldBackgroundSessions];
+ [newBackgroundSessions addObject:sessionIdentifier];
+ GTM_LOG_BACKGROUND_SESSION(@"Add to background sessions: %@", newBackgroundSessions);
+
+ NSUserDefaults *userDefaults = [[self class] fetcherUserDefaults];
+ [userDefaults setObject:newBackgroundSessions
+ forKey:kGTMSessionFetcherPersistedDestinationKey];
+ [userDefaults synchronize];
+}
+
+- (void)removePersistedBackgroundSessionFromDefaults {
+ NSString *sessionIdentifier = self.sessionIdentifier;
+ if (!sessionIdentifier) return;
+
+ NSArray *oldBackgroundSessions = [[self class] activePersistedBackgroundSessions];
+ if (!oldBackgroundSessions) {
+ return;
+ }
+ NSMutableArray *newBackgroundSessions =
+ [NSMutableArray arrayWithArray:oldBackgroundSessions];
+ NSUInteger sessionIndex = [newBackgroundSessions indexOfObject:sessionIdentifier];
+ if (sessionIndex == NSNotFound) {
+ return;
+ }
+ [newBackgroundSessions removeObjectAtIndex:sessionIndex];
+ GTM_LOG_BACKGROUND_SESSION(@"Remove from background sessions: %@", newBackgroundSessions);
+
+ NSUserDefaults *userDefaults = [[self class] fetcherUserDefaults];
+ if (newBackgroundSessions.count == 0) {
+ [userDefaults removeObjectForKey:kGTMSessionFetcherPersistedDestinationKey];
+ } else {
+ [userDefaults setObject:newBackgroundSessions
+ forKey:kGTMSessionFetcherPersistedDestinationKey];
+ }
+ [userDefaults synchronize];
+}
+
++ (GTM_NULLABLE NSArray *)activePersistedBackgroundSessions {
+ NSUserDefaults *userDefaults = [[self class] fetcherUserDefaults];
+ NSArray *oldBackgroundSessions =
+ [userDefaults arrayForKey:kGTMSessionFetcherPersistedDestinationKey];
+ if (oldBackgroundSessions.count == 0) {
+ return nil;
+ }
+ NSMutableArray *activeBackgroundSessions = nil;
+ NSMapTable *sessionIdentifierToFetcherMap = [self sessionIdentifierToFetcherMap];
+ for (NSString *sessionIdentifier in oldBackgroundSessions) {
+ GTMSessionFetcher *fetcher = [sessionIdentifierToFetcherMap objectForKey:sessionIdentifier];
+ if (fetcher) {
+ if (!activeBackgroundSessions) {
+ activeBackgroundSessions = [[NSMutableArray alloc] init];
+ }
+ [activeBackgroundSessions addObject:sessionIdentifier];
+ }
+ }
+ return activeBackgroundSessions;
+}
+
++ (NSArray *)fetchersForBackgroundSessions {
+ NSUserDefaults *userDefaults = [[self class] fetcherUserDefaults];
+ NSArray *backgroundSessions =
+ [userDefaults arrayForKey:kGTMSessionFetcherPersistedDestinationKey];
+ NSMapTable *sessionIdentifierToFetcherMap = [self sessionIdentifierToFetcherMap];
+ NSMutableArray *fetchers = [NSMutableArray array];
+ for (NSString *sessionIdentifier in backgroundSessions) {
+ GTMSessionFetcher *fetcher = [sessionIdentifierToFetcherMap objectForKey:sessionIdentifier];
+ if (!fetcher) {
+ fetcher = [self fetcherWithSessionIdentifier:sessionIdentifier];
+ GTMSESSION_ASSERT_DEBUG(fetcher != nil,
+ @"Unexpected invalid session identifier: %@", sessionIdentifier);
+ [fetcher beginFetchWithCompletionHandler:nil];
+ }
+ GTM_LOG_BACKGROUND_SESSION(@"%@ restoring session %@ by creating fetcher %@ %p",
+ [self class], sessionIdentifier, fetcher, fetcher);
+ if (fetcher != nil) {
+ [fetchers addObject:fetcher];
+ }
+ }
+ return fetchers;
+}
+
+#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
++ (void)application:(UIApplication *)application
+ handleEventsForBackgroundURLSession:(NSString *)identifier
+ completionHandler:(GTMSessionFetcherSystemCompletionHandler)completionHandler {
+ GTMSessionFetcher *fetcher = [self fetcherWithSessionIdentifier:identifier];
+ if (fetcher != nil) {
+ fetcher.systemCompletionHandler = completionHandler;
+ } else {
+ GTM_LOG_BACKGROUND_SESSION(@"%@ did not create background session identifier: %@",
+ [self class], identifier);
+ }
+}
+#endif
+
+- (NSString * GTM_NULLABLE_TYPE)sessionIdentifier {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _sessionIdentifier;
+ } // @synchronized(self)
+}
+
+- (void)setSessionIdentifier:(NSString *)sessionIdentifier {
+ GTMSESSION_ASSERT_DEBUG(sessionIdentifier != nil, @"Invalid session identifier");
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ GTMSESSION_ASSERT_DEBUG(!_session, @"Unable to set session identifier after session created");
+ _sessionIdentifier = [sessionIdentifier copy];
+ _usingBackgroundSession = YES;
+ _canShareSession = NO;
+ [self restoreDefaultStateForSessionIdentifierMetadata];
+ } // @synchronized(self)
+}
+
+- (void)setSessionIdentifierInternal:(GTM_NULLABLE NSString *)sessionIdentifier {
+ // This internal method only does a synchronized set of the session identifier.
+ // It does not have side effects on the background session, shared session, or
+ // session identifier metadata.
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _sessionIdentifier = [sessionIdentifier copy];
+ } // @synchronized(self)
+}
+
+- (NSDictionary * GTM_NULLABLE_TYPE)sessionUserInfo {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (_sessionUserInfo == nil) {
+ // We'll return the metadata dictionary with internal keys removed. This avoids the user
+ // re-using the userInfo dictionary later and accidentally including the internal keys.
+ NSMutableDictionary *metadata = [[self sessionIdentifierMetadataUnsynchronized] mutableCopy];
+ NSSet *keysToRemove = [metadata keysOfEntriesPassingTest:^BOOL(id key, id obj, BOOL *stop) {
+ return [key hasPrefix:@"_"];
+ }];
+ [metadata removeObjectsForKeys:[keysToRemove allObjects]];
+ if (metadata.count > 0) {
+ _sessionUserInfo = metadata;
+ }
+ }
+ return _sessionUserInfo;
+ } // @synchronized(self)
+}
+
+- (void)setSessionUserInfo:(NSDictionary * GTM_NULLABLE_TYPE)dictionary {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ GTMSESSION_ASSERT_DEBUG(_sessionIdentifier == nil, @"Too late to assign userInfo");
+ _sessionUserInfo = dictionary;
+ } // @synchronized(self)
+}
+
+- (GTM_NULLABLE NSDictionary *)sessionIdentifierDefaultMetadata {
+ GTMSessionCheckSynchronized(self);
+
+ NSMutableDictionary *defaultUserInfo = [[NSMutableDictionary alloc] init];
+ if (_destinationFileURL) {
+ defaultUserInfo[kGTMSessionIdentifierDestinationFileURLMetadataKey] =
+ [_destinationFileURL absoluteString];
+ }
+ if (_bodyFileURL) {
+ defaultUserInfo[kGTMSessionIdentifierBodyFileURLMetadataKey] = [_bodyFileURL absoluteString];
+ }
+ return (defaultUserInfo.count > 0) ? defaultUserInfo : nil;
+}
+
+- (void)restoreDefaultStateForSessionIdentifierMetadata {
+ GTMSessionCheckSynchronized(self);
+
+ NSDictionary *metadata = [self sessionIdentifierMetadataUnsynchronized];
+ NSString *destinationFileURLString = metadata[kGTMSessionIdentifierDestinationFileURLMetadataKey];
+ if (destinationFileURLString) {
+ _destinationFileURL = [NSURL URLWithString:destinationFileURLString];
+ GTM_LOG_BACKGROUND_SESSION(@"Restoring destination file URL: %@", _destinationFileURL);
+ }
+ NSString *bodyFileURLString = metadata[kGTMSessionIdentifierBodyFileURLMetadataKey];
+ if (bodyFileURLString) {
+ _bodyFileURL = [NSURL URLWithString:bodyFileURLString];
+ GTM_LOG_BACKGROUND_SESSION(@"Restoring body file URL: %@", _bodyFileURL);
+ }
+}
+
+- (NSDictionary * GTM_NULLABLE_TYPE)sessionIdentifierMetadata {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return [self sessionIdentifierMetadataUnsynchronized];
+ }
+}
+
+- (NSDictionary * GTM_NULLABLE_TYPE)sessionIdentifierMetadataUnsynchronized {
+ GTMSessionCheckSynchronized(self);
+
+ // Session Identifier format: "com.google.<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:(NSDictionary * GTM_NULLABLE_TYPE)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 * GTM_NONNULL_TYPE)_sessionUserInfo];
+
+ if (metadataToInclude) {
+ [metadataDict addEntriesFromDictionary:(NSDictionary *)metadataToInclude];
+ }
+ NSDictionary *defaultMetadataDict = [self sessionIdentifierDefaultMetadata];
+ if (defaultMetadataDict) {
+ [metadataDict addEntriesFromDictionary:defaultMetadataDict];
+ }
+ if (metadataDict.count > 0) {
+ NSData *metadataData = [NSJSONSerialization dataWithJSONObject:metadataDict
+ options:0
+ error:NULL];
+ GTMSESSION_ASSERT_DEBUG(metadataData != nil,
+ @"Session identifier user info failed to convert to JSON");
+ if (metadataData.length > 0) {
+ NSString *metadataString = [[NSString alloc] initWithData:metadataData
+ encoding:NSUTF8StringEncoding];
+ _sessionIdentifier =
+ [_sessionIdentifier stringByAppendingFormat:@"_%@", metadataString];
+ }
+ }
+ _didCreateSessionIdentifier = YES;
+ result = _sessionIdentifier;
+ } // @synchronized(self)
+ return result;
+}
+
+- (void)failToBeginFetchWithError:(NSError *)error {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _hasStoppedFetching = YES;
+ }
+
+ if (error == nil) {
+ error = [NSError errorWithDomain:kGTMSessionFetcherErrorDomain
+ code:GTMSessionFetcherErrorDownloadFailed
+ userInfo:nil];
+ }
+
+ [self invokeFetchCallbacksOnCallbackQueueWithData:nil
+ error:error];
+ [self releaseCallbacks];
+
+ [_service fetcherDidStop:self];
+
+ self.authorizer = nil;
+}
+
++ (GTMSessionCookieStorage *)staticCookieStorage {
+ static GTMSessionCookieStorage *gCookieStorage = nil;
+
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ gCookieStorage = [[GTMSessionCookieStorage alloc] init];
+ });
+ return gCookieStorage;
+}
+
+#if GTM_BACKGROUND_TASK_FETCHING
+
+- (void)endBackgroundTask {
+ // Whenever the connection stops or background execution expires,
+ // we need to tell UIApplication we're done.
+ UIBackgroundTaskIdentifier bgTaskID;
+ @synchronized(self) {
+ bgTaskID = self.backgroundTaskIdentifier;
+ if (bgTaskID != UIBackgroundTaskInvalid) {
+ self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
+ }
+ }
+
+ if (bgTaskID != UIBackgroundTaskInvalid) {
+ id<GTMUIApplicationProtocol> app = [[self class] fetcherUIApplication];
+ [app endBackgroundTask:bgTaskID];
+ }
+}
+
+#endif // GTM_BACKGROUND_TASK_FETCHING
+
+- (void)authorizeRequest {
+ GTMSessionCheckNotSynchronized(self);
+
+ id authorizer = self.authorizer;
+ SEL asyncAuthSel = @selector(authorizeRequest:delegate:didFinishSelector:);
+ if ([authorizer respondsToSelector:asyncAuthSel]) {
+ SEL callbackSel = @selector(authorizer:request:finishedWithError:);
+ NSMutableURLRequest *mutableRequest = [self.request mutableCopy];
+ [authorizer authorizeRequest:mutableRequest
+ delegate:self
+ didFinishSelector:callbackSel];
+ } else {
+ GTMSESSION_ASSERT_DEBUG(authorizer == nil, @"invalid authorizer for fetch");
+
+ // No authorizing possible, and authorizing happens only after any delay;
+ // just begin fetching
+ [self beginFetchMayDelay:NO
+ mayAuthorize:NO];
+ }
+}
+
+- (void)authorizer:(id<GTMFetcherAuthorizationProtocol>)auth
+ request:(NSMutableURLRequest *)authorizedRequest
+ finishedWithError:(NSError *)error {
+ GTMSessionCheckNotSynchronized(self);
+
+ if (error != nil) {
+ // We can't fetch without authorization
+ [self failToBeginFetchWithError:error];
+ } else {
+ @synchronized(self) {
+ _request = authorizedRequest;
+ }
+ [self beginFetchMayDelay:NO
+ mayAuthorize:NO];
+ }
+}
+
+
+- (BOOL)canFetchWithBackgroundSession {
+ // Subclasses may override.
+ return YES;
+}
+
+// Returns YES if the fetcher has been started and has not yet stopped.
+//
+// Fetching includes waiting for authorization or for retry, waiting to be allowed by the
+// service object to start the request, and actually fetching the request.
+- (BOOL)isFetching {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return [self isFetchingUnsynchronized];
+ }
+}
+
+- (BOOL)isFetchingUnsynchronized {
+ GTMSessionCheckSynchronized(self);
+
+ BOOL hasBegun = (_initialBeginFetchDate != nil);
+ return hasBegun && !_hasStoppedFetching;
+}
+
+- (NSURLResponse * GTM_NULLABLE_TYPE)response {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ NSURLResponse *response = [self responseUnsynchronized];
+ return response;
+ } // @synchronized(self)
+}
+
+- (NSURLResponse * GTM_NULLABLE_TYPE)responseUnsynchronized {
+ GTMSessionCheckSynchronized(self);
+
+ NSURLResponse *response = _sessionTask.response;
+ if (!response) response = _response;
+ return response;
+}
+
+- (NSInteger)statusCode {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ NSInteger statusCode = [self statusCodeUnsynchronized];
+ return statusCode;
+ } // @synchronized(self)
+}
+
+- (NSInteger)statusCodeUnsynchronized {
+ GTMSessionCheckSynchronized(self);
+
+ NSURLResponse *response = [self responseUnsynchronized];
+ NSInteger statusCode;
+
+ if ([response respondsToSelector:@selector(statusCode)]) {
+ statusCode = [(NSHTTPURLResponse *)response statusCode];
+ } else {
+ // Default to zero, in hopes of hinting "Unknown" (we can't be
+ // sure that things are OK enough to use 200).
+ statusCode = 0;
+ }
+ return statusCode;
+}
+
+- (NSDictionary * GTM_NULLABLE_TYPE)responseHeaders {
+ GTMSessionCheckNotSynchronized(self);
+
+ NSURLResponse *response = self.response;
+ if ([response respondsToSelector:@selector(allHeaderFields)]) {
+ NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
+ return headers;
+ }
+ return nil;
+}
+
+- (NSDictionary * GTM_NULLABLE_TYPE)responseHeadersUnsynchronized {
+ GTMSessionCheckSynchronized(self);
+
+ NSURLResponse *response = [self responseUnsynchronized];
+ if ([response respondsToSelector:@selector(allHeaderFields)]) {
+ NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
+ return headers;
+ }
+ return nil;
+}
+
+- (void)releaseCallbacks {
+ // Avoid releasing blocks in the sync section since objects dealloc'd by
+ // the blocks being released may call back into the fetcher or fetcher
+ // service.
+ dispatch_queue_t NS_VALID_UNTIL_END_OF_SCOPE holdCallbackQueue;
+ GTMSessionFetcherCompletionHandler NS_VALID_UNTIL_END_OF_SCOPE holdCompletionHandler;
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ holdCallbackQueue = _callbackQueue;
+ holdCompletionHandler = _completionHandler;
+
+ _callbackQueue = nil;
+ _completionHandler = nil; // Setter overridden in upload. Setter assumed to be used externally.
+ }
+
+ // Set local callback pointers to nil here rather than let them release at the end of the scope
+ // to make any problems due to the blocks being released be a bit more obvious in a stack trace.
+ holdCallbackQueue = nil;
+ holdCompletionHandler = nil;
+
+ self.configurationBlock = nil;
+ self.didReceiveResponseBlock = nil;
+ self.challengeBlock = nil;
+ self.willRedirectBlock = nil;
+ self.sendProgressBlock = nil;
+ self.receivedProgressBlock = nil;
+ self.downloadProgressBlock = nil;
+ self.accumulateDataBlock = nil;
+ self.willCacheURLResponseBlock = nil;
+ self.retryBlock = nil;
+ self.testBlock = nil;
+ self.resumeDataBlock = nil;
+ if (@available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.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.
+ _userStoppedFetching = YES;
+ } // @synchronized(self)
+ [self stopFetchReleasingCallbacks:YES];
+}
+
+// Cancel the fetch of the URL that's currently in progress.
+//
+// If shouldReleaseCallbacks is NO then the fetch will be retried so the callbacks
+// need to still be retained.
+- (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks {
+ [self removePersistedBackgroundSessionFromDefaults];
+
+ id<GTMSessionFetcherServiceProtocol> service;
+ NSMutableURLRequest *request;
+
+ // If the task or the retry timer is all that's retaining the fetcher,
+ // we want to be sure this instance survives stopping at least long enough for
+ // the stack to unwind.
+ __autoreleasing GTMSessionFetcher *holdSelf = self;
+
+ BOOL hasCanceledTask = NO;
+
+ [holdSelf destroyRetryTimer];
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _hasStoppedFetching = YES;
+
+ service = _service;
+ request = _request;
+
+ if (_sessionTask) {
+ // In case cancelling the task or session calls this recursively, we want
+ // to ensure that we'll only release the task and delegate once,
+ // so first set _sessionTask to nil
+ //
+ // This may be called in a callback from the task, so use autorelease to avoid
+ // releasing the task in its own callback.
+ __autoreleasing NSURLSessionTask *oldTask = _sessionTask;
+ if (!_isUsingTestBlock) {
+ _response = _sessionTask.response;
+ }
+ _sessionTask = nil;
+
+ if ([oldTask state] != NSURLSessionTaskStateCompleted) {
+ // For download tasks, when the fetch is stopped, we may provide resume data that can
+ // be used to create a new session.
+ BOOL mayResume = (_resumeDataBlock
+ && [oldTask respondsToSelector:@selector(cancelByProducingResumeData:)]);
+ if (!mayResume) {
+ [oldTask cancel];
+ // A side effect of stopping the task is that URLSession:task:didCompleteWithError:
+ // will be invoked asynchronously on the delegate queue.
+ } else {
+ void (^resumeBlock)(NSData *) = _resumeDataBlock;
+ _resumeDataBlock = nil;
+
+ // Save callbackQueue since releaseCallbacks clears it.
+ dispatch_queue_t callbackQueue = _callbackQueue;
+ dispatch_group_enter(_callbackGroup);
+ [(NSURLSessionDownloadTask *)oldTask cancelByProducingResumeData:^(NSData *resumeData) {
+ [self invokeOnCallbackQueue:callbackQueue
+ afterUserStopped:YES
+ block:^{
+ resumeBlock(resumeData);
+ dispatch_group_leave(self->_callbackGroup);
+ }];
+ }];
+ }
+ hasCanceledTask = YES;
+ }
+ }
+
+ // If the task was canceled, wait until the URLSession:task:didCompleteWithError: to call
+ // finishTasksAndInvalidate, since calling it immediately tends to crash, see radar 18471901.
+ if (_session) {
+ BOOL shouldInvalidate = _shouldInvalidateSession;
+#if TARGET_OS_IPHONE
+ // Don't invalidate if we've got a systemCompletionHandler, since
+ // URLSessionDidFinishEventsForBackgroundURLSession: won't be called if invalidated.
+ shouldInvalidate = shouldInvalidate && !self.systemCompletionHandler;
+#endif
+ if (shouldInvalidate) {
+ __autoreleasing NSURLSession *oldSession = _session;
+ _session = nil;
+
+ if (!hasCanceledTask) {
+ [oldSession finishTasksAndInvalidate];
+ } else {
+ _sessionNeedingInvalidation = oldSession;
+ }
+ }
+ }
+ } // @synchronized(self)
+
+ // send the stopped notification
+ [self sendStopNotificationIfNeeded];
+
+ [_authorizer stopAuthorizationForRequest:request];
+
+ if (shouldReleaseCallbacks) {
+ [self releaseCallbacks];
+
+ self.authorizer = nil;
+ }
+
+ [service fetcherDidStop:self];
+
+#if GTM_BACKGROUND_TASK_FETCHING
+ [self endBackgroundTask];
+#endif
+}
+
+- (void)setStopNotificationNeeded:(BOOL)flag {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _isStopNotificationNeeded = flag;
+ } // @synchronized(self)
+}
+
+- (void)sendStopNotificationIfNeeded {
+ BOOL sendNow = NO;
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (_isStopNotificationNeeded) {
+ _isStopNotificationNeeded = NO;
+ sendNow = YES;
+ }
+ } // @synchronized(self)
+
+ if (sendNow) {
+ [self postNotificationOnMainThreadWithName:kGTMSessionFetcherStoppedNotification
+ userInfo:nil
+ requireAsync:NO];
+ }
+}
+
+- (void)retryFetch {
+ [self stopFetchReleasingCallbacks:NO];
+
+ // A retry will need a configuration with a fresh session identifier.
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (_sessionIdentifier && _didCreateSessionIdentifier) {
+ [self forgetSessionIdentifierForFetcher];
+ _configuration = nil;
+ }
+
+ if (_canShareSession) {
+ // Force a grab of the current session from the fetcher service in case
+ // the service's old one has become invalid.
+ _session = nil;
+ }
+ } // @synchronized(self)
+
+ [self beginFetchForRetry];
+}
+
+- (BOOL)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds {
+ // Uncovered in upload fetcher testing, because the chunk fetcher is being waited on, and gets
+ // released by the upload code. The uploader just holds onto it with an ivar, and that gets
+ // nilled in the chunk fetcher callback.
+ // Used once in while loop just to avoid unused variable compiler warning.
+ __autoreleasing GTMSessionFetcher *holdSelf = self;
+
+ NSDate *giveUpDate = [NSDate dateWithTimeIntervalSinceNow:timeoutInSeconds];
+
+ BOOL shouldSpinRunLoop = ([NSThread isMainThread] &&
+ (!self.callbackQueue
+ || self.callbackQueue == dispatch_get_main_queue()));
+ BOOL expired = NO;
+
+ // Loop until the callbacks have been called and released, and until
+ // the connection is no longer pending, until there are no callback dispatches
+ // in flight, or until the timeout has expired.
+ int64_t delta = (int64_t)(100 * NSEC_PER_MSEC); // 100 ms
+ while (1) {
+ BOOL isTaskInProgress = (holdSelf->_sessionTask
+ && [_sessionTask state] != NSURLSessionTaskStateCompleted);
+ BOOL needsToCallCompletion = (_completionHandler != nil);
+ BOOL isCallbackInProgress = (_callbackGroup
+ && dispatch_group_wait(_callbackGroup, dispatch_time(DISPATCH_TIME_NOW, delta)));
+
+ if (!isTaskInProgress && !needsToCallCompletion && !isCallbackInProgress) break;
+
+ expired = ([giveUpDate timeIntervalSinceNow] < 0);
+ if (expired) {
+ GTMSESSION_LOG_DEBUG(@"GTMSessionFetcher waitForCompletionWithTimeout:%0.1f expired -- "
+ @"%@%@%@", timeoutInSeconds,
+ isTaskInProgress ? @"taskInProgress " : @"",
+ needsToCallCompletion ? @"needsToCallCompletion " : @"",
+ isCallbackInProgress ? @"isCallbackInProgress" : @"");
+ break;
+ }
+
+ // Run the current run loop 1/1000 of a second to give the networking
+ // code a chance to work
+ const NSTimeInterval kSpinInterval = 0.001;
+ if (shouldSpinRunLoop) {
+ NSDate *stopDate = [NSDate dateWithTimeIntervalSinceNow:kSpinInterval];
+ [[NSRunLoop currentRunLoop] runUntilDate:stopDate];
+ } else {
+ [NSThread sleepForTimeInterval:kSpinInterval];
+ }
+ }
+ return !expired;
+}
+
++ (void)setGlobalTestBlock:(GTMSessionFetcherTestBlock GTM_NULLABLE_TYPE)block {
+#if GTM_DISABLE_FETCHER_TEST_BLOCK
+ GTMSESSION_ASSERT_DEBUG(block == nil, @"test blocks disabled");
+#endif
+ gGlobalTestBlock = [block copy];
+}
+
+#if GTM_BACKGROUND_TASK_FETCHING
+
+static GTM_NULLABLE_TYPE id<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 * GTM_NULLABLE_TYPE))handler {
+ [self setSessionTask:task];
+ GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ task:%@ willPerformHTTPRedirection:%@ newRequest:%@",
+ [self class], self, session, task, redirectResponse, redirectRequest);
+
+ if ([self userStoppedFetching]) {
+ handler(nil);
+ return;
+ }
+ if (redirectRequest && redirectResponse) {
+ // Copy the original request, including the body.
+ NSURLRequest *originalRequest = self.request;
+ NSMutableURLRequest *newRequest = [originalRequest mutableCopy];
+
+ // 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);
+ [self invokeOnCallbackQueueAfterUserStopped:YES
+ block:^{
+ willRedirectBlock(redirectResponse, redirectRequest, ^(NSURLRequest *clientRequest) {
+
+ // Update the request for future logging.
+ [self updateMutableRequest:[clientRequest mutableCopy]];
+
+ handler(clientRequest);
+ });
+ }];
+ } // @synchronized(self)
+ return;
+ }
+ // Continues here if the client did not provide a redirect block.
+
+ // Update the request for future logging.
+ [self updateMutableRequest:[redirectRequest mutableCopy]];
+ }
+ handler(redirectRequest);
+}
+
+- (void)URLSession:(NSURLSession *)session
+ dataTask:(NSURLSessionDataTask *)dataTask
+didReceiveResponse:(NSURLResponse *)response
+ completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))handler {
+ [self setSessionTask:dataTask];
+ GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ dataTask:%@ didReceiveResponse:%@",
+ [self class], self, session, dataTask, response);
+ void (^accumulateAndFinish)(NSURLSessionResponseDisposition) =
+ ^(NSURLSessionResponseDisposition dispositionValue) {
+ // This method is called when the server has determined that it
+ // has enough information to create the NSURLResponse
+ // it can be called multiple times, for example in the case of a
+ // redirect, so each time we reset the data.
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ BOOL hadPreviousData = self->_downloadedLength > 0;
+
+ [self->_downloadedData setLength:0];
+ self->_downloadedLength = 0;
+
+ if (hadPreviousData && (dispositionValue != NSURLSessionResponseCancel)) {
+ // Tell the accumulate block to discard prior data.
+ GTMSessionFetcherAccumulateDataBlock accumulateBlock = self->_accumulateDataBlock;
+ if (accumulateBlock) {
+ [self invokeOnCallbackQueueUnlessStopped:^{
+ accumulateBlock(nil);
+ }];
+ }
+ }
+ } // @synchronized(self)
+ handler(dispositionValue);
+ };
+
+ GTMSessionFetcherDidReceiveResponseBlock receivedResponseBlock;
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ receivedResponseBlock = _didReceiveResponseBlock;
+ if (receivedResponseBlock) {
+ // We will ultimately need to call back to NSURLSession's handler with the disposition value
+ // for this delegate method even if the user has stopped the fetcher.
+ [self invokeOnCallbackQueueAfterUserStopped:YES
+ block:^{
+ receivedResponseBlock(response, ^(NSURLSessionResponseDisposition desiredDisposition) {
+ accumulateAndFinish(desiredDisposition);
+ });
+ }];
+ }
+ } // @synchronized(self)
+
+ if (receivedResponseBlock == nil) {
+ accumulateAndFinish(NSURLSessionResponseAllow);
+ }
+}
+
+- (void)URLSession:(NSURLSession *)session
+ dataTask:(NSURLSessionDataTask *)dataTask
+didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask {
+ GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ dataTask:%@ didBecomeDownloadTask:%@",
+ [self class], self, session, dataTask, downloadTask);
+ [self setSessionTask:downloadTask];
+}
+
+
+- (void)URLSession:(NSURLSession *)session
+ task:(NSURLSessionTask *)task
+didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
+ completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition,
+ NSURLCredential * GTM_NULLABLE_TYPE credential))handler {
+ [self setSessionTask:task];
+ GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ task:%@ didReceiveChallenge:%@",
+ [self class], self, session, task, challenge);
+
+ GTMSessionFetcherChallengeBlock challengeBlock = self.challengeBlock;
+ if (challengeBlock) {
+ // The fetcher user has provided custom challenge handling.
+ //
+ // We will ultimately need to call back to NSURLSession's handler with the disposition value
+ // for this delegate method even if the user has stopped the fetcher.
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ [self invokeOnCallbackQueueAfterUserStopped:YES
+ block:^{
+ challengeBlock(self, challenge, handler);
+ }];
+ }
+ } else {
+ // No challenge block was provided by the client.
+ [self respondToChallenge:challenge
+ completionHandler:handler];
+ }
+}
+
+- (void)respondToChallenge:(NSURLAuthenticationChallenge *)challenge
+ completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition,
+ NSURLCredential * GTM_NULLABLE_TYPE credential))handler {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ NSInteger previousFailureCount = [challenge previousFailureCount];
+ if (previousFailureCount <= 2) {
+ NSURLProtectionSpace *protectionSpace = [challenge protectionSpace];
+ NSString *authenticationMethod = [protectionSpace authenticationMethod];
+ if ([authenticationMethod isEqual:NSURLAuthenticationMethodServerTrust]) {
+ // SSL.
+ //
+ // Background sessions seem to require an explicit check of the server trust object
+ // rather than default handling.
+ SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
+ if (serverTrust == NULL) {
+ // No server trust information is available.
+ handler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
+ } else {
+ // Server trust information is available.
+ void (^callback)(SecTrustRef, BOOL) = ^(SecTrustRef trustRef, BOOL allow){
+ if (allow) {
+ NSURLCredential *trustCredential = [NSURLCredential credentialForTrust:trustRef];
+ handler(NSURLSessionAuthChallengeUseCredential, trustCredential);
+ } else {
+ GTMSESSION_LOG_DEBUG(@"Cancelling authentication challenge for %@", self->_request.URL);
+ handler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
+ }
+ };
+ if (_allowInvalidServerCertificates) {
+ callback(serverTrust, YES);
+ } else {
+ [[self class] evaluateServerTrust:serverTrust
+ forRequest:_request
+ completionHandler:callback];
+ }
+ }
+ return;
+ }
+
+ NSURLCredential *credential = _credential;
+
+ if ([[challenge protectionSpace] isProxy] && _proxyCredential != nil) {
+ credential = _proxyCredential;
+ }
+
+ if (credential) {
+ handler(NSURLSessionAuthChallengeUseCredential, credential);
+ } else {
+ // The credential is still nil; tell the OS to use the default handling. This is needed
+ // for things that can come out of the keychain (proxies, client certificates, etc.).
+ //
+ // Note: Looking up a credential with NSURLCredentialStorage's
+ // defaultCredentialForProtectionSpace: is *not* the same invoking the handler with
+ // NSURLSessionAuthChallengePerformDefaultHandling. In the case of
+ // NSURLAuthenticationMethodClientCertificate, you can get nil back from
+ // NSURLCredentialStorage, while using this code path instead works.
+ handler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
+ }
+
+ } else {
+ // We've failed auth 3 times. The completion handler will be called with code
+ // NSURLErrorCancelled.
+ handler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
+ }
+ } // @synchronized(self)
+}
+
+// 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.
++ (GTM_NULLABLE NSURL *)redirectURLWithOriginalRequestURL:(GTM_NULLABLE NSURL *)originalRequestURL
+ redirectRequestURL:(GTM_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) {
+ NSDate *serviceStoppedAllDate = [self->_service stoppedAllFetchersDate];
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ // Avoid a race between stopFetching and the callback.
+ if (self->_userStoppedFetching) {
+ return;
+ }
+
+ // Also avoid calling back if the service has stopped all fetchers
+ // since this one was created. The fetcher may have stopped before
+ // stopAllFetchers was invoked, so _userStoppedFetching wasn't set,
+ // but the app still won't expect the callback to fire after
+ // the service's stopAllFetchers was invoked.
+ if (serviceStoppedAllDate
+ && [self->_initialBeginFetchDate compare:serviceStoppedAllDate] != NSOrderedDescending) {
+ // stopAllFetchers was called after this fetcher began.
+ return;
+ }
+ } // @synchronized(self)
+ }
+ block();
+ });
+ }
+}
+
+- (void)invokeFetchCallbacksOnCallbackQueueWithData:(GTM_NULLABLE NSData *)data
+ error:(GTM_NULLABLE NSError *)error {
+ // Callbacks will be released in the method stopFetchReleasingCallbacks:
+ GTMSessionFetcherCompletionHandler handler;
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ handler = _completionHandler;
+
+ if (handler) {
+ [self invokeOnCallbackQueueUnlessStopped:^{
+ handler(data, error);
+
+ // Post a notification, primarily to allow code to collect responses for
+ // testing.
+ //
+ // The observing code is not likely on the fetcher's callback
+ // queue, so this posts explicitly to the main queue.
+ NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
+ if (data) {
+ userInfo[kGTMSessionFetcherCompletionDataKey] = data;
+ }
+ if (error) {
+ userInfo[kGTMSessionFetcherCompletionErrorKey] = error;
+ }
+ [self postNotificationOnMainThreadWithName:kGTMSessionFetcherCompletionInvokedNotification
+ userInfo:userInfo
+ requireAsync:NO];
+ }];
+ }
+ } // @synchronized(self)
+}
+
+- (void)postNotificationOnMainThreadWithName:(NSString *)noteName
+ userInfo:(GTM_NULLABLE NSDictionary *)userInfo
+ requireAsync:(BOOL)requireAsync {
+ dispatch_block_t postBlock = ^{
+ [[NSNotificationCenter defaultCenter] postNotificationName:noteName
+ object:self
+ userInfo:userInfo];
+ };
+
+ if ([NSThread isMainThread] && !requireAsync) {
+ // Post synchronously for compatibility with older code using the fetcher.
+
+ // Avoid calling out to other code from inside a sync block to avoid risk
+ // of a deadlock or of recursive sync.
+ GTMSessionCheckNotSynchronized(self);
+
+ postBlock();
+ } else {
+ dispatch_async(dispatch_get_main_queue(), postBlock);
+ }
+}
+
+- (void)URLSession:(NSURLSession *)session
+ task:(NSURLSessionTask *)uploadTask
+ needNewBodyStream:(void (^)(NSInputStream * GTM_NULLABLE_TYPE bodyStream))completionHandler {
+ [self setSessionTask:uploadTask];
+ GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ task:%@ needNewBodyStream:",
+ [self class], self, session, uploadTask);
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ GTMSessionFetcherBodyStreamProvider provider = _bodyStreamProvider;
+#if !STRIP_GTM_FETCH_LOGGING
+ if ([self respondsToSelector:@selector(loggedStreamProviderForStreamProvider:)]) {
+ provider = [self performSelector:@selector(loggedStreamProviderForStreamProvider:)
+ withObject:provider];
+ }
+#endif
+ if (provider) {
+ [self invokeOnCallbackQueueUnlessStopped:^{
+ provider(completionHandler);
+ }];
+ } else {
+ GTMSESSION_ASSERT_DEBUG(NO, @"NSURLSession expects a stream provider");
+
+ completionHandler(nil);
+ }
+ } // @synchronized(self)
+}
+
+- (void)URLSession:(NSURLSession *)session
+ task:(NSURLSessionTask *)task
+ didSendBodyData:(int64_t)bytesSent
+ totalBytesSent:(int64_t)totalBytesSent
+totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
+ [self setSessionTask:task];
+ GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ task:%@ didSendBodyData:%lld"
+ @" totalBytesSent:%lld totalBytesExpectedToSend:%lld",
+ [self class], self, session, task, bytesSent, totalBytesSent,
+ totalBytesExpectedToSend);
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (!_sendProgressBlock) {
+ return;
+ }
+ // We won't hold on to send progress block; it's ok to not send it if the upload finishes.
+ [self invokeOnCallbackQueueUnlessStopped:^{
+ GTMSessionFetcherSendProgressBlock progressBlock;
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ progressBlock = self->_sendProgressBlock;
+ }
+ if (progressBlock) {
+ progressBlock(bytesSent, totalBytesSent, totalBytesExpectedToSend);
+ }
+ }];
+ } // @synchronized(self)
+}
+
+- (void)URLSession:(NSURLSession *)session
+ dataTask:(NSURLSessionDataTask *)dataTask
+ didReceiveData:(NSData *)data {
+ [self setSessionTask:dataTask];
+ NSUInteger bufferLength = data.length;
+ GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ dataTask:%@ didReceiveData:%p (%llu bytes)",
+ [self class], self, session, dataTask, data,
+ (unsigned long long)bufferLength);
+ if (bufferLength == 0) {
+ // Observed on completing an out-of-process upload.
+ return;
+ }
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ GTMSessionFetcherAccumulateDataBlock accumulateBlock = _accumulateDataBlock;
+ if (accumulateBlock) {
+ // Let the client accumulate the data.
+ _downloadedLength += bufferLength;
+ [self invokeOnCallbackQueueUnlessStopped:^{
+ accumulateBlock(data);
+ }];
+ } else if (!_userStoppedFetching) {
+ // Append to the mutable data buffer unless the fetch has been cancelled.
+
+ // Resumed upload tasks may not yet have a data buffer.
+ if (_downloadedData == nil) {
+ // Using NSClassFromString for iOS 6 compatibility.
+ GTMSESSION_ASSERT_DEBUG(
+ ![dataTask isKindOfClass:NSClassFromString(@"NSURLSessionDownloadTask")],
+ @"Resumed download tasks should not receive data bytes");
+ _downloadedData = [[NSMutableData alloc] init];
+ }
+
+ [_downloadedData appendData:data];
+ _downloadedLength = (int64_t)_downloadedData.length;
+
+ // We won't hold on to receivedProgressBlock here; it's ok to not send
+ // it if the transfer finishes.
+ if (_receivedProgressBlock) {
+ [self invokeOnCallbackQueueUnlessStopped:^{
+ GTMSessionFetcherReceivedProgressBlock progressBlock;
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ progressBlock = self->_receivedProgressBlock;
+ }
+ if (progressBlock) {
+ progressBlock((int64_t)bufferLength, self->_downloadedLength);
+ }
+ }];
+ }
+ }
+ } // @synchronized(self)
+}
+
+- (void)URLSession:(NSURLSession *)session
+ dataTask:(NSURLSessionDataTask *)dataTask
+ willCacheResponse:(NSCachedURLResponse *)proposedResponse
+ completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {
+ GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ dataTask:%@ willCacheResponse:%@ %@",
+ [self class], self, session, dataTask,
+ proposedResponse, proposedResponse.response);
+ GTMSessionFetcherWillCacheURLResponseBlock callback;
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ callback = _willCacheURLResponseBlock;
+
+ if (callback) {
+ [self invokeOnCallbackQueueAfterUserStopped:YES
+ block:^{
+ callback(proposedResponse, completionHandler);
+ }];
+ }
+ } // @synchronized(self)
+ if (!callback) {
+ completionHandler(proposedResponse);
+ }
+}
+
+
+- (void)URLSession:(NSURLSession *)session
+ downloadTask:(NSURLSessionDownloadTask *)downloadTask
+ didWriteData:(int64_t)bytesWritten
+ totalBytesWritten:(int64_t)totalBytesWritten
+totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
+ GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ downloadTask:%@ didWriteData:%lld"
+ @" bytesWritten:%lld totalBytesExpectedToWrite:%lld",
+ [self class], self, session, downloadTask, bytesWritten,
+ totalBytesWritten, totalBytesExpectedToWrite);
+ [self setSessionTask:downloadTask];
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if ((totalBytesExpectedToWrite != NSURLSessionTransferSizeUnknown) &&
+ (totalBytesExpectedToWrite < totalBytesWritten)) {
+ // Have observed cases were bytesWritten == totalBytesExpectedToWrite,
+ // but totalBytesWritten > totalBytesExpectedToWrite, so setting to unkown in these cases.
+ totalBytesExpectedToWrite = NSURLSessionTransferSizeUnknown;
+ }
+
+ 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 {
+ GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ downloadTask:%@ didResumeAtOffset:%lld"
+ @" expectedTotalBytes:%lld",
+ [self class], self, session, downloadTask, fileOffset,
+ expectedTotalBytes);
+ [self setSessionTask:downloadTask];
+}
+
+- (void)URLSession:(NSURLSession *)session
+ downloadTask:(NSURLSessionDownloadTask *)downloadTask
+didFinishDownloadingToURL:(NSURL *)downloadLocationURL {
+ // Download may have relaunched app, so update _sessionTask.
+ [self setSessionTask:downloadTask];
+ GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ downloadTask:%@ didFinishDownloadingToURL:%@",
+ [self class], self, session, downloadTask, downloadLocationURL);
+ NSNumber *fileSizeNum;
+ [downloadLocationURL getResourceValue:&fileSizeNum
+ forKey:NSURLFileSizeKey
+ error:NULL];
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ NSURL *destinationURL = _destinationFileURL;
+
+ _downloadedLength = fileSizeNum.longLongValue;
+
+ // Overwrite any previous file at the destination URL.
+ NSFileManager *fileMgr = [NSFileManager defaultManager];
+ NSError *removeError;
+ if (![fileMgr removeItemAtURL:destinationURL error:&removeError]
+ && removeError.code != NSFileNoSuchFileError) {
+ GTMSESSION_LOG_DEBUG(@"Could not remove previous file at %@ due to %@",
+ downloadLocationURL.path, removeError);
+ }
+
+ NSInteger statusCode = [self statusCodeUnsynchronized];
+ if (statusCode < 200 || statusCode > 399) {
+ // In OS X 10.11, the response body is written to a file even on a server
+ // status error. For convenience of the fetcher client, we'll skip saving the
+ // downloaded body to the destination URL so that clients do not need to know
+ // to delete the file following fetch errors.
+ 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];
+ GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ task:%@ didCompleteWithError:%@",
+ [self class], self, session, task, error);
+
+ NSInteger status = self.statusCode;
+ BOOL forceAssumeRetry = NO;
+ BOOL succeeded = NO;
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+#if !GTM_DISABLE_FETCHER_TEST_BLOCK
+ // The task is never resumed when a testBlock is used. When the session is destroyed,
+ // we should ignore the callback, since the testBlock support code itself invokes
+ // shouldRetryNowForStatus: and finishWithError:shouldRetry:
+ if (_isUsingTestBlock) return;
+#endif
+
+ if (error == nil) {
+ error = _downloadFinishedError;
+ }
+ succeeded = (error == nil && status >= 0 && status < 300);
+ if (succeeded) {
+ // Succeeded.
+ _bodyLength = task.countOfBytesSent;
+ }
+ } // @synchronized(self)
+
+ if (succeeded) {
+ [self finishWithError:nil shouldRetry:NO];
+ return;
+ }
+ // For background redirects, no delegate method is called, so we cannot restore a stripped
+ // Authorization header, so if a 403 ("Forbidden") was generated due to a missing OAuth 2 header,
+ // set the current request's URL to the redirected URL, so we in effect restore the Authorization
+ // header.
+ if ((status == 403) && self.usingBackgroundSession) {
+ NSURL *redirectURL = self.response.URL;
+ NSURLRequest *request = self.request;
+ if (![request.URL isEqual:redirectURL]) {
+ NSString *authorizationHeader = [request.allHTTPHeaderFields objectForKey:@"Authorization"];
+ if (authorizationHeader != nil) {
+ NSMutableURLRequest *mutableRequest = [request mutableCopy];
+ mutableRequest.URL = redirectURL;
+ [self updateMutableRequest:mutableRequest];
+ // Avoid assuming the session is still valid.
+ self.session = nil;
+ forceAssumeRetry = YES;
+ }
+ }
+ }
+
+ // If invalidating the session was deferred in stopFetchReleasingCallbacks: then do it now.
+ NSURLSession *oldSession = self.sessionNeedingInvalidation;
+ if (oldSession) {
+ [self setSessionNeedingInvalidation:NULL];
+ [oldSession finishTasksAndInvalidate];
+ }
+
+ // Failed.
+ [self shouldRetryNowForStatus:status
+ error:error
+ forceAssumeRetry:forceAssumeRetry
+ response:^(BOOL shouldRetry) {
+ [self finishWithError:error shouldRetry:shouldRetry];
+ }];
+}
+
+- (void)URLSession:(NSURLSession *)session
+ task:(NSURLSessionTask *)task
+ didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics
+ API_AVAILABLE(ios(10.0), macosx(10.12), tvos(10.0), watchos(3.0)) {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+ GTMSessionFetcherMetricsCollectionBlock metricsCollectionBlock = _metricsCollectionBlock;
+ if (metricsCollectionBlock) {
+ [self invokeOnCallbackQueueUnlessStopped:^{
+ metricsCollectionBlock(metrics);
+ }];
+ }
+ }
+}
+
+#if TARGET_OS_IPHONE
+- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
+ GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSessionDidFinishEventsForBackgroundURLSession:%@",
+ [self class], self, session);
+ [self removePersistedBackgroundSessionFromDefaults];
+
+ GTMSessionFetcherSystemCompletionHandler handler;
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ handler = self.systemCompletionHandler;
+ self.systemCompletionHandler = nil;
+ } // @synchronized(self)
+ if (handler) {
+ GTM_LOG_BACKGROUND_SESSION(@"%@ %p Calling system completionHandler", [self class], self);
+ handler();
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ NSURLSession *oldSession = _session;
+ _session = nil;
+ if (_shouldInvalidateSession) {
+ [oldSession finishTasksAndInvalidate];
+ }
+ } // @synchronized(self)
+ }
+}
+#endif
+
+- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(GTM_NULLABLE NSError *)error {
+ // This may happen repeatedly for retries. On authentication callbacks, the retry
+ // may begin before the prior session sends the didBecomeInvalid delegate message.
+ GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSession:%@ didBecomeInvalidWithError:%@",
+ [self class], self, session, error);
+ if (session == (NSURLSession *)self.session) {
+ GTM_LOG_SESSION_DELEGATE(@" Unexpected retained invalid session: %@", session);
+ self.session = nil;
+ }
+}
+
+- (void)finishWithError:(GTM_NULLABLE NSError *)error shouldRetry:(BOOL)shouldRetry {
+ [self removePersistedBackgroundSessionFromDefaults];
+
+ BOOL shouldStopFetching = YES;
+ NSData *downloadedData = nil;
+#if !STRIP_GTM_FETCH_LOGGING
+ BOOL shouldDeferLogging = NO;
+#endif
+ BOOL shouldBeginRetryTimer = NO;
+ NSInteger status = [self statusCode];
+ NSURL *destinationURL = self.destinationFileURL;
+
+ BOOL fetchSucceeded = (error == nil && status >= 0 && status < 300);
+
+#if !STRIP_GTM_FETCH_LOGGING
+ if (!fetchSucceeded) {
+ if (!shouldDeferLogging && !self.hasLoggedError) {
+ [self logNowWithError:error];
+ self.hasLoggedError = YES;
+ }
+ }
+#endif // !STRIP_GTM_FETCH_LOGGING
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+#if !STRIP_GTM_FETCH_LOGGING
+ shouldDeferLogging = _deferResponseBodyLogging;
+#endif
+ if (fetchSucceeded) {
+ // Success
+ if ((_downloadedData.length > 0) && (destinationURL != nil)) {
+ // Overwrite any previous file at the destination URL.
+ NSFileManager *fileMgr = [NSFileManager defaultManager];
+ [fileMgr removeItemAtURL:destinationURL
+ error:NULL];
+ NSURL *destinationFolderURL = [destinationURL URLByDeletingLastPathComponent];
+ BOOL didMoveDownload = NO;
+ if ([fileMgr createDirectoryAtURL:destinationFolderURL
+ withIntermediateDirectories:YES
+ attributes:nil
+ error:&error]) {
+ didMoveDownload = [_downloadedData writeToURL:destinationURL
+ options:NSDataWritingAtomic
+ error:&error];
+ }
+ if (didMoveDownload) {
+ _downloadedData = nil;
+ } else {
+ _downloadFinishedError = error;
+ }
+ }
+ downloadedData = _downloadedData;
+ } else {
+ // Unsuccessful with error or status over 300. Retry or notify the delegate of failure
+ if (shouldRetry) {
+ // Retrying.
+ shouldBeginRetryTimer = YES;
+ shouldStopFetching = NO;
+ } else {
+ if (error == nil) {
+ // Create an error.
+ NSDictionary *userInfo = 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) {
+ [self invokeFetchCallbacksOnCallbackQueueWithData:downloadedData
+ error:error];
+ // The upload subclass doesn't want to release callbacks until upload chunks have completed.
+ BOOL shouldRelease = [self shouldReleaseCallbacksUponCompletion];
+ [self stopFetchReleasingCallbacks:shouldRelease];
+ }
+
+#if !STRIP_GTM_FETCH_LOGGING
+ // _hasLoggedError is only set by this method
+ if (!shouldDeferLogging && !_hasLoggedError) {
+ [self logNowWithError:error];
+ }
+#endif
+}
+
+- (BOOL)shouldReleaseCallbacksUponCompletion {
+ // A subclass can override this to keep callbacks around after the
+ // connection has finished successfully
+ return YES;
+}
+
+- (void)logNowWithError:(GTM_NULLABLE NSError *)error {
+ GTMSessionCheckNotSynchronized(self);
+
+ // If the logging category is available, then log the current request,
+ // response, data, and error
+ if ([self respondsToSelector:@selector(logFetchWithError:)]) {
+ [self performSelector:@selector(logFetchWithError:) withObject:error];
+ }
+}
+
+#pragma mark Retries
+
+- (BOOL)isRetryError:(NSError *)error {
+ struct RetryRecord {
+ __unsafe_unretained NSString *const domain;
+ NSInteger code;
+ };
+
+ struct RetryRecord retries[] = {
+ { kGTMSessionFetcherStatusDomain, 408 }, // request timeout
+ { kGTMSessionFetcherStatusDomain, 502 }, // failure gatewaying to another server
+ { kGTMSessionFetcherStatusDomain, 503 }, // service unavailable
+ { kGTMSessionFetcherStatusDomain, 504 }, // request timeout
+ { NSURLErrorDomain, NSURLErrorTimedOut },
+ { NSURLErrorDomain, NSURLErrorNetworkConnectionLost },
+ { nil, 0 }
+ };
+
+ // NSError's isEqual always returns false for equal but distinct instances
+ // of NSError, so we have to compare the domain and code values explicitly
+ NSString *domain = error.domain;
+ NSInteger code = error.code;
+ for (int idx = 0; retries[idx].domain != nil; idx++) {
+ if (code == retries[idx].code && [domain isEqual:retries[idx].domain]) {
+ return YES;
+ }
+ }
+ return NO;
+}
+
+// shouldRetryNowForStatus:error: responds with YES if the user has enabled retries
+// and the status or error is one that is suitable for retrying. "Suitable"
+// means either the isRetryError:'s list contains the status or error, or the
+// user's retry block is present and returns YES when called, or the
+// authorizer may be able to fix.
+- (void)shouldRetryNowForStatus:(NSInteger)status
+ error:(NSError *)error
+ forceAssumeRetry:(BOOL)forceAssumeRetry
+ response:(GTMSessionFetcherRetryResponse)response {
+ // Determine if a refreshed authorizer may avoid an authorization error
+ BOOL willRetry = NO;
+
+ // We assume _authorizer is immutable after beginFetch, and _hasAttemptedAuthRefresh is modified
+ // only in this method, and this method is invoked on the serial delegate queue.
+ //
+ // We want to avoid calling the authorizer from inside a sync block.
+ BOOL isFirstAuthError = (_authorizer != nil
+ && !_hasAttemptedAuthRefresh
+ && status == GTMSessionFetcherStatusUnauthorized); // 401
+
+ BOOL hasPrimed = NO;
+ if (isFirstAuthError) {
+ if ([_authorizer respondsToSelector:@selector(primeForRefresh)]) {
+ hasPrimed = [_authorizer primeForRefresh];
+ }
+ }
+
+ BOOL shouldRetryForAuthRefresh = NO;
+ if (hasPrimed) {
+ shouldRetryForAuthRefresh = YES;
+ _hasAttemptedAuthRefresh = YES;
+ [self updateRequestValue:nil forHTTPHeaderField:@"Authorization"];
+ }
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ BOOL shouldDoRetry = [self isRetryEnabledUnsynchronized];
+ if (shouldDoRetry && ![self hasRetryAfterInterval]) {
+
+ // Determine if we're doing exponential backoff retries
+ shouldDoRetry = [self nextRetryIntervalUnsynchronized] < _maxRetryInterval;
+
+ if (shouldDoRetry) {
+ // If an explicit max retry interval was set, we expect repeated backoffs to take
+ // up to roughly twice that for repeated fast failures. If the initial attempt is
+ // already more than 3 times the max retry interval, then failures have taken a long time
+ // (such as from network timeouts) so don't retry again to avoid the app becoming
+ // unexpectedly unresponsive.
+ if (_maxRetryInterval > 0) {
+ NSTimeInterval maxAllowedIntervalBeforeRetry = _maxRetryInterval * 3;
+ NSTimeInterval timeSinceInitialRequest = -[_initialRequestDate timeIntervalSinceNow];
+ if (timeSinceInitialRequest > maxAllowedIntervalBeforeRetry) {
+ shouldDoRetry = NO;
+ }
+ }
+ }
+ }
+ BOOL canRetry = shouldRetryForAuthRefresh || forceAssumeRetry || shouldDoRetry;
+ if (canRetry) {
+ NSDictionary *userInfo =
+ 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;
+
+- (GTM_NULLABLE GTMSessionFetcherSystemCompletionHandler)systemCompletionHandler {
+ return [[self class] systemCompletionHandlerForSessionIdentifier:_sessionIdentifier];
+}
+
+- (void)setSystemCompletionHandler:(GTM_NULLABLE GTMSessionFetcherSystemCompletionHandler)systemCompletionHandler {
+ [[self class] setSystemCompletionHandler:systemCompletionHandler
+ forSessionIdentifier:_sessionIdentifier];
+}
+
++ (void)setSystemCompletionHandler:(GTM_NULLABLE GTMSessionFetcherSystemCompletionHandler)systemCompletionHandler
+ forSessionIdentifier:(NSString *)sessionIdentifier {
+ if (!sessionIdentifier) {
+ NSLog(@"%s with nil identifier", __PRETTY_FUNCTION__);
+ return;
+ }
+
+ @synchronized([GTMSessionFetcher class]) {
+ if (gSystemCompletionHandlers == nil && systemCompletionHandler != nil) {
+ gSystemCompletionHandlers = [[NSMutableDictionary alloc] init];
+ }
+ // Use setValue: to remove the object if completionHandler is nil.
+ [gSystemCompletionHandlers setValue:systemCompletionHandler
+ forKey:sessionIdentifier];
+ }
+}
+
++ (GTM_NULLABLE GTMSessionFetcherSystemCompletionHandler)systemCompletionHandlerForSessionIdentifier:(NSString *)sessionIdentifier {
+ if (!sessionIdentifier) {
+ return nil;
+ }
+ @synchronized([GTMSessionFetcher class]) {
+ return [gSystemCompletionHandlers objectForKey:sessionIdentifier];
+ }
+}
+#endif // TARGET_OS_IPHONE
+
+#pragma mark Getters and Setters
+
+@synthesize downloadResumeData = _downloadResumeData,
+ configuration = _configuration,
+ configurationBlock = _configurationBlock,
+ sessionTask = _sessionTask,
+ wasCreatedFromBackgroundSession = _wasCreatedFromBackgroundSession,
+ sessionUserInfo = _sessionUserInfo,
+ taskDescription = _taskDescription,
+ taskPriority = _taskPriority,
+ usingBackgroundSession = _usingBackgroundSession,
+ canShareSession = _canShareSession,
+ completionHandler = _completionHandler,
+ credential = _credential,
+ proxyCredential = _proxyCredential,
+ bodyData = _bodyData,
+ bodyLength = _bodyLength,
+ service = _service,
+ serviceHost = _serviceHost,
+ accumulateDataBlock = _accumulateDataBlock,
+ receivedProgressBlock = _receivedProgressBlock,
+ downloadProgressBlock = _downloadProgressBlock,
+ resumeDataBlock = _resumeDataBlock,
+ didReceiveResponseBlock = _didReceiveResponseBlock,
+ challengeBlock = _challengeBlock,
+ willRedirectBlock = _willRedirectBlock,
+ sendProgressBlock = _sendProgressBlock,
+ willCacheURLResponseBlock = _willCacheURLResponseBlock,
+ retryBlock = _retryBlock,
+ metricsCollectionBlock = _metricsCollectionBlock,
+ retryFactor = _retryFactor,
+ allowedInsecureSchemes = _allowedInsecureSchemes,
+ allowLocalhostRequest = _allowLocalhostRequest,
+ allowInvalidServerCertificates = _allowInvalidServerCertificates,
+ cookieStorage = _cookieStorage,
+ callbackQueue = _callbackQueue,
+ initialBeginFetchDate = _initialBeginFetchDate,
+ testBlock = _testBlock,
+ testBlockAccumulateDataChunkCount = _testBlockAccumulateDataChunkCount,
+ comment = _comment,
+ log = _log;
+
+#if !STRIP_GTM_FETCH_LOGGING
+@synthesize redirectedFromURL = _redirectedFromURL,
+ logRequestBody = _logRequestBody,
+ logResponseBody = _logResponseBody,
+ hasLoggedError = _hasLoggedError;
+#endif
+
+#if GTM_BACKGROUND_TASK_FETCHING
+@synthesize backgroundTaskIdentifier = _backgroundTaskIdentifier,
+ skipBackgroundTask = _skipBackgroundTask;
+#endif
+
+- (GTM_NULLABLE NSURLRequest *)request {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return [_request copy];
+ } // @synchronized(self)
+}
+
+- (void)setRequest:(GTM_NULLABLE NSURLRequest *)request {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (![self isFetchingUnsynchronized]) {
+ _request = [request mutableCopy];
+ } else {
+ GTMSESSION_ASSERT_DEBUG(0, @"request may not be set after beginFetch has been invoked");
+ }
+ } // @synchronized(self)
+}
+
+- (GTM_NULLABLE NSMutableURLRequest *)mutableRequestForTesting {
+ // Allow tests only to modify the request, useful during retries.
+ return _request;
+}
+
+// Internal method for updating the request property such as on redirects.
+- (void)updateMutableRequest:(GTM_NULLABLE NSMutableURLRequest *)request {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _request = request;
+ } // @synchronized(self)
+}
+
+// Set a header field value on the request. Header field value changes will not
+// affect a fetch after the fetch has begun.
+- (void)setRequestValue:(GTM_NULLABLE NSString *)value forHTTPHeaderField:(NSString *)field {
+ if (![self isFetching]) {
+ [self updateRequestValue:value forHTTPHeaderField:field];
+ } else {
+ GTMSESSION_ASSERT_DEBUG(0, @"request may not be set after beginFetch has been invoked");
+ }
+}
+
+// Internal method for updating request headers.
+- (void)updateRequestValue:(GTM_NULLABLE NSString *)value forHTTPHeaderField:(NSString *)field {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ [_request setValue:value forHTTPHeaderField:field];
+ } // @synchronized(self)
+}
+
+- (void)setResponse:(GTM_NULLABLE NSURLResponse *)response {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _response = response;
+ } // @synchronized(self)
+}
+
+- (int64_t)bodyLength {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (_bodyLength == NSURLSessionTransferSizeUnknown) {
+ if (_bodyData) {
+ _bodyLength = (int64_t)_bodyData.length;
+ } else if (_bodyFileURL) {
+ NSNumber *fileSizeNum = nil;
+ NSError *fileSizeError = nil;
+ if ([_bodyFileURL getResourceValue:&fileSizeNum
+ forKey:NSURLFileSizeKey
+ error:&fileSizeError]) {
+ _bodyLength = [fileSizeNum longLongValue];
+ }
+ }
+ }
+ return _bodyLength;
+ } // @synchronized(self)
+}
+
+- (BOOL)useUploadTask {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _useUploadTask;
+ } // @synchronized(self)
+}
+
+- (void)setUseUploadTask:(BOOL)flag {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (flag != _useUploadTask) {
+ GTMSESSION_ASSERT_DEBUG(![self isFetchingUnsynchronized],
+ @"useUploadTask should not change after beginFetch has been invoked");
+ _useUploadTask = flag;
+ }
+ } // @synchronized(self)
+}
+
+- (GTM_NULLABLE NSURL *)bodyFileURL {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _bodyFileURL;
+ } // @synchronized(self)
+}
+
+- (void)setBodyFileURL:(GTM_NULLABLE NSURL *)fileURL {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ // The comparison here is a trivial optimization and forgiveness for any client that
+ // repeatedly sets the property, so it just uses pointer comparison rather than isEqual:.
+ if (fileURL != _bodyFileURL) {
+ GTMSESSION_ASSERT_DEBUG(![self isFetchingUnsynchronized],
+ @"fileURL should not change after beginFetch has been invoked");
+
+ _bodyFileURL = fileURL;
+ }
+ } // @synchronized(self)
+}
+
+- (GTM_NULLABLE GTMSessionFetcherBodyStreamProvider)bodyStreamProvider {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _bodyStreamProvider;
+ } // @synchronized(self)
+}
+
+- (void)setBodyStreamProvider:(GTM_NULLABLE GTMSessionFetcherBodyStreamProvider)block {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ GTMSESSION_ASSERT_DEBUG(![self isFetchingUnsynchronized],
+ @"stream provider should not change after beginFetch has been invoked");
+
+ _bodyStreamProvider = [block copy];
+ } // @synchronized(self)
+}
+
+- (GTM_NULLABLE id<GTMFetcherAuthorizationProtocol>)authorizer {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _authorizer;
+ } // @synchronized(self)
+}
+
+- (void)setAuthorizer:(GTM_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)
+}
+
+- (GTM_NULLABLE NSData *)downloadedData {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _downloadedData;
+ } // @synchronized(self)
+}
+
+- (void)setDownloadedData:(GTM_NULLABLE NSData *)data {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _downloadedData = [data mutableCopy];
+ } // @synchronized(self)
+}
+
+- (int64_t)downloadedLength {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _downloadedLength;
+ } // @synchronized(self)
+}
+
+- (void)setDownloadedLength:(int64_t)length {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _downloadedLength = length;
+ } // @synchronized(self)
+}
+
+- (dispatch_queue_t GTM_NONNULL_TYPE)callbackQueue {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _callbackQueue;
+ } // @synchronized(self)
+}
+
+- (void)setCallbackQueue:(dispatch_queue_t GTM_NULLABLE_TYPE)queue {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _callbackQueue = queue ?: dispatch_get_main_queue();
+ } // @synchronized(self)
+}
+
+- (GTM_NULLABLE NSURLSession *)session {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _session;
+ } // @synchronized(self)
+}
+
+- (NSInteger)servicePriority {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _servicePriority;
+ } // @synchronized(self)
+}
+
+- (void)setServicePriority:(NSInteger)value {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (value != _servicePriority) {
+ GTMSESSION_ASSERT_DEBUG(![self isFetchingUnsynchronized],
+ @"servicePriority should not change after beginFetch has been invoked");
+
+ _servicePriority = value;
+ }
+ } // @synchronized(self)
+}
+
+
+- (void)setSession:(GTM_NULLABLE NSURLSession *)session {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _session = session;
+ } // @synchronized(self)
+}
+
+- (BOOL)canShareSession {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _canShareSession;
+ } // @synchronized(self)
+}
+
+- (void)setCanShareSession:(BOOL)flag {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _canShareSession = flag;
+ } // @synchronized(self)
+}
+
+- (BOOL)useBackgroundSession {
+ // This reflects if the user requested a background session, not necessarily
+ // if one was created. That is tracked with _usingBackgroundSession.
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _userRequestedBackgroundSession;
+ } // @synchronized(self)
+}
+
+- (void)setUseBackgroundSession:(BOOL)flag {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (flag != _userRequestedBackgroundSession) {
+ GTMSESSION_ASSERT_DEBUG(![self isFetchingUnsynchronized],
+ @"useBackgroundSession should not change after beginFetch has been invoked");
+
+ _userRequestedBackgroundSession = flag;
+ }
+ } // @synchronized(self)
+}
+
+- (BOOL)isUsingBackgroundSession {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _usingBackgroundSession;
+ } // @synchronized(self)
+}
+
+- (void)setUsingBackgroundSession:(BOOL)flag {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _usingBackgroundSession = flag;
+ } // @synchronized(self)
+}
+
+- (GTM_NULLABLE NSURLSession *)sessionNeedingInvalidation {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _sessionNeedingInvalidation;
+ } // @synchronized(self)
+}
+
+- (void)setSessionNeedingInvalidation:(GTM_NULLABLE NSURLSession *)session {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _sessionNeedingInvalidation = session;
+ } // @synchronized(self)
+}
+
+- (NSOperationQueue * GTM_NONNULL_TYPE)sessionDelegateQueue {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _delegateQueue;
+ } // @synchronized(self)
+}
+
+- (void)setSessionDelegateQueue:(NSOperationQueue * GTM_NULLABLE_TYPE)queue {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (queue != _delegateQueue) {
+ if ([self isFetchingUnsynchronized]) {
+ GTMSESSION_ASSERT_DEBUG(0, @"sessionDelegateQueue should not change after fetch begins");
+ } else {
+ _delegateQueue = queue ?: [NSOperationQueue mainQueue];
+ }
+ }
+ } // @synchronized(self)
+}
+
+- (BOOL)userStoppedFetching {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _userStoppedFetching;
+ } // @synchronized(self)
+}
+
+- (GTM_NULLABLE id)userData {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _userData;
+ } // @synchronized(self)
+}
+
+- (void)setUserData:(GTM_NULLABLE id)theObj {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _userData = theObj;
+ } // @synchronized(self)
+}
+
+- (GTM_NULLABLE NSURL *)destinationFileURL {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _destinationFileURL;
+ } // @synchronized(self)
+}
+
+- (void)setDestinationFileURL:(GTM_NULLABLE NSURL *)destinationFileURL {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (((_destinationFileURL == nil) && (destinationFileURL == nil)) ||
+ [_destinationFileURL isEqual:destinationFileURL]) {
+ return;
+ }
+ if (_sessionIdentifier) {
+ // This is something we don't expect to happen in production.
+ // However if it ever happen, leave a system log.
+ NSLog(@"%@: Destination File URL changed from (%@) to (%@) after session identifier has "
+ @"been created.",
+ [self class], _destinationFileURL, destinationFileURL);
+#if DEBUG
+ // On both the simulator and devices, the path can change to the download file, but the name
+ // shouldn't change. Technically, this isn't supported in the fetcher, but the change of
+ // URL is expected to happen only across development runs through Xcode.
+ NSString *oldFilename = [_destinationFileURL lastPathComponent];
+ NSString *newFilename = [destinationFileURL lastPathComponent];
+ #pragma unused(oldFilename)
+ #pragma unused(newFilename)
+ GTMSESSION_ASSERT_DEBUG([oldFilename isEqualToString:newFilename],
+ @"Destination File URL cannot be changed after session identifier has been created");
+#endif
+ }
+ _destinationFileURL = destinationFileURL;
+ } // @synchronized(self)
+}
+
+- (void)setProperties:(GTM_NULLABLE NSDictionary *)dict {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _properties = [dict mutableCopy];
+ } // @synchronized(self)
+}
+
+- (GTM_NULLABLE NSDictionary *)properties {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _properties;
+ } // @synchronized(self)
+}
+
+- (void)setProperty:(GTM_NULLABLE id)obj forKey:(NSString *)key {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (_properties == nil && obj != nil) {
+ _properties = [[NSMutableDictionary alloc] init];
+ }
+ [_properties setValue:obj forKey:key];
+ } // @synchronized(self)
+}
+
+- (GTM_NULLABLE id)propertyForKey:(NSString *)key {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return [_properties objectForKey:key];
+ } // @synchronized(self)
+}
+
+- (void)addPropertiesFromDictionary:(NSDictionary *)dict {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (_properties == nil && dict != nil) {
+ [self setProperties:[dict mutableCopy]];
+ } else {
+ [_properties addEntriesFromDictionary:dict];
+ }
+ } // @synchronized(self)
+}
+
+- (void)setCommentWithFormat:(id)format, ... {
+#if !STRIP_GTM_FETCH_LOGGING
+ NSString *result = format;
+ if (format) {
+ va_list argList;
+ va_start(argList, format);
+
+ result = [[NSString alloc] initWithFormat:format
+ arguments:argList];
+ va_end(argList);
+ }
+ [self setComment:result];
+#endif
+}
+
+#if !STRIP_GTM_FETCH_LOGGING
+- (NSData *)loggedStreamData {
+ return _loggedStreamData;
+}
+
+- (void)appendLoggedStreamData:dataToAdd {
+ if (!_loggedStreamData) {
+ _loggedStreamData = [NSMutableData data];
+ }
+ [_loggedStreamData appendData:dataToAdd];
+}
+
+- (void)clearLoggedStreamData {
+ _loggedStreamData = nil;
+}
+
+- (void)setDeferResponseBodyLogging:(BOOL)deferResponseBodyLogging {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (deferResponseBodyLogging != _deferResponseBodyLogging) {
+ _deferResponseBodyLogging = deferResponseBodyLogging;
+ if (!deferResponseBodyLogging && !self.hasLoggedError) {
+ [_delegateQueue addOperationWithBlock:^{
+ [self logNowWithError:nil];
+ }];
+ }
+ }
+ } // @synchronized(self)
+}
+
+- (BOOL)deferResponseBodyLogging {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _deferResponseBodyLogging;
+ } // @synchronized(self)
+}
+
+#else
++ (void)setLoggingEnabled:(BOOL)flag {
+}
+
++ (BOOL)isLoggingEnabled {
+ return NO;
+}
+#endif // STRIP_GTM_FETCH_LOGGING
+
+@end
+
+@implementation GTMSessionFetcher (BackwardsCompatibilityOnly)
+
+- (void)setCookieStorageMethod:(NSInteger)method {
+ // For backwards compatibility with the old fetcher, we'll support the old constants.
+ //
+ // Clients using the GTMSessionFetcher class should set the cookie storage explicitly
+ // themselves.
+ NSHTTPCookieStorage *storage = nil;
+ switch(method) {
+ case 0: // kGTMHTTPFetcherCookieStorageMethodStatic
+ // nil storage will use [[self class] staticCookieStorage] when the fetch begins.
+ break;
+ case 1: // kGTMHTTPFetcherCookieStorageMethodFetchHistory
+ // Do nothing; use whatever was set by the fetcher service.
+ return;
+ case 2: // kGTMHTTPFetcherCookieStorageMethodSystemDefault
+ storage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
+ break;
+ case 3: // kGTMHTTPFetcherCookieStorageMethodNone
+ // Create temporary storage for this fetcher only.
+ storage = [[GTMSessionCookieStorage alloc] init];
+ break;
+ default:
+ GTMSESSION_ASSERT_DEBUG(0, @"Invalid cookie storage method: %d", (int)method);
+ }
+ self.cookieStorage = storage;
+}
+
+@end
+
+@implementation GTMSessionCookieStorage {
+ NSMutableArray *_cookies;
+ NSHTTPCookieAcceptPolicy _policy;
+}
+
+- (id)init {
+ self = [super init];
+ if (self != nil) {
+ _cookies = [[NSMutableArray alloc] init];
+ }
+ return self;
+}
+
+- (GTM_NULLABLE NSArray *)cookies {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return [_cookies copy];
+ } // @synchronized(self)
+}
+
+- (void)setCookie:(NSHTTPCookie *)cookie {
+ if (!cookie) return;
+ if (_policy == NSHTTPCookieAcceptPolicyNever) return;
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ [self internalSetCookie:cookie];
+ } // @synchronized(self)
+}
+
+// Note: this should only be called from inside a @synchronized(self) block.
+- (void)internalSetCookie:(NSHTTPCookie *)newCookie {
+ GTMSessionCheckSynchronized(self);
+
+ if (_policy == NSHTTPCookieAcceptPolicyNever) return;
+
+ BOOL isValidCookie = (newCookie.name.length > 0
+ && newCookie.domain.length > 0
+ && newCookie.path.length > 0);
+ GTMSESSION_ASSERT_DEBUG(isValidCookie, @"invalid cookie: %@", newCookie);
+
+ if (isValidCookie) {
+ // Remove the cookie if it's currently in the array.
+ NSHTTPCookie *oldCookie = [self cookieMatchingCookie:newCookie];
+ if (oldCookie) {
+ [_cookies removeObjectIdenticalTo:oldCookie];
+ }
+
+ if (![[self class] hasCookieExpired:newCookie]) {
+ [_cookies addObject:newCookie];
+ }
+ }
+}
+
+// Add all cookies in the new cookie array to the storage,
+// replacing stored cookies as appropriate.
+//
+// Side effect: removes expired cookies from the storage array.
+- (void)setCookies:(GTM_NULLABLE NSArray *)newCookies {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ [self removeExpiredCookies];
+
+ for (NSHTTPCookie *newCookie in newCookies) {
+ [self internalSetCookie:newCookie];
+ }
+ } // @synchronized(self)
+}
+
+- (void)setCookies:(NSArray *)cookies forURL:(GTM_NULLABLE NSURL *)URL mainDocumentURL:(GTM_NULLABLE NSURL *)mainDocumentURL {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ if (_policy == NSHTTPCookieAcceptPolicyNever) {
+ return;
+ }
+
+ if (_policy == NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain) {
+ NSString *mainHost = mainDocumentURL.host;
+ NSString *associatedHost = URL.host;
+ if (!mainHost || ![associatedHost hasSuffix:mainHost]) {
+ return;
+ }
+ }
+ } // @synchronized(self)
+ [self setCookies:cookies];
+}
+
+- (void)deleteCookie:(NSHTTPCookie *)cookie {
+ if (!cookie) return;
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ NSHTTPCookie *foundCookie = [self cookieMatchingCookie:cookie];
+ if (foundCookie) {
+ [_cookies removeObjectIdenticalTo:foundCookie];
+ }
+ } // @synchronized(self)
+}
+
+// Retrieve all cookies appropriate for the given URL, considering
+// domain, path, cookie name, expiration, security setting.
+// Side effect: removed expired cookies from the storage array.
+- (GTM_NULLABLE NSArray *)cookiesForURL:(NSURL *)theURL {
+ NSMutableArray *foundCookies = nil;
+
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ [self removeExpiredCookies];
+
+ // We'll prepend "." to the desired domain, since we want the
+ // actual domain "nytimes.com" to still match the cookie domain
+ // ".nytimes.com" when we check it below with hasSuffix.
+ NSString *host = theURL.host.lowercaseString;
+ NSString *path = theURL.path;
+ NSString *scheme = [theURL scheme];
+
+ NSString *requestingDomain = nil;
+ BOOL isLocalhostRetrieval = NO;
+
+ if (IsLocalhost(host)) {
+ isLocalhostRetrieval = YES;
+ } else {
+ if (host.length > 0) {
+ requestingDomain = [@"." stringByAppendingString:host];
+ }
+ }
+
+ for (NSHTTPCookie *storedCookie in _cookies) {
+ NSString *cookieDomain = storedCookie.domain.lowercaseString;
+ NSString *cookiePath = storedCookie.path;
+ BOOL cookieIsSecure = [storedCookie isSecure];
+
+ BOOL isDomainOK;
+
+ if (isLocalhostRetrieval) {
+ // Prior to 10.5.6, the domain stored into NSHTTPCookies for localhost
+ // is "localhost.local"
+ isDomainOK = (IsLocalhost(cookieDomain)
+ || [cookieDomain isEqual:@"localhost.local"]);
+ } else {
+ // Ensure we're matching exact domain names. We prepended a dot to the
+ // requesting domain, so we can also prepend one here if needed before
+ // checking if the request contains the cookie domain.
+ if (![cookieDomain hasPrefix:@"."]) {
+ cookieDomain = [@"." stringByAppendingString:cookieDomain];
+ }
+ isDomainOK = [requestingDomain hasSuffix:cookieDomain];
+ }
+
+ BOOL isPathOK = [cookiePath isEqual:@"/"] || [path hasPrefix:cookiePath];
+ BOOL isSecureOK = (!cookieIsSecure
+ || [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame);
+
+ if (isDomainOK && isPathOK && isSecureOK) {
+ if (foundCookies == nil) {
+ foundCookies = [NSMutableArray array];
+ }
+ [foundCookies addObject:storedCookie];
+ }
+ }
+ } // @synchronized(self)
+ return foundCookies;
+}
+
+// Override methods from the NSHTTPCookieStorage (NSURLSessionTaskAdditions) category.
+- (void)storeCookies:(NSArray *)cookies forTask:(NSURLSessionTask *)task {
+ NSURLRequest *currentRequest = task.currentRequest;
+ [self setCookies:cookies forURL:currentRequest.URL mainDocumentURL:nil];
+}
+
+- (void)getCookiesForTask:(NSURLSessionTask *)task
+ completionHandler:(void (^)(GTM_NSArrayOf(NSHTTPCookie *) *))completionHandler {
+ if (completionHandler) {
+ NSURLRequest *currentRequest = task.currentRequest;
+ NSURL *currentRequestURL = currentRequest.URL;
+ NSArray *cookies = [self cookiesForURL:currentRequestURL];
+ completionHandler(cookies);
+ }
+}
+
+// Return a cookie from the array with the same name, domain, and path as the
+// given cookie, or else return nil if none found.
+//
+// Both the cookie being tested and all cookies in the storage array should
+// be valid (non-nil name, domains, paths).
+//
+// Note: this should only be called from inside a @synchronized(self) block
+- (GTM_NULLABLE NSHTTPCookie *)cookieMatchingCookie:(NSHTTPCookie *)cookie {
+ GTMSessionCheckSynchronized(self);
+
+ NSString *name = cookie.name;
+ NSString *domain = cookie.domain;
+ NSString *path = cookie.path;
+
+ GTMSESSION_ASSERT_DEBUG(name && domain && path,
+ @"Invalid stored cookie (name:%@ domain:%@ path:%@)", name, domain, path);
+
+ for (NSHTTPCookie *storedCookie in _cookies) {
+ if ([storedCookie.name isEqual:name]
+ && [storedCookie.domain isEqual:domain]
+ && [storedCookie.path isEqual:path]) {
+ return storedCookie;
+ }
+ }
+ return nil;
+}
+
+// Internal routine to remove any expired cookies from the array, excluding
+// cookies with nil expirations.
+//
+// Note: this should only be called from inside a @synchronized(self) block
+- (void)removeExpiredCookies {
+ GTMSessionCheckSynchronized(self);
+
+ // Count backwards since we're deleting items from the array
+ for (NSInteger idx = (NSInteger)_cookies.count - 1; idx >= 0; idx--) {
+ NSHTTPCookie *storedCookie = [_cookies objectAtIndex:(NSUInteger)idx];
+ if ([[self class] hasCookieExpired:storedCookie]) {
+ [_cookies removeObjectAtIndex:(NSUInteger)idx];
+ }
+ }
+}
+
++ (BOOL)hasCookieExpired:(NSHTTPCookie *)cookie {
+ NSDate *expiresDate = [cookie expiresDate];
+ if (expiresDate == nil) {
+ // Cookies seem to have a Expires property even when the expiresDate method returns nil.
+ id expiresVal = [[cookie properties] objectForKey:NSHTTPCookieExpires];
+ if ([expiresVal isKindOfClass:[NSDate class]]) {
+ expiresDate = expiresVal;
+ }
+ }
+ BOOL hasExpired = (expiresDate != nil && [expiresDate timeIntervalSinceNow] < 0);
+ return hasExpired;
+}
+
+- (void)removeAllCookies {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ [_cookies removeAllObjects];
+ } // @synchronized(self)
+}
+
+- (NSHTTPCookieAcceptPolicy)cookieAcceptPolicy {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ return _policy;
+ } // @synchronized(self)
+}
+
+- (void)setCookieAcceptPolicy:(NSHTTPCookieAcceptPolicy)cookieAcceptPolicy {
+ @synchronized(self) {
+ GTMSessionMonitorSynchronized(self);
+
+ _policy = cookieAcceptPolicy;
+ } // @synchronized(self)
+}
+
+@end
+
+void GTMSessionFetcherAssertValidSelector(id GTM_NULLABLE_TYPE obj, SEL GTM_NULLABLE_TYPE sel, ...) {
+ // Verify that the object's selector is implemented with the proper
+ // number and type of arguments
+#if DEBUG
+ va_list argList;
+ va_start(argList, sel);
+
+ if (obj && sel) {
+ // Check that the selector is implemented
+ if (![obj respondsToSelector:sel]) {
+ NSLog(@"\"%@\" selector \"%@\" is unimplemented or misnamed",
+ NSStringFromClass([(id)obj class]),
+ NSStringFromSelector((SEL)sel));
+ NSCAssert(0, @"callback selector unimplemented or misnamed");
+ } else {
+ const char *expectedArgType;
+ unsigned int argCount = 2; // skip self and _cmd
+ NSMethodSignature *sig = [obj methodSignatureForSelector:sel];
+
+ // Check that each expected argument is present and of the correct type
+ while ((expectedArgType = va_arg(argList, const char*)) != 0) {
+
+ if ([sig numberOfArguments] > argCount) {
+ const char *foundArgType = [sig getArgumentTypeAtIndex:argCount];
+
+ if (0 != strncmp(foundArgType, expectedArgType, strlen(expectedArgType))) {
+ NSLog(@"\"%@\" selector \"%@\" argument %d should be type %s",
+ NSStringFromClass([(id)obj class]),
+ NSStringFromSelector((SEL)sel), (argCount - 2), expectedArgType);
+ NSCAssert(0, @"callback selector argument type mistake");
+ }
+ }
+ argCount++;
+ }
+
+ // Check that the proper number of arguments are present in the selector
+ if (argCount != [sig numberOfArguments]) {
+ NSLog(@"\"%@\" selector \"%@\" should have %d arguments",
+ NSStringFromClass([(id)obj class]),
+ NSStringFromSelector((SEL)sel), (argCount - 2));
+ NSCAssert(0, @"callback selector arguments incorrect");
+ }
+ }
+ }
+
+ va_end(argList);
+#endif
+}
+
+NSString *GTMFetcherCleanedUserAgentString(NSString *str) {
+ // Reference http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html
+ // and http://www-archive.mozilla.org/build/user-agent-strings.html
+
+ if (str == nil) return @"";
+
+ NSMutableString *result = [NSMutableString stringWithString:str];
+
+ // Replace spaces and commas with underscores
+ [result replaceOccurrencesOfString:@" "
+ withString:@"_"
+ options:0
+ range:NSMakeRange(0, result.length)];
+ [result replaceOccurrencesOfString:@","
+ withString:@"_"
+ options:0
+ range:NSMakeRange(0, result.length)];
+
+ // Delete http token separators and remaining whitespace
+ static NSCharacterSet *charsToDelete = nil;
+ if (charsToDelete == nil) {
+ // Make a set of unwanted characters
+ NSString *const kSeparators = @"()<>@;:\\\"/[]?={}";
+
+ NSMutableCharacterSet *mutableChars =
+ [[NSCharacterSet whitespaceAndNewlineCharacterSet] mutableCopy];
+ [mutableChars addCharactersInString:kSeparators];
+ charsToDelete = [mutableChars copy]; // hang on to an immutable copy
+ }
+
+ while (1) {
+ NSRange separatorRange = [result rangeOfCharacterFromSet:charsToDelete];
+ if (separatorRange.location == NSNotFound) break;
+
+ [result deleteCharactersInRange:separatorRange];
+ };
+
+ return result;
+}
+
+NSString *GTMFetcherSystemVersionString(void) {
+ static NSString *sSavedSystemString;
+
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ // The Xcode 8 SDKs finally cleaned up this mess by providing TARGET_OS_OSX
+ // and TARGET_OS_IOS, but to build with older SDKs, those don't exist and
+ // instead one has to rely on TARGET_OS_MAC (which is true for iOS, watchOS,
+ // and tvOS) and TARGET_OS_IPHONE (which is true for iOS, watchOS, tvOS). So
+ // one has to order these carefully so you pick off the specific things
+ // first.
+ // If the code can ever assume Xcode 8 or higher (even when building for
+ // older OSes), then
+ // TARGET_OS_MAC -> TARGET_OS_OSX
+ // TARGET_OS_IPHONE -> TARGET_OS_IOS
+ // TARGET_IPHONE_SIMULATOR -> TARGET_OS_SIMULATOR
+#if TARGET_OS_WATCH
+ // watchOS - WKInterfaceDevice
+
+ WKInterfaceDevice *currentDevice = [WKInterfaceDevice currentDevice];
+
+ NSString *rawModel = [currentDevice model];
+ NSString *model = GTMFetcherCleanedUserAgentString(rawModel);
+
+ NSString *systemVersion = [currentDevice systemVersion];
+
+#if TARGET_OS_SIMULATOR
+ NSString *hardwareModel = @"sim";
+#else
+ NSString *hardwareModel;
+ struct utsname unameRecord;
+ if (uname(&unameRecord) == 0) {
+ NSString *machineName = @(unameRecord.machine);
+ hardwareModel = GTMFetcherCleanedUserAgentString(machineName);
+ }
+ if (hardwareModel.length == 0) {
+ hardwareModel = @"unk";
+ }
+#endif
+
+ sSavedSystemString = [[NSString alloc] initWithFormat:@"%@/%@ hw/%@",
+ model, systemVersion, hardwareModel];
+ // Example: Apple_Watch/3.0 hw/Watch1_2
+#elif TARGET_OS_TV || TARGET_OS_IPHONE
+ // iOS and tvOS have UIDevice, use that.
+ UIDevice *currentDevice = [UIDevice currentDevice];
+
+ NSString *rawModel = [currentDevice model];
+ NSString *model = GTMFetcherCleanedUserAgentString(rawModel);
+
+ NSString *systemVersion = [currentDevice systemVersion];
+
+#if TARGET_IPHONE_SIMULATOR || TARGET_OS_SIMULATOR
+ NSString *hardwareModel = @"sim";
+#else
+ NSString *hardwareModel;
+ struct utsname unameRecord;
+ if (uname(&unameRecord) == 0) {
+ NSString *machineName = @(unameRecord.machine);
+ hardwareModel = GTMFetcherCleanedUserAgentString(machineName);
+ }
+ if (hardwareModel.length == 0) {
+ hardwareModel = @"unk";
+ }
+#endif
+
+ sSavedSystemString = [[NSString alloc] initWithFormat:@"%@/%@ hw/%@",
+ model, systemVersion, hardwareModel];
+ // Example: iPod_Touch/2.2 hw/iPod1_1
+ // Example: Apple_TV/9.2 hw/AppleTV5,3
+#elif TARGET_OS_MAC
+ // Mac build
+ NSProcessInfo *procInfo = [NSProcessInfo processInfo];
+#if !defined(MAC_OS_X_VERSION_10_10)
+ BOOL hasOperatingSystemVersion = NO;
+#elif MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_10
+ BOOL hasOperatingSystemVersion =
+ [procInfo respondsToSelector:@selector(operatingSystemVersion)];
+#else
+ BOOL hasOperatingSystemVersion = YES;
+#endif
+ NSString *versString;
+ if (hasOperatingSystemVersion) {
+#if defined(MAC_OS_X_VERSION_10_10)
+ // A reference to NSOperatingSystemVersion requires the 10.10 SDK.
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wunguarded-availability"
+// Disable unguarded availability warning as we can't use the @availability macro until we require
+// all clients to build with Xcode 9 or above.
+ NSOperatingSystemVersion version = procInfo.operatingSystemVersion;
+#pragma clang diagnostic pop
+ versString = [NSString stringWithFormat:@"%ld.%ld.%ld",
+ (long)version.majorVersion, (long)version.minorVersion,
+ (long)version.patchVersion];
+#else
+#pragma unused(procInfo)
+#endif
+ } else {
+ // With Gestalt inexplicably deprecated in 10.8, we're reduced to reading
+ // the system plist file.
+ NSString *const kPath = @"/System/Library/CoreServices/SystemVersion.plist";
+ NSDictionary *plist = [NSDictionary dictionaryWithContentsOfFile:kPath];
+ versString = [plist objectForKey:@"ProductVersion"];
+ if (versString.length == 0) {
+ versString = @"10.?.?";
+ }
+ }
+
+ sSavedSystemString = [[NSString alloc] initWithFormat:@"MacOSX/%@", versString];
+#elif defined(_SYS_UTSNAME_H)
+ // Foundation-only build
+ struct utsname unameRecord;
+ uname(&unameRecord);
+
+ sSavedSystemString = [NSString stringWithFormat:@"%s/%s",
+ unameRecord.sysname, unameRecord.release]; // "Darwin/8.11.1"
+#else
+#error No branch taken for a default user agent
+#endif
+ });
+ return sSavedSystemString;
+}
+
+NSString *GTMFetcherStandardUserAgentString(NSBundle * GTM_NULLABLE_TYPE bundle) {
+ NSString *result = [NSString stringWithFormat:@"%@ %@",
+ GTMFetcherApplicationIdentifier(bundle),
+ GTMFetcherSystemVersionString()];
+ return result;
+}
+
+NSString *GTMFetcherApplicationIdentifier(NSBundle * GTM_NULLABLE_TYPE bundle) {
+ @synchronized([GTMSessionFetcher class]) {
+ static NSMutableDictionary *sAppIDMap = nil;
+
+ // If there's a bundle ID, use that; otherwise, use the process name
+ if (bundle == nil) {
+ bundle = [NSBundle mainBundle];
+ }
+ NSString *bundleID = [bundle bundleIdentifier];
+ if (bundleID == nil) {
+ bundleID = @"";
+ }
+
+ NSString *identifier = [sAppIDMap objectForKey:bundleID];
+ if (identifier) return identifier;
+
+ // Apps may add a string to the info.plist to uniquely identify different builds.
+ identifier = [bundle objectForInfoDictionaryKey:@"GTMUserAgentID"];
+ if (identifier.length == 0) {
+ if (bundleID.length > 0) {
+ identifier = bundleID;
+ } else {
+ // Fall back on the procname, prefixed by "proc" to flag that it's
+ // autogenerated and perhaps unreliable
+ NSString *procName = [[NSProcessInfo processInfo] processName];
+ identifier = [NSString stringWithFormat:@"proc_%@", procName];
+ }
+ }
+
+ // Clean up whitespace and special characters
+ identifier = GTMFetcherCleanedUserAgentString(identifier);
+
+ // If there's a version number, append that
+ NSString *version = [bundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
+ if (version.length == 0) {
+ version = [bundle objectForInfoDictionaryKey:@"CFBundleVersion"];
+ }
+
+ // Clean up whitespace and special characters
+ version = GTMFetcherCleanedUserAgentString(version);
+
+ // Glue the two together (cleanup done above or else cleanup would strip the
+ // slash)
+ if (version.length > 0) {
+ identifier = [identifier stringByAppendingFormat:@"/%@", version];
+ }
+
+ if (sAppIDMap == nil) {
+ sAppIDMap = [[NSMutableDictionary alloc] init];
+ }
+ [sAppIDMap setObject:identifier forKey:bundleID];
+ return identifier;
+ }
+}
+
+#if DEBUG && (!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];
+ }
+}
+
++ (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)
+GTM_ASSUME_NONNULL_END