As of iOS 13.3.1, apps installed with free developer accounts that contain embedded frameworks fail to launch. To work around this, we now link all dependencies via Cocoapods as static libraries.
4649 lines
172 KiB
Objective-C
4649 lines
172 KiB
Objective-C
/* 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
|
|
[self setSessionIdentifierInternal:nil];
|
|
self.useBackgroundSession = NO;
|
|
}
|
|
|
|
#if GTM_ALLOW_INSECURE_REQUESTS
|
|
BOOL shouldCheckSecurity = NO;
|
|
#else
|
|
BOOL shouldCheckSecurity = (fetchRequestURL != nil
|
|
&& !isDataRequest
|
|
&& [[self class] appAllowsInsecureRequests]);
|
|
#endif
|
|
|
|
if (shouldCheckSecurity) {
|
|
// Allow https only for requests, unless overridden by the client.
|
|
//
|
|
// Non-https requests may too easily be snooped, so we disallow them by default.
|
|
//
|
|
// file: and data: schemes are usually safe if they are hardcoded in the client or provided
|
|
// by a trusted source, but since it's fairly rare to need them, it's safest to make clients
|
|
// explicitly whitelist them.
|
|
BOOL isSecure =
|
|
requestScheme != nil && [requestScheme caseInsensitiveCompare:@"https"] == NSOrderedSame;
|
|
if (!isSecure) {
|
|
BOOL allowRequest = NO;
|
|
NSString *host = fetchRequestURL.host;
|
|
|
|
// Check schemes first. A file scheme request may be allowed here, or as a localhost request.
|
|
for (NSString *allowedScheme in _allowedInsecureSchemes) {
|
|
if (requestScheme != nil &&
|
|
[requestScheme caseInsensitiveCompare:allowedScheme] == NSOrderedSame) {
|
|
allowRequest = YES;
|
|
break;
|
|
}
|
|
}
|
|
if (!allowRequest) {
|
|
// Check for localhost requests. Security checks only occur for non-https requests, so
|
|
// this check won't happen for an https request to localhost.
|
|
BOOL isLocalhostRequest = (host.length == 0 && [fetchRequestURL isFileURL]) || IsLocalhost(host);
|
|
if (isLocalhostRequest) {
|
|
if (self.allowLocalhostRequest) {
|
|
allowRequest = YES;
|
|
} else {
|
|
GTMSESSION_ASSERT_DEBUG(NO, @"Fetch request for localhost but fetcher"
|
|
@" allowLocalhostRequest is not set: %@", fetchRequestURL);
|
|
}
|
|
} else {
|
|
GTMSESSION_ASSERT_DEBUG(NO, @"Insecure fetch request has a scheme (%@)"
|
|
@" not found in fetcher allowedInsecureSchemes (%@): %@",
|
|
requestScheme, _allowedInsecureSchemes ?: @" @[] ", fetchRequestURL);
|
|
}
|
|
}
|
|
|
|
if (!allowRequest) {
|
|
#if !DEBUG
|
|
NSLog(@"Insecure fetch disallowed for %@", fetchRequestURL.description ?: @"nil request URL");
|
|
#endif
|
|
[self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorInsecureRequest)];
|
|
return;
|
|
}
|
|
} // !isSecure
|
|
} // (requestURL != nil) && !isDataRequest
|
|
|
|
if (self.cookieStorage == nil) {
|
|
self.cookieStorage = [[self class] staticCookieStorage];
|
|
}
|
|
|
|
BOOL isRecreatingSession = (self.sessionIdentifier != nil) && (fetchRequest == nil);
|
|
|
|
self.canShareSession = !isRecreatingSession && !self.usingBackgroundSession;
|
|
|
|
if (!self.session && self.canShareSession) {
|
|
self.session = [_service sessionForFetcherCreation];
|
|
// If _session is nil, then the service's session creation semaphore will block
|
|
// until this fetcher invokes fetcherDidCreateSession: below, so this *must* invoke
|
|
// that method, even if the session fails to be created.
|
|
}
|
|
|
|
if (!self.session) {
|
|
// Create a session.
|
|
if (!_configuration) {
|
|
if (priorSessionIdentifier || self.usingBackgroundSession) {
|
|
NSString *sessionIdentifier = priorSessionIdentifier;
|
|
if (!sessionIdentifier) {
|
|
sessionIdentifier = [self createSessionIdentifierWithMetadata:nil];
|
|
}
|
|
NSMapTable *sessionIdentifierToFetcherMap = [[self class] sessionIdentifierToFetcherMap];
|
|
[sessionIdentifierToFetcherMap setObject:self forKey:self.sessionIdentifier];
|
|
|
|
if (@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.useBackgroundSession) {
|
|
// Tell UIApplication that we want to continue even when the app is in the
|
|
// background.
|
|
#if DEBUG
|
|
NSString *bgTaskName = [NSString stringWithFormat:@"%@-%@",
|
|
[self class], fetchRequest.URL.host];
|
|
#else
|
|
NSString *bgTaskName = @"GTMSessionFetcher";
|
|
#endif
|
|
__block UIBackgroundTaskIdentifier bgTaskID = [app beginBackgroundTaskWithName:bgTaskName
|
|
expirationHandler:^{
|
|
// Background task expiration callback - this block is always invoked by
|
|
// UIApplication on the main thread.
|
|
if (bgTaskID != UIBackgroundTaskInvalid) {
|
|
@synchronized(self) {
|
|
if (bgTaskID == self.backgroundTaskIdentifier) {
|
|
self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
|
|
}
|
|
}
|
|
[app endBackgroundTask:bgTaskID];
|
|
}
|
|
}];
|
|
@synchronized(self) {
|
|
self.backgroundTaskIdentifier = bgTaskID;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (!_initialRequestDate) {
|
|
_initialRequestDate = [[NSDate alloc] init];
|
|
}
|
|
|
|
// We don't expect to reach here even on retry or auth until a stop notification has been sent
|
|
// for the previous task, but we should ensure that we don't unbalance that.
|
|
GTMSESSION_ASSERT_DEBUG(!_isStopNotificationNeeded, @"Start notification without a prior stop");
|
|
[self sendStopNotificationIfNeeded];
|
|
|
|
[self addPersistedBackgroundSessionToDefaults];
|
|
|
|
[self setStopNotificationNeeded:YES];
|
|
|
|
[self postNotificationOnMainThreadWithName:kGTMSessionFetcherStartedNotification
|
|
userInfo:nil
|
|
requireAsync:NO];
|
|
|
|
// The service needs to know our task if it is serving as NSURLSession delegate.
|
|
[_service fetcherDidBeginFetching:self];
|
|
|
|
if (_testBlock) {
|
|
#if !GTM_DISABLE_FETCHER_TEST_BLOCK
|
|
[self simulateFetchForTestBlock];
|
|
#endif
|
|
} else {
|
|
// We resume the session task after posting the notification since the
|
|
// delegate callbacks may happen immediately if the fetch is started off
|
|
// the main thread or the session delegate queue is on a background thread,
|
|
// and we don't want to post a start notification after a premature finish
|
|
// of the session task.
|
|
[newSessionTask resume];
|
|
}
|
|
}
|
|
|
|
NSData * GTM_NULLABLE_TYPE GTMDataFromInputStream(NSInputStream *inputStream, NSError **outError) {
|
|
NSMutableData *data = [NSMutableData data];
|
|
|
|
[inputStream open];
|
|
NSInteger numberOfBytesRead = 0;
|
|
while ([inputStream hasBytesAvailable]) {
|
|
uint8_t buffer[512];
|
|
numberOfBytesRead = [inputStream read:buffer maxLength:sizeof(buffer)];
|
|
if (numberOfBytesRead > 0) {
|
|
[data appendBytes:buffer length:(NSUInteger)numberOfBytesRead];
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
[inputStream close];
|
|
NSError *streamError = inputStream.streamError;
|
|
|
|
if (streamError) {
|
|
data = nil;
|
|
}
|
|
if (outError) {
|
|
*outError = streamError;
|
|
}
|
|
return data;
|
|
}
|
|
|
|
#if !GTM_DISABLE_FETCHER_TEST_BLOCK
|
|
|
|
- (void)simulateFetchForTestBlock {
|
|
// This is invoked on the same thread as the beginFetch method was.
|
|
//
|
|
// Callbacks will all occur on the callback queue.
|
|
_testBlock(self, ^(NSURLResponse *response, NSData *responseData, NSError *error) {
|
|
// Callback from test block.
|
|
if (response == nil && responseData == nil && error == nil) {
|
|
// Assume the fetcher should execute rather than be tested.
|
|
self->_testBlock = nil;
|
|
self->_isUsingTestBlock = NO;
|
|
[self->_sessionTask resume];
|
|
return;
|
|
}
|
|
|
|
GTMSessionFetcherBodyStreamProvider bodyStreamProvider = self.bodyStreamProvider;
|
|
if (bodyStreamProvider) {
|
|
bodyStreamProvider(^(NSInputStream *bodyStream){
|
|
// Read from the input stream into an NSData buffer. We'll drain the stream
|
|
// explicitly on a background queue.
|
|
[self invokeOnCallbackQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)
|
|
afterUserStopped:NO
|
|
block:^{
|
|
NSError *streamError;
|
|
NSData *streamedData = GTMDataFromInputStream(bodyStream, &streamError);
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
// Continue callbacks on the main thread, since serial behavior
|
|
// is more reliable for tests.
|
|
[self simulateDataCallbacksForTestBlockWithBodyData:streamedData
|
|
response:response
|
|
responseData:responseData
|
|
error:(error ?: streamError)];
|
|
});
|
|
}];
|
|
});
|
|
} else {
|
|
// No input stream; use the supplied data or file URL.
|
|
NSURL *bodyFileURL = self.bodyFileURL;
|
|
if (bodyFileURL) {
|
|
NSError *readError;
|
|
self->_bodyData = [NSData dataWithContentsOfURL:bodyFileURL
|
|
options:NSDataReadingMappedIfSafe
|
|
error:&readError];
|
|
error = readError;
|
|
}
|
|
|
|
// No stream provider.
|
|
|
|
// In real fetches, nothing happens until the run loop spins, so apps have leeway to
|
|
// set callbacks after they call beginFetch. We'll mirror that fetcher behavior by
|
|
// delaying callbacks here at least to the next spin of the run loop. That keeps
|
|
// immediate, synchronous setting of callback blocks after beginFetch working in tests.
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self simulateDataCallbacksForTestBlockWithBodyData:self->_bodyData
|
|
response:response
|
|
responseData:responseData
|
|
error:error];
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
- (void)simulateByteTransferReportWithDataLength:(int64_t)totalDataLength
|
|
block:(GTMSessionFetcherSendProgressBlock)block {
|
|
// This utility method simulates transfer progress with up to three callbacks.
|
|
// It is used to call back to any of the progress blocks.
|
|
int64_t sendReportSize = totalDataLength / 3 + 1;
|
|
int64_t totalSent = 0;
|
|
while (totalSent < totalDataLength) {
|
|
int64_t bytesRemaining = totalDataLength - totalSent;
|
|
sendReportSize = MIN(sendReportSize, bytesRemaining);
|
|
totalSent += sendReportSize;
|
|
[self invokeOnCallbackQueueUnlessStopped:^{
|
|
block(sendReportSize, totalSent, totalDataLength);
|
|
}];
|
|
}
|
|
}
|
|
|
|
- (void)simulateDataCallbacksForTestBlockWithBodyData:(NSData * GTM_NULLABLE_TYPE)bodyData
|
|
response:(NSURLResponse *)response
|
|
responseData:(NSData *)suppliedData
|
|
error:(NSError *)suppliedError {
|
|
__block NSData *responseData = suppliedData;
|
|
__block NSError *responseError = suppliedError;
|
|
|
|
// This method does the test simulation of callbacks once the upload
|
|
// and download data are known.
|
|
@synchronized(self) {
|
|
GTMSessionMonitorSynchronized(self);
|
|
|
|
// Get copies of ivars we'll access in async invocations. This simulation assumes
|
|
// they won't change during fetcher execution.
|
|
NSURL *destinationFileURL = _destinationFileURL;
|
|
GTMSessionFetcherWillRedirectBlock willRedirectBlock = _willRedirectBlock;
|
|
GTMSessionFetcherDidReceiveResponseBlock didReceiveResponseBlock = _didReceiveResponseBlock;
|
|
GTMSessionFetcherSendProgressBlock sendProgressBlock = _sendProgressBlock;
|
|
GTMSessionFetcherDownloadProgressBlock downloadProgressBlock = _downloadProgressBlock;
|
|
GTMSessionFetcherAccumulateDataBlock accumulateDataBlock = _accumulateDataBlock;
|
|
GTMSessionFetcherReceivedProgressBlock receivedProgressBlock = _receivedProgressBlock;
|
|
GTMSessionFetcherWillCacheURLResponseBlock willCacheURLResponseBlock =
|
|
_willCacheURLResponseBlock;
|
|
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;
|
|
}
|
|
|
|
- (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];
|
|
}];
|
|
}
|
|
|
|
#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,
|
|
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
|