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