This commit is contained in:
Yao
2024-12-20 17:49:45 +08:00
parent 86b0363ce1
commit 654d456c7d
7011 changed files with 1705926 additions and 7 deletions

View 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

View File

@@ -0,0 +1,889 @@
/* 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
#include <unistd.h>
#import "GTMSessionFetcher/GTMSessionFetcherLogging.h"
#ifndef STRIP_GTM_FETCH_LOGGING
#error GTMSessionFetcher headers should have defaulted this if it wasn't already defined.
#endif
#if !STRIP_GTM_FETCH_LOGGING
// Sensitive credential strings are replaced in logs with _snip_
//
// Apps that must see the contents of sensitive tokens can set this to 1
#ifndef SKIP_GTM_FETCH_LOGGING_SNIPPING
#define SKIP_GTM_FETCH_LOGGING_SNIPPING 0
#endif
// If GTMReadMonitorInputStream is available, it can be used for
// capturing uploaded streams of data
//
// We locally declare methods of GTMReadMonitorInputStream so we
// do not need to import the header, as some projects may not have it available
#if !GTMSESSION_BUILD_COMBINED_SOURCES
@interface GTMReadMonitorInputStream : NSInputStream
+ (instancetype)inputStreamWithStream:(NSInputStream *)input;
@property(assign) id readDelegate;
@property(assign) SEL readSelector;
@end
#else
@class GTMReadMonitorInputStream;
#endif // !GTMSESSION_BUILD_COMBINED_SOURCES
@interface GTMSessionFetcher (GTMSessionFetcherLoggingUtilities)
+ (NSString *)headersStringForDictionary:(NSDictionary *)dict;
+ (NSString *)snipSubstringOfString:(NSString *)originalStr
betweenStartString:(NSString *)startStr
endString:(NSString *)endStr;
- (void)inputStream:(GTMReadMonitorInputStream *)stream
readIntoBuffer:(void *)buffer
length:(int64_t)length;
@end
@implementation GTMSessionFetcher (GTMSessionFetcherLogging)
// fetchers come and fetchers go, but statics are forever
static BOOL gIsLoggingEnabled = NO;
static BOOL gIsLoggingToFile = YES;
static NSString *gLoggingDirectoryPath = nil;
static NSString *gLogDirectoryForCurrentRun = nil;
static NSString *gLoggingDateStamp = nil;
static NSString *gLoggingProcessName = nil;
+ (void)setLoggingDirectory:(NSString *)path {
gLoggingDirectoryPath = [path copy];
}
+ (NSString *)loggingDirectory {
if (!gLoggingDirectoryPath) {
NSArray *paths = nil;
#if TARGET_IPHONE_SIMULATOR
// default to a directory called GTMHTTPDebugLogs into a sandbox-safe
// directory that a developer can find easily, the application home
paths = @[ NSHomeDirectory() ];
#elif TARGET_OS_IPHONE
// Neither ~/Desktop nor ~/Home is writable on an actual iOS, watchOS, or tvOS device.
// Put it in ~/Documents.
paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
#else
// default to a directory called GTMHTTPDebugLogs in the desktop folder
paths = NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, NSUserDomainMask, YES);
#endif
NSString *desktopPath = paths.firstObject;
if (desktopPath) {
NSString *const kGTMLogFolderName = @"GTMHTTPDebugLogs";
NSString *logsFolderPath = [desktopPath stringByAppendingPathComponent:kGTMLogFolderName];
NSFileManager *fileMgr = [NSFileManager defaultManager];
BOOL isDir;
BOOL doesFolderExist = [fileMgr fileExistsAtPath:logsFolderPath isDirectory:&isDir];
if (!doesFolderExist) {
// make the directory
doesFolderExist = [fileMgr createDirectoryAtPath:logsFolderPath
withIntermediateDirectories:YES
attributes:nil
error:NULL];
if (doesFolderExist) {
// The directory has been created. Exclude it from backups.
NSURL *pathURL = [NSURL fileURLWithPath:logsFolderPath isDirectory:YES];
[pathURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:NULL];
}
}
if (doesFolderExist) {
// it's there; store it in the global
gLoggingDirectoryPath = [logsFolderPath copy];
}
}
}
return gLoggingDirectoryPath;
}
+ (void)setLogDirectoryForCurrentRun:(NSString *)logDirectoryForCurrentRun {
// Set the path for this run's logs.
gLogDirectoryForCurrentRun = [logDirectoryForCurrentRun copy];
}
+ (NSString *)logDirectoryForCurrentRun {
// make a directory for this run's logs, like SyncProto_logs_10-16_01-56-58PM
if (gLogDirectoryForCurrentRun) return gLogDirectoryForCurrentRun;
NSString *parentDir = [self loggingDirectory];
NSString *logNamePrefix = [self processNameLogPrefix];
NSString *dateStamp = [self loggingDateStamp];
NSString *dirName = [NSString stringWithFormat:@"%@%@", logNamePrefix, dateStamp];
NSString *logDirectory = [parentDir stringByAppendingPathComponent:dirName];
if (gIsLoggingToFile) {
NSFileManager *fileMgr = [NSFileManager defaultManager];
// Be sure that the first time this app runs, it's not writing to a preexisting folder
static BOOL gShouldReuseFolder = NO;
if (!gShouldReuseFolder) {
gShouldReuseFolder = YES;
NSString *origLogDir = logDirectory;
for (int ctr = 2; ctr < 20; ++ctr) {
if (![fileMgr fileExistsAtPath:logDirectory]) break;
// append a digit
logDirectory = [origLogDir stringByAppendingFormat:@"_%d", ctr];
}
}
if (![fileMgr createDirectoryAtPath:logDirectory
withIntermediateDirectories:YES
attributes:nil
error:NULL])
return nil;
}
gLogDirectoryForCurrentRun = logDirectory;
return gLogDirectoryForCurrentRun;
}
+ (void)setLoggingEnabled:(BOOL)isLoggingEnabled {
gIsLoggingEnabled = isLoggingEnabled;
}
+ (BOOL)isLoggingEnabled {
return gIsLoggingEnabled;
}
+ (void)setLoggingToFileEnabled:(BOOL)isLoggingToFileEnabled {
gIsLoggingToFile = isLoggingToFileEnabled;
}
+ (BOOL)isLoggingToFileEnabled {
return gIsLoggingToFile;
}
+ (void)setLoggingProcessName:(NSString *)processName {
gLoggingProcessName = [processName copy];
}
+ (NSString *)loggingProcessName {
// get the process name (once per run) replacing spaces with underscores
if (!gLoggingProcessName) {
NSString *procName = [[NSProcessInfo processInfo] processName];
gLoggingProcessName = [procName stringByReplacingOccurrencesOfString:@" " withString:@"_"];
}
return gLoggingProcessName;
}
+ (void)setLoggingDateStamp:(NSString *)dateStamp {
gLoggingDateStamp = [dateStamp copy];
}
+ (NSString *)loggingDateStamp {
// We'll pick one date stamp per run, so a run that starts at a later second
// will get a unique results html file
if (!gLoggingDateStamp) {
// produce a string like 08-21_01-41-23PM
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setFormatterBehavior:NSDateFormatterBehavior10_4];
[formatter setDateFormat:@"M-dd_hh-mm-ssa"];
gLoggingDateStamp = [formatter stringFromDate:[NSDate date]];
}
return gLoggingDateStamp;
}
+ (NSString *)processNameLogPrefix {
static NSString *gPrefix = nil;
if (!gPrefix) {
NSString *processName = [self loggingProcessName];
gPrefix = [[NSString alloc] initWithFormat:@"%@_log_", processName];
}
return gPrefix;
}
+ (NSString *)symlinkNameSuffix {
return @"_log_newest.html";
}
+ (NSString *)htmlFileName {
return @"aperçu_http_log.html";
}
// formattedStringFromData returns a prettyprinted string for JSON input,
// and a plain string for other input data
- (NSString *)formattedStringFromData:(NSData *)inputData
contentType:(NSString *)contentType
JSON:(NSDictionary **)outJSON {
if (!inputData) return nil;
// if the content type is JSON and we have the parsing class available, use that
if ([contentType hasPrefix:@"application/json"] && inputData.length > 5) {
// convert from JSON string to NSObjects and back to a formatted string
NSMutableDictionary *obj =
[NSJSONSerialization JSONObjectWithData:inputData
options:NSJSONReadingMutableContainers
error:NULL];
if (obj) {
if (outJSON) *outJSON = obj;
if ([obj isKindOfClass:[NSMutableDictionary class]]) {
// for security and privacy, omit OAuth 2 response access and refresh tokens
if ([obj valueForKey:@"refresh_token"] != nil) {
[obj setObject:@"_snip_" forKey:@"refresh_token"];
}
if ([obj valueForKey:@"access_token"] != nil) {
[obj setObject:@"_snip_" forKey:@"access_token"];
}
}
NSData *data = [NSJSONSerialization dataWithJSONObject:obj
options:NSJSONWritingPrettyPrinted
error:NULL];
if (data) {
NSString *jsonStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
return jsonStr;
}
}
}
NSString *dataStr = [[NSString alloc] initWithData:inputData encoding:NSUTF8StringEncoding];
return dataStr;
}
// stringFromStreamData creates a string given the supplied data
//
// If NSString can create a UTF-8 string from the data, then that is returned.
//
// Otherwise, this routine tries to find a MIME boundary at the beginning of the data block, and
// uses that to break up the data into parts. Each part will be used to try to make a UTF-8 string.
// For parts that fail, a replacement string showing the part header and <<n bytes>> is supplied
// in place of the binary data.
- (NSString *)stringFromStreamData:(NSData *)data contentType:(NSString *)contentType {
if (!data) return nil;
// optimistically, see if the whole data block is UTF-8
NSString *streamDataStr = [self formattedStringFromData:data contentType:contentType JSON:NULL];
if (streamDataStr) return streamDataStr;
// Munge a buffer by replacing non-ASCII bytes with underscores, and turn that munged buffer an
// NSString. That gives us a string we can use with NSScanner.
NSMutableData *mutableData = [NSMutableData dataWithData:data];
unsigned char *bytes = (unsigned char *)mutableData.mutableBytes;
for (unsigned int idx = 0; idx < mutableData.length; ++idx) {
if (bytes[idx] > 0x7F || bytes[idx] == 0) {
bytes[idx] = '_';
}
}
NSString *mungedStr = [[NSString alloc] initWithData:mutableData encoding:NSUTF8StringEncoding];
if (mungedStr) {
// scan for the boundary string
NSString *boundary = nil;
NSScanner *scanner = [NSScanner scannerWithString:mungedStr];
if ([scanner scanUpToString:@"\r\n" intoString:&boundary] && [boundary hasPrefix:@"--"]) {
// we found a boundary string; use it to divide the string into parts
NSArray *mungedParts = [mungedStr componentsSeparatedByString:boundary];
// look at each munged part in the original string, and try to convert those into UTF-8
NSMutableArray *origParts = [NSMutableArray array];
NSUInteger offset = 0;
for (NSString *mungedPart in mungedParts) {
NSUInteger partSize = mungedPart.length;
NSData *origPartData = [data subdataWithRange:NSMakeRange(offset, partSize)];
NSString *origPartStr = [[NSString alloc] initWithData:origPartData
encoding:NSUTF8StringEncoding];
if (origPartStr) {
// we could make this original part into UTF-8; use the string
[origParts addObject:origPartStr];
} else {
// this part can't be made into UTF-8; scan the header, if we can
NSString *header = nil;
NSScanner *headerScanner = [NSScanner scannerWithString:mungedPart];
if (![headerScanner scanUpToString:@"\r\n\r\n" intoString:&header]) {
// we couldn't find a header
header = @"";
}
// make a part string with the header and <<n bytes>>
NSString *binStr = [NSString
stringWithFormat:@"\r%@\r<<%lu bytes>>\r", header, (long)(partSize - header.length)];
[origParts addObject:binStr];
}
offset += partSize + boundary.length;
}
// rejoin the original parts
streamDataStr = [origParts componentsJoinedByString:boundary];
}
}
if (!streamDataStr) {
// give up; just make a string showing the uploaded bytes
streamDataStr = [NSString stringWithFormat:@"<<%u bytes>>", (unsigned int)data.length];
}
return streamDataStr;
}
// logFetchWithError is called following a successful or failed fetch attempt
//
// This method does all the work for appending to and creating log files
- (void)logFetchWithError:(NSError *)error {
if (![[self class] isLoggingEnabled]) return;
NSString *logDirectory = [[self class] logDirectoryForCurrentRun];
if (!logDirectory) return;
NSString *processName = [[self class] loggingProcessName];
// TODO: add Javascript to display response data formatted in hex
// each response's NSData goes into its own xml or txt file, though all responses for this run of
// the app share a main html file. This counter tracks all fetch responses for this app run.
//
// we'll use a local variable since this routine may be reentered while waiting for formatting
// to be completed.
static int gResponseCounter = 0;
int responseCounter = ++gResponseCounter;
NSURLResponse *response = [self response];
NSDictionary *responseHeaders = [self responseHeaders];
NSString *responseDataStr = nil;
NSDictionary *responseJSON = nil;
// if there's response data, decide what kind of file to put it in based on the first bytes of the
// file or on the mime type supplied by the server
NSString *responseMIMEType = [response MIMEType];
BOOL isResponseImage = NO;
// file name for an image data file
NSString *responseDataFileName = nil;
int64_t responseDataLength = self.downloadedLength;
if (responseDataLength > 0) {
NSData *downloadedData = self.downloadedData;
if (downloadedData == nil && responseDataLength > 0 && responseDataLength < 20000 &&
self.destinationFileURL) {
// There's a download file that's not too big, so get the data to display from the downloaded
// file.
NSURL *destinationURL = self.destinationFileURL;
downloadedData = [NSData dataWithContentsOfURL:destinationURL];
}
NSString *responseType = [responseHeaders valueForKey:@"Content-Type"];
responseDataStr = [self formattedStringFromData:downloadedData
contentType:responseType
JSON:&responseJSON];
NSString *responseDataExtn = nil;
NSData *dataToWrite = nil;
if ([responseMIMEType isEqual:@"application/atom+xml"] ||
[responseMIMEType hasSuffix:@"/xml"]) {
responseDataExtn = @"xml";
dataToWrite = downloadedData;
} else if ([responseMIMEType isEqual:@"image/jpeg"]) {
responseDataExtn = @"jpg";
dataToWrite = downloadedData;
isResponseImage = YES;
} else if ([responseMIMEType isEqual:@"image/gif"]) {
responseDataExtn = @"gif";
dataToWrite = downloadedData;
isResponseImage = YES;
} else if ([responseMIMEType isEqual:@"image/png"]) {
responseDataExtn = @"png";
dataToWrite = downloadedData;
isResponseImage = YES;
} else {
// add more non-text types here
}
// if we have an extension, save the raw data in a file with that extension
if (responseDataExtn && dataToWrite) {
// generate a response file base name like
NSString *responseBaseName =
[NSString stringWithFormat:@"fetch_%d_response", responseCounter];
responseDataFileName = [responseBaseName stringByAppendingPathExtension:responseDataExtn];
NSString *responseDataFilePath =
[logDirectory stringByAppendingPathComponent:responseDataFileName];
NSError *downloadedError = nil;
if (gIsLoggingToFile && ![dataToWrite writeToFile:responseDataFilePath
options:0
error:&downloadedError]) {
NSLog(@"%@ logging write error:%@ (%@)", [self class], downloadedError,
responseDataFileName);
}
}
}
// we'll have one main html file per run of the app
NSString *htmlName = [[self class] htmlFileName];
NSString *htmlPath = [logDirectory stringByAppendingPathComponent:htmlName];
// if the html file exists (from logging previous fetches) we don't need
// to re-write the header or the scripts
NSFileManager *fileMgr = [NSFileManager defaultManager];
BOOL didFileExist = [fileMgr fileExistsAtPath:htmlPath];
NSMutableString *outputHTML = [NSMutableString string];
// we need a header to say we'll have UTF-8 text
if (!didFileExist) {
[outputHTML
appendFormat:@"<html><head><meta http-equiv=\"content-type\" "
"content=\"text/html; charset=UTF-8\"><title>%@ HTTP fetch log %@</title>",
processName, [[self class] loggingDateStamp]];
}
// now write the visible html elements
NSString *copyableFileName = [NSString stringWithFormat:@"fetch_%d.txt", responseCounter];
NSDate *now = [NSDate date];
// write the date & time, the comment, and the link to the plain-text (copyable) log
[outputHTML appendFormat:@"<b>%@ &nbsp;&nbsp;&nbsp;&nbsp; ", now];
NSString *comment = [self comment];
if (comment.length > 0) {
[outputHTML appendFormat:@"%@ &nbsp;&nbsp;&nbsp;&nbsp; ", comment];
}
[outputHTML
appendFormat:@"</b><a href='%@'><i>request/response log</i></a><br>", copyableFileName];
NSTimeInterval elapsed = -self.initialBeginFetchDate.timeIntervalSinceNow;
[outputHTML appendFormat:@"elapsed: %5.3fsec<br>", elapsed];
// write the request URL
NSURLRequest *request = self.request;
NSString *requestMethod = request.HTTPMethod;
NSURL *requestURL = request.URL;
// Save the request URL for next time in case this redirects.
NSString *redirectedFromURLString = [self.redirectedFromURL absoluteString];
self.redirectedFromURL = [requestURL copy];
if (redirectedFromURLString) {
[outputHTML appendFormat:@"<FONT COLOR='#990066'><i>redirected from %@</i></FONT><br>",
redirectedFromURLString];
}
[outputHTML appendFormat:@"<b>request:</b> %@ <code>%@</code><br>\n", requestMethod, requestURL];
// write the request headers
NSDictionary *requestHeaders = request.allHTTPHeaderFields;
NSUInteger numberOfRequestHeaders = requestHeaders.count;
if (numberOfRequestHeaders > 0) {
// Indicate if the request is authorized; warn if the request is authorized but non-SSL
NSString *auth = [requestHeaders objectForKey:@"Authorization"];
NSString *headerDetails = @"";
if (auth) {
BOOL isInsecure = [[requestURL scheme] isEqual:@"http"];
if (isInsecure) {
// 26A0 =
headerDetails =
@"&nbsp;&nbsp;&nbsp;<i>authorized, non-SSL</i><FONT COLOR='#FF00FF'> &#x26A0;</FONT> ";
} else {
headerDetails = @"&nbsp;&nbsp;&nbsp;<i>authorized</i>";
}
}
NSString *cookiesHdr = [requestHeaders objectForKey:@"Cookie"];
if (cookiesHdr) {
headerDetails = [headerDetails stringByAppendingString:@"&nbsp;&nbsp;&nbsp;<i>cookies</i>"];
}
NSString *matchHdr = [requestHeaders objectForKey:@"If-Match"];
if (matchHdr) {
headerDetails = [headerDetails stringByAppendingString:@"&nbsp;&nbsp;&nbsp;<i>if-match</i>"];
}
matchHdr = [requestHeaders objectForKey:@"If-None-Match"];
if (matchHdr) {
headerDetails =
[headerDetails stringByAppendingString:@"&nbsp;&nbsp;&nbsp;<i>if-none-match</i>"];
}
[outputHTML appendFormat:@"&nbsp;&nbsp; headers: %d %@<br>", (int)numberOfRequestHeaders,
headerDetails];
} else {
[outputHTML appendFormat:@"&nbsp;&nbsp; headers: none<br>"];
}
// write the request post data
NSData *bodyData = nil;
NSData *loggedStreamData = self.loggedStreamData;
if (loggedStreamData) {
bodyData = loggedStreamData;
} else {
bodyData = self.bodyData;
if (bodyData == nil) {
bodyData = self.request.HTTPBody;
}
}
uint64_t bodyDataLength = bodyData.length;
if (bodyData.length == 0) {
// If the data is in a body upload file URL, read that in if it's not huge.
NSURL *bodyFileURL = self.bodyFileURL;
if (bodyFileURL) {
NSNumber *fileSizeNum = nil;
NSError *fileSizeError = nil;
if ([bodyFileURL getResourceValue:&fileSizeNum
forKey:NSURLFileSizeKey
error:&fileSizeError]) {
bodyDataLength = [fileSizeNum unsignedLongLongValue];
if (bodyDataLength > 0 && bodyDataLength < 50000) {
bodyData = [NSData dataWithContentsOfURL:bodyFileURL
options:NSDataReadingUncached
error:&fileSizeError];
}
}
}
}
NSString *bodyDataStr = nil;
NSString *postType = [requestHeaders valueForKey:@"Content-Type"];
if (bodyDataLength > 0) {
[outputHTML appendFormat:@"&nbsp;&nbsp; data: %llu bytes, <code>%@</code><br>\n",
bodyDataLength, postType ? postType : @"(no type)"];
NSString *logRequestBody = self.logRequestBody;
if (logRequestBody) {
bodyDataStr = [logRequestBody copy];
self.logRequestBody = nil;
} else {
bodyDataStr = [self stringFromStreamData:bodyData contentType:postType];
if (bodyDataStr) {
// remove OAuth 2 client secret and refresh token
bodyDataStr = [[self class] snipSubstringOfString:bodyDataStr
betweenStartString:@"client_secret="
endString:@"&"];
bodyDataStr = [[self class] snipSubstringOfString:bodyDataStr
betweenStartString:@"refresh_token="
endString:@"&"];
// remove ClientLogin password
bodyDataStr = [[self class] snipSubstringOfString:bodyDataStr
betweenStartString:@"&Passwd="
endString:@"&"];
}
}
} else {
// no post data
}
// write the response status, MIME type, URL
NSInteger status = [self statusCode];
if (response) {
NSString *statusString = @"";
if (status != 0) {
if (status == 200 || status == 201) {
statusString = [NSString stringWithFormat:@"%ld", (long)status];
// report any JSON-RPC error
if ([responseJSON isKindOfClass:[NSDictionary class]]) {
NSDictionary *jsonError = [responseJSON objectForKey:@"error"];
if ([jsonError isKindOfClass:[NSDictionary class]]) {
NSString *jsonCode = [[jsonError valueForKey:@"code"] description];
NSString *jsonMessage = [jsonError valueForKey:@"message"];
if (jsonCode || jsonMessage) {
// 2691 =
NSString *const jsonErrFmt = @"&nbsp;&nbsp;&nbsp;<i>JSON error:</i> <FONT "
@"COLOR='#FF00FF'>%@ %@ &nbsp;&#x2691;</FONT>";
statusString =
[statusString stringByAppendingFormat:jsonErrFmt, jsonCode ? jsonCode : @"",
jsonMessage ? jsonMessage : @""];
}
}
}
} else {
// purple for anything other than 200 or 201
NSString *flag = status >= 400 ? @"&nbsp;&#x2691;" : @""; // 2691 =
NSString *explanation = [NSHTTPURLResponse localizedStringForStatusCode:status];
NSString *const statusFormat = @"<FONT COLOR='#FF00FF'>%ld %@ %@</FONT>";
statusString = [NSString stringWithFormat:statusFormat, (long)status, explanation, flag];
}
}
// show the response URL only if it's different from the request URL
NSString *responseURLStr = @"";
NSURL *responseURL = response.URL;
if (responseURL && ![responseURL isEqual:request.URL]) {
NSString *const responseURLFormat =
@"<FONT COLOR='#FF00FF'>response URL:</FONT> <code>%@</code><br>\n";
responseURLStr = [NSString stringWithFormat:responseURLFormat, [responseURL absoluteString]];
}
[outputHTML appendFormat:@"<b>response:</b>&nbsp;&nbsp;status %@<br>\n%@", statusString,
responseURLStr];
// Write the response headers
NSUInteger numberOfResponseHeaders = responseHeaders.count;
if (numberOfResponseHeaders > 0) {
// Indicate if the server is setting cookies
NSString *cookiesSet = [responseHeaders valueForKey:@"Set-Cookie"];
NSString *cookiesStr =
cookiesSet ? @"&nbsp;&nbsp;<FONT COLOR='#990066'><i>sets cookies</i></FONT>" : @"";
// Indicate if the server is redirecting
NSString *location = [responseHeaders valueForKey:@"Location"];
BOOL isRedirect = status >= 300 && status <= 399 && location != nil;
NSString *redirectsStr =
isRedirect ? @"&nbsp;&nbsp;<FONT COLOR='#990066'><i>redirects</i></FONT>" : @"";
[outputHTML appendFormat:@"&nbsp;&nbsp; headers: %d %@ %@<br>\n",
(int)numberOfResponseHeaders, cookiesStr, redirectsStr];
} else {
[outputHTML appendString:@"&nbsp;&nbsp; headers: none<br>\n"];
}
}
// error
if (error) {
[outputHTML appendFormat:@"<b>Error:</b> %@ <br>\n", error.description];
}
// Write the response data
if (responseDataFileName) {
if (isResponseImage) {
// Make a small inline image that links to the full image file
[outputHTML appendFormat:@"&nbsp;&nbsp; data: %lld bytes, <code>%@</code><br>",
responseDataLength, responseMIMEType];
NSString *const fmt = @"<a href=\"%@\"><img src='%@' alt='image' style='border:solid "
@"thin;max-height:32'></a>\n";
[outputHTML appendFormat:fmt, responseDataFileName, responseDataFileName];
} else {
// The response data was XML; link to the xml file
NSString *const fmt = @"&nbsp;&nbsp; data: %lld bytes, "
@"<code>%@</code>&nbsp;&nbsp;&nbsp;<i><a href=\"%@\">%@</a></i>\n";
[outputHTML appendFormat:fmt, responseDataLength, responseMIMEType, responseDataFileName,
[responseDataFileName pathExtension]];
}
} else {
// The response data was not an image; just show the length and MIME type
[outputHTML appendFormat:@"&nbsp;&nbsp; data: %lld bytes, <code>%@</code>\n",
responseDataLength,
responseMIMEType ? responseMIMEType : @"(no response type)"];
}
// Make a single string of the request and response, suitable for copying
// to the clipboard and pasting into a bug report
NSMutableString *copyable = [NSMutableString string];
if (comment) {
[copyable appendFormat:@"%@\n\n", comment];
}
[copyable appendFormat:@"%@ elapsed: %5.3fsec\n", now, elapsed];
if (redirectedFromURLString) {
[copyable appendFormat:@"Redirected from %@\n", redirectedFromURLString];
}
[copyable appendFormat:@"Request: %@ %@\n", requestMethod, requestURL];
if (requestHeaders.count > 0) {
[copyable appendFormat:@"Request headers:\n%@\n",
[[self class] headersStringForDictionary:requestHeaders]];
}
if (bodyDataLength > 0) {
[copyable appendFormat:@"Request body: (%llu bytes)\n", bodyDataLength];
if (bodyDataStr) {
[copyable appendFormat:@"%@\n", bodyDataStr];
}
[copyable appendString:@"\n"];
}
if (response) {
[copyable appendFormat:@"Response: status %d\n", (int)status];
[copyable appendFormat:@"Response headers:\n%@\n",
[[self class] headersStringForDictionary:responseHeaders]];
[copyable appendFormat:@"Response body: (%lld bytes)\n", responseDataLength];
if (responseDataLength > 0) {
NSString *logResponseBody = self.logResponseBody;
if (logResponseBody) {
// The user has provided the response body text.
responseDataStr = [logResponseBody copy];
self.logResponseBody = nil;
}
if (responseDataStr != nil) {
[copyable appendFormat:@"%@\n", responseDataStr];
} else {
// Even though it's redundant, we'll put in text to indicate that all the bytes are binary.
if (self.destinationFileURL) {
[copyable appendFormat:@"<<%lld bytes>> to file %@\n", responseDataLength,
self.destinationFileURL.path];
} else {
[copyable appendFormat:@"<<%lld bytes>>\n", responseDataLength];
}
}
}
}
if (error) {
[copyable appendFormat:@"Error: %@\n", error];
}
// Save to log property before adding the separator
self.log = copyable;
[copyable appendString:@"-----------------------------------------------------------\n"];
// Write the copyable version to another file (linked to at the top of the html file, above)
//
// Ideally, something to just copy this to the clipboard like
// <span onCopy='window.event.clipboardData.setData(\"Text\",
// \"copyable stuff\");return false;'>Copy here.</span>"
// would work everywhere, but it only works in Safari as of 8/2010
if (gIsLoggingToFile) {
NSString *parentDir = [[self class] loggingDirectory];
NSString *copyablePath = [logDirectory stringByAppendingPathComponent:copyableFileName];
NSError *copyableError = nil;
if (![copyable writeToFile:copyablePath
atomically:NO
encoding:NSUTF8StringEncoding
error:&copyableError]) {
// Error writing to file
NSLog(@"%@ logging write error:%@ (%@)", [self class], copyableError, copyablePath);
}
[outputHTML appendString:@"<br><hr><p>"];
// Append the HTML to the main output file
const char *htmlBytes = outputHTML.UTF8String;
NSOutputStream *stream = [NSOutputStream outputStreamToFileAtPath:htmlPath append:YES];
[stream open];
[stream write:(const uint8_t *)htmlBytes maxLength:strlen(htmlBytes)];
[stream close];
// Make a symlink to the latest html
NSString *const symlinkNameSuffix = [[self class] symlinkNameSuffix];
NSString *symlinkName = [processName stringByAppendingString:symlinkNameSuffix];
NSString *symlinkPath = [parentDir stringByAppendingPathComponent:symlinkName];
[fileMgr removeItemAtPath:symlinkPath error:NULL];
[fileMgr createSymbolicLinkAtPath:symlinkPath withDestinationPath:htmlPath error:NULL];
#if TARGET_OS_IPHONE
static BOOL gReportedLoggingPath = NO;
if (!gReportedLoggingPath) {
gReportedLoggingPath = YES;
NSLog(@"GTMSessionFetcher logging to \"%@\"", parentDir);
}
#endif
}
}
- (NSInputStream *)loggedInputStreamForInputStream:(NSInputStream *)inputStream {
if (!inputStream) return nil;
if (![GTMSessionFetcher isLoggingEnabled]) return inputStream;
[self clearLoggedStreamData]; // Clear any previous data.
Class monitorClass = NSClassFromString(@"GTMReadMonitorInputStream");
if (!monitorClass) {
NSString const *str = @"<<Uploaded stream log unavailable without GTMReadMonitorInputStream>>";
NSData *stringData = [str dataUsingEncoding:NSUTF8StringEncoding];
[self appendLoggedStreamData:stringData];
return inputStream;
}
inputStream = [monitorClass inputStreamWithStream:inputStream];
GTMReadMonitorInputStream *readMonitorInputStream = (GTMReadMonitorInputStream *)inputStream;
[readMonitorInputStream setReadDelegate:self];
SEL readSel = @selector(inputStream:readIntoBuffer:length:);
[readMonitorInputStream setReadSelector:readSel];
return inputStream;
}
- (GTMSessionFetcherBodyStreamProvider)loggedStreamProviderForStreamProvider:
(GTMSessionFetcherBodyStreamProvider)streamProvider {
if (!streamProvider) return nil;
if (![GTMSessionFetcher isLoggingEnabled]) return streamProvider;
[self clearLoggedStreamData]; // Clear any previous data.
Class monitorClass = NSClassFromString(@"GTMReadMonitorInputStream");
if (!monitorClass) {
NSString const *str = @"<<Uploaded stream log unavailable without GTMReadMonitorInputStream>>";
NSData *stringData = [str dataUsingEncoding:NSUTF8StringEncoding];
[self appendLoggedStreamData:stringData];
return streamProvider;
}
GTMSessionFetcherBodyStreamProvider loggedStreamProvider =
^(GTMSessionFetcherBodyStreamProviderResponse response) {
streamProvider(^(NSInputStream *bodyStream) {
bodyStream = [self loggedInputStreamForInputStream:bodyStream];
response(bodyStream);
});
};
return loggedStreamProvider;
}
@end
@implementation GTMSessionFetcher (GTMSessionFetcherLoggingUtilities)
- (void)inputStream:(GTMReadMonitorInputStream *)stream
readIntoBuffer:(void *)buffer
length:(int64_t)length {
// append the captured data
NSData *data = [NSData dataWithBytesNoCopy:buffer length:(NSUInteger)length freeWhenDone:NO];
[self appendLoggedStreamData:data];
}
#pragma mark Fomatting Utilities
+ (NSString *)snipSubstringOfString:(NSString *)originalStr
betweenStartString:(NSString *)startStr
endString:(NSString *)endStr {
#if SKIP_GTM_FETCH_LOGGING_SNIPPING
return originalStr;
#else
if (!originalStr) return nil;
// Find the start string, and replace everything between it
// and the end string (or the end of the original string) with "_snip_"
NSRange startRange = [originalStr rangeOfString:startStr];
if (startRange.location == NSNotFound) return originalStr;
// We found the start string
NSUInteger originalLength = originalStr.length;
NSUInteger startOfTarget = NSMaxRange(startRange);
NSRange targetAndRest = NSMakeRange(startOfTarget, originalLength - startOfTarget);
NSRange endRange = [originalStr rangeOfString:endStr options:0 range:targetAndRest];
NSRange replaceRange;
if (endRange.location == NSNotFound) {
// Found no end marker so replace to end of string
replaceRange = targetAndRest;
} else {
// Replace up to the endStr
replaceRange = NSMakeRange(startOfTarget, endRange.location - startOfTarget);
}
NSString *result = [originalStr stringByReplacingCharactersInRange:replaceRange
withString:@"_snip_"];
return result;
#endif // SKIP_GTM_FETCH_LOGGING_SNIPPING
}
+ (NSString *)headersStringForDictionary:(NSDictionary *)dict {
// Format the dictionary in http header style, like
// Accept: application/json
// Cache-Control: no-cache
// Content-Type: application/json; charset=utf-8
//
// Pad the key names, but not beyond 16 chars, since long custom header
// keys just create too much whitespace
NSArray *keys = [dict.allKeys sortedArrayUsingSelector:@selector(compare:)];
NSMutableString *str = [NSMutableString string];
for (NSString *key in keys) {
NSString *value = [dict valueForKey:key];
if ([key isEqual:@"Authorization"]) {
// Remove OAuth 1 token
value = [[self class] snipSubstringOfString:value
betweenStartString:@"oauth_token=\""
endString:@"\""];
// Remove OAuth 2 bearer token (draft 16, and older form)
value = [[self class] snipSubstringOfString:value
betweenStartString:@"Bearer "
endString:@"\n"];
value = [[self class] snipSubstringOfString:value
betweenStartString:@"OAuth "
endString:@"\n"];
// Remove Google ClientLogin
value = [[self class] snipSubstringOfString:value
betweenStartString:@"GoogleLogin auth="
endString:@"\n"];
}
[str appendFormat:@" %@: %@\n", key, value];
}
return str;
}
@end
#endif // !STRIP_GTM_FETCH_LOGGING

View File

@@ -0,0 +1,30 @@
/* 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.
*/
#import "GTMSessionFetcher/GTMSessionFetcherService.h"
// Internal methods from GTMSessionFetcherService, not intended for public use.
@interface GTMSessionFetcherService (Internal)
// Methods for use by the fetcher class only.
- (nullable NSURLSession *)session;
- (nullable NSURLSession *)sessionWithCreationBlock:
(nonnull NS_NOESCAPE GTMSessionFetcherSessionCreationBlock)creationBlock;
- (nullable id<NSURLSessionDelegate>)sessionDelegate;
- (nullable NSDate *)stoppedAllFetchersDate;
- (void)fetcherDidStop:(nonnull GTMSessionFetcher *)fetcher callbacksPending: (BOOL)callbacksPending;
@end

View File

@@ -0,0 +1,1419 @@
/* 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/GTMSessionFetcherService.h"
#import "GTMSessionFetcherService+Internal.h"
#include <os/lock.h>
NSString *const kGTMSessionFetcherServiceSessionBecameInvalidNotification =
@"kGTMSessionFetcherServiceSessionBecameInvalidNotification";
NSString *const kGTMSessionFetcherServiceSessionKey = @"kGTMSessionFetcherServiceSessionKey";
static id<GTMUserAgentProvider> SharedStandardUserAgentProvider(void) {
static dispatch_once_t onceToken;
static id<GTMUserAgentProvider> standardUserAgentProvider;
dispatch_once(&onceToken, ^{
standardUserAgentProvider = [[GTMStandardUserAgentProvider alloc] initWithBundle:nil];
});
return standardUserAgentProvider;
}
#if !GTMSESSION_BUILD_COMBINED_SOURCES
@interface GTMSessionFetcher (ServiceMethods)
- (BOOL)beginFetchMayDelay:(BOOL)mayDelay
mayAuthorize:(BOOL)mayAuthorize
mayDecorate:(BOOL)mayDecorate;
@end
#endif // !GTMSESSION_BUILD_COMBINED_SOURCES
@interface GTMSessionFetcherService ()
@property(atomic, strong, readwrite) NSDictionary *delayedFetchersByHost;
@property(atomic, strong, readwrite) NSDictionary *runningFetchersByHost;
// Ordered collection of id<GTMFetcherDecoratorProtocol>, held weakly.
@property(atomic, strong, readonly) NSPointerArray *decoratorsPointerArray;
@end
// Since NSURLSession doesn't support a separate delegate per task (!), instances of this
// class serve as a session delegate trampoline.
//
// This class maps a session's tasks to fetchers, and resends delegate messages to the task's
// fetcher.
@interface GTMSessionFetcherSessionDelegateDispatcher : NSObject <NSURLSessionDelegate>
// The session for the tasks in this dispatcher's task-to-fetcher map.
@property(atomic) NSURLSession *session;
// The timer interval for invalidating a session that has no active tasks.
@property(atomic) NSTimeInterval discardInterval;
// The current discard timer.
@property(atomic, readonly) NSTimer *discardTimer;
- (instancetype)initWithParentService:(GTMSessionFetcherService *)parentService
sessionDiscardInterval:(NSTimeInterval)discardInterval;
- (void)setFetcher:(GTMSessionFetcher *)fetcher forTask:(NSURLSessionTask *)task;
- (void)removeFetcher:(GTMSessionFetcher *)fetcher;
// Before using a session, tells the delegate dispatcher to stop the discard timer.
- (void)startSessionUsage;
// When abandoning a delegate dispatcher, we want to avoid the session retaining
// the delegate after tasks complete.
- (void)abandon;
@end
@implementation GTMSessionFetcherService {
NSMutableDictionary *_delayedFetchersByHost;
NSMutableDictionary *_runningFetchersByHost;
NSUInteger _maxRunningFetchersPerHost;
// When this ivar is nil, the service will not reuse sessions.
GTMSessionFetcherSessionDelegateDispatcher *_delegateDispatcher;
// Fetchers will wait on this if another fetcher is creating the shared NSURLSession.
os_unfair_lock _sessionCreationLock;
BOOL _callbackQueueIsConcurrent;
dispatch_queue_t _callbackQueue;
NSOperationQueue *_delegateQueue;
NSHTTPCookieStorage *_cookieStorage;
id<GTMUserAgentProvider> _userAgentProvider;
NSTimeInterval _timeout;
NSURLCredential *_credential; // Username & password.
NSURLCredential *_proxyCredential; // Credential supplied to proxy servers.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated"
id<GTMFetcherAuthorizationProtocol> _authorizer;
#pragma clang diagnostic pop
// For waitForCompletionOfAllFetchersWithTimeout: we need to wait on stopped fetchers since
// they've not yet finished invoking their queued callbacks. This array is nil except when
// waiting on fetchers.
NSMutableArray *_stoppedFetchersToWaitFor;
// For fetchers that enqueued their callbacks before stopAllFetchers was called on the service,
// set a barrier so the callbacks know to bail out.
NSDate *_stoppedAllFetchersDate;
}
// Clang-format likes to cram all @synthesize items onto the fewest lines, rather than one-per.
// clang-format off
@synthesize maxRunningFetchersPerHost = _maxRunningFetchersPerHost,
configuration = _configuration,
configurationBlock = _configurationBlock,
cookieStorage = _cookieStorage,
challengeBlock = _challengeBlock,
credential = _credential,
proxyCredential = _proxyCredential,
allowedInsecureSchemes = _allowedInsecureSchemes,
allowLocalhostRequest = _allowLocalhostRequest,
allowInvalidServerCertificates = _allowInvalidServerCertificates,
retryEnabled = _retryEnabled,
retryBlock = _retryBlock,
maxRetryInterval = _maxRetryInterval,
minRetryInterval = _minRetryInterval,
metricsCollectionBlock = _metricsCollectionBlock,
properties = _properties,
unusedSessionTimeout = _unusedSessionTimeout,
userAgentProvider = _userAgentProvider,
decoratorsPointerArray = _decoratorsPointerArray,
testBlock = _testBlock;
// clang-format on
#if GTM_BACKGROUND_TASK_FETCHING
@synthesize skipBackgroundTask = _skipBackgroundTask;
#endif
- (instancetype)init {
self = [super init];
if (self) {
_delayedFetchersByHost = [[NSMutableDictionary alloc] init];
_runningFetchersByHost = [[NSMutableDictionary alloc] init];
_maxRunningFetchersPerHost = 10;
_unusedSessionTimeout = 60.0;
_delegateDispatcher = [[GTMSessionFetcherSessionDelegateDispatcher alloc]
initWithParentService:self
sessionDiscardInterval:_unusedSessionTimeout];
_callbackQueue = dispatch_get_main_queue();
_delegateQueue = [[NSOperationQueue alloc] init];
_delegateQueue.maxConcurrentOperationCount = 1;
_delegateQueue.name = @"com.google.GTMSessionFetcher.NSURLSessionDelegateQueue";
_sessionCreationLock = OS_UNFAIR_LOCK_INIT;
// Starting with the SDKs for OS X 10.11/iOS 9, the service has a default useragent.
// Apps can remove this and get the default system "CFNetwork" useragent by setting the
// fetcher service's userAgent or userAgentProvider properties to nil.
//
// Formatting the User-Agent string can be expensive, so create a shared cache
// which asynchronously calculates and caches the standard User-Agent.
_userAgentProvider = SharedStandardUserAgentProvider();
}
return self;
}
- (void)dealloc {
[self detachAuthorizer];
[_delegateDispatcher abandon];
}
#pragma mark Generate a new fetcher
// Creates a serial queue targetting the service's callback, meant to be provided to a new
// GTMSessionFetcher instance.
//
// This method is not intended to be overrideable by clients.
- (nonnull dispatch_queue_t)serialQueueForNewFetcher:(GTMSessionFetcher *)fetcher {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
if (!_callbackQueueIsConcurrent) return _callbackQueue;
static const char *kQueueLabel = "com.google.GTMSessionFetcher.serialCallbackQueue";
return dispatch_queue_create_with_target(kQueueLabel, DISPATCH_QUEUE_SERIAL, _callbackQueue);
}
}
// Clients may override this method. Clients should not override any other library methods.
- (id)fetcherWithRequest:(NSURLRequest *)request fetcherClass:(Class)fetcherClass {
GTMSessionFetcher *fetcher = [[fetcherClass alloc] initWithRequest:request
configuration:self.configuration];
fetcher.callbackQueue = [self serialQueueForNewFetcher:fetcher];
fetcher.sessionDelegateQueue = self.sessionDelegateQueue;
fetcher.challengeBlock = self.challengeBlock;
fetcher.credential = self.credential;
fetcher.proxyCredential = self.proxyCredential;
fetcher.authorizer = self.authorizer;
fetcher.cookieStorage = self.cookieStorage;
fetcher.allowedInsecureSchemes = self.allowedInsecureSchemes;
fetcher.allowLocalhostRequest = self.allowLocalhostRequest;
fetcher.allowInvalidServerCertificates = self.allowInvalidServerCertificates;
fetcher.configurationBlock = self.configurationBlock;
fetcher.retryEnabled = self.retryEnabled;
fetcher.retryBlock = self.retryBlock;
fetcher.maxRetryInterval = self.maxRetryInterval;
fetcher.minRetryInterval = self.minRetryInterval;
if (@available(iOS 10.0, *)) {
fetcher.metricsCollectionBlock = self.metricsCollectionBlock;
}
fetcher.properties = self.properties;
fetcher.service = self;
#if GTM_BACKGROUND_TASK_FETCHING
fetcher.skipBackgroundTask = self.skipBackgroundTask;
#endif
fetcher.userAgentProvider = self.userAgentProvider;
fetcher.testBlock = self.testBlock;
return fetcher;
}
- (GTMSessionFetcher *)fetcherWithRequest:(NSURLRequest *)request {
return [self fetcherWithRequest:request fetcherClass:[GTMSessionFetcher class]];
}
- (GTMSessionFetcher *)fetcherWithURL:(NSURL *)requestURL {
return [self fetcherWithRequest:[NSURLRequest requestWithURL:requestURL]];
}
- (GTMSessionFetcher *)fetcherWithURLString:(NSString *)requestURLString {
NSURL *url = [NSURL URLWithString:requestURLString];
return [self fetcherWithURL:url];
}
- (void)addDecorator:(id<GTMFetcherDecoratorProtocol>)decorator {
@synchronized(self) {
if (!_decoratorsPointerArray) {
_decoratorsPointerArray = [NSPointerArray weakObjectsPointerArray];
}
[_decoratorsPointerArray addPointer:(__bridge void *)decorator];
}
}
- (nullable NSArray<id<GTMFetcherDecoratorProtocol>> *)decorators {
@synchronized(self) {
return _decoratorsPointerArray.allObjects;
}
}
- (void)removeDecorator:(id<GTMFetcherDecoratorProtocol>)decorator {
@synchronized(self) {
NSUInteger i = 0;
for (id<GTMFetcherDecoratorProtocol> decoratorCandidate in _decoratorsPointerArray) {
if (decoratorCandidate == decorator) {
break;
}
++i;
}
GTMSESSION_ASSERT_DEBUG(i < _decoratorsPointerArray.count,
@"decorator %@ must be passed to -addDecorator: before removing",
decorator);
if (i < _decoratorsPointerArray.count) {
[_decoratorsPointerArray removePointerAtIndex:i];
}
}
}
// Returns a session for the fetcher's host, or nil.
- (NSURLSession *)session {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
NSURLSession *session = _delegateDispatcher.session;
return session;
}
}
- (NSURLSession *)sessionWithCreationBlock:
(NS_NOESCAPE GTMSessionFetcherSessionCreationBlock)creationBlock {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
if (!_delegateDispatcher) {
// This fetcher is creating a non-shared session, so skip locking.
return creationBlock(nil);
}
}
@try {
NSURLSession *session;
// Wait if another fetcher is currently creating a session; avoid waiting inside the
// @synchronized block as that could deadlock.
os_unfair_lock_lock(&_sessionCreationLock);
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
// Before getting the NSURLSession for task creation, it is
// important to invalidate and nil out the session discard timer; otherwise
// the session can be invalidated between when it is returned to the
// fetcher, and when the fetcher attempts to create its NSURLSessionTask.
[_delegateDispatcher startSessionUsage];
session = _delegateDispatcher.session;
if (!session) {
session = creationBlock(_delegateDispatcher);
_delegateDispatcher.session = session;
}
}
return session;
} @finally {
// Ensure the lock is always released, even if creationBlock throws.
os_unfair_lock_unlock(&_sessionCreationLock);
}
}
- (id<NSURLSessionDelegate>)sessionDelegate {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _delegateDispatcher;
}
}
#pragma mark Queue Management
- (void)addRunningFetcher:(GTMSessionFetcher *)fetcher forHost:(NSString *)host {
// Add to the array of running fetchers for this host, creating the array if needed.
NSMutableArray *runningForHost = [_runningFetchersByHost objectForKey:host];
if (runningForHost == nil) {
runningForHost = [NSMutableArray arrayWithObject:fetcher];
[_runningFetchersByHost setObject:runningForHost forKey:host];
} else {
[runningForHost addObject:fetcher];
}
}
- (void)addDelayedFetcher:(GTMSessionFetcher *)fetcher forHost:(NSString *)host {
// Add to the array of delayed fetchers for this host, creating the array if needed.
NSMutableArray *delayedForHost = [_delayedFetchersByHost objectForKey:host];
if (delayedForHost == nil) {
delayedForHost = [NSMutableArray arrayWithObject:fetcher];
[_delayedFetchersByHost setObject:delayedForHost forKey:host];
} else {
[delayedForHost addObject:fetcher];
}
}
- (BOOL)isDelayingFetcher:(GTMSessionFetcher *)fetcher {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
NSString *host = fetcher.request.URL.host;
if (host == nil) {
return NO;
}
NSArray *delayedForHost = [_delayedFetchersByHost objectForKey:host];
NSUInteger idx = [delayedForHost indexOfObjectIdenticalTo:fetcher];
BOOL isDelayed = (delayedForHost != nil) && (idx != NSNotFound);
return isDelayed;
}
}
- (BOOL)fetcherShouldBeginFetching:(GTMSessionFetcher *)fetcher {
// Entry point from the fetcher
NSURL *requestURL = fetcher.request.URL;
NSString *host = requestURL.host;
// Addresses "file:///path" case where localhost is the implicit host.
if (host.length == 0 && [requestURL isFileURL]) {
host = @"localhost";
}
if (host.length == 0) {
// Data URIs legitimately have no host, reject other hostless URLs.
GTMSESSION_ASSERT_DEBUG([[requestURL scheme] isEqual:@"data"], @"%@ lacks host", fetcher);
return YES;
}
BOOL shouldBeginResult;
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
NSMutableArray *runningForHost = [_runningFetchersByHost objectForKey:host];
if (runningForHost != nil && [runningForHost indexOfObjectIdenticalTo:fetcher] != NSNotFound) {
GTMSESSION_ASSERT_DEBUG(NO, @"%@ was already running", fetcher);
return YES;
}
BOOL shouldRunNow = (fetcher.usingBackgroundSession || _maxRunningFetchersPerHost == 0 ||
_maxRunningFetchersPerHost >
[[self class] numberOfNonBackgroundSessionFetchers:runningForHost]);
if (shouldRunNow) {
[self addRunningFetcher:fetcher forHost:host];
shouldBeginResult = YES;
} else {
[self addDelayedFetcher:fetcher forHost:host];
shouldBeginResult = NO;
}
} // @synchronized(self)
// We'll save the host that serves as the key for this fetcher's array
// to avoid any chance of the underlying request changing, stranding
// the fetcher in the wrong array
fetcher.serviceHost = host;
return shouldBeginResult;
}
- (void)startFetcher:(GTMSessionFetcher *)fetcher {
[fetcher beginFetchMayDelay:NO mayAuthorize:YES mayDecorate:YES];
}
// Internal utility. Returns a fetcher's delegate if it's a dispatcher, or nil if the fetcher
// is its own delegate (possibly via proxy) and has no dispatcher.
- (GTMSessionFetcherSessionDelegateDispatcher *)delegateDispatcherForFetcher:
(GTMSessionFetcher *)fetcher {
GTMSessionCheckNotSynchronized(self);
NSURLSession *fetcherSession = fetcher.session;
if (fetcherSession) {
id<NSURLSessionDelegate> fetcherDelegate = fetcherSession.delegate;
// If the delegate is non-nil and claims to be a GTMSessionFetcher, there is no dispatcher;
// assume the fetcher is the delegate or has been proxied (some third-party frameworks
// are known to swizzle NSURLSession to proxy its delegate).
BOOL hasDispatcher =
(fetcherDelegate != nil && ![fetcherDelegate isKindOfClass:[GTMSessionFetcher class]]);
if (hasDispatcher) {
GTMSESSION_ASSERT_DEBUG(
[fetcherDelegate isKindOfClass:[GTMSessionFetcherSessionDelegateDispatcher class]],
@"Fetcher delegate class: %@", [fetcherDelegate class]);
return (GTMSessionFetcherSessionDelegateDispatcher *)fetcherDelegate;
}
}
return nil;
}
- (void)fetcherDidBeginFetching:(GTMSessionFetcher *)fetcher {
// If this fetcher has a separate delegate with a shared session, then
// this fetcher should be added to the delegate's map of tasks to fetchers.
GTMSessionFetcherSessionDelegateDispatcher *delegateDispatcher =
[self delegateDispatcherForFetcher:fetcher];
if (delegateDispatcher) {
GTMSESSION_ASSERT_DEBUG(fetcher.canShareSession, @"Inappropriate shared session: %@", fetcher);
// There should already be a session, from this or a previous fetcher.
//
// Sanity check that the fetcher's session is the delegate's shared session.
NSURLSession *sharedSession = delegateDispatcher.session;
NSURLSession *fetcherSession = fetcher.session;
GTMSESSION_ASSERT_DEBUG(sharedSession != nil, @"Missing delegate session: %@", fetcher);
GTMSESSION_ASSERT_DEBUG(fetcherSession == sharedSession,
@"Inconsistent session: %@ %@ (shared: %@)", fetcher, fetcherSession,
sharedSession);
if (sharedSession != nil && fetcherSession == sharedSession) {
NSURLSessionTask *task = fetcher.sessionTask;
GTMSESSION_ASSERT_DEBUG(task != nil, @"Missing session task: %@", fetcher);
if (task) {
[delegateDispatcher setFetcher:fetcher forTask:task];
}
}
}
}
- (void)stopFetcher:(GTMSessionFetcher *)fetcher {
[fetcher stopFetching];
}
- (void)fetcherDidStop:(GTMSessionFetcher *)fetcher {
[self fetcherDidStop:fetcher callbacksPending:false];
}
- (void)fetcherDidStop:(GTMSessionFetcher *)fetcher callbacksPending:(BOOL) callbacksPending {
// Entry point from the fetcher
NSString *host = fetcher.serviceHost;
if (!host) {
// fetcher has been stopped previously
return;
}
// This removeFetcher: invocation is a fallback; typically, fetchers are removed from the task
// map when the task completes.
if (!callbacksPending) {
GTMSessionFetcherSessionDelegateDispatcher *delegateDispatcher =
[self delegateDispatcherForFetcher:fetcher];
[delegateDispatcher removeFetcher:fetcher];
}
NSMutableArray *fetchersToStart;
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
// If a test is waiting for all fetchers to stop, it needs to wait for this one
// to invoke its callbacks on the callback queue.
[_stoppedFetchersToWaitFor addObject:fetcher];
NSMutableArray *runningForHost = [_runningFetchersByHost objectForKey:host];
[runningForHost removeObject:fetcher];
NSMutableArray *delayedForHost = [_delayedFetchersByHost objectForKey:host];
[delayedForHost removeObject:fetcher];
while (delayedForHost.count > 0 &&
[[self class] numberOfNonBackgroundSessionFetchers:runningForHost] <
_maxRunningFetchersPerHost) {
// Start another delayed fetcher running, scanning for the minimum
// priority value, defaulting to FIFO for equal priorities
GTMSessionFetcher *nextFetcher = nil;
for (GTMSessionFetcher *delayedFetcher in delayedForHost) {
if (nextFetcher == nil || delayedFetcher.servicePriority < nextFetcher.servicePriority) {
nextFetcher = delayedFetcher;
}
}
if (nextFetcher) {
[self addRunningFetcher:nextFetcher forHost:host];
runningForHost = [_runningFetchersByHost objectForKey:host];
[delayedForHost removeObjectIdenticalTo:nextFetcher];
if (!fetchersToStart) {
fetchersToStart = [NSMutableArray array];
}
[fetchersToStart addObject:nextFetcher];
}
}
if (runningForHost.count == 0) {
// None left; remove the empty array
[_runningFetchersByHost removeObjectForKey:host];
}
if (delayedForHost.count == 0) {
[_delayedFetchersByHost removeObjectForKey:host];
}
} // @synchronized(self)
// Start fetchers outside of the synchronized block to avoid a deadlock.
for (GTMSessionFetcher *nextFetcher in fetchersToStart) {
[self startFetcher:nextFetcher];
}
// The fetcher is no longer in the running or the delayed array,
// so remove its host and thread properties
fetcher.serviceHost = nil;
}
- (NSUInteger)numberOfFetchers {
NSUInteger running = [self numberOfRunningFetchers];
NSUInteger delayed = [self numberOfDelayedFetchers];
return running + delayed;
}
- (NSUInteger)numberOfRunningFetchers {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
NSUInteger sum = 0;
for (NSString *host in _runningFetchersByHost) {
NSArray *fetchers = [_runningFetchersByHost objectForKey:host];
sum += fetchers.count;
}
return sum;
}
}
- (NSUInteger)numberOfDelayedFetchers {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
NSUInteger sum = 0;
for (NSString *host in _delayedFetchersByHost) {
NSArray *fetchers = [_delayedFetchersByHost objectForKey:host];
sum += fetchers.count;
}
return sum;
}
}
- (NSArray *)issuedFetchers {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
NSMutableArray *allFetchers = [NSMutableArray array];
void (^accumulateFetchers)(id, id, BOOL *) =
^(NSString *host, NSArray *fetchersForHost, BOOL *stop) {
[allFetchers addObjectsFromArray:fetchersForHost];
};
[_runningFetchersByHost enumerateKeysAndObjectsUsingBlock:accumulateFetchers];
[_delayedFetchersByHost enumerateKeysAndObjectsUsingBlock:accumulateFetchers];
GTMSESSION_ASSERT_DEBUG(allFetchers.count == [NSSet setWithArray:allFetchers].count,
@"Fetcher appears multiple times\n running: %@\n delayed: %@",
_runningFetchersByHost, _delayedFetchersByHost);
return allFetchers.count > 0 ? allFetchers : nil;
}
}
- (NSArray *)issuedFetchersWithRequestURL:(NSURL *)requestURL {
NSString *host = requestURL.host;
if (host.length == 0) return nil;
NSURL *targetURL = [requestURL absoluteURL];
NSArray *allFetchers = [self issuedFetchers];
NSIndexSet *indexes = [allFetchers
indexesOfObjectsPassingTest:^BOOL(GTMSessionFetcher *fetcher, NSUInteger idx, BOOL *stop) {
NSURL *fetcherURL = [fetcher.request.URL absoluteURL];
return [fetcherURL isEqual:targetURL];
}];
NSArray *result = nil;
if (indexes.count > 0) {
result = [allFetchers objectsAtIndexes:indexes];
}
return result;
}
- (void)stopAllFetchers {
NSArray *delayedFetchersByHost;
NSArray *runningFetchersByHost;
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
// Set the time barrier so fetchers know not to call back even if
// the stop calls below occur after the fetchers naturally
// stopped and so were removed from _runningFetchersByHost,
// but while the callbacks were already enqueued before stopAllFetchers
// was invoked.
_stoppedAllFetchersDate = [[NSDate alloc] init];
// Remove fetchers from the delayed list to avoid fetcherDidStop: from
// starting more fetchers running as a side effect of stopping one
delayedFetchersByHost = _delayedFetchersByHost.allValues;
[_delayedFetchersByHost removeAllObjects];
runningFetchersByHost = _runningFetchersByHost.allValues;
[_runningFetchersByHost removeAllObjects];
}
for (NSArray *delayedForHost in delayedFetchersByHost) {
for (GTMSessionFetcher *fetcher in delayedForHost) {
[self stopFetcher:fetcher];
}
}
for (NSArray *runningForHost in runningFetchersByHost) {
for (GTMSessionFetcher *fetcher in runningForHost) {
[self stopFetcher:fetcher];
}
}
}
- (NSDate *)stoppedAllFetchersDate {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _stoppedAllFetchersDate;
}
}
#pragma mark Accessors
- (BOOL)reuseSession {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _delegateDispatcher != nil;
}
}
- (void)setReuseSession:(BOOL)shouldReuse {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
BOOL wasReusing = (_delegateDispatcher != nil);
if (shouldReuse != wasReusing) {
[self abandonDispatcher];
if (shouldReuse) {
_delegateDispatcher = [[GTMSessionFetcherSessionDelegateDispatcher alloc]
initWithParentService:self
sessionDiscardInterval:_unusedSessionTimeout];
} else {
_delegateDispatcher = nil;
}
}
}
}
- (void)resetSession {
GTMSessionCheckNotSynchronized(self);
os_unfair_lock_lock(&_sessionCreationLock);
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
[self resetSessionInternal];
}
os_unfair_lock_unlock(&_sessionCreationLock);
}
- (void)resetSessionInternal {
GTMSessionCheckSynchronized(self);
// The old dispatchers may be retained as delegates of any ongoing sessions by those sessions.
if (_delegateDispatcher) {
[self abandonDispatcher];
_delegateDispatcher = [[GTMSessionFetcherSessionDelegateDispatcher alloc]
initWithParentService:self
sessionDiscardInterval:_unusedSessionTimeout];
}
}
- (void)resetSessionForDispatcherDiscardTimer:(NSTimer *)timer {
GTMSessionCheckNotSynchronized(self);
os_unfair_lock_lock(&_sessionCreationLock);
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
if (_delegateDispatcher.discardTimer == timer) {
// If the delegate dispatcher's current discardTimer is the same object as the timer
// that fired, no fetcher has recently attempted to start using the session by calling
// startSessionUsage, which invalidates and nils out the timer.
[self resetSessionInternal];
} else {
// A fetcher has invalidated the timer between its triggering and now, potentially
// meaning a fetcher has requested access to the NSURLSession, and may be in the process
// of starting a new task. The dispatcher should not be abandoned, as this can lead
// to a race condition between calling -finishTasksAndInvalidate on the NSURLSession
// and the fetcher attempting to create a new task.
}
}
os_unfair_lock_unlock(&_sessionCreationLock);
}
- (NSTimeInterval)unusedSessionTimeout {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _unusedSessionTimeout;
}
}
- (void)setUnusedSessionTimeout:(NSTimeInterval)timeout {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_unusedSessionTimeout = timeout;
_delegateDispatcher.discardInterval = timeout;
}
}
// This method should be called inside of @synchronized(self)
- (void)abandonDispatcher {
GTMSessionCheckSynchronized(self);
[_delegateDispatcher abandon];
}
- (NSDictionary *)runningFetchersByHost {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return [_runningFetchersByHost copy];
}
}
- (void)setRunningFetchersByHost:(NSDictionary *)dict {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_runningFetchersByHost = [dict mutableCopy];
}
}
- (NSDictionary *)delayedFetchersByHost {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return [_delayedFetchersByHost copy];
}
}
- (void)setDelayedFetchersByHost:(NSDictionary *)dict {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_delayedFetchersByHost = [dict mutableCopy];
}
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated"
- (id<GTMFetcherAuthorizationProtocol>)authorizer {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _authorizer;
}
}
- (void)setAuthorizer:(id<GTMFetcherAuthorizationProtocol>)obj {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
if (obj != _authorizer) {
[self detachAuthorizer];
}
_authorizer = obj;
}
// Use the fetcher service for the authorization fetches if the auth
// object supports fetcher services
if ([obj respondsToSelector:@selector(setFetcherService:)]) {
[obj setFetcherService:self];
}
}
#pragma clang diagnostic pop
// This should be called inside a @synchronized(self) block except during dealloc.
- (void)detachAuthorizer {
// This method is called by the fetcher service's dealloc and setAuthorizer:
// methods; do not override.
//
// The fetcher service retains the authorizer, and the authorizer has a
// weak pointer to the fetcher service (a non-zeroing pointer for
// compatibility with iOS 4 and Mac OS X 10.5/10.6.)
//
// When this fetcher service no longer uses the authorizer, we want to remove
// the authorizer's dependence on the fetcher service. Authorizers can still
// function without a fetcher service.
if ([_authorizer respondsToSelector:@selector(fetcherService)]) {
id authFetcherService = [_authorizer fetcherService];
if (authFetcherService == self) {
[_authorizer setFetcherService:nil];
}
}
}
- (nonnull dispatch_queue_t)callbackQueue {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _callbackQueue;
} // @synchronized(self)
}
- (void)setCallbackQueue:(dispatch_queue_t)queue {
[self setCallbackQueue:queue isConcurrent:NO];
}
- (void)setConcurrentCallbackQueue:(dispatch_queue_t)queue {
[self setCallbackQueue:queue isConcurrent:YES];
}
- (void)setCallbackQueue:(dispatch_queue_t)queue isConcurrent:(BOOL)isConcurrent {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
#if DEBUG
// Warn when changing from a concurrent queue to a serial queue.
if (_callbackQueueIsConcurrent && (!isConcurrent || !queue)) {
GTMSESSION_LOG_DEBUG(
@"WARNING: Resetting the service callback queue from concurrent to serial");
}
#endif // DEBUG
_callbackQueue = queue ?: dispatch_get_main_queue();
_callbackQueueIsConcurrent = queue ? isConcurrent : NO;
} // @synchronized(self)
}
- (NSOperationQueue *)sessionDelegateQueue {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _delegateQueue;
} // @synchronized(self)
}
- (void)setSessionDelegateQueue:(NSOperationQueue *)queue {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_delegateQueue = queue ?: [NSOperationQueue mainQueue];
} // @synchronized(self)
}
- (nullable NSString *)userAgent {
@synchronized(self) {
return _userAgentProvider.userAgent;
}
}
- (void)setUserAgent:(nullable NSString *)userAgent {
@synchronized(self) {
if (userAgent) {
_userAgentProvider = [[GTMUserAgentStringProvider alloc]
initWithUserAgentString:(NSString *_Nonnull)userAgent];
} else {
// Support setUserAgent:nil to disable `GTMStandardUserAgentProvider`.
_userAgentProvider = nil;
}
}
}
- (nullable id<GTMUserAgentProvider>)userAgentProvider {
@synchronized(self) {
return _userAgentProvider;
}
}
- (void)setUserAgentProvider:(nullable id<GTMUserAgentProvider>)userAgentProvider {
@synchronized(self) {
_userAgentProvider = userAgentProvider;
}
}
- (NSOperationQueue *)delegateQueue {
// Provided for compatibility with the old fetcher service. The gtm-oauth2 code respects
// any custom delegate queue for calling the app.
return nil;
}
+ (NSUInteger)numberOfNonBackgroundSessionFetchers:(NSArray *)fetchers {
NSUInteger sum = 0;
for (GTMSessionFetcher *fetcher in fetchers) {
if (!fetcher.usingBackgroundSession) {
++sum;
}
}
return sum;
}
@end
@implementation GTMSessionFetcherService (TestingSupport)
+ (instancetype)mockFetcherServiceWithFakedData:(NSData *)fakedDataOrNil
fakedError:(NSError *)fakedErrorOrNil {
#if !GTM_DISABLE_FETCHER_TEST_BLOCK
NSURL *url = [NSURL URLWithString:@"http://example.invalid"];
NSHTTPURLResponse *fakedResponse =
[[NSHTTPURLResponse alloc] initWithURL:url
statusCode:(fakedErrorOrNil ? 500 : 200)HTTPVersion:@"HTTP/1.1"
headerFields:nil];
return [self mockFetcherServiceWithFakedData:fakedDataOrNil
fakedResponse:fakedResponse
fakedError:fakedErrorOrNil];
#else
GTMSESSION_ASSERT_DEBUG(0, @"Test blocks disabled");
return nil;
#endif // GTM_DISABLE_FETCHER_TEST_BLOCK
}
+ (instancetype)mockFetcherServiceWithFakedData:(NSData *)fakedDataOrNil
fakedResponse:(NSHTTPURLResponse *)fakedResponse
fakedError:(NSError *)fakedErrorOrNil {
#if !GTM_DISABLE_FETCHER_TEST_BLOCK
GTMSessionFetcherService *service = [[self alloc] init];
service.allowedInsecureSchemes = @[ @"http" ];
service.testBlock =
^(GTMSessionFetcher *fetcherToTest, GTMSessionFetcherTestResponse testResponse) {
testResponse(fakedResponse, fakedDataOrNil, fakedErrorOrNil);
};
return service;
#else
GTMSESSION_ASSERT_DEBUG(0, @"Test blocks disabled");
return nil;
#endif // GTM_DISABLE_FETCHER_TEST_BLOCK
}
#pragma mark Synchronous Wait for Unit Testing
- (BOOL)waitForCompletionOfAllFetchersWithTimeout:(NSTimeInterval)timeoutInSeconds {
NSDate *giveUpDate = [NSDate dateWithTimeIntervalSinceNow:timeoutInSeconds];
_stoppedFetchersToWaitFor = [NSMutableArray array];
BOOL shouldSpinRunLoop = [NSThread isMainThread];
const NSTimeInterval kSpinInterval = 0.001;
BOOL didTimeOut = NO;
while (([self numberOfFetchers] > 0 || _stoppedFetchersToWaitFor.count > 0)) {
didTimeOut = [giveUpDate timeIntervalSinceNow] < 0;
if (didTimeOut) break;
GTMSessionFetcher *stoppedFetcher = _stoppedFetchersToWaitFor.firstObject;
if (stoppedFetcher) {
[_stoppedFetchersToWaitFor removeObject:stoppedFetcher];
[stoppedFetcher waitForCompletionWithTimeout:10.0 * kSpinInterval];
}
if (shouldSpinRunLoop) {
NSDate *stopDate = [NSDate dateWithTimeIntervalSinceNow:kSpinInterval];
[[NSRunLoop currentRunLoop] runUntilDate:stopDate];
} else {
[NSThread sleepForTimeInterval:kSpinInterval];
}
}
_stoppedFetchersToWaitFor = nil;
return !didTimeOut;
}
@end
@implementation GTMSessionFetcherSessionDelegateDispatcher {
__weak GTMSessionFetcherService *_parentService;
NSURLSession *_session;
// The task map maps NSURLSessionTasks to GTMSessionFetchers
NSMutableDictionary *_taskToFetcherMap;
// The discard timer will invalidate sessions after the session's last task completes.
NSTimer *_discardTimer;
NSTimeInterval _discardInterval;
}
@synthesize discardInterval = _discardInterval, session = _session;
- (instancetype)init {
[self doesNotRecognizeSelector:_cmd];
return nil;
}
- (instancetype)initWithParentService:(GTMSessionFetcherService *)parentService
sessionDiscardInterval:(NSTimeInterval)discardInterval {
self = [super init];
if (self) {
_discardInterval = discardInterval;
_parentService = parentService;
}
return self;
}
- (NSString *)description {
return
[NSString stringWithFormat:@"%@ %p %@ %@", [self class], self, _session ?: @"<no session>",
_taskToFetcherMap.count > 0 ? _taskToFetcherMap : @"<no tasks>"];
}
- (NSTimer *)discardTimer {
GTMSessionCheckNotSynchronized(self);
@synchronized(self) {
return _discardTimer;
}
}
// This method should be called inside of a @synchronized(self) block.
- (void)startDiscardTimer {
GTMSessionCheckSynchronized(self);
[_discardTimer invalidate];
_discardTimer = nil;
if (_discardInterval > 0) {
_discardTimer = [NSTimer timerWithTimeInterval:_discardInterval
target:self
selector:@selector(discardTimerFired:)
userInfo:nil
repeats:NO];
[_discardTimer setTolerance:(_discardInterval / 10)];
[[NSRunLoop mainRunLoop] addTimer:_discardTimer forMode:NSRunLoopCommonModes];
}
}
// This method should be called inside of a @synchronized(self) block.
- (void)destroyDiscardTimer {
GTMSessionCheckSynchronized(self);
[_discardTimer invalidate];
_discardTimer = nil;
}
- (void)discardTimerFired:(NSTimer *)timer {
GTMSessionFetcherService *service;
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
NSUInteger numberOfTasks = _taskToFetcherMap.count;
if (numberOfTasks == 0) {
service = _parentService;
}
}
// Inform the service that the discard timer has fired, and should check whether the
// service can abandon us. -resetSession cannot be called directly, as there is a
// race condition that must be guarded against with the NSURLSession being returned
// from sessionForFetcherCreation outside other locks. The service can take steps
// to prevent resetting the session if that has occurred.
//
// The service must be called from outside the @synchronized block.
[service resetSessionForDispatcherDiscardTimer:timer];
}
- (void)abandon {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
[self destroySessionAndTimer];
}
}
- (void)startSessionUsage {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
[self destroyDiscardTimer];
}
}
// This method should be called inside of a @synchronized(self) block.
- (void)destroySessionAndTimer {
GTMSessionCheckSynchronized(self);
[self destroyDiscardTimer];
// Break any retain cycle from the session holding the delegate.
[_session finishTasksAndInvalidate];
// Immediately clear the session so no new task may be issued with it.
//
// The _taskToFetcherMap needs to stay valid until the outstanding tasks finish.
_session = nil;
}
- (void)setFetcher:(GTMSessionFetcher *)fetcher forTask:(NSURLSessionTask *)task {
GTMSESSION_ASSERT_DEBUG(fetcher != nil, @"missing fetcher");
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
if (_taskToFetcherMap == nil) {
_taskToFetcherMap = [[NSMutableDictionary alloc] init];
}
if (fetcher) {
[_taskToFetcherMap setObject:fetcher forKey:task];
[self destroyDiscardTimer];
}
}
}
- (void)removeFetcher:(GTMSessionFetcher *)fetcher {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
// Typically, a fetcher should be removed when its task invokes
// URLSession:task:didCompleteWithError:.
//
// When fetching with a testBlock, though, the task completed delegate
// method may not be invoked, requiring cleanup here.
NSArray *tasks = [_taskToFetcherMap allKeysForObject:fetcher];
GTMSESSION_ASSERT_DEBUG(tasks.count <= 1, @"fetcher task not unmapped: %@", tasks);
[_taskToFetcherMap removeObjectsForKeys:tasks];
if (_taskToFetcherMap.count == 0) {
[self startDiscardTimer];
}
}
}
// This helper method provides synchronized access to the task map for the delegate
// methods below.
- (id)fetcherForTask:(NSURLSessionTask *)task {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return [_taskToFetcherMap objectForKey:task];
}
}
- (void)removeTaskFromMap:(NSURLSessionTask *)task {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
[_taskToFetcherMap removeObjectForKey:task];
}
}
- (void)setSession:(NSURLSession *)session {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_session = session;
}
}
- (NSURLSession *)session {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _session;
}
}
- (NSTimeInterval)discardInterval {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _discardInterval;
}
}
- (void)setDiscardInterval:(NSTimeInterval)interval {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_discardInterval = interval;
}
}
// NSURLSessionDelegate protocol methods.
// - (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session;
//
// TODO(seh): How do we route this to an appropriate fetcher?
- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(NSError *)error {
GTMSESSION_LOG_DEBUG_VERBOSE(@"%@ %p URLSession:%@ didBecomeInvalidWithError:%@", [self class],
self, session, error);
NSDictionary *localTaskToFetcherMap;
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_session = nil;
localTaskToFetcherMap = [_taskToFetcherMap copy];
}
// Any "suspended" tasks may not have received callbacks from NSURLSession when the session
// completes; we'll call them now.
[localTaskToFetcherMap enumerateKeysAndObjectsUsingBlock:^(
NSURLSessionTask *task, GTMSessionFetcher *fetcher, BOOL *stop) {
if (fetcher.session == session) {
// Our delegate method URLSession:task:didCompleteWithError: will rely on
// _taskToFetcherMap so that should still contain this fetcher.
NSError *canceledError = [NSError errorWithDomain:NSURLErrorDomain
code:NSURLErrorCancelled
userInfo:nil];
[self URLSession:session task:task didCompleteWithError:canceledError];
} else {
GTMSESSION_ASSERT_DEBUG(0, @"Unexpected session in fetcher: %@ has %@ (expected %@)", fetcher,
fetcher.session, session);
}
}];
// Our tests rely on this notification to know the session discard timer fired.
NSDictionary *userInfo = @{kGTMSessionFetcherServiceSessionKey : session};
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc postNotificationName:kGTMSessionFetcherServiceSessionBecameInvalidNotification
object:_parentService
userInfo:userInfo];
}
#pragma mark - NSURLSessionTaskDelegate
// NSURLSessionTaskDelegate protocol methods.
//
// We won't test here if the fetcher responds to these since we only want this
// class to implement the same delegate methods the fetcher does (so NSURLSession's
// tests for respondsToSelector: will have the same result whether the session
// delegate is the fetcher or this dispatcher.)
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
newRequest:(NSURLRequest *)request
completionHandler:(void (^)(NSURLRequest *))completionHandler {
id<NSURLSessionTaskDelegate> fetcher = [self fetcherForTask:task];
[fetcher URLSession:session
task:task
willPerformHTTPRedirection:response
newRequest:request
completionHandler:completionHandler];
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))handler {
id<NSURLSessionTaskDelegate> fetcher = [self fetcherForTask:task];
[fetcher URLSession:session task:task didReceiveChallenge:challenge completionHandler:handler];
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
needNewBodyStream:(void (^)(NSInputStream *bodyStream))handler {
id<NSURLSessionTaskDelegate> fetcher = [self fetcherForTask:task];
[fetcher URLSession:session task:task needNewBodyStream:handler];
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didSendBodyData:(int64_t)bytesSent
totalBytesSent:(int64_t)totalBytesSent
totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
id<NSURLSessionTaskDelegate> fetcher = [self fetcherForTask:task];
[fetcher URLSession:session
task:task
didSendBodyData:bytesSent
totalBytesSent:totalBytesSent
totalBytesExpectedToSend:totalBytesExpectedToSend];
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error {
id<NSURLSessionTaskDelegate> fetcher = [self fetcherForTask:task];
// This is the usual way tasks are removed from the task map.
[self removeTaskFromMap:task];
[fetcher URLSession:session task:task didCompleteWithError:error];
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics
API_AVAILABLE(ios(10.0), macosx(10.12), tvos(10.0), watchos(6.0)) {
id<NSURLSessionTaskDelegate> fetcher = [self fetcherForTask:task];
[fetcher URLSession:session task:task didFinishCollectingMetrics:metrics];
}
// NSURLSessionDataDelegate protocol methods.
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition))handler {
id<NSURLSessionDataDelegate> fetcher = [self fetcherForTask:dataTask];
[fetcher URLSession:session
dataTask:dataTask
didReceiveResponse:response
completionHandler:handler];
}
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask {
id<NSURLSessionDataDelegate> fetcher = [self fetcherForTask:dataTask];
GTMSESSION_ASSERT_DEBUG(fetcher != nil, @"Missing fetcher for %@", dataTask);
[self removeTaskFromMap:dataTask];
if (fetcher) {
GTMSESSION_ASSERT_DEBUG([fetcher isKindOfClass:[GTMSessionFetcher class]],
@"Expecting GTMSessionFetcher");
[self setFetcher:(GTMSessionFetcher *)fetcher forTask:downloadTask];
}
[fetcher URLSession:session dataTask:dataTask didBecomeDownloadTask:downloadTask];
}
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data {
id<NSURLSessionDataDelegate> fetcher = [self fetcherForTask:dataTask];
[fetcher URLSession:session dataTask:dataTask didReceiveData:data];
}
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
willCacheResponse:(NSCachedURLResponse *)proposedResponse
completionHandler:(void (^)(NSCachedURLResponse *))handler {
id<NSURLSessionDataDelegate> fetcher = [self fetcherForTask:dataTask];
[fetcher URLSession:session
dataTask:dataTask
willCacheResponse:proposedResponse
completionHandler:handler];
}
// NSURLSessionDownloadDelegate protocol methods.
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location {
id<NSURLSessionDownloadDelegate> fetcher = [self fetcherForTask:downloadTask];
[fetcher URLSession:session downloadTask:downloadTask didFinishDownloadingToURL:location];
}
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalWritten
totalBytesExpectedToWrite:(int64_t)totalExpected {
id<NSURLSessionDownloadDelegate> fetcher = [self fetcherForTask:downloadTask];
[fetcher URLSession:session
downloadTask:downloadTask
didWriteData:bytesWritten
totalBytesWritten:totalWritten
totalBytesExpectedToWrite:totalExpected];
}
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes {
id<NSURLSessionDownloadDelegate> fetcher = [self fetcherForTask:downloadTask];
[fetcher URLSession:session
downloadTask:downloadTask
didResumeAtOffset:fileOffset
expectedTotalBytes:expectedTotalBytes];
}
@end

View File

@@ -0,0 +1,2171 @@
/* 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/GTMSessionUploadFetcher.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
static NSString *const kGTMSessionIdentifierIsUploadChunkFetcherMetadataKey = @"_upChunk";
static NSString *const kGTMSessionIdentifierUploadFileURLMetadataKey = @"_upFileURL";
static NSString *const kGTMSessionIdentifierUploadFileLengthMetadataKey = @"_upFileLen";
static NSString *const kGTMSessionIdentifierUploadLocationURLMetadataKey = @"_upLocURL";
static NSString *const kGTMSessionIdentifierUploadMIMETypeMetadataKey = @"_uploadMIME";
static NSString *const kGTMSessionIdentifierUploadChunkSizeMetadataKey = @"_upChSize";
static NSString *const kGTMSessionIdentifierUploadCurrentOffsetMetadataKey = @"_upOffset";
static NSString *const kGTMSessionIdentifierUploadAllowsCellularAccess = @"_upAllowsCellularAccess";
static NSString *const kGTMSessionHeaderXGoogUploadChunkGranularity =
@"X-Goog-Upload-Chunk-Granularity";
static NSString *const kGTMSessionHeaderXGoogUploadCommand = @"X-Goog-Upload-Command";
static NSString *const kGTMSessionHeaderXGoogUploadContentLength = @"X-Goog-Upload-Content-Length";
static NSString *const kGTMSessionHeaderXGoogUploadContentType = @"X-Goog-Upload-Content-Type";
static NSString *const kGTMSessionHeaderXGoogUploadOffset = @"X-Goog-Upload-Offset";
static NSString *const kGTMSessionHeaderXGoogUploadProtocol = @"X-Goog-Upload-Protocol";
static NSString *const kGTMSessionXGoogUploadProtocolResumable = @"resumable";
static NSString *const kGTMSessionHeaderXGoogUploadSizeReceived = @"X-Goog-Upload-Size-Received";
static NSString *const kGTMSessionHeaderXGoogUploadStatus = @"X-Goog-Upload-Status";
static NSString *const kGTMSessionHeaderXGoogUploadURL = @"X-Goog-Upload-URL";
static const NSTimeInterval kDefaultMaxUploadRetryInterval = 60.0 * 10.;
static const NSTimeInterval kDefaultMinUploadRetryInterval = 1.0;
// Property of chunk fetchers identifying the parent upload fetcher. Non-retained NSValue.
static NSString *const kGTMSessionUploadFetcherChunkParentKey = @"_uploadFetcherChunkParent";
int64_t const kGTMSessionUploadFetcherUnknownFileSize = -1;
int64_t const kGTMSessionUploadFetcherStandardChunkSize = (int64_t)LLONG_MAX;
#if TARGET_OS_IPHONE
int64_t const kGTMSessionUploadFetcherMaximumDemandBufferSize =
10 * 1024 * 1024; // 10 MB for iOS, watchOS, tvOS
#else
int64_t const kGTMSessionUploadFetcherMaximumDemandBufferSize =
100 * 1024 * 1024; // 100 MB for macOS
#endif
typedef NS_ENUM(NSUInteger, GTMSessionUploadFetcherStatus) {
kStatusUnknown,
kStatusActive,
kStatusFinal,
kStatusCancelled,
};
NSString *const kGTMSessionFetcherUploadLocationObtainedNotification =
@"kGTMSessionFetcherUploadLocationObtainedNotification";
NSString *const kGTMSessionFetcherUploadInitialBackoffStartedNotification =
@"kGTMSessionFetcherUploadInitialBackoffStartedNotification";
#if !GTMSESSION_BUILD_COMBINED_SOURCES
@interface GTMSessionFetcher (ProtectedMethods)
// Access to non-public method on the parent fetcher class.
- (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks;
- (void)createSessionIdentifierWithMetadata:(NSDictionary *)metadata;
- (GTMSessionFetcherCompletionHandler)completionHandlerWithTarget:(id)target
didFinishSelector:(SEL)finishedSelector;
- (void)invokeOnCallbackQueue:(dispatch_queue_t)callbackQueue
afterUserStopped:(BOOL)afterStopped
block:(void (^)(void))block;
- (NSTimer *)retryTimer;
- (void)beginFetchForRetry;
@property(readwrite, strong) NSData *downloadedData;
- (void)releaseCallbacks;
- (NSInteger)statusCodeUnsynchronized;
- (BOOL)userStoppedFetching;
@end
#endif // !GTMSESSION_BUILD_COMBINED_SOURCES
@interface GTMSessionUploadFetcher ()
// Changing readonly to readwrite.
@property(atomic, strong, readwrite) NSURLRequest *lastChunkRequest;
@property(atomic, readwrite, assign) int64_t currentOffset;
// Internal properties.
@property(strong, atomic, nullable) GTMSessionFetcher *fetcherInFlight; // Synchronized on self.
@property(assign, atomic, getter=isSubdataGenerating) BOOL subdataGenerating;
@property(assign, atomic) BOOL shouldInitiateOffsetQuery;
@property(assign, atomic) int64_t uploadGranularity;
@property(assign, atomic) BOOL allowsCellularAccess;
@end
@implementation GTMSessionUploadFetcher {
GTMSessionFetcher *_chunkFetcher;
// We'll call through to the delegate's completion handler.
GTMSessionFetcherCompletionHandler _delegateCompletionHandler;
dispatch_queue_t _delegateCallbackQueue;
// The initial fetch's body length and bytes actually sent are
// needed for calculating progress during subsequent chunk uploads
int64_t _initialBodyLength;
int64_t _initialBodySent;
// The upload server address for the chunks of this upload session.
NSURL *_uploadLocationURL;
// _uploadData, _uploadDataProvider, or _uploadFileHandle may be set, but only one.
NSData *_uploadData;
NSFileHandle *_uploadFileHandle;
GTMSessionUploadFetcherDataProvider _uploadDataProvider;
NSURL *_uploadFileURL;
int64_t _uploadFileLength;
NSString *_uploadMIMEType;
int64_t _chunkSize;
int64_t _uploadGranularity;
double _uploadRetryFactor;
NSTimeInterval _nextUploadRetryInterval;
NSTimeInterval _maxUploadRetryInterval;
NSTimeInterval _minUploadRetryInterval;
BOOL _isPaused;
BOOL _isRestartedUpload;
BOOL _shouldInitiateOffsetQuery;
NSTimer *_uploadRetryTimer;
// Tied to useBackgroundSession property, since this property is applicable to chunk fetchers.
BOOL _useBackgroundSessionOnChunkFetchers;
// We keep the latest offset into the upload data just for progress reporting.
int64_t _currentOffset;
NSDictionary *_recentChunkReponseHeaders;
NSInteger _recentChunkStatusCode;
// For waiting, we need to know the fetcher in flight, if any, and if subdata generation
// is in progress.
GTMSessionFetcher *_fetcherInFlight;
BOOL _isSubdataGenerating;
BOOL _isCancelInFlight;
GTMSessionUploadFetcherCancellationHandler _cancellationHandler;
}
- (NSTimeInterval)nextUploadRetryIntervalUnsynchronized {
GTMSessionCheckSynchronized(self);
// The next wait interval is the factor (2.0) times the last interval,
// but never less than the minimum interval.
NSTimeInterval secs = _nextUploadRetryInterval * _uploadRetryFactor;
if (_maxUploadRetryInterval > 0) {
secs = MIN(secs, _maxUploadRetryInterval);
}
secs = MAX(secs, _minUploadRetryInterval);
return secs;
}
+ (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 uploadFetchersForBackgroundSessions];
#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 uploadFetchersForBackgroundSessions];
});
}
+ (instancetype)uploadFetcherWithRequest:(NSURLRequest *)request
uploadMIMEType:(NSString *)uploadMIMEType
chunkSize:(int64_t)chunkSize
fetcherService:(GTMSessionFetcherService *)fetcherService {
GTMSessionUploadFetcher *fetcher = [self uploadFetcherWithRequest:request
fetcherService:fetcherService];
[fetcher setLocationURL:nil
uploadMIMEType:uploadMIMEType
chunkSize:chunkSize
allowsCellularAccess:request.allowsCellularAccess];
return fetcher;
}
+ (instancetype)uploadFetcherWithLocation:(nullable NSURL *)uploadLocationURL
uploadMIMEType:(NSString *)uploadMIMEType
chunkSize:(int64_t)chunkSize
fetcherService:(nullable GTMSessionFetcherService *)fetcherServiceOrNil {
return [self uploadFetcherWithLocation:uploadLocationURL
uploadMIMEType:uploadMIMEType
chunkSize:chunkSize
allowsCellularAccess:YES
fetcherService:fetcherServiceOrNil];
}
+ (instancetype)uploadFetcherWithLocation:(nullable NSURL *)uploadLocationURL
uploadMIMEType:(NSString *)uploadMIMEType
chunkSize:(int64_t)chunkSize
allowsCellularAccess:(BOOL)allowsCellularAccess
fetcherService:(GTMSessionFetcherService *)fetcherService {
GTMSessionUploadFetcher *fetcher = [self uploadFetcherWithRequest:nil
fetcherService:fetcherService];
[fetcher setLocationURL:uploadLocationURL
uploadMIMEType:uploadMIMEType
chunkSize:chunkSize
allowsCellularAccess:allowsCellularAccess];
return fetcher;
}
+ (instancetype)uploadFetcherForSessionIdentifierMetadata:(NSDictionary *)metadata {
GTMSESSION_ASSERT_DEBUG(
[metadata[kGTMSessionIdentifierIsUploadChunkFetcherMetadataKey] boolValue],
@"Session identifier metadata is not for an upload fetcher: %@", metadata);
NSNumber *uploadFileLengthNum = metadata[kGTMSessionIdentifierUploadFileLengthMetadataKey];
GTMSESSION_ASSERT_DEBUG(uploadFileLengthNum != nil,
@"Session metadata missing an UploadFileSize");
if (uploadFileLengthNum == nil) return nil;
int64_t uploadFileLength = [uploadFileLengthNum longLongValue];
GTMSESSION_ASSERT_DEBUG(uploadFileLength >= 0, @"Session metadata UploadFileSize is unknown");
NSString *uploadFileURLString = metadata[kGTMSessionIdentifierUploadFileURLMetadataKey];
GTMSESSION_ASSERT_DEBUG(uploadFileURLString, @"Session metadata missing an UploadFileURL");
if (uploadFileURLString == nil) return nil;
NSURL *uploadFileURL = [NSURL URLWithString:uploadFileURLString];
// There used to be a call here to NSURL checkResourceIsReachableAndReturnError: to check for the
// existence of the file (also tried NSFileManager fileExistsAtPath:). We've determined
// empirically that the check can fail at startup even when the upload file does in fact exist.
// For now, we'll go ahead and restore the background upload fetcher. If the file doesn't exist,
// it will fail later.
NSString *uploadLocationURLString = metadata[kGTMSessionIdentifierUploadLocationURLMetadataKey];
NSURL *uploadLocationURL =
uploadLocationURLString ? [NSURL URLWithString:uploadLocationURLString] : nil;
NSString *uploadMIMEType = metadata[kGTMSessionIdentifierUploadMIMETypeMetadataKey];
int64_t uploadChunkSize =
[metadata[kGTMSessionIdentifierUploadChunkSizeMetadataKey] longLongValue];
if (uploadChunkSize <= 0) {
uploadChunkSize = kGTMSessionUploadFetcherStandardChunkSize;
}
int64_t currentOffset =
[metadata[kGTMSessionIdentifierUploadCurrentOffsetMetadataKey] longLongValue];
BOOL allowsCellularAccess = YES;
if (metadata[kGTMSessionIdentifierUploadAllowsCellularAccess]) {
allowsCellularAccess = [metadata[kGTMSessionIdentifierUploadAllowsCellularAccess] boolValue];
}
GTMSESSION_ASSERT_DEBUG(currentOffset <= uploadFileLength,
@"CurrentOffset (%lld) exceeds UploadFileSize (%lld)", currentOffset,
uploadFileLength);
if (currentOffset > uploadFileLength) return nil;
GTMSessionUploadFetcher *uploadFetcher = [self uploadFetcherWithLocation:uploadLocationURL
uploadMIMEType:uploadMIMEType
chunkSize:uploadChunkSize
allowsCellularAccess:allowsCellularAccess
fetcherService:nil];
// Set the upload file length before setting the upload file URL tries to determine the length.
[uploadFetcher setUploadFileLength:uploadFileLength];
uploadFetcher.uploadFileURL = uploadFileURL;
uploadFetcher.sessionUserInfo = metadata;
uploadFetcher.useBackgroundSession = YES;
uploadFetcher.currentOffset = currentOffset;
uploadFetcher.delegateCallbackQueue = uploadFetcher.callbackQueue;
uploadFetcher.allowedInsecureSchemes = @[ @"http" ]; // Allowed on restored upload fetcher.
return uploadFetcher;
}
+ (instancetype)uploadFetcherWithRequest:(NSURLRequest *)request
fetcherService:(GTMSessionFetcherService *)fetcherService {
// Internal utility method for instantiating fetchers
GTMSessionUploadFetcher *fetcher;
if ([fetcherService isKindOfClass:[GTMSessionFetcherService class]]) {
fetcher = [fetcherService fetcherWithRequest:request fetcherClass:self];
} else {
fetcher = [self fetcherWithRequest:request];
}
fetcher.useBackgroundSession = YES;
return fetcher;
}
+ (NSPointerArray *)uploadFetcherPointerArrayForBackgroundSessions {
static NSPointerArray *gUploadFetcherPointerArrayForBackgroundSessions = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
gUploadFetcherPointerArrayForBackgroundSessions = [NSPointerArray weakObjectsPointerArray];
});
return gUploadFetcherPointerArrayForBackgroundSessions;
}
+ (instancetype)uploadFetcherForSessionIdentifier:(NSString *)sessionIdentifier {
GTMSESSION_ASSERT_DEBUG(sessionIdentifier != nil, @"Invalid session identifier");
NSArray *uploadFetchersForBackgroundSessions = [self uploadFetchersForBackgroundSessions];
for (GTMSessionUploadFetcher *uploadFetcher in uploadFetchersForBackgroundSessions) {
if ([uploadFetcher.chunkFetcher.sessionIdentifier isEqual:sessionIdentifier]) {
return uploadFetcher;
}
}
return nil;
}
+ (NSArray *)uploadFetchersForBackgroundSessions {
NSMutableSet *restoredSessionIdentifiers = [[NSMutableSet alloc] init];
NSMutableArray *uploadFetchers = [[NSMutableArray alloc] init];
NSPointerArray *uploadFetcherPointerArray = [self uploadFetcherPointerArrayForBackgroundSessions];
// Collect the background session upload fetchers that are still in memory.
@synchronized(uploadFetcherPointerArray) {
[uploadFetcherPointerArray compact];
for (GTMSessionUploadFetcher *uploadFetcher in uploadFetcherPointerArray) {
NSString *sessionIdentifier = uploadFetcher.chunkFetcher.sessionIdentifier;
if (sessionIdentifier) {
[restoredSessionIdentifiers addObject:sessionIdentifier];
[uploadFetchers addObject:uploadFetcher];
}
}
} // @synchronized(uploadFetcherPointerArray)
// The system may have other ongoing background upload sessions. Restore upload fetchers for those
// too.
NSArray *fetchers = [GTMSessionFetcher fetchersForBackgroundSessions];
for (GTMSessionFetcher *fetcher in fetchers) {
NSString *sessionIdentifier = fetcher.sessionIdentifier;
if (!sessionIdentifier || [restoredSessionIdentifiers containsObject:sessionIdentifier]) {
continue;
}
NSDictionary *sessionIdentifierMetadata = [fetcher sessionIdentifierMetadata];
if (sessionIdentifierMetadata == nil) {
continue;
}
if (![sessionIdentifierMetadata[kGTMSessionIdentifierIsUploadChunkFetcherMetadataKey]
boolValue]) {
continue;
}
GTMSessionUploadFetcher *uploadFetcher =
[self uploadFetcherForSessionIdentifierMetadata:sessionIdentifierMetadata];
if (uploadFetcher == nil) {
// Something went wrong with this upload fetcher, so kill the restored chunk fetcher.
[fetcher stopFetching];
continue;
}
[uploadFetchers addObject:uploadFetcher];
uploadFetcher->_chunkFetcher = fetcher;
uploadFetcher->_fetcherInFlight = fetcher;
[uploadFetcher attachSendProgressBlockToChunkFetcher:fetcher];
fetcher.completionHandler =
[fetcher completionHandlerWithTarget:uploadFetcher
didFinishSelector:@selector(chunkFetcher:finishedWithData:error:)];
GTMSESSION_LOG_DEBUG(@"%@ restoring upload fetcher %@ for chunk fetcher %@", [self class],
uploadFetcher, fetcher);
}
return uploadFetchers;
}
- (void)setUploadData:(NSData *)data {
BOOL changed = NO;
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
if (_uploadData != data) {
_uploadData = data;
changed = YES;
}
}
if (changed) {
[self setupRequestHeaders];
}
}
- (NSData *)uploadData {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _uploadData;
}
}
- (void)setUploadFileHandle:(NSFileHandle *)fh {
BOOL changed = NO;
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
if (_uploadFileHandle != fh) {
_uploadFileHandle = fh;
changed = YES;
}
}
if (changed) {
[self setupRequestHeaders];
}
}
- (NSFileHandle *)uploadFileHandle {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _uploadFileHandle;
}
}
- (void)setUploadFileURL:(NSURL *)uploadURL {
BOOL changed = NO;
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
if (_uploadFileURL != uploadURL) {
_uploadFileURL = uploadURL;
changed = YES;
}
}
if (changed) {
[self setupRequestHeaders];
}
}
- (NSURL *)uploadFileURL {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _uploadFileURL;
}
}
- (void)setUploadFileLength:(int64_t)fullLength {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
if (_uploadFileLength == kGTMSessionUploadFetcherUnknownFileSize &&
fullLength != kGTMSessionUploadFetcherUnknownFileSize) {
_uploadFileLength = fullLength;
}
}
}
- (void)setUploadDataLength:(int64_t)fullLength
provider:(GTMSessionUploadFetcherDataProvider)block {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_uploadDataProvider = [block copy];
_uploadFileLength = fullLength;
}
[self setupRequestHeaders];
}
- (GTMSessionUploadFetcherDataProvider)uploadDataProvider {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _uploadDataProvider;
}
}
- (void)setUploadMIMEType:(NSString *)uploadMIMEType {
GTMSESSION_ASSERT_DEBUG(0, @"TODO: disallow setUploadMIMEType by making declaration readonly");
// (and uploadMIMEType, chunksize, currentOffset)
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_uploadMIMEType = uploadMIMEType;
}
}
- (NSString *)uploadMIMEType {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _uploadMIMEType;
}
}
- (int64_t)chunkSize {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _chunkSize;
}
}
- (void)setupRequestHeaders {
GTMSessionCheckNotSynchronized(self);
#if DEBUG
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
int hasData = (_uploadData != nil) ? 1 : 0;
int hasFileHandle = (_uploadFileHandle != nil) ? 1 : 0;
int hasFileURL = (_uploadFileURL != nil) ? 1 : 0;
int hasUploadDataProvider = (_uploadDataProvider != nil) ? 1 : 0;
__unused int numberOfSources = hasData + hasFileHandle + hasFileURL + hasUploadDataProvider;
GTMSESSION_ASSERT_DEBUG(numberOfSources == 1, @"Need just one upload source (%d)",
numberOfSources);
} // @synchronized(self)
#endif
// Add our custom headers to the initial request indicating the data
// type and total size to be delivered later in the chunk requests.
NSMutableURLRequest *mutableRequest = [self.request mutableCopy];
GTMSESSION_ASSERT_DEBUG((mutableRequest == nil) != (_uploadLocationURL == nil),
@"Request and location are mutually exclusive");
if (!mutableRequest) return;
[mutableRequest setValue:kGTMSessionXGoogUploadProtocolResumable
forHTTPHeaderField:kGTMSessionHeaderXGoogUploadProtocol];
[mutableRequest setValue:@"start" forHTTPHeaderField:kGTMSessionHeaderXGoogUploadCommand];
[mutableRequest setValue:_uploadMIMEType
forHTTPHeaderField:kGTMSessionHeaderXGoogUploadContentType];
[mutableRequest setValue:@([self fullUploadLength]).stringValue
forHTTPHeaderField:kGTMSessionHeaderXGoogUploadContentLength];
NSString *method = mutableRequest.HTTPMethod;
if (method == nil || [method caseInsensitiveCompare:@"GET"] == NSOrderedSame) {
[mutableRequest setHTTPMethod:@"POST"];
}
// Ensure the user agent header identifies this to the upload server as a
// GTMSessionUploadFetcher client. The /1 can be incremented in the unlikely circumstance
// we need to make a bug fix in the client that the server can recognize.
NSString *const kUserAgentStub = @"(GTMSUF/1)";
NSString *userAgent = [mutableRequest valueForHTTPHeaderField:@"User-Agent"];
if (userAgent == nil || [userAgent rangeOfString:kUserAgentStub].location == NSNotFound) {
if (userAgent.length == 0) {
userAgent = GTMFetcherStandardUserAgentString(nil);
}
userAgent = [userAgent stringByAppendingFormat:@" %@", kUserAgentStub];
[mutableRequest setValue:userAgent forHTTPHeaderField:@"User-Agent"];
}
[self setRequest:mutableRequest];
}
- (void)setLocationURL:(nullable NSURL *)location
uploadMIMEType:(NSString *)uploadMIMEType
chunkSize:(int64_t)chunkSize
allowsCellularAccess:(BOOL)allowsCellularAccess {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
GTMSESSION_ASSERT_DEBUG(chunkSize > 0, @"chunk size is zero");
_allowsCellularAccess = allowsCellularAccess;
// When resuming an upload, set the known upload target URL.
_uploadLocationURL = location;
_uploadMIMEType = uploadMIMEType;
_chunkSize = chunkSize;
// Indicate that we've not yet determined the file handle's length
_uploadFileLength = kGTMSessionUploadFetcherUnknownFileSize;
// Indicate that we've not yet determined the upload fetcher status
_recentChunkStatusCode = -1;
// If this is restarting an upload begun by another fetcher,
// the location is specified but the request is nil
_isRestartedUpload = (location != nil);
} // @synchronized(self)
}
- (int64_t)fullUploadLength {
int64_t result;
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
if (_uploadData) {
result = (int64_t)_uploadData.length;
} else {
if (_uploadFileLength == kGTMSessionUploadFetcherUnknownFileSize) {
if (_uploadFileHandle) {
// First time through, seek to end to determine file length
_uploadFileLength = (int64_t)[_uploadFileHandle seekToEndOfFile];
} else if (_uploadDataProvider) {
// _uploadFileLength is set when the _uploadDataProvider is set.
GTMSESSION_ASSERT_DEBUG(_uploadFileLength >= 0, @"No uploadDataProvider length set");
} else {
NSNumber *filesizeNum;
NSError *valueError;
if ([_uploadFileURL getResourceValue:&filesizeNum
forKey:NSURLFileSizeKey
error:&valueError]) {
_uploadFileLength = filesizeNum.longLongValue;
} else {
GTMSESSION_ASSERT_DEBUG(NO, @"Cannot get file size: %@\n %@", valueError,
_uploadFileURL.path);
_uploadFileLength = 0;
}
}
}
result = _uploadFileLength;
}
} // @synchronized(self)
return result;
}
// Make a subdata of the upload data.
- (void)generateChunkSubdataWithOffset:(int64_t)offset
length:(int64_t)length
response:(GTMSessionUploadFetcherDataProviderResponse)response {
GTMSessionUploadFetcherDataProvider uploadDataProvider = self.uploadDataProvider;
if (uploadDataProvider) {
uploadDataProvider(offset, length, response);
return;
}
NSData *uploadData = self.uploadData;
if (uploadData) {
// NSData provided.
NSData *resultData;
if (offset == 0 && length == (int64_t)uploadData.length) {
resultData = uploadData;
} else {
int64_t dataLength = (int64_t)uploadData.length;
// Ensure our range is valid. b/18007814
if (offset + length > dataLength) {
NSString *errorMessage = [NSString
stringWithFormat:
@"Range invalid for upload data. offset: %lld\tlength: %lld\tdataLength: %lld",
offset, length, dataLength];
GTMSESSION_ASSERT_DEBUG(NO, @"%@", errorMessage);
response(nil, kGTMSessionUploadFetcherUnknownFileSize,
[self uploadChunkUnavailableErrorWithDescription:errorMessage]);
return;
}
NSRange range = NSMakeRange((NSUInteger)offset, (NSUInteger)length);
@try {
resultData = [uploadData subdataWithRange:range];
} @catch (NSException *exception) {
NSString *errorMessage = exception.description;
GTMSESSION_ASSERT_DEBUG(NO, @"%@", errorMessage);
response(nil, kGTMSessionUploadFetcherUnknownFileSize,
[self uploadChunkUnavailableErrorWithDescription:errorMessage]);
return;
}
}
response(resultData, kGTMSessionUploadFetcherUnknownFileSize, nil);
return;
}
NSURL *uploadFileURL = self.uploadFileURL;
if (uploadFileURL) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self generateChunkSubdataFromFileURL:uploadFileURL
offset:offset
length:length
response:response];
});
return;
}
GTMSESSION_ASSERT_DEBUG(_uploadFileHandle, @"Unexpectedly missing upload data package");
NSFileHandle *uploadFileHandle = self.uploadFileHandle;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self generateChunkSubdataFromFileHandle:uploadFileHandle
offset:offset
length:length
response:response];
});
}
- (void)generateChunkSubdataFromFileHandle:(NSFileHandle *)fileHandle
offset:(int64_t)offset
length:(int64_t)length
response:(GTMSessionUploadFetcherDataProviderResponse)response {
NSData *resultData;
NSError *error;
@try {
[fileHandle seekToFileOffset:(unsigned long long)offset];
resultData = [fileHandle readDataOfLength:(NSUInteger)length];
} @catch (NSException *exception) {
GTMSESSION_ASSERT_DEBUG(NO, @"uploadFileHandle failed to read, %@", exception);
error = [self uploadChunkUnavailableErrorWithDescription:exception.description];
}
// The response always re-dispatches to the main thread, so we skip doing that here.
response(resultData, kGTMSessionUploadFetcherUnknownFileSize, error);
}
- (void)generateChunkSubdataFromFileURL:(NSURL *)fileURL
offset:(int64_t)offset
length:(int64_t)length
response:(GTMSessionUploadFetcherDataProviderResponse)response {
GTMSessionCheckNotSynchronized(self);
NSData *resultData;
NSError *error;
int64_t fullUploadLength = [self fullUploadLength];
NSData *mappedData =
[NSData dataWithContentsOfURL:fileURL
options:NSDataReadingMappedAlways + NSDataReadingUncached
error:&error];
if (!mappedData) {
// We could not create an NSData by memory-mapping the file.
#if TARGET_IPHONE_SIMULATOR
// NSTemporaryDirectory() can differ in the simulator between app restarts,
// yet the contents for the new path remains unchanged, so try the latest temp path.
if ([error.domain isEqual:NSCocoaErrorDomain] && (error.code == NSFileReadNoSuchFileError)) {
NSString *filename = [fileURL lastPathComponent];
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:filename];
NSURL *newFileURL = [NSURL fileURLWithPath:filePath];
if (![newFileURL isEqual:fileURL]) {
[self generateChunkSubdataFromFileURL:newFileURL
offset:offset
length:length
response:response];
return;
}
}
#endif
// If the file is just too large to create an NSData for, or if for some other reason we can't
// map it, create an NSFileHandle instead to read a subset into an NSData.
#if DEBUG
NSNumber *fileSizeNum;
BOOL hasFileSize = [fileURL getResourceValue:&fileSizeNum forKey:NSURLFileSizeKey error:NULL];
GTMSESSION_LOG_DEBUG(@"Note: uploadFileURL is falling back to creating upload chunks by reading"
@" an NSFileHandle since uploadFileURL failed to map the upload file,"
@" file size %@, %@",
hasFileSize ? fileSizeNum : @"unknown", error);
#endif
NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingFromURL:fileURL error:&error];
if (fileHandle != nil) {
[self generateChunkSubdataFromFileHandle:fileHandle
offset:offset
length:length
response:response];
return;
}
GTMSESSION_ASSERT_DEBUG(NO, @"uploadFileURL failed to read, %@", error);
// Fall through with the error.
} else {
// Successfully created an NSData by memory-mapping the file.
if ((NSUInteger)(offset + length) > mappedData.length) {
NSString *errorMessage = [NSString
stringWithFormat:@"Range invalid for upload data. offset: %lld\tlength: "
@"%lld\tdataLength: %lld\texpected UploadLength: %lld",
offset, length, (long long)mappedData.length, fullUploadLength];
GTMSESSION_ASSERT_DEBUG(NO, @"%@", errorMessage);
response(nil, kGTMSessionUploadFetcherUnknownFileSize,
[self uploadChunkUnavailableErrorWithDescription:errorMessage]);
return;
}
if (offset > 0 || length < fullUploadLength) {
NSRange range = NSMakeRange((NSUInteger)offset, (NSUInteger)length);
resultData = [mappedData subdataWithRange:range];
} else {
resultData = mappedData;
}
}
// The response always re-dispatches to the main thread, so we skip re-dispatching here.
response(resultData, kGTMSessionUploadFetcherUnknownFileSize, error);
}
- (NSError *)uploadChunkUnavailableErrorWithDescription:(NSString *)description {
// The description in the userInfo is intended as a clue to programmers, not
// for client code to examine or rely on.
NSDictionary *userInfo = @{@"description" : description};
return [NSError errorWithDomain:kGTMSessionFetcherErrorDomain
code:GTMSessionFetcherErrorUploadChunkUnavailable
userInfo:userInfo];
}
- (NSError *)prematureFailureErrorWithUserInfo:(NSDictionary *)userInfo {
// An error for if we get an unexpected status from the upload server or
// otherwise cannot continue. This is an issue beyond the upload protocol;
// there's no way the client can do anything useful except give up.
NSError *error = [NSError errorWithDomain:kGTMSessionFetcherStatusDomain
code:501 // Not implemented
userInfo:userInfo];
return error;
}
+ (GTMSessionUploadFetcherStatus)uploadStatusFromResponseHeaders:(NSDictionary *)responseHeaders {
NSString *statusString = [responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadStatus];
if ([statusString isEqual:@"active"]) {
return kStatusActive;
}
if ([statusString isEqual:@"final"]) {
return kStatusFinal;
}
if ([statusString isEqual:@"cancelled"]) {
return kStatusCancelled;
}
return kStatusUnknown;
}
#pragma mark Method overrides affecting the initial fetch only
- (void)setCompletionHandler:(GTMSessionFetcherCompletionHandler)handler {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_delegateCompletionHandler = handler;
}
}
- (void)setDelegateCallbackQueue:(nullable dispatch_queue_t)queue {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_delegateCallbackQueue = queue;
}
}
- (nullable dispatch_queue_t)delegateCallbackQueue {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _delegateCallbackQueue;
}
}
- (BOOL)isRestartedUpload {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _isRestartedUpload;
}
}
- (nullable GTMSessionFetcher *)chunkFetcher {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _chunkFetcher;
}
}
- (void)setChunkFetcher:(nullable GTMSessionFetcher *)fetcher {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_chunkFetcher = fetcher;
}
}
- (void)setFetcherInFlight:(nullable GTMSessionFetcher *)fetcher {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_fetcherInFlight = fetcher;
}
}
- (nullable GTMSessionFetcher *)fetcherInFlight {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _fetcherInFlight;
}
}
- (void)setCancellationHandler:
(nullable GTMSessionUploadFetcherCancellationHandler)cancellationHandler {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_cancellationHandler = cancellationHandler;
}
}
- (nullable GTMSessionUploadFetcherCancellationHandler)cancellationHandler {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _cancellationHandler;
}
}
- (void)beginFetchForRetry {
GTMSessionCheckNotSynchronized(self);
// Override the superclass to reset the initial body length and fetcher-in-flight,
// then call the superclass implementation.
[self setInitialBodyLength:[self bodyLength]];
GTMSESSION_ASSERT_DEBUG(self.fetcherInFlight == nil, @"unexpected fetcher in flight: %@",
self.fetcherInFlight);
self.fetcherInFlight = self;
[super beginFetchForRetry];
}
- (void)destroyUploadRetryTimer {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
[_uploadRetryTimer invalidate];
_uploadRetryTimer = nil;
}
}
- (void)beginFetchWithCompletionHandler:(GTMSessionFetcherCompletionHandler)handler {
GTMSessionCheckNotSynchronized(self);
[self setInitialBodyLength:[self bodyLength]];
if (_minUploadRetryInterval <= 0.0) {
_minUploadRetryInterval = kDefaultMinUploadRetryInterval;
}
if (_maxUploadRetryInterval <= 0.0) {
_maxUploadRetryInterval = kDefaultMaxUploadRetryInterval;
}
if (_uploadRetryFactor <= 0.0) {
_uploadRetryFactor = 2.0;
}
// We'll hold onto the superclass's callback queue so we can invoke the handler
// even after the superclass has released the queue and its callback handler, as
// happens during auth failure.
[self setDelegateCallbackQueue:self.callbackQueue];
self.completionHandler = handler;
if ([self isRestartedUpload]) {
// When restarting an upload, we know the destination location for chunk fetches,
// but we need to query to find the initial offset.
if (![self isPaused]) {
[self sendQueryForUploadOffsetWithFetcherProperties:self.properties];
}
return;
}
// We don't want to call into the client's completion block immediately
// after the finish of the initial connection (the delegate is called only
// when uploading finishes), so we substitute our own completion block to be
// called when the initial connection finishes
GTMSESSION_ASSERT_DEBUG(self.fetcherInFlight == nil, @"unexpected fetcher in flight: %@",
self.fetcherInFlight);
self.fetcherInFlight = self;
[super beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
self.fetcherInFlight = nil;
// callback
BOOL hasTestBlock = (self.testBlock != nil);
if (![self isRestartedUpload] && !hasTestBlock) {
if (error == nil) {
[self beginChunkFetches];
} else {
if ([self retryTimer] == nil) {
[self invokeFinalCallbackWithData:nil error:error shouldInvalidateLocation:YES];
}
}
} else {
// If there was no initial request, then this fetch is resuming some
// other uploadFetcher's initial request, and the superclass's connection
// is never used, so at this point we call the user's actual completion
// block.
if (!hasTestBlock) {
[self invokeFinalCallbackWithData:data error:error shouldInvalidateLocation:YES];
} else {
// There was a test block, so we won't do chunk fetches, but we simulate obtaining
// the data to be uploaded from the upload data provider block or the file handle,
// and then call back.
[self generateChunkSubdataWithOffset:0
length:[self fullUploadLength]
response:^(NSData *generateData, int64_t fullUploadLength,
NSError *generateError) {
[self invokeFinalCallbackWithData:data
error:error
shouldInvalidateLocation:YES];
}];
}
}
}];
}
- (void)beginChunkFetches {
GTMSessionCheckNotSynchronized(self);
#if DEBUG
// The initial response of the resumable upload protocol should have an
// empty body
//
// This assert typically happens because the upload create/edit link URL was
// not supplied with the request, and the server is thus expecting a non-
// resumable request/response.
if (self.downloadedData.length > 0) {
NSData *downloadedData = self.downloadedData;
__unused NSString *str = [[NSString alloc] initWithData:downloadedData
encoding:NSUTF8StringEncoding];
GTMSESSION_ASSERT_DEBUG(NO, @"unexpected response data (uploading to the wrong URL?)\n%@", str);
}
#endif
// We need to get the upload URL from the location header to continue.
NSDictionary *responseHeaders = [self responseHeaders];
[self retrieveUploadChunkGranularityFromResponseHeaders:responseHeaders];
GTMSessionUploadFetcherStatus uploadStatus =
[[self class] uploadStatusFromResponseHeaders:responseHeaders];
GTMSESSION_ASSERT_DEBUG(uploadStatus != kStatusUnknown,
@"beginChunkFetches has unexpected upload status for headers %@",
responseHeaders);
BOOL isPrematureStop = (uploadStatus == kStatusFinal) || (uploadStatus == kStatusCancelled);
NSString *uploadLocationURLStr = [responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadURL];
BOOL hasUploadLocation = (uploadLocationURLStr.length > 0);
if (isPrematureStop || !hasUploadLocation) {
GTMSESSION_ASSERT_DEBUG(NO, @"Premature failure: upload-status:\"%@\" location:%@",
[responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadStatus],
uploadLocationURLStr);
// We cannot continue since we do not know the location to use
// as our upload destination.
NSDictionary *userInfo = nil;
NSData *downloadedData = self.downloadedData;
if (downloadedData.length > 0) {
userInfo = @{kGTMSessionFetcherStatusDataKey : downloadedData};
}
NSError *failureError = [self prematureFailureErrorWithUserInfo:userInfo];
[self invokeFinalCallbackWithData:nil error:failureError shouldInvalidateLocation:YES];
return;
}
self.uploadLocationURL = [NSURL URLWithString:uploadLocationURLStr];
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc postNotificationName:kGTMSessionFetcherUploadLocationObtainedNotification object:self];
// we've now sent all of the initial post body data, so we need to include
// its size in future progress indicator callbacks
[self setInitialBodySent:[self initialBodyLength]];
// just in case the user paused us during the initial fetch...
if (![self isPaused]) {
[self uploadNextChunkWithOffset:0];
}
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didSendBodyData:(int64_t)bytesSent
totalBytesSent:(int64_t)totalBytesSent
totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
// Overrides the superclass.
[self invokeDelegateWithDidSendBytes:bytesSent
totalBytesSent:totalBytesSent
totalBytesExpectedToSend:totalBytesExpectedToSend + [self fullUploadLength]];
}
- (BOOL)shouldReleaseCallbacksUponCompletion {
// Overrides the superclass.
// We don't want the superclass to release the delegate and callback
// blocks once the initial fetch has finished
//
// This is invoked for only successful completion of the connection;
// an error always will invoke and release the callbacks
return NO;
}
- (void)invokeFinalCallbackWithData:(NSData *)data
error:(NSError *)error
shouldInvalidateLocation:(BOOL)shouldInvalidateLocation {
dispatch_queue_t queue;
GTMSessionFetcherCompletionHandler handler;
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
if (shouldInvalidateLocation) {
_uploadLocationURL = nil;
}
// Wait to dispatch the completion handler until after releasing callbacks. Because
// action on the upload fetcher often takes place on a background queue, there can
// be issues with CI tests failing due to load making the dispatched callback to
// execute and the tests resume and assert callbacks have been released prior to that
// actually occurring.
//
// However under normal operation this should also be a perfectly fine change.
queue = _delegateCallbackQueue;
handler = _delegateCompletionHandler;
} // @synchronized(self)
[self releaseUploadAndBaseCallbacks:!self.userStoppedFetching];
if (queue && handler) {
[self invokeOnCallbackQueue:queue
afterUserStopped:NO
block:^{
handler(data, error);
}];
}
}
- (void)releaseUploadAndBaseCallbacks:(BOOL)shouldReleaseCancellation {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_delegateCallbackQueue = nil;
_delegateCompletionHandler = nil;
_uploadDataProvider = nil;
if (shouldReleaseCancellation) {
_cancellationHandler = nil;
}
}
// Release the base class's callbacks, too, if needed.
[self releaseCallbacks];
}
- (void)stopFetchReleasingCallbacks:(BOOL)shouldReleaseCallbacks {
GTMSessionCheckNotSynchronized(self);
[self destroyUploadRetryTimer];
// Clear _fetcherInFlight when stopped. Moved from stopFetching, since that's a public method,
// where this method does the work. Fixes issue clearing value when retryBlock included.
GTMSessionFetcher *fetcherInFlight = self.fetcherInFlight;
if (fetcherInFlight == self) {
self.fetcherInFlight = nil;
}
[super stopFetchReleasingCallbacks:shouldReleaseCallbacks];
if (shouldReleaseCallbacks) {
[self releaseUploadAndBaseCallbacks:NO];
}
}
#pragma mark Chunk fetching methods
- (void)uploadNextChunkWithOffset:(int64_t)offset {
// use the properties in each chunk fetcher
NSDictionary *props = [self properties];
[self uploadNextChunkWithOffset:offset fetcherProperties:props];
}
- (void)sendQueryForUploadOffsetWithFetcherProperties:(NSDictionary *)props {
GTMSessionFetcher *queryFetcher = [self uploadFetcherWithProperties:props isQueryFetch:YES];
queryFetcher.bodyData = [NSData data];
NSString *originalComment = self.comment;
[queryFetcher
setCommentWithFormat:@"%@ (query offset)", originalComment ? originalComment : @"upload"];
[queryFetcher setRequestValue:@"query" forHTTPHeaderField:kGTMSessionHeaderXGoogUploadCommand];
self.fetcherInFlight = queryFetcher;
[queryFetcher beginFetchWithDelegate:self
didFinishSelector:@selector(queryFetcher:finishedWithData:error:)];
}
- (void)queryFetcher:(GTMSessionFetcher *)queryFetcher
finishedWithData:(NSData *)data
error:(NSError *)error {
self.fetcherInFlight = nil;
NSDictionary *responseHeaders = [queryFetcher responseHeaders];
NSString *sizeReceivedHeader;
GTMSessionUploadFetcherStatus uploadStatus =
[[self class] uploadStatusFromResponseHeaders:responseHeaders];
GTMSESSION_ASSERT_DEBUG(uploadStatus != kStatusUnknown || error != nil,
@"query fetcher completion has unexpected upload status for headers %@",
responseHeaders);
if (error == nil) {
sizeReceivedHeader = [responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadSizeReceived];
if (uploadStatus == kStatusCancelled ||
(uploadStatus == kStatusActive && sizeReceivedHeader == nil)) {
NSDictionary *userInfo = nil;
if (data.length > 0) {
userInfo = @{kGTMSessionFetcherStatusDataKey : data};
}
error = [self prematureFailureErrorWithUserInfo:userInfo];
}
}
if (error == nil) {
int64_t offset = [sizeReceivedHeader longLongValue];
int64_t fullUploadLength = [self fullUploadLength];
if (uploadStatus == kStatusFinal ||
(offset >= fullUploadLength &&
fullUploadLength != kGTMSessionUploadFetcherUnknownFileSize)) {
// Handle we're done
[self chunkFetcher:queryFetcher finishedWithData:data error:nil];
} else {
[self retrieveUploadChunkGranularityFromResponseHeaders:responseHeaders];
[self uploadNextChunkWithOffset:offset];
}
} else {
// Handle query error
[self chunkFetcher:queryFetcher finishedWithData:data error:error];
}
}
- (void)sendCancelUploadWithFetcherProperties:(NSDictionary *)props {
@synchronized(self) {
_isCancelInFlight = YES;
}
GTMSessionFetcher *cancelFetcher = [self uploadFetcherWithProperties:props isQueryFetch:YES];
cancelFetcher.bodyData = [NSData data];
NSString *originalComment = self.comment;
[cancelFetcher
setCommentWithFormat:@"%@ (cancel)", originalComment ? originalComment : @"upload"];
[cancelFetcher setRequestValue:@"cancel" forHTTPHeaderField:kGTMSessionHeaderXGoogUploadCommand];
self.fetcherInFlight = cancelFetcher;
[cancelFetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
self.fetcherInFlight = nil;
if (![self triggerCancellationHandlerForFetch:cancelFetcher data:data error:error]) {
if (error) {
GTMSESSION_LOG_DEBUG(@"cancelFetcher %@", error);
}
}
@synchronized(self) {
self->_isCancelInFlight = NO;
}
}];
}
- (void)uploadNextChunkWithOffset:(int64_t)offset fetcherProperties:(NSDictionary *)props {
GTMSessionCheckNotSynchronized(self);
// Example chunk headers:
// X-Goog-Upload-Command: upload, finalize
// X-Goog-Upload-Offset: 0
// Content-Length: 2000000
// Content-Type: image/jpeg
//
// {bytes 0-1999999}
// The chunk upload URL requires no authentication header.
GTMSessionFetcher *chunkFetcher = [self uploadFetcherWithProperties:props isQueryFetch:NO];
[self attachSendProgressBlockToChunkFetcher:chunkFetcher];
int64_t chunkSize = [self updateChunkFetcher:chunkFetcher forChunkAtOffset:offset];
BOOL isUploadingFileURL = (self.uploadFileURL != nil);
int64_t fullUploadLength = [self fullUploadLength];
// The chunk size may have changed, so determine again if we're uploading the full file.
BOOL isUploadingFullFile =
(offset == 0 && fullUploadLength != kGTMSessionUploadFetcherUnknownFileSize &&
chunkSize >= fullUploadLength);
if (isUploadingFullFile && isUploadingFileURL) {
// The data is the full upload file URL.
chunkFetcher.bodyFileURL = self.uploadFileURL;
[self beginChunkFetcher:chunkFetcher offset:offset];
} else {
// Make an NSData for the subset for this upload chunk.
self.subdataGenerating = YES;
[self generateChunkSubdataWithOffset:offset
length:chunkSize
response:^(NSData *chunkData, int64_t uploadFileLength,
NSError *chunkError) {
// The subdata methods may leave us on a background thread.
dispatch_async(dispatch_get_main_queue(), ^{
self.subdataGenerating = NO;
// dont allow the updating of fileLength for uploads not using a
// data provider as they should know the file length before the
// upload starts.
if (self.uploadDataProvider != nil && uploadFileLength > 0) {
[self setUploadFileLength:uploadFileLength];
// Update the command and content-length headers if this is
// the last chunk to be sent.
if (offset + chunkSize >= uploadFileLength) {
int64_t updatedChunkSize =
[self updateChunkFetcher:chunkFetcher
forChunkAtOffset:offset];
if (updatedChunkSize == 0) {
// Calling beginChunkFetcher early when there is no more
// data to send allows us to properly handle nil chunkData
// below without having to account for the case where we
// are just finalizing the file.
chunkFetcher.bodyData = [[NSData alloc] init];
[self beginChunkFetcher:chunkFetcher offset:offset];
return;
}
}
}
if (chunkData == nil) {
NSError *responseError = chunkError;
if (!responseError) {
responseError =
[self uploadChunkUnavailableErrorWithDescription:
@"chunkData is nil"];
}
[self invokeFinalCallbackWithData:nil
error:responseError
shouldInvalidateLocation:YES];
return;
}
BOOL didWriteFile = NO;
if (isUploadingFileURL) {
// Make a temporary file with the data subset.
NSString *tempName =
[NSString stringWithFormat:@"GTMUpload_temp_%@",
[[NSUUID UUID] UUIDString]];
NSString *tempPath = [NSTemporaryDirectory()
stringByAppendingPathComponent:tempName];
NSError *writeError;
didWriteFile = [chunkData writeToFile:tempPath
options:NSDataWritingAtomic
error:&writeError];
if (didWriteFile) {
chunkFetcher.bodyFileURL = [NSURL fileURLWithPath:tempPath];
} else {
GTMSESSION_LOG_DEBUG(@"writeToFile failed: %@\n%@",
writeError, tempPath);
}
}
if (!didWriteFile) {
chunkFetcher.bodyData = [chunkData copy];
}
[self beginChunkFetcher:chunkFetcher offset:offset];
});
}];
}
}
- (void)beginUploadRetryTimer {
if (![NSThread isMainThread]) {
// Defer creating and starting the timer until we're on the main thread to ensure it has
// a run loop.
dispatch_async(dispatch_get_main_queue(), ^{
[self beginUploadRetryTimer];
});
return;
}
[self destroyUploadRetryTimer];
if (_nextUploadRetryInterval == 0.0) {
[self.chunkFetcher beginFetchWithDelegate:self
didFinishSelector:@selector(chunkFetcher:finishedWithData:error:)];
return;
}
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
NSTimeInterval nextInterval = _nextUploadRetryInterval;
NSTimeInterval maxInterval = _maxUploadRetryInterval;
NSTimeInterval newInterval = MIN(nextInterval, (maxInterval > 0 ? maxInterval : DBL_MAX));
NSTimeInterval newIntervalTolerance = (newInterval / 10) > 1.0 ?: 1.0;
_nextUploadRetryInterval = newInterval;
_uploadRetryTimer = [NSTimer timerWithTimeInterval:newInterval
target:self
selector:@selector(uploadRetryTimerFired:)
userInfo:nil
repeats:NO];
_uploadRetryTimer.tolerance = newIntervalTolerance;
[[NSRunLoop mainRunLoop] addTimer:_uploadRetryTimer forMode:NSDefaultRunLoopMode];
} // @synchronized(self)
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc postNotificationName:kGTMSessionFetcherUploadInitialBackoffStartedNotification object:self];
}
- (void)uploadRetryTimerFired:(NSTimer *)timer {
[self destroyUploadRetryTimer];
NSOperationQueue *queue = self.sessionDelegateQueue;
[queue addOperationWithBlock:^{
[self.chunkFetcher beginFetchWithDelegate:self
didFinishSelector:@selector(chunkFetcher:finishedWithData:error:)];
}];
}
- (NSTimer *)uploadRetryTimer {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _uploadRetryTimer;
} // @synchronized(self)
}
- (NSTimeInterval)maxUploadRetryInterval {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _maxUploadRetryInterval;
} // @synchronized(self)
}
- (void)setMaxUploadRetryInterval:(NSTimeInterval)secs {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
if (secs > 0) {
_maxUploadRetryInterval = secs;
} else {
_maxUploadRetryInterval = kDefaultMaxUploadRetryInterval;
}
} // @synchronized(self)
}
- (void)setMinUploadRetryInterval:(NSTimeInterval)secs {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
if (secs > 0) {
_minUploadRetryInterval = secs;
} else {
_minUploadRetryInterval = kDefaultMinUploadRetryInterval;
}
} // @synchronized(self)
}
- (NSTimeInterval)minUploadRetryInterval {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _minUploadRetryInterval;
} // @synchronized(self)
}
- (void)beginChunkFetcher:(GTMSessionFetcher *)chunkFetcher offset:(int64_t)offset {
// Track the current offset for progress reporting
self.currentOffset = offset;
// Hang on to the fetcher in case we need to cancel it. We set these before beginning the
// chunk fetch so the observers notified of chunk fetches can inspect the upload fetcher to
// match to the chunk.
self.chunkFetcher = chunkFetcher;
self.fetcherInFlight = chunkFetcher;
// Update the last chunk request, including any request headers.
self.lastChunkRequest = chunkFetcher.request;
if (_nextUploadRetryInterval < _maxUploadRetryInterval) {
[self beginUploadRetryTimer];
} else {
NSError *responseError =
[self uploadChunkUnavailableErrorWithDescription:@"Retry Limit Reached"];
[self invokeFinalCallbackWithData:nil error:responseError shouldInvalidateLocation:NO];
}
}
- (void)attachSendProgressBlockToChunkFetcher:(GTMSessionFetcher *)chunkFetcher {
chunkFetcher.sendProgressBlock =
^(int64_t bytesSent, int64_t totalBytesSent, int64_t totalBytesExpectedToSend) {
// The total bytes expected include the initial body and the full chunked
// data, independent of how big this fetcher's chunk is.
int64_t initialBodySent = [self bodyLength]; // TODO(grobbins) use [self initialBodySent]
int64_t totalSent = initialBodySent + self.currentOffset + totalBytesSent;
int64_t totalExpected = initialBodySent + [self fullUploadLength];
[self invokeDelegateWithDidSendBytes:bytesSent
totalBytesSent:totalSent
totalBytesExpectedToSend:totalExpected];
};
}
- (NSDictionary *)uploadSessionIdentifierMetadata {
NSMutableDictionary *metadata = [NSMutableDictionary dictionary];
metadata[kGTMSessionIdentifierIsUploadChunkFetcherMetadataKey] = @YES;
GTMSESSION_ASSERT_DEBUG(self.uploadFileURL,
@"Invalid upload fetcher to create session identifier for metadata");
metadata[kGTMSessionIdentifierUploadFileURLMetadataKey] = [self.uploadFileURL absoluteString];
metadata[kGTMSessionIdentifierUploadFileLengthMetadataKey] = @([self fullUploadLength]);
if (self.uploadLocationURL) {
metadata[kGTMSessionIdentifierUploadLocationURLMetadataKey] =
[self.uploadLocationURL absoluteString];
}
if (self.uploadMIMEType) {
metadata[kGTMSessionIdentifierUploadMIMETypeMetadataKey] = self.uploadMIMEType;
}
metadata[kGTMSessionIdentifierUploadChunkSizeMetadataKey] = @(self.chunkSize);
metadata[kGTMSessionIdentifierUploadCurrentOffsetMetadataKey] = @(self.currentOffset);
metadata[kGTMSessionIdentifierUploadAllowsCellularAccess] = @(self.request.allowsCellularAccess);
return metadata;
}
- (GTMSessionFetcher *)uploadFetcherWithProperties:(NSDictionary *)properties
isQueryFetch:(BOOL)isQueryFetch {
GTMSessionCheckNotSynchronized(self);
// Common code to make a request for a query command or for a chunk upload.
NSURL *uploadLocationURL = self.uploadLocationURL;
NSMutableURLRequest *chunkRequest = [NSMutableURLRequest requestWithURL:uploadLocationURL];
[chunkRequest setHTTPMethod:@"PUT"];
// copy the user-agent from the original connection
// n.b. that self.request is nil for upload fetchers created with an existing upload location
// URL.
NSURLRequest *origRequest = self.request;
chunkRequest.allowsCellularAccess = origRequest.allowsCellularAccess;
if (!origRequest) {
chunkRequest.allowsCellularAccess = _allowsCellularAccess;
}
NSString *userAgent = [origRequest valueForHTTPHeaderField:@"User-Agent"];
if (userAgent.length > 0) {
[chunkRequest setValue:userAgent forHTTPHeaderField:@"User-Agent"];
}
[chunkRequest setValue:kGTMSessionXGoogUploadProtocolResumable
forHTTPHeaderField:kGTMSessionHeaderXGoogUploadProtocol];
// To avoid timeouts when debugging, copy the timeout of the initial fetcher.
NSTimeInterval origTimeout = [origRequest timeoutInterval];
[chunkRequest setTimeoutInterval:origTimeout];
//
// Make a new chunk fetcher.
//
GTMSessionFetcher *chunkFetcher = [GTMSessionFetcher fetcherWithRequest:chunkRequest];
chunkFetcher.callbackQueue = self.callbackQueue;
chunkFetcher.sessionUserInfo = self.sessionUserInfo;
chunkFetcher.configurationBlock = self.configurationBlock;
chunkFetcher.allowedInsecureSchemes = self.allowedInsecureSchemes;
chunkFetcher.allowLocalhostRequest = self.allowLocalhostRequest;
chunkFetcher.allowInvalidServerCertificates = self.allowInvalidServerCertificates;
chunkFetcher.stopFetchingTriggersCompletionHandler = self.stopFetchingTriggersCompletionHandler;
chunkFetcher.useUploadTask = !isQueryFetch;
if (self.uploadFileURL && !isQueryFetch && self.useBackgroundSession) {
[chunkFetcher createSessionIdentifierWithMetadata:[self uploadSessionIdentifierMetadata]];
}
// Give the chunk fetcher the same properties as the previous chunk fetcher
chunkFetcher.properties = [properties mutableCopy];
[chunkFetcher setProperty:[NSValue valueWithNonretainedObject:self]
forKey:kGTMSessionUploadFetcherChunkParentKey];
// copy other fetcher settings to the new fetcher
chunkFetcher.retryEnabled = self.retryEnabled;
chunkFetcher.maxRetryInterval = self.maxRetryInterval;
if ([self isRetryEnabled]) {
// We interpose our own retry method both so we can change the request to ask the server to
// tell us where to resume the chunk.
chunkFetcher.retryBlock =
^(BOOL suggestedWillRetry, NSError *chunkError, GTMSessionFetcherRetryResponse response) {
void (^finish)(BOOL) = ^(BOOL shouldRetry) {
// We'll retry by sending an offset query.
if (shouldRetry) {
self.shouldInitiateOffsetQuery = !isQueryFetch;
// We don't know what our actual offset is anymore, but the server will tell us.
self.currentOffset = 0;
}
// We don't actually want to retry this specific fetcher.
response(NO);
};
GTMSessionFetcherRetryBlock retryBlock = self.retryBlock;
if (retryBlock) {
// Ask the client, then call the finish block above.
retryBlock(suggestedWillRetry, chunkError, finish);
} else {
finish(suggestedWillRetry);
}
};
}
return chunkFetcher;
}
- (void)chunkFetcher:(GTMSessionFetcher *)chunkFetcher
finishedWithData:(NSData *)data
error:(NSError *)error {
BOOL hasDestroyedOldChunkFetcher = NO;
self.fetcherInFlight = nil;
NSDictionary *responseHeaders = [chunkFetcher responseHeaders];
GTMSessionUploadFetcherStatus uploadStatus =
[[self class] uploadStatusFromResponseHeaders:responseHeaders];
GTMSESSION_ASSERT_DEBUG(
uploadStatus != kStatusUnknown || error != nil || self.wasCreatedFromBackgroundSession,
@"chunk fetcher completion has kStatusUnknown upload status for headers %@ fetcher %@",
responseHeaders, self);
BOOL isUploadStatusStopped = (uploadStatus == kStatusFinal || uploadStatus == kStatusCancelled);
// Check if the fetcher was actually querying. If it failed, do not retry,
// as it would enter an infinite retry loop.
NSString *uploadCommand =
chunkFetcher.request.allHTTPHeaderFields[kGTMSessionHeaderXGoogUploadCommand];
BOOL isQueryFetch = [uploadCommand isEqual:@"query"];
// TODO
// Maybe here we can check to see if the request had x goog content length set. (the file length
// one).
NSString *previousContentLengthValue =
[chunkFetcher.request valueForHTTPHeaderField:@"Content-Length"];
// The Content-Length header may not be present if the chunk fetcher was recreated from
// a background session.
BOOL hasKnownChunkSize = (previousContentLengthValue != nil);
int64_t previousContentLength = [previousContentLengthValue longLongValue];
BOOL needsQuery = (!hasKnownChunkSize && !isUploadStatusStopped);
if (error || (needsQuery && !isQueryFetch)) {
NSInteger status = error.code;
// Status 4xx indicates a bad offset in the Google upload protocol. However, do not retry status
// 404 per spec, nor if the upload size appears to have been zero (since the server will just
// keep asking us to retry.)
if (self.shouldInitiateOffsetQuery || (needsQuery && !isQueryFetch) ||
([error.domain isEqual:kGTMSessionFetcherStatusDomain] && status >= 400 && status <= 499 &&
status != 404 && uploadStatus == kStatusActive && previousContentLength > 0)) {
self.shouldInitiateOffsetQuery = NO;
[self destroyChunkFetcher];
hasDestroyedOldChunkFetcher = YES;
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_nextUploadRetryInterval = self.nextUploadRetryIntervalUnsynchronized;
}
[self sendQueryForUploadOffsetWithFetcherProperties:chunkFetcher.properties];
} else {
// Some unexpected status has occurred; handle it as we would a regular
// object fetcher failure.
[self invokeFinalCallbackWithData:data error:error shouldInvalidateLocation:NO];
}
} else {
int64_t newOffset;
// The chunk has uploaded successfully.
NSString *uploadSizeReceived =
[chunkFetcher.responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadSizeReceived];
if (uploadSizeReceived) {
newOffset = [uploadSizeReceived longLongValue];
} else {
newOffset = self.currentOffset + previousContentLength;
}
#if DEBUG
// Verify that if we think all of the uploading data has been sent, the server responded with
// the "final" upload status.
__unused BOOL hasUploadAllData = (newOffset == [self fullUploadLength]);
__unused BOOL isFinalStatus = (uploadStatus == kStatusFinal);
GTMSESSION_ASSERT_DEBUG(hasUploadAllData == isFinalStatus || !hasKnownChunkSize,
@"uploadStatus:%@ newOffset:%lld (%lld + %lld) fullUploadLength:%lld"
@" chunkFetcher:%@ requestHeaders:%@ responseHeaders:%@",
[responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadStatus],
newOffset, self.currentOffset, previousContentLength,
[self fullUploadLength], chunkFetcher,
chunkFetcher.request.allHTTPHeaderFields, responseHeaders);
#endif
if (isUploadStatusStopped || (!_uploadData && _uploadFileLength == 0) ||
(_currentOffset > _uploadFileLength && _uploadFileLength > 0)) {
// This was the last chunk.
if (error == nil && uploadStatus == kStatusCancelled) {
// Report cancelled status as an error.
NSDictionary *userInfo = nil;
if (data.length > 0) {
userInfo = @{kGTMSessionFetcherStatusDataKey : data};
}
data = nil;
error = [self prematureFailureErrorWithUserInfo:userInfo];
} else {
// The upload is in final status.
//
// Take the chunk fetcher's data as the superclass data.
self.downloadedData = data;
self.statusCode = chunkFetcher.statusCode;
}
// we're done
[self invokeFinalCallbackWithData:data error:error shouldInvalidateLocation:YES];
} else {
// Start the next chunk.
self.currentOffset = newOffset;
// We want to destroy this chunk fetcher before creating the next one, but
// we want to pass on its properties
NSDictionary *props = [chunkFetcher properties];
// We no longer need to be able to cancel this chunkFetcher. Destroy it
// before we create a new chunk fetcher.
[self destroyChunkFetcher];
hasDestroyedOldChunkFetcher = YES;
[self uploadNextChunkWithOffset:newOffset fetcherProperties:props];
}
}
if (!hasDestroyedOldChunkFetcher) {
[self destroyChunkFetcher];
}
}
- (void)destroyChunkFetcher {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
if (_fetcherInFlight == _chunkFetcher) {
_fetcherInFlight = nil;
}
[_chunkFetcher stopFetching];
NSURL *chunkFileURL = _chunkFetcher.bodyFileURL;
BOOL wasTemporaryUploadFile = ![chunkFileURL isEqual:_uploadFileURL];
if (wasTemporaryUploadFile) {
NSError *error;
[[NSFileManager defaultManager] removeItemAtURL:chunkFileURL error:&error];
if (error) {
GTMSESSION_LOG_DEBUG(@"removingItemAtURL failed: %@\n%@", error, chunkFileURL);
}
}
_recentChunkReponseHeaders = _chunkFetcher.responseHeaders;
// To avoid retain cycles, remove all properties except the parent identifier.
_chunkFetcher.properties =
@{kGTMSessionUploadFetcherChunkParentKey : [NSValue valueWithNonretainedObject:self]};
_chunkFetcher.retryBlock = nil;
_chunkFetcher.sendProgressBlock = nil;
_chunkFetcher = nil;
} // @synchronized(self)
}
// This method calculates the proper values to pass to the client's send progress block.
//
// The actual total bytes sent include the initial body sent, plus the
// offset into the batched data prior to the current chunk fetcher
- (void)invokeDelegateWithDidSendBytes:(int64_t)bytesSent
totalBytesSent:(int64_t)totalBytesSent
totalBytesExpectedToSend:(int64_t)totalBytesExpected {
GTMSessionCheckNotSynchronized(self);
// 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"
// Ensure the chunk fetcher survives the callback in case the user pauses the upload process.
__block GTMSessionFetcher *holdFetcher = self.chunkFetcher;
#pragma clang diagnostic pop
[self invokeOnCallbackQueue:self.delegateCallbackQueue
afterUserStopped:NO
block:^{
GTMSessionFetcherSendProgressBlock sendProgressBlock =
self.sendProgressBlock;
if (sendProgressBlock) {
sendProgressBlock(bytesSent, totalBytesSent, totalBytesExpected);
}
holdFetcher = nil;
}];
}
- (void)retrieveUploadChunkGranularityFromResponseHeaders:(NSDictionary *)responseHeaders {
GTMSessionCheckNotSynchronized(self);
// Standard granularity for Google uploads is 256K.
NSString *chunkGranularityHeader =
[responseHeaders objectForKey:kGTMSessionHeaderXGoogUploadChunkGranularity];
self.uploadGranularity = chunkGranularityHeader.longLongValue;
}
#pragma mark -
- (BOOL)isPaused {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _isPaused;
} // @synchronized(self)
}
- (void)pauseFetching {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_isPaused = YES;
} // @synchronized(self)
// Pausing just means stopping the current chunk from uploading;
// when we resume, we will send a query request to the server to
// figure out what bytes to resume sending.
//
// We won't try to cancel the initial data upload, but rather will check
// for being paused in beginChunkFetches.
[self destroyChunkFetcher];
}
- (void)resumeFetching {
BOOL wasPaused;
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
wasPaused = _isPaused;
_isPaused = NO;
} // @synchronized(self)
if (wasPaused) {
[self sendQueryForUploadOffsetWithFetcherProperties:self.properties];
}
}
- (void)stopFetching {
// Overrides the superclass
[self destroyChunkFetcher];
// If we think the server is waiting for more data, then tell it there won't be more.
if (self.uploadLocationURL) {
[self sendCancelUploadWithFetcherProperties:[self properties]];
self.uploadLocationURL = nil;
} else {
[self invokeOnCallbackQueue:self.callbackQueue
afterUserStopped:YES
block:^{
// Repeated calls to stopFetching may cause this path to be reached
// despite having sent a real cancel request, check here to ensure that
// the cancellation handler invocation which fires will definitely be
// for the real request sent previously.
@synchronized(self) {
if (self->_isCancelInFlight) {
return;
}
}
[self triggerCancellationHandlerForFetch:nil data:nil error:nil];
}];
}
[super stopFetching];
}
// Fires the cancellation handler, returning whether there was a handler to be fired.
- (BOOL)triggerCancellationHandlerForFetch:(GTMSessionFetcher *)fetcher
data:(NSData *)data
error:(NSError *)error {
GTMSessionUploadFetcherCancellationHandler handler = self.cancellationHandler;
if (handler) {
handler(fetcher, data, error);
self.cancellationHandler = nil;
return YES;
}
return NO;
}
#pragma mark -
- (int64_t)updateChunkFetcher:(GTMSessionFetcher *)chunkFetcher forChunkAtOffset:(int64_t)offset {
BOOL isUploadingFileURL = (self.uploadFileURL != nil);
// Upload another chunk, meeting server-required granularity.
int64_t chunkSize = self.chunkSize;
int64_t fullUploadLength = [self fullUploadLength];
BOOL isFileLengthKnown = fullUploadLength >= 0;
BOOL isUploadingFullFile = (offset == 0 && isFileLengthKnown && chunkSize >= fullUploadLength);
if (!isUploadingFileURL || !isUploadingFullFile) {
// We're not uploading the entire file and given the file URL. Since we'll be
// allocating a subdata block for a chunk, we need to bound it to something that
// won't blow the process's memory.
if (chunkSize > kGTMSessionUploadFetcherMaximumDemandBufferSize) {
chunkSize = kGTMSessionUploadFetcherMaximumDemandBufferSize;
}
}
int64_t granularity = self.uploadGranularity;
if (granularity > 0) {
if (chunkSize < granularity) {
chunkSize = granularity;
} else {
chunkSize = chunkSize - (chunkSize % granularity);
}
}
GTMSESSION_ASSERT_DEBUG(offset <= fullUploadLength || fullUploadLength == 0,
@"offset %lld exceeds data length %lld", offset, fullUploadLength);
if (granularity > 0 && offset < fullUploadLength) {
offset = offset - (offset % granularity);
}
// If the chunk size is bigger than the remaining data, or else
// it's close enough in size to the remaining data that we'd rather
// avoid having a whole extra http fetch for the leftover bit, then make
// this chunk size exactly match the remaining data size
NSString *command;
int64_t thisChunkSize = chunkSize;
BOOL isChunkTooBig = (thisChunkSize >= (fullUploadLength - offset));
BOOL isChunkAlmostBigEnough = (fullUploadLength - offset - 2500 < thisChunkSize);
BOOL isFinalChunk = (isChunkTooBig || isChunkAlmostBigEnough) && isFileLengthKnown;
if (isFinalChunk) {
thisChunkSize = fullUploadLength - offset;
if (thisChunkSize > 0) {
command = @"upload, finalize";
} else {
command = @"finalize";
}
} else {
command = @"upload";
}
NSString *lengthStr = @(thisChunkSize).stringValue;
NSString *offsetStr = @(offset).stringValue;
[chunkFetcher setRequestValue:command forHTTPHeaderField:kGTMSessionHeaderXGoogUploadCommand];
[chunkFetcher setRequestValue:lengthStr forHTTPHeaderField:@"Content-Length"];
[chunkFetcher setRequestValue:offsetStr forHTTPHeaderField:kGTMSessionHeaderXGoogUploadOffset];
if (_uploadFileLength != kGTMSessionUploadFetcherUnknownFileSize) {
[chunkFetcher setRequestValue:@([self fullUploadLength]).stringValue
forHTTPHeaderField:kGTMSessionHeaderXGoogUploadContentLength];
}
// Append the range of bytes in this chunk to the fetcher comment.
NSString *baseComment = self.comment;
[chunkFetcher setCommentWithFormat:@"%@ (%lld-%lld)", baseComment ? baseComment : @"upload",
offset, MAX(0, offset + thisChunkSize - 1)];
return thisChunkSize;
}
// Public properties.
// clang-format off
@synthesize currentOffset = _currentOffset,
allowsCellularAccess = _allowsCellularAccess,
delegateCompletionHandler = _delegateCompletionHandler,
chunkFetcher = _chunkFetcher,
lastChunkRequest = _lastChunkRequest,
subdataGenerating = _subdataGenerating,
shouldInitiateOffsetQuery = _shouldInitiateOffsetQuery,
uploadGranularity = _uploadGranularity,
uploadRetryFactor = _uploadRetryFactor;
// clang-format on
// Internal properties.
@dynamic fetcherInFlight;
@dynamic activeFetcher;
@dynamic statusCode;
@dynamic delegateCallbackQueue;
+ (void)removePointer:(void *)pointer fromPointerArray:(NSPointerArray *)pointerArray {
for (NSUInteger index = 0, count = pointerArray.count; index < count; ++index) {
void *pointerAtIndex = [pointerArray pointerAtIndex:index];
if (pointerAtIndex == pointer) {
[pointerArray removePointerAtIndex:index];
return;
}
}
}
- (BOOL)useBackgroundSession {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _useBackgroundSessionOnChunkFetchers;
} // @synchronized(self
}
- (void)setUseBackgroundSession:(BOOL)useBackgroundSession {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
if (_useBackgroundSessionOnChunkFetchers != useBackgroundSession) {
_useBackgroundSessionOnChunkFetchers = useBackgroundSession;
NSPointerArray *uploadFetcherPointerArrayForBackgroundSessions =
[[self class] uploadFetcherPointerArrayForBackgroundSessions];
@synchronized(uploadFetcherPointerArrayForBackgroundSessions) {
if (_useBackgroundSessionOnChunkFetchers) {
[uploadFetcherPointerArrayForBackgroundSessions addPointer:(__bridge void *)self];
} else {
[[self class] removePointer:(__bridge void *)self
fromPointerArray:uploadFetcherPointerArrayForBackgroundSessions];
}
} // @synchronized(uploadFetcherPointerArrayForBackgroundSessions)
}
} // @synchronized(self)
}
- (BOOL)canFetchWithBackgroundSession {
// The initial upload fetcher is always a foreground session; the
// useBackgroundSession property will apply only to chunk fetchers,
// not to queries.
return NO;
}
- (NSDictionary *)responseHeaders {
GTMSessionCheckNotSynchronized(self);
// Overrides the superclass
// If asked for the fetcher's response, use the most recent chunk fetcher's response,
// since the original request's response lacks useful information like the actual
// Content-Type.
NSDictionary *dict = self.chunkFetcher.responseHeaders;
if (dict) {
return dict;
}
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
if (_recentChunkReponseHeaders) {
return _recentChunkReponseHeaders;
}
} // @synchronized(self
// No chunk fetcher yet completed, so return whatever we have from the initial fetch.
return [super responseHeaders];
}
- (NSInteger)statusCodeUnsynchronized {
GTMSessionCheckSynchronized(self);
if (_recentChunkStatusCode != -1) {
// Overrides the superclass to indicate status appropriate to the initial
// or latest chunk fetch
return _recentChunkStatusCode;
} else {
return [super statusCodeUnsynchronized];
}
}
- (void)setStatusCode:(NSInteger)val {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_recentChunkStatusCode = val;
}
}
- (int64_t)initialBodyLength {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _initialBodyLength;
}
}
- (void)setInitialBodyLength:(int64_t)length {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_initialBodyLength = length;
}
}
- (int64_t)initialBodySent {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _initialBodySent;
}
}
- (void)setInitialBodySent:(int64_t)length {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_initialBodySent = length;
}
}
- (NSURL *)uploadLocationURL {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
return _uploadLocationURL;
}
}
- (void)setUploadLocationURL:(NSURL *)locationURL {
@synchronized(self) {
GTMSessionMonitorSynchronized(self);
_uploadLocationURL = locationURL;
}
}
- (GTMSessionFetcher *)activeFetcher {
GTMSessionFetcher *result = self.fetcherInFlight;
if (result) return result;
return self;
}
- (BOOL)isFetching {
// If there is an active chunk fetcher, then the upload fetcher is considered
// to still be fetching.
if (self.fetcherInFlight != nil) return YES;
return [super isFetching];
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
- (BOOL)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds {
NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:timeoutInSeconds];
while (self.fetcherInFlight || self.subdataGenerating) {
if ([timeoutDate timeIntervalSinceNow] < 0) return NO;
if (self.subdataGenerating) {
// Allow time for subdata generation.
NSDate *stopDate = [NSDate dateWithTimeIntervalSinceNow:0.001];
[[NSRunLoop currentRunLoop] runUntilDate:stopDate];
} else {
// Wait for any chunk or query fetchers that still have pending callbacks or
// notifications.
BOOL timedOut;
if (self.fetcherInFlight == self) {
timedOut = ![super waitForCompletionWithTimeout:timeoutInSeconds];
} else {
timedOut = ![self.fetcherInFlight waitForCompletionWithTimeout:timeoutInSeconds];
}
if (timedOut) return NO;
}
}
return YES;
}
#pragma clang diagnostic pop
@end
@implementation GTMSessionFetcher (GTMSessionUploadFetcherMethods)
- (GTMSessionUploadFetcher *)parentUploadFetcher {
NSValue *property = [self propertyForKey:kGTMSessionUploadFetcherChunkParentKey];
if (!property) return nil;
GTMSessionUploadFetcher *uploadFetcher = property.nonretainedObjectValue;
GTMSESSION_ASSERT_DEBUG([uploadFetcher isKindOfClass:[GTMSessionUploadFetcher class]],
@"Unexpected parent upload fetcher class: %@", [uploadFetcher class]);
return uploadFetcher;
}
@end

View File

@@ -0,0 +1,1413 @@
/* 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.
*/
// GTMSessionFetcher is a wrapper around NSURLSession for http operations.
//
// What does this offer on top of of NSURLSession?
//
// - Block-style callbacks for useful functionality like progress rather
// than delegate methods.
// - Out-of-process uploads and downloads using NSURLSession, including
// management of fetches after relaunch.
// - Integration with GTMAppAuth for invisible management and refresh of
// authorization tokens.
// - Pretty-printed http logging.
// - Cookies handling that does not interfere with or get interfered with
// by WebKit cookies or on Mac by Safari and other apps.
// - Credentials handling for the http operation.
// - Rate-limiting and cookie grouping when fetchers are created with
// GTMSessionFetcherService.
//
// If the bodyData or bodyFileURL property is set, then a POST request is assumed.
//
// Each fetcher is assumed to be for a one-shot fetch request; don't reuse the object
// for a second fetch.
//
// The fetcher will be self-retained as long as a connection is pending.
//
// To keep user activity private, URLs must have an https scheme (unless the property
// allowedInsecureSchemes is set to permit the scheme.)
//
// Callbacks will be released when the fetch completes or is stopped, so there is no need
// to use weak self references in the callback blocks.
//
// Sample usage:
//
// _fetcherService = [[GTMSessionFetcherService alloc] init];
//
// GTMSessionFetcher *myFetcher = [_fetcherService fetcherWithURLString:myURLString];
// myFetcher.retryEnabled = YES;
// myFetcher.comment = @"First profile image";
//
// // Optionally specify a file URL or NSData for the request body to upload.
// myFetcher.bodyData = [postString dataUsingEncoding:NSUTF8StringEncoding];
//
// [myFetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
// if (error != nil) {
// // Server status code or network error.
// //
// // If the domain is kGTMSessionFetcherStatusDomain then the error code
// // is a failure status from the server.
// } else {
// // Fetch succeeded.
// }
// }];
//
// There is also a beginFetch call that takes a pointer and selector for the completion handler;
// a pointer and selector is a better style when the callback is a substantial, separate method.
//
// NOTE: Fetches may retrieve data from the server even though the server
// returned an error, so the criteria for success is a non-nil error.
// The completion handler is called when the server status is >= 300 with an NSError
// having domain kGTMSessionFetcherStatusDomain and code set to the server status.
//
// Status codes are at <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html>
//
//
// Background session support:
//
// Out-of-process uploads and downloads may be created by setting the fetcher's
// useBackgroundSession property. Data to be uploaded should be provided via
// the uploadFileURL property; the download destination should be specified with
// the destinationFileURL. NOTE: Background upload files should be in a location
// that will be valid even after the device is restarted, so the file should not
// be uploaded from a system temporary or cache directory.
//
// Background session transfers are slower, and should typically be used only
// for very large downloads or uploads (hundreds of megabytes).
//
// When background sessions are used in iOS apps, the application delegate must
// pass through the parameters from UIApplicationDelegate's
// application:handleEventsForBackgroundURLSession:completionHandler: to the
// fetcher class.
//
// When the application has been relaunched, it may also create a new fetcher
// instance to handle completion of the transfers.
//
// - (void)application:(UIApplication *)application
// handleEventsForBackgroundURLSession:(NSString *)identifier
// completionHandler:(void (^)())completionHandler {
// // Application was re-launched on completing an out-of-process download.
//
// // Pass the URLSession info related to this re-launch to the fetcher class.
// [GTMSessionFetcher application:application
// handleEventsForBackgroundURLSession:identifier
// completionHandler:completionHandler];
//
// // Get a fetcher related to this re-launch and re-hook up a completionHandler to it.
// GTMSessionFetcher *fetcher = [GTMSessionFetcher fetcherWithSessionIdentifier:identifier];
// NSURL *destinationFileURL = fetcher.destinationFileURL;
// fetcher.completionHandler = ^(NSData *data, NSError *error) {
// [self downloadCompletedToFile:destinationFileURL error:error];
// };
// }
//
//
// Threading and queue support:
//
// Networking always happens on a background thread; there is no advantage to
// changing thread or queue to create or start a fetcher.
//
// Callbacks are run on the main thread; alternatively, the app may set the
// fetcher's callbackQueue to a dispatch queue.
//
// Once the fetcher's beginFetch method has been called, the fetcher's methods and
// properties may be accessed from any thread.
//
// Downloading to disk:
//
// To have downloaded data saved directly to disk, specify a file URL for the
// destinationFileURL property.
//
// HTTP methods and headers:
//
// Alternative HTTP methods, like PUT, and custom headers can be specified by
// creating the fetcher with an appropriate NSMutableURLRequest.
//
// Custom headers can also be provided per-request via an instance of `GTMFetcherDecoratorProtocol`
// passed to `-[GTMSessionFetcherService addDecorator:]`.
//
// Caching:
//
// The fetcher avoids caching. That is best for API requests, but may hurt
// repeat fetches of static data. Apps may enable a persistent disk cache by
// customizing the config:
//
// fetcher.configurationBlock = ^(GTMSessionFetcher *configFetcher,
// NSURLSessionConfiguration *config) {
// config.URLCache = [NSURLCache sharedURLCache];
// };
//
// Or use the standard system config to share cookie storage with web views
// and to enable disk caching:
//
// fetcher.configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
//
//
// Cookies:
//
// There are three supported mechanisms for remembering cookies between fetches.
//
// By default, a standalone GTMSessionFetcher uses a mutable array held
// statically to track cookies for all instantiated fetchers. This avoids
// cookies being set by servers for the application from interfering with
// Safari and WebKit cookie settings, and vice versa.
// The fetcher cookies are lost when the application quits.
//
// To rely instead on WebKit's global NSHTTPCookieStorage, set the fetcher's
// cookieStorage property:
// myFetcher.cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
//
// To share cookies with other apps, use the method introduced in iOS 9/OS X 10.11:
// myFetcher.cookieStorage =
// [NSHTTPCookieStorage sharedCookieStorageForGroupContainerIdentifier:kMyCompanyContainedID];
//
// To ignore existing cookies and only have cookies related to the single fetch
// be applied, make a temporary cookie storage object:
// myFetcher.cookieStorage = [[GTMSessionCookieStorage alloc] init];
//
// Note: cookies set while following redirects will be sent to the server, as
// the redirects are followed by the fetcher.
//
// To completely disable cookies, adjust the session configuration appropriately
// in the fetcher or fetcher service:
// fetcher.configurationBlock = ^(GTMSessionFetcher *configFetcher,
// NSURLSessionConfiguration *config) {
// config.HTTPCookieAcceptPolicy = NSHTTPCookieAcceptPolicyNever;
// config.HTTPShouldSetCookies = NO;
// };
//
// If the fetcher is created from a GTMSessionFetcherService object
// then the cookie storage mechanism is set to use the cookie storage in the
// service object rather than the static storage. Disabling cookies in the
// session configuration set on a service object will disable cookies for all
// fetchers created from that GTMSessionFetcherService object, since the session
// configuration is propagated to the fetcher.
//
//
// Monitoring data transfers.
//
// The fetcher supports a variety of properties for progress monitoring
// progress with callback blocks.
// GTMSessionFetcherSendProgressBlock sendProgressBlock
// GTMSessionFetcherReceivedProgressBlock receivedProgressBlock
// GTMSessionFetcherDownloadProgressBlock downloadProgressBlock
//
// If supplied by the server, the anticipated total download size is available
// as [[myFetcher response] expectedContentLength] (and may be -1 for unknown
// download sizes.)
//
//
// Automatic retrying of fetches
//
// The fetcher can optionally create a timer and reattempt certain kinds of
// fetch failures (status codes 408, request timeout; 502, gateway failure;
// 503, service unavailable; 504, gateway timeout; networking errors
// NSURLErrorTimedOut and NSURLErrorNetworkConnectionLost.) The user may
// set a retry selector to customize the type of errors which will be retried.
//
// Retries are done in an exponential-backoff fashion (that is, after 1 second,
// 2, 4, 8, and so on.)
//
// Enabling automatic retries looks like this:
// myFetcher.retryEnabled = YES;
//
// With retries enabled, the completion callbacks are called only
// when no more retries will be attempted. Calling the fetcher's stopFetching
// method will terminate the retry timer, without the finished or failure
// selectors being invoked.
//
// Optionally, the client may set the maximum retry interval:
// myFetcher.maxRetryInterval = 60.0; // in seconds; default is 60 seconds
// // for downloads, 600 for uploads
//
// Servers should never send a 400 or 500 status for errors that are retryable
// by clients, as those values indicate permanent failures. In nearly all
// cases, the default standard retry behavior is correct for clients, and no
// custom client retry behavior is needed or appropriate. Servers that send
// non-retryable status codes and expect the client to retry the request are
// faulty.
//
// Still, the client may provide a block to determine if a status code or other
// error should be retried. The block returns YES to set the retry timer or NO
// to fail without additional fetch attempts.
//
// The retry method may return the |suggestedWillRetry| argument to get the
// default retry behavior. Server status codes are present in the
// error argument, and have the domain kGTMSessionFetcherStatusDomain. The
// user's method may look something like this:
//
// myFetcher.retryBlock = ^(BOOL suggestedWillRetry, NSError *error,
// GTMSessionFetcherRetryResponse response) {
// // Perhaps examine error.domain and error.code, or fetcher.retryCount
// //
// // Respond with YES to start the retry timer, NO to proceed to the failure
// // callback, or suggestedWillRetry to get default behavior for the
// // current error domain and code values.
// response(suggestedWillRetry);
// };
#import <Foundation/Foundation.h>
#if TARGET_OS_IPHONE
#import <UIKit/UIKit.h>
#endif
#if TARGET_OS_WATCH
#import <WatchKit/WatchKit.h>
#endif
// By default it is stripped from non DEBUG builds. Developers can override
// this in their project settings.
#ifndef STRIP_GTM_FETCH_LOGGING
#if !DEBUG
#define STRIP_GTM_FETCH_LOGGING 1
#else
#define STRIP_GTM_FETCH_LOGGING 0
#endif
#endif
// Logs in debug builds.
#ifndef GTMSESSION_LOG_DEBUG
#if DEBUG
#define GTMSESSION_LOG_DEBUG(...) NSLog(__VA_ARGS__)
#else
#define GTMSESSION_LOG_DEBUG(...) \
do { \
} while (0)
#endif
#endif
// Asserts in debug builds (or logs in debug builds if GTMSESSION_ASSERT_AS_LOG
// or NS_BLOCK_ASSERTIONS are defined.)
#ifndef GTMSESSION_ASSERT_DEBUG
#if DEBUG && !defined(NS_BLOCK_ASSERTIONS) && !GTMSESSION_ASSERT_AS_LOG
#undef GTMSESSION_ASSERT_AS_LOG
#define GTMSESSION_ASSERT_AS_LOG 1
#endif
#if DEBUG && !GTMSESSION_ASSERT_AS_LOG
#define GTMSESSION_ASSERT_DEBUG(...) NSAssert(__VA_ARGS__)
#elif DEBUG
#define GTMSESSION_ASSERT_DEBUG(pred, ...) \
if (!(pred)) { \
NSLog(__VA_ARGS__); \
}
#else
#define GTMSESSION_ASSERT_DEBUG(pred, ...) \
do { \
} while (0)
#endif
#endif
// Asserts in debug builds, logs in release builds (or logs in debug builds if
// GTMSESSION_ASSERT_AS_LOG is defined.)
#ifndef GTMSESSION_ASSERT_DEBUG_OR_LOG
#if DEBUG && !GTMSESSION_ASSERT_AS_LOG
#define GTMSESSION_ASSERT_DEBUG_OR_LOG(...) NSAssert(__VA_ARGS__)
#else
#define GTMSESSION_ASSERT_DEBUG_OR_LOG(pred, ...) \
if (!(pred)) { \
NSLog(__VA_ARGS__); \
}
#endif
#endif
// Macro useful for more verbose logging from NSURLSession during debugging.
#if 0
#define GTMSESSION_LOG_DEBUG_VERBOSE(...) GTMSESSION_LOG_DEBUG(__VA_ARGS__)
#else
#define GTMSESSION_LOG_DEBUG_VERBOSE(...)
#endif
// For iOS, the fetcher can declare itself a background task to allow fetches
// to finish when the app leaves the foreground.
//
// (This is unrelated to providing a background configuration, which allows
// out-of-process uploads and downloads.)
//
// To disallow use of background tasks during fetches, the target should define
// GTM_BACKGROUND_TASK_FETCHING to 0, or alternatively may set the
// skipBackgroundTask property to YES.
#if !defined(GTM_BACKGROUND_TASK_FETCHING) && \
(TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_MACCATALYST)
#define GTM_BACKGROUND_TASK_FETCHING 1
#endif
#ifdef __cplusplus
extern "C" {
#endif
// When creating background sessions to perform out-of-process uploads and
// downloads, on app launch any background sessions must be reconnected in
// order to receive events that occurred while the app was not running.
//
// The fetcher will automatically attempt to recreate the sessions on app
// start, but doing so reads from NSUserDefaults. This may have launch-time
// performance impacts.
//
// To avoid launch performance impacts, on iPhone/iPad with iOS 13+ the
// GTMSessionFetcher class will register for the app launch notification and
// perform the reconnect then.
//
// Apps targeting Mac or older iOS SDKs can opt into the new behavior by defining
// GTMSESSION_RECONNECT_BACKGROUND_SESSIONS_ON_LAUNCH=1.
//
// Apps targeting new SDKs can force the old behavior by defining
// GTMSESSION_RECONNECT_BACKGROUND_SESSIONS_ON_LAUNCH = 0.
#ifndef GTMSESSION_RECONNECT_BACKGROUND_SESSIONS_ON_LAUNCH
// Default to the on-launch behavior for iOS 13+.
#if TARGET_OS_IOS && defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0
#define GTMSESSION_RECONNECT_BACKGROUND_SESSIONS_ON_LAUNCH 1
#else
#define GTMSESSION_RECONNECT_BACKGROUND_SESSIONS_ON_LAUNCH 0
#endif
#endif
NS_ASSUME_NONNULL_BEGIN
// Notifications
//
// Fetch started and stopped, and fetch retry delay started and stopped.
extern NSString *const kGTMSessionFetcherStartedNotification;
extern NSString *const kGTMSessionFetcherStoppedNotification;
extern NSString *const kGTMSessionFetcherRetryDelayStartedNotification;
extern NSString *const kGTMSessionFetcherRetryDelayStoppedNotification;
// Completion handler notification. This is intended for use by code capturing
// and replaying fetch requests and results for testing. For fetches where
// destinationFileURL or accumulateDataBlock is set for the fetcher, the data
// will be nil for successful fetches.
//
// This notification is posted on the main thread.
extern NSString *const kGTMSessionFetcherCompletionInvokedNotification;
extern NSString *const kGTMSessionFetcherCompletionDataKey;
extern NSString *const kGTMSessionFetcherCompletionErrorKey;
// Constants for NSErrors created by the fetcher (excluding server status errors,
// and error objects originating in the OS.)
extern NSString *const kGTMSessionFetcherErrorDomain;
// The fetcher turns server error status values (3XX, 4XX, 5XX) into NSErrors
// with domain kGTMSessionFetcherStatusDomain.
//
// Any server response body data accompanying the status error is added to the
// userInfo dictionary with key kGTMSessionFetcherStatusDataKey.
extern NSString *const kGTMSessionFetcherStatusDomain;
extern NSString *const kGTMSessionFetcherStatusDataKey;
extern NSString *const kGTMSessionFetcherStatusDataContentTypeKey;
// When a fetch fails with an error, these keys are included in the error userInfo
// dictionary if retries were attempted.
extern NSString *const kGTMSessionFetcherNumberOfRetriesDoneKey;
extern NSString *const kGTMSessionFetcherElapsedIntervalWithRetriesKey;
// Background session support requires access to NSUserDefaults.
// If [NSUserDefaults standardUserDefaults] doesn't yield the correct NSUserDefaults for your usage,
// ie for an App Extension, then implement this class/method to return the correct NSUserDefaults.
// https://developer.apple.com/library/ios/documentation/General/Conceptual/ExtensibilityPG/ExtensionScenarios.html#//apple_ref/doc/uid/TP40014214-CH21-SW6
@interface GTMSessionFetcherUserDefaultsFactory : NSObject
+ (NSUserDefaults *)fetcherUserDefaults;
@end
#ifdef __cplusplus
}
#endif
typedef NS_ENUM(NSInteger, GTMSessionFetcherError) {
GTMSessionFetcherErrorDownloadFailed = -1,
GTMSessionFetcherErrorUploadChunkUnavailable = -2,
GTMSessionFetcherErrorBackgroundExpiration = -3,
GTMSessionFetcherErrorBackgroundFetchFailed = -4,
GTMSessionFetcherErrorInsecureRequest = -5,
GTMSessionFetcherErrorTaskCreationFailed = -6,
// This error is only used if `stopFetchingTriggersCompletionHandler` is
// enabled and `-stopFetching` is called on that fetcher.
GTMSessionFetcherErrorUserCancelled = -7,
};
typedef NS_ENUM(NSInteger, GTMSessionFetcherStatus) {
// Standard http status codes.
GTMSessionFetcherStatusNotModified = 304,
GTMSessionFetcherStatusBadRequest = 400,
GTMSessionFetcherStatusUnauthorized = 401,
GTMSessionFetcherStatusForbidden = 403,
GTMSessionFetcherStatusPreconditionFailed = 412
};
#ifdef __cplusplus
extern "C" {
#endif
@class GTMSessionCookieStorage;
@class GTMSessionFetcher;
@class GTMSessionFetcherService;
// The configuration block is for modifying the NSURLSessionConfiguration only.
// DO NOT change any fetcher properties in the configuration block.
typedef void (^GTMSessionFetcherConfigurationBlock)(GTMSessionFetcher *fetcher,
NSURLSessionConfiguration *configuration);
typedef void (^GTMSessionFetcherSystemCompletionHandler)(void);
typedef void (^GTMSessionFetcherCompletionHandler)(NSData *_Nullable data,
NSError *_Nullable error);
typedef NSURLSession *_Nullable (^GTMSessionFetcherSessionCreationBlock)(
id<NSURLSessionDelegate> _Nullable sessionDelegate);
typedef void (^GTMSessionFetcherBodyStreamProviderResponse)(NSInputStream *bodyStream);
typedef void (^GTMSessionFetcherBodyStreamProvider)(
GTMSessionFetcherBodyStreamProviderResponse response);
typedef void (^GTMSessionFetcherDidReceiveResponseDispositionBlock)(
NSURLSessionResponseDisposition disposition);
typedef void (^GTMSessionFetcherDidReceiveResponseBlock)(
NSURLResponse *response, GTMSessionFetcherDidReceiveResponseDispositionBlock dispositionBlock);
typedef void (^GTMSessionFetcherChallengeDispositionBlock)(
NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *_Nullable credential);
typedef void (^GTMSessionFetcherChallengeBlock)(
GTMSessionFetcher *fetcher, NSURLAuthenticationChallenge *challenge,
GTMSessionFetcherChallengeDispositionBlock dispositionBlock);
typedef void (^GTMSessionFetcherWillRedirectResponse)(NSURLRequest *_Nullable redirectedRequest);
typedef void (^GTMSessionFetcherWillRedirectBlock)(NSHTTPURLResponse *redirectResponse,
NSURLRequest *redirectRequest,
GTMSessionFetcherWillRedirectResponse response);
typedef void (^GTMSessionFetcherAccumulateDataBlock)(NSData *_Nullable buffer);
typedef void (^GTMSessionFetcherSimulateByteTransferBlock)(NSData *_Nullable buffer,
int64_t bytesWritten,
int64_t totalBytesWritten,
int64_t totalBytesExpectedToWrite);
typedef void (^GTMSessionFetcherReceivedProgressBlock)(int64_t bytesWritten,
int64_t totalBytesWritten);
typedef void (^GTMSessionFetcherDownloadProgressBlock)(int64_t bytesWritten,
int64_t totalBytesWritten,
int64_t totalBytesExpectedToWrite);
typedef void (^GTMSessionFetcherSendProgressBlock)(int64_t bytesSent, int64_t totalBytesSent,
int64_t totalBytesExpectedToSend);
typedef void (^GTMSessionFetcherWillCacheURLResponseResponse)(
NSCachedURLResponse *_Nullable cachedResponse);
typedef void (^GTMSessionFetcherWillCacheURLResponseBlock)(
NSCachedURLResponse *proposedResponse,
GTMSessionFetcherWillCacheURLResponseResponse responseBlock);
typedef void (^GTMSessionFetcherRetryResponse)(BOOL shouldRetry);
typedef void (^GTMSessionFetcherRetryBlock)(BOOL suggestedWillRetry, NSError *_Nullable error,
GTMSessionFetcherRetryResponse response);
API_AVAILABLE(ios(10.0), macosx(10.12), tvos(10.0), watchos(6.0))
typedef void (^GTMSessionFetcherMetricsCollectionBlock)(NSURLSessionTaskMetrics *metrics);
typedef void (^GTMSessionFetcherTestResponse)(NSHTTPURLResponse *_Nullable response,
NSData *_Nullable data, NSError *_Nullable error);
typedef void (^GTMSessionFetcherTestBlock)(GTMSessionFetcher *fetcherToTest,
GTMSessionFetcherTestResponse testResponse);
// Provides access to a user-agent string calculated on demand.
//
// Methods and properties on this protocol must be thread-safe. In addition,
// |userAgentCache| must not block the calling thread to perform I/O.
@protocol GTMUserAgentProvider <NSObject>
// Non-nil user-agent string if |userAgent| has already been cached and is safe
// to read without blocking the calling thread, |nil| otherwise.
@property(atomic, readonly, nullable, copy) NSString *cachedUserAgent;
// The user-agent string, calculated on demand. This might block the calling thread if
// |userAgentCached| is NO.
@property(atomic, readonly, copy) NSString *userAgent;
@end
/// Provides a User-Agent string that is known at the time the fetcher is created.
__attribute__((objc_subclassing_restricted))
@interface GTMUserAgentStringProvider : NSObject<GTMUserAgentProvider>
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithUserAgentString:(NSString *)userAgentString NS_DESIGNATED_INITIALIZER;
@end
// Calculates the User-Agent string on demand using |GTMFetcherStandardUserAgentString()| given an
// optional bundle.
__attribute__((objc_subclassing_restricted))
@interface GTMStandardUserAgentProvider : NSObject<GTMUserAgentProvider>
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithBundle:(nullable NSBundle *)bundle NS_DESIGNATED_INITIALIZER;
@end
void GTMSessionFetcherAssertValidSelector(id _Nullable obj, SEL _Nullable sel, ...);
// Utility functions for applications self-identifying to servers via a
// user-agent header
// The "standard" user agent includes the application identifier, taken from the bundle,
// followed by a space and the system version string. Pass nil to use +mainBundle as the source
// of the bundle identifier.
//
// Applications may use this as a starting point for their own user agent strings, perhaps
// with additional sections appended. Use GTMFetcherCleanedUserAgentString() below to
// clean up any string being added to the user agent.
NSString *GTMFetcherStandardUserAgentString(NSBundle *_Nullable bundle);
// Make a generic name and version for the current application, like
// com.example.MyApp/1.2.3 relying on the bundle identifier and the
// CFBundleShortVersionString or CFBundleVersion.
//
// The bundle ID may be overridden as the base identifier string by
// adding to the bundle's Info.plist a "GTMUserAgentID" key.
//
// The application version may be overridden by adding to the bundle's
// Info.plist a "GTMUserAgentVersion" key.
//
// If no bundle ID or override is available, the process name preceded
// by "proc_" is used.
NSString *GTMFetcherApplicationIdentifier(NSBundle *_Nullable bundle);
// Make an identifier like "MacOSX/10.7.1" or "iPod_Touch/4.1 hw/iPod1_1"
NSString *GTMFetcherSystemVersionString(void);
// Make a parseable user-agent identifier from the given string, replacing whitespace
// and commas with underscores, and removing other characters that may interfere
// with parsing of the full user-agent string.
//
// For example, @"[My App]" would become @"My_App"
NSString *GTMFetcherCleanedUserAgentString(NSString *str);
// Grab the data from an input stream. Since streams cannot be assumed to be rewindable,
// this may be destructive; the caller can try to rewind the stream (by setting the
// NSStreamFileCurrentOffsetKey property) or can just use the NSData to make a new
// NSInputStream. This function is intended to facilitate testing rather than be used in
// production.
//
// This function operates synchronously on the current thread. Depending on how the
// input stream is implemented, it may be appropriate to dispatch to a different
// queue before calling this function.
//
// Failure is indicated by a returned data value of nil.
NSData *_Nullable GTMDataFromInputStream(NSInputStream *inputStream, NSError **outError);
#ifdef __cplusplus
} // extern "C"
#endif
// Completion handler passed to -[GTMFetcherDecoratorProtocol fetcherWillStart:completionHandler:].
typedef void (^GTMFetcherDecoratorFetcherWillStartCompletionHandler)(NSURLRequest *_Nullable,
NSError *_Nullable);
// Allows intercepting a request and optionally modifying it before the request (or a retry)
// is sent. See `-[GTMSessionFetcherService addDecorator:]` and `-[GTMSessionFetcherService
// removeDecorator:]`.
//
// Decorator methods must be thread-safe, as they might be invoked on any queue.
@protocol GTMFetcherDecoratorProtocol <NSObject>
// Invoked just before a fetcher's request starts.
//
// After the decorator's work is complete, the decorator must invoke `handler(request, error)`
// either synchronously or asynchronously (on any queue).
//
// If no changes are to be made, pass `nil` for both `request` and `error`.
//
// Otherwise, if `error` is non-nil, then the fetcher is stopped with the given error, and any
// further decorators' `-fetcherWillStart:completionHandler:` methods are not invoked.
//
// Otherwise, the decorator may use `[fetcher.request mutableCopy]`, make changes to the mutable
// copy of the request, and pass the result to the handler via the `request` parameter.
//
// To distinguish the initial fetch from retries, the decorator can look at `fetcher.retryCount`.
//
// This method must not block the caller (e.g., performing synchronous I/O). Perform any blocking
// work or I/O on a different queue, then invoke `handler` with the results after the blocking work
// completes.
- (void)fetcherWillStart:(GTMSessionFetcher *)fetcher
completionHandler:(GTMFetcherDecoratorFetcherWillStartCompletionHandler)handler;
// Invoked just after a fetcher's request finishes (either on success or on failure).
//
// After the decorator's work is complete, the decorator must invoke `handler()` either
// synchronously or asynchronously (on any queue).
//
// To access the result of the fetch, the decorator can look at `fetcher.response`.
//
// This method must not block the caller (e.g., performing synchronous I/O). Perform any blocking
// work or I/O on a different queue, then invoke `handler` with the results after the blocking work
// completes.
- (void)fetcherDidFinish:(GTMSessionFetcher *)fetcher
withData:(nullable NSData *)data
error:(nullable NSError *)error
completionHandler:(void (^)(void))handler;
@end
// This protocol allows abstract references to the fetcher service.
//
// Apps should not need to use this protocol.
@protocol GTMSessionFetcherServiceProtocol <NSObject>
- (GTMSessionFetcher *)fetcherWithRequest:(NSURLRequest *)request;
@property(atomic, strong, null_resettable, readonly) dispatch_queue_t callbackQueue;
// These properties are being removed from the protocol; clients should not attempt new
// accesses to them.
@property(atomic, assign) BOOL reuseSession;
@property(atomic, readonly, strong, nullable) NSOperationQueue *delegateQueue;
@end // @protocol GTMSessionFetcherServiceProtocol
__deprecated_msg("implement GTMSessionFetcherAuthorizer instead")
@protocol GTMFetcherAuthorizationProtocol<NSObject>
@required
// This protocol allows us to call the authorizer without requiring its sources
// in this project. This protocol is deprecated in favor of GTMSessionFetcherAuthorizer,
// and implementations should move to that protocol in anticipation of
// GTMFetcherAuthorizationProtocol being deleted in a future release.
// This method is being phased out. While implementing it is necessary to satisfy
// the protocol's @required restrictions, conforming implementations that implement
// authorizeRequest:completionHandler: will have that called instead.
// be removed in a future version when GTMFetcherAuthorizationProtocol is
// also removed.
- (void)authorizeRequest:(nullable NSMutableURLRequest *)request
delegate:(id)delegate
didFinishSelector:(SEL)sel
__deprecated_msg("implement authorizeRequest:completionHandler: instead");
- (void)stopAuthorization;
- (void)stopAuthorizationForRequest:(NSURLRequest *)request;
- (BOOL)isAuthorizingRequest:(NSURLRequest *)request;
- (BOOL)isAuthorizedRequest:(NSURLRequest *)request;
@property(atomic, strong, readonly, nullable) NSString *userEmail;
@optional
// This method is prefered over authorizeRequest:delegate:didFinishSelector:, and
// becomes a required method in the GTMSessionFetcherAuthorizer protocol.
- (void)authorizeRequest:(nullable NSMutableURLRequest *)request
completionHandler:(void (^)(NSError *_Nullable error))handler;
// Indicate if authorization may be attempted. Even if this succeeds,
// authorization may fail if the user's permissions have been revoked.
@property(atomic, readonly) BOOL canAuthorize;
// For development only, allow authorization of non-SSL requests, allowing
// transmission of the bearer token unencrypted.
@property(atomic, assign) BOOL shouldAuthorizeAllRequests;
@property(atomic, weak, nullable) id<GTMSessionFetcherServiceProtocol> fetcherService;
- (BOOL)primeForRefresh;
@end
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated"
// This is the preferred, forward-going protocol for fetcher authorization. it
// currently implements the deprecated GTMFetcherAuthorizationProtocol in order
// to avoid changing the GTMSessionFetcher API surface while implementations
// migrate. In a future release, the non-deprecated method declarations will be
// moved here and the GTMFetcherAuthorizationProtocol and the deprecated methods
// deleted.
@protocol GTMSessionFetcherAuthorizer <GTMFetcherAuthorizationProtocol>
// This protocol allows us to call the authorizer without requiring its sources
// in this project.
#pragma clang diagnostic pop
@required
// Authorizers should implement this method rather than the selector-based
// callback form from the old protocol.
- (void)authorizeRequest:(nullable NSMutableURLRequest *)request
completionHandler:(void (^)(NSError *_Nullable error))handler;
@optional
// This method is re-declared here as @optional only to quash deprecation warnings
// on the @required declaration from GTMFetcherAuthorizationProtocol, which
// must still be provided by conforming implementations. Once the old protocol has
// been removed, this method will be marked unavailable to trigger implementations
// to stop providing it, and it will eventually be removed.
- (void)authorizeRequest:(nullable NSMutableURLRequest *)request
delegate:(id)delegate
didFinishSelector:(SEL)sel;
@end
#if GTM_BACKGROUND_TASK_FETCHING
// A protocol for an alternative target for messages from GTMSessionFetcher to UIApplication.
// Set the target using +[GTMSessionFetcher setSubstituteUIApplication:]
@protocol GTMUIApplicationProtocol <NSObject>
- (UIBackgroundTaskIdentifier)beginBackgroundTaskWithName:(nullable NSString *)taskName
expirationHandler:(void (^__nullable)(void))handler;
- (void)endBackgroundTask:(UIBackgroundTaskIdentifier)identifier;
@end
#endif
#pragma mark -
// GTMSessionFetcher objects are used for async retrieval of an http get or post
//
// See additional comments at the beginning of this file
@interface GTMSessionFetcher : NSObject <NSURLSessionDelegate>
// Create a fetcher
//
// fetcherWithRequest will return an autoreleased fetcher, but if
// the connection is successfully created, the connection should retain the
// fetcher for the life of the connection as well. So the caller doesn't have
// to retain the fetcher explicitly unless they want to be able to cancel it.
+ (instancetype)fetcherWithRequest:(nullable NSURLRequest *)request;
// Convenience methods that make a request, like +fetcherWithRequest
+ (instancetype)fetcherWithURL:(NSURL *)requestURL;
+ (instancetype)fetcherWithURLString:(NSString *)requestURLString;
// Methods for creating fetchers to continue previous fetches.
+ (instancetype)fetcherWithDownloadResumeData:(NSData *)resumeData;
+ (nullable instancetype)fetcherWithSessionIdentifier:(NSString *)sessionIdentifier;
// Returns an array of currently active fetchers for background sessions,
// both restarted and newly created ones.
+ (NSArray<GTMSessionFetcher *> *)fetchersForBackgroundSessions;
// Designated initializer.
//
// Applications should create fetchers with a "fetcherWith..." method on a fetcher
// service or a class method, not with this initializer.
//
// The configuration should typically be nil. Applications needing to customize
// the configuration may do so by setting the configurationBlock property.
- (instancetype)initWithRequest:(nullable NSURLRequest *)request
configuration:(nullable NSURLSessionConfiguration *)configuration;
// The fetcher's request. This may not be set after beginFetch has been invoked. The request
// may change due to redirects.
@property(atomic, strong, nullable) NSURLRequest *request;
// 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;
// Data used for resuming a download task.
@property(atomic, readonly, nullable) NSData *downloadResumeData;
// The configuration; this must be set before the fetch begins. If no configuration is
// set or inherited from the fetcher service, then the fetcher uses an ephemeral config.
//
// NOTE: This property should typically be nil. Applications needing to customize
// the configuration should do so by setting the configurationBlock property.
// That allows the fetcher to pick an appropriate base configuration, with the
// application setting only the configuration properties it needs to customize.
@property(atomic, strong, nullable) NSURLSessionConfiguration *configuration;
// A block the client may use to customize the configuration used to create the session.
//
// This is called synchronously, either on the thread that begins the fetch or, during a retry,
// on the main thread. The configuration block may be called repeatedly if multiple fetchers are
// created.
//
// The configuration block is for modifying the NSURLSessionConfiguration only.
// DO NOT change any fetcher properties in the configuration block. Fetcher properties
// may be set in the fetcher service prior to fetcher creation, or on the fetcher prior
// to invoking beginFetch.
@property(atomic, copy, nullable) GTMSessionFetcherConfigurationBlock configurationBlock;
// A session is created as needed by the fetcher. A fetcher service object
// may maintain sessions for multiple fetches to the same host.
@property(atomic, strong, nullable) NSURLSession *session;
// The task in flight.
@property(atomic, readonly, nullable) NSURLSessionTask *sessionTask;
// The background session identifier.
@property(atomic, readonly, nullable) NSString *sessionIdentifier;
// Indicates a fetcher created to finish a background session task.
@property(atomic, readonly) BOOL wasCreatedFromBackgroundSession;
// Indicates the client has committed to reconnecting this background session when
// the app restarts. If this value is YES, the session fetcher will not automatically
// call beginFetchWithCompletionHandler: on the restored fetcher on app start, and
// the session will not handle system events until the client explicitly does.
@property(atomic, assign) BOOL clientWillReconnectBackgroundSession;
// Additional user-supplied data to encode into the session identifier. Since session identifier
// length limits are unspecified, this should be kept small. Key names beginning with an underscore
// are reserved for use by the fetcher.
@property(atomic, strong, nullable) NSDictionary<NSString *, NSString *> *sessionUserInfo;
// The human-readable description to be assigned to the task.
@property(atomic, copy, nullable) NSString *taskDescription;
// The priority assigned to the task, if any. Use NSURLSessionTaskPriorityLow,
// NSURLSessionTaskPriorityDefault, or NSURLSessionTaskPriorityHigh.
@property(atomic, assign) float taskPriority;
// An optional provider to calculate the User-Agent string on demand. If non-nil and
// an HTTP header field for User-Agent is not set, this is queried before sending out
// the network request for the User-Agent string.
@property(atomic, strong, nullable) id<GTMUserAgentProvider> userAgentProvider;
// The fetcher encodes information used to resume a session in the session identifier.
// This method, intended for internal use returns the encoded information. The sessionUserInfo
// dictionary is stored as identifier metadata.
- (nullable NSDictionary<NSString *, NSString *> *)sessionIdentifierMetadata;
#if TARGET_OS_IPHONE && !TARGET_OS_WATCH
// The app should pass to this method the completion handler passed in the app delegate method
// application:handleEventsForBackgroundURLSession:completionHandler:
+ (void)application:(UIApplication *)application
handleEventsForBackgroundURLSession:(NSString *)identifier
completionHandler:(GTMSessionFetcherSystemCompletionHandler)completionHandler;
#endif
// Indicate that a newly created session should be a background session.
// A new session identifier will be created by the fetcher.
//
// Warning: The only thing background sessions are for is rare download
// of huge, batched files of data. And even just for those, there's a lot
// of pain and hackery needed to get transfers to actually happen reliably
// with background sessions.
//
// Don't try to upload or download in many background sessions, since the system
// will impose an exponentially increasing time penalty to prevent the app from
// getting too much background execution time.
//
// References:
//
// "Moving to Fewer, Larger Transfers"
// https://forums.developer.apple.com/thread/14853
//
// "NSURLSessions Resume Rate Limiter"
// https://forums.developer.apple.com/thread/14854
//
// "Background Session Task state persistence"
// https://forums.developer.apple.com/thread/11554
//
@property(atomic, assign) BOOL useBackgroundSession;
// Indicates if the fetcher was started using a background session.
@property(atomic, readonly, getter=isUsingBackgroundSession) BOOL usingBackgroundSession;
// Indicates if uploads should use an upload task. This is always set for file or stream-provider
// bodies, but may be set explicitly for NSData bodies.
@property(atomic, assign) BOOL useUploadTask;
// Indicates that the fetcher is using a session that may be shared with other fetchers.
@property(atomic, readonly) BOOL canShareSession;
// By default, the fetcher allows only secure (https) schemes unless this
// property is set, or the GTM_ALLOW_INSECURE_REQUESTS build flag is set.
//
// For example, during debugging when fetching from a development server that lacks SSL support,
// this may be set to @[ @"http" ], or when the fetcher is used to retrieve local files,
// this may be set to @[ @"file" ].
//
// This should be left as nil for release builds to avoid creating the opportunity for
// leaking private user behavior and data. If a server is providing insecure URLs
// for fetching by the client app, report the problem as server security & privacy bug.
//
// For builds with the iOS 9/OS X 10.11 and later SDKs, this property is required only when
// the app specifies NSAppTransportSecurity/NSAllowsArbitraryLoads in the main bundle's Info.plist.
@property(atomic, copy, nullable) NSArray<NSString *> *allowedInsecureSchemes;
// By default, the fetcher prohibits localhost requests unless this property is set,
// or the GTM_ALLOW_INSECURE_REQUESTS build flag is set.
//
// For localhost requests, the URL scheme is not checked when this property is set.
//
// For builds with the iOS 9/OS X 10.11 and later SDKs, this property is required only when
// the app specifies NSAppTransportSecurity/NSAllowsArbitraryLoads in the main bundle's Info.plist.
@property(atomic, assign) BOOL allowLocalhostRequest;
// By default, the fetcher requires valid server certs. This may be bypassed
// temporarily for development against a test server with an invalid cert.
@property(atomic, assign) BOOL allowInvalidServerCertificates;
// Cookie storage object for this fetcher. If nil, the fetcher will use a static cookie
// storage instance shared among fetchers. If this fetcher was created by a fetcher service
// object, it will be set to use the service object's cookie storage. See Cookies section above for
// the full discussion.
//
// Because as of Jan 2014 standalone instances of NSHTTPCookieStorage do not actually
// store any cookies (Radar 15735276) we use our own subclass, GTMSessionCookieStorage,
// to hold cookies in memory.
@property(atomic, strong, nullable) NSHTTPCookieStorage *cookieStorage;
// Setting the credential is optional; it is used if the connection receives
// an authentication challenge.
@property(atomic, strong, nullable) NSURLCredential *credential;
// Setting the proxy credential is optional; it is used if the connection
// receives an authentication challenge from a proxy.
@property(atomic, strong, nullable) NSURLCredential *proxyCredential;
// If body data, body file URL, or body stream provider is not set, then a GET request
// method is assumed.
@property(atomic, strong, nullable) NSData *bodyData;
// File to use as the request body. This forces use of an upload task.
@property(atomic, strong, nullable) NSURL *bodyFileURL;
// Length of body to send, expected or actual.
@property(atomic, readonly) int64_t bodyLength;
// The body stream provider may be called repeatedly to provide a body.
// Setting a body stream provider forces use of an upload task.
@property(atomic, copy, nullable) GTMSessionFetcherBodyStreamProvider bodyStreamProvider;
#pragma clang diagnostic push
// For now retain the existing API surface of accepting a GTMFetcherAuthorizationProtocol
// for the authorizer, but the intent is that this will change to take the new
// GTMSessionFetcherAuthorizer protocol instead in a future major version update.
#pragma clang diagnostic ignored "-Wdeprecated"
// Object to add authorization to the request, if needed.
//
// This may not be changed once beginFetch has been invoked.
@property(atomic, strong, nullable) id<GTMFetcherAuthorizationProtocol> authorizer;
#pragma clang diagnostic pop
// The service object that created and monitors this fetcher, if any.
@property(atomic, strong) GTMSessionFetcherService *service;
// The host, if any, used to classify this fetcher in the fetcher service.
@property(atomic, copy, nullable) NSString *serviceHost;
// The priority, if any, used for starting fetchers in the fetcher service.
//
// Lower values are higher priority; the default is 0, and values may
// be negative or positive. This priority affects only the start order of
// fetchers that are being delayed by a fetcher service when the running fetchers
// exceeds the service's maxRunningFetchersPerHost. A priority of NSIntegerMin will
// exempt this fetcher from delay.
@property(atomic, assign) NSInteger servicePriority;
// The delegate's optional didReceiveResponse block may be used to inspect or alter
// the session task response.
//
// This is called on the callback queue.
@property(atomic, copy, nullable) GTMSessionFetcherDidReceiveResponseBlock didReceiveResponseBlock;
// The delegate's optional challenge block may be used to inspect or alter
// the session task challenge.
//
// If this block is not set, the fetcher's default behavior for the NSURLSessionTask
// didReceiveChallenge: delegate method is to use the fetcher's respondToChallenge: method
// which relies on the fetcher's credential and proxyCredential properties.
//
// Warning: This may be called repeatedly if the challenge fails. Check
// challenge.previousFailureCount to identify repeated invocations.
//
// This is called on the callback queue.
@property(atomic, copy, nullable) GTMSessionFetcherChallengeBlock challengeBlock;
// The delegate's optional willRedirect block may be used to inspect or alter
// the redirection.
//
// This is called on the callback queue.
@property(atomic, copy, nullable) GTMSessionFetcherWillRedirectBlock willRedirectBlock;
// The optional send progress block reports body bytes uploaded.
//
// This is called on the callback queue.
@property(atomic, copy, nullable) GTMSessionFetcherSendProgressBlock sendProgressBlock;
// The optional accumulate block may be set by clients wishing to accumulate data
// themselves rather than let the fetcher append each buffer to an NSData.
//
// When this is called with nil data (such as on redirect) the client
// should empty its accumulation buffer.
//
// This is called on the callback queue.
@property(atomic, copy, nullable) GTMSessionFetcherAccumulateDataBlock accumulateDataBlock;
// The optional received progress block may be used to monitor data
// received from a data task.
//
// This is called on the callback queue.
@property(atomic, copy, nullable) GTMSessionFetcherReceivedProgressBlock receivedProgressBlock;
// The delegate's optional downloadProgress block may be used to monitor download
// progress in writing to disk.
//
// This is called on the callback queue.
@property(atomic, copy, nullable) GTMSessionFetcherDownloadProgressBlock downloadProgressBlock;
// The delegate's optional willCacheURLResponse block may be used to alter the cached
// NSURLResponse. The user may prevent caching by passing nil to the block's response.
//
// This is called on the callback queue.
@property(atomic, copy, nullable)
GTMSessionFetcherWillCacheURLResponseBlock willCacheURLResponseBlock;
// Enable retrying; see comments at the top of this file. Setting
// retryEnabled=YES resets the min and max retry intervals.
@property(atomic, assign, getter=isRetryEnabled) BOOL retryEnabled;
// Retry block is optional for retries.
//
// If present, this block should call the response block with YES to cause a retry or NO to end the
// fetch.
// See comments at the top of this file.
@property(atomic, copy, nullable) GTMSessionFetcherRetryBlock retryBlock;
// The optional block for collecting the metrics of the present session.
//
// This is called on the callback queue.
@property(atomic, copy, nullable)
GTMSessionFetcherMetricsCollectionBlock metricsCollectionBlock API_AVAILABLE(
ios(10.0), macosx(10.12), tvos(10.0), watchos(6.0));
// Retry intervals must be strictly less than maxRetryInterval, else
// they will be limited to maxRetryInterval and no further retries will
// be attempted. Setting maxRetryInterval to 0.0 will reset it to the
// default value, 60 seconds for downloads and 600 seconds for uploads.
@property(atomic, assign) NSTimeInterval maxRetryInterval;
// Starting retry interval. Setting minRetryInterval to 0.0 will reset it
// to a random value between 1.0 and 2.0 seconds. Clients should normally not
// set this except for unit testing.
@property(atomic, assign) NSTimeInterval minRetryInterval;
// Multiplier used to increase the interval between retries, typically 2.0.
// Clients should not need to set this.
@property(atomic, assign) double retryFactor;
// Number of retries attempted.
@property(atomic, readonly) NSUInteger retryCount;
// Interval delay to precede next retry.
@property(atomic, readonly) NSTimeInterval nextRetryInterval;
#if GTM_BACKGROUND_TASK_FETCHING
// Skip use of a UIBackgroundTask, thus requiring fetches to complete when the app is in the
// foreground.
//
// Targets should define GTM_BACKGROUND_TASK_FETCHING to 0 to avoid use of a UIBackgroundTask
// on iOS to allow fetches to complete in the background. This property is available when
// it's not practical to set the preprocessor define.
@property(atomic, assign) BOOL skipBackgroundTask;
#endif // GTM_BACKGROUND_TASK_FETCHING
// Begin fetching the request
//
// The delegate may optionally implement the callback or pass nil for the selector or handler.
//
// The delegate and all callback blocks are retained between the beginFetch call until after the
// finish callback, or until the fetch is stopped.
//
// An error is passed to the callback for server statuses 300 or
// higher, with the status stored as the error object's code.
//
// finishedSEL has a signature like:
// - (void)fetcher:(GTMSessionFetcher *)fetcher
// finishedWithData:(NSData *)data
// error:(NSError *)error;
//
// If the application has specified a destinationFileURL or an accumulateDataBlock
// for the fetcher, the data parameter passed to the callback will be nil.
- (void)beginFetchWithDelegate:(nullable id)delegate didFinishSelector:(nullable SEL)finishedSEL;
- (void)beginFetchWithCompletionHandler:(nullable GTMSessionFetcherCompletionHandler)handler;
// Returns YES if this fetcher is in the process of fetching a URL.
@property(atomic, readonly, getter=isFetching) BOOL fetching;
// Cancel the fetch of the request that's currently in progress. The completion handler
// will be called with `GTMSessionFetcherErrorUserCancelled` if the property
// `stopFetchingTriggersCompletionHandler` is `YES`.
- (void)stopFetching;
// Call callbacks with `GTMSessionFetcherErrorUserCancelled` after a `stopFetching`.
// It cannot be changed once the fetcher starts. This should be set to `YES` from
// Swift clients before `beginFetch` with `async/await` since the Swift runtime
// requires the completion handler to be called.
@property(atomic, assign) BOOL stopFetchingTriggersCompletionHandler;
// A block to be called when the fetch completes.
@property(atomic, copy, nullable) GTMSessionFetcherCompletionHandler completionHandler;
// A block to be called if download resume data becomes available.
@property(atomic, strong, nullable) void (^resumeDataBlock)(NSData *);
// Return the status code from the server response.
@property(atomic, readonly) NSInteger statusCode;
// Return the http headers from the response.
@property(atomic, strong, readonly, nullable) NSDictionary<NSString *, NSString *> *responseHeaders;
// The response, once it's been received.
@property(atomic, strong, readonly, nullable) NSURLResponse *response;
// Bytes downloaded so far.
@property(atomic, readonly) int64_t downloadedLength;
// Buffer of currently-downloaded data, if available.
@property(atomic, readonly, strong, nullable) NSData *downloadedData;
// Local path to which the downloaded file will be moved.
//
// If a file already exists at the path, it will be overwritten.
// Will create the enclosing folders if they are not present.
@property(atomic, strong, nullable) NSURL *destinationFileURL;
// The time this fetcher originally began fetching. This is useful as a time
// barrier for ignoring irrelevant fetch notifications or callbacks.
@property(atomic, strong, readonly, nullable) NSDate *initialBeginFetchDate;
// userData is retained solely for the convenience of the client.
@property(atomic, strong, nullable) id userData;
// Stored property values are retained solely for the convenience of the client.
@property(atomic, copy, nullable) NSDictionary<NSString *, id> *properties;
- (void)setProperty:(nullable id)obj
forKey:(NSString *)key; // Pass nil for obj to remove the property.
- (nullable id)propertyForKey:(NSString *)key;
- (void)addPropertiesFromDictionary:(NSDictionary<NSString *, id> *)dict;
// Comments are useful for logging, so are strongly recommended for each fetcher.
@property(atomic, copy, nullable) NSString *comment;
- (void)setCommentWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1, 2);
// Log of request and response, if logging is enabled
@property(atomic, copy, nullable) NSString *log;
// Callbacks are run on this queue. If none is supplied, the main queue is used.
//
// CAUTION: This block MUST be a serial queue. Setting a concurrent queue can result in callbacks
// being dispatched concurrently, leading events to appear out-of-order.
@property(atomic, strong, null_resettable) dispatch_queue_t callbackQueue;
// The queue used internally by the session to invoke its delegate methods in the fetcher.
//
// Application callbacks are always called by the fetcher on the callbackQueue above,
// not on this queue. Apps should generally not change this queue.
//
// The default delegate queue is the main queue.
//
// This value is ignored after the session has been created, so this
// property should be set in the fetcher service rather in the fetcher as it applies
// to a shared session.
@property(atomic, strong, null_resettable) NSOperationQueue *sessionDelegateQueue;
// DEPRECATED: Callers should use XCTestExpectation instead.
//
// Spin the run loop or sleep the thread, discarding events, until the fetch has completed.
//
// This is only for use in testing or in tools without a user interface.
//
// Note: Synchronous fetches should never be used by shipping apps; they are
// sufficient reason for rejection from the app store.
//
// Returns NO if timed out.
- (BOOL)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds
__deprecated_msg("Use XCTestExpectation instead");
// Test block is optional for testing.
//
// If present, this block will cause the fetcher to skip starting the session, and instead
// use the test block response values when calling the completion handler and delegate code.
//
// Test code can set this on the fetcher or on the fetcher service. For testing libraries
// that use a fetcher without exposing either the fetcher or the fetcher service, the global
// method setGlobalTestBlock: will set the block for all fetchers that do not have a test
// block set.
//
// The test code can pass nil for all response parameters to indicate that the fetch
// should proceed.
//
// Applications can exclude test block support by setting GTM_DISABLE_FETCHER_TEST_BLOCK.
@property(atomic, copy, nullable) GTMSessionFetcherTestBlock testBlock;
+ (void)setGlobalTestBlock:(nullable GTMSessionFetcherTestBlock)block;
// When using the testBlock, |testBlockAccumulateDataChunkCount| is the desired number of chunks to
// divide the response data into if the client has streaming enabled. The data will be divided up to
// |testBlockAccumulateDataChunkCount| chunks; however, the exact amount may vary depending on the
// size of the response data (e.g. a 1-byte response can only be divided into one chunk).
@property(atomic, readwrite) NSUInteger testBlockAccumulateDataChunkCount;
#if GTM_BACKGROUND_TASK_FETCHING
// For testing or to override UIApplication invocations, apps may specify an alternative
// target for messages to UIApplication.
+ (void)setSubstituteUIApplication:(nullable id<GTMUIApplicationProtocol>)substituteUIApplication;
+ (nullable id<GTMUIApplicationProtocol>)substituteUIApplication;
#endif // GTM_BACKGROUND_TASK_FETCHING
// Exposed for testing.
+ (GTMSessionCookieStorage *)staticCookieStorage;
+ (BOOL)appAllowsInsecureRequests;
#if STRIP_GTM_FETCH_LOGGING
// If logging is stripped, provide a stub for the main method
// for controlling logging.
+ (void)setLoggingEnabled:(BOOL)flag;
+ (BOOL)isLoggingEnabled;
#else
// These methods let an application log specific body text, such as the text description of a binary
// request or response. The application should set the fetcher to defer response body logging until
// the response has been received and the log response body has been set by the app. For example:
//
// fetcher.logRequestBody = [binaryObject stringDescription];
// fetcher.deferResponseBodyLogging = YES;
// [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
// if (error == nil) {
// fetcher.logResponseBody = [[[MyThing alloc] initWithData:data] stringDescription];
// }
// fetcher.deferResponseBodyLogging = NO;
// }];
@property(atomic, copy, nullable) NSString *logRequestBody;
@property(atomic, assign) BOOL deferResponseBodyLogging;
@property(atomic, copy, nullable) NSString *logResponseBody;
// Internal logging support.
@property(atomic, readonly) NSData *loggedStreamData;
@property(atomic, assign) BOOL hasLoggedError;
@property(atomic, strong, nullable) NSURL *redirectedFromURL;
- (void)appendLoggedStreamData:(NSData *)dataToAdd;
- (void)clearLoggedStreamData;
#endif // STRIP_GTM_FETCH_LOGGING
@end
// Until we can just instantiate NSHTTPCookieStorage for local use, we'll
// implement all the public methods ourselves. This stores cookies only in
// memory. Additional methods are provided for testing.
//
// iOS 9/OS X 10.11 added +[NSHTTPCookieStorage sharedCookieStorageForGroupContainerIdentifier:]
// which may also be used to create cookie storage.
@interface GTMSessionCookieStorage : NSHTTPCookieStorage
// Add the array off cookies to the storage, replacing duplicates.
// Also removes expired cookies from the storage.
- (void)setCookies:(nullable NSArray<NSHTTPCookie *> *)cookies;
- (void)removeAllCookies;
@end
// Macros to monitor synchronization blocks in debug builds.
// These report problems using GTMSessionCheckDebug.
//
// GTMSessionMonitorSynchronized Start monitoring a top-level-only
// @sync scope.
// GTMSessionMonitorRecursiveSynchronized Start monitoring a top-level or
// recursive @sync scope.
// GTMSessionCheckSynchronized Verify that the current execution
// is inside a @sync scope.
// GTMSessionCheckNotSynchronized Verify that the current execution
// is not inside a @sync scope.
//
// Example usage:
//
// - (void)myExternalMethod {
// @synchronized(self) {
// GTMSessionMonitorSynchronized(self)
//
// - (void)myInternalMethod {
// GTMSessionCheckSynchronized(self);
//
// - (void)callMyCallbacks {
// GTMSessionCheckNotSynchronized(self);
//
// GTMSessionCheckNotSynchronized is available for verifying the code isn't
// in a deadlockable @sync state when posting notifications and invoking
// callbacks. Don't use GTMSessionCheckNotSynchronized immediately before a
// @sync scope; the normal recursiveness check of GTMSessionMonitorSynchronized
// can catch those.
#ifdef __OBJC__
// If asserts are entirely no-ops, the synchronization monitor is just a bunch
// of counting code that doesn't report exceptional circumstances in any way.
// Only build the synchronization monitor code if NS_BLOCK_ASSERTIONS is not
// defined or asserts are being logged instead.
#if DEBUG && (!defined(NS_BLOCK_ASSERTIONS) || GTMSESSION_ASSERT_AS_LOG)
#define __GTMSessionMonitorSynchronizedVariableInner(varname, counter) varname##counter
#define __GTMSessionMonitorSynchronizedVariable(varname, counter) \
__GTMSessionMonitorSynchronizedVariableInner(varname, counter)
#define GTMSessionMonitorSynchronized(obj) \
NS_VALID_UNTIL_END_OF_SCOPE id __GTMSessionMonitorSynchronizedVariable(__monitor, __COUNTER__) = \
[[GTMSessionSyncMonitorInternal alloc] initWithSynchronizationObject:obj \
allowRecursive:NO \
functionName:__func__]
#define GTMSessionMonitorRecursiveSynchronized(obj) \
NS_VALID_UNTIL_END_OF_SCOPE id __GTMSessionMonitorSynchronizedVariable(__monitor, __COUNTER__) = \
[[GTMSessionSyncMonitorInternal alloc] initWithSynchronizationObject:obj \
allowRecursive:YES \
functionName:__func__]
#define GTMSessionCheckSynchronized(obj) \
{ \
GTMSESSION_ASSERT_DEBUG( \
[GTMSessionSyncMonitorInternal functionsHoldingSynchronizationOnObject:obj], \
@"GTMSessionCheckSynchronized(" #obj ") failed: not sync'd" \
@" on " #obj " in %s. Call stack:\n%@", \
__func__, [NSThread callStackSymbols]); \
}
#define GTMSessionCheckNotSynchronized(obj) \
{ \
GTMSESSION_ASSERT_DEBUG( \
![GTMSessionSyncMonitorInternal functionsHoldingSynchronizationOnObject:obj], \
@"GTMSessionCheckNotSynchronized(" #obj ") failed: was sync'd" \
@" on " #obj " in %s by %@. Call stack:\n%@", \
__func__, [GTMSessionSyncMonitorInternal functionsHoldingSynchronizationOnObject:obj], \
[NSThread callStackSymbols]); \
}
// GTMSessionSyncMonitorInternal is a private class that keeps track of the
// beginning and end of synchronized scopes.
//
// This class should not be used directly, but only via the
// GTMSessionMonitorSynchronized macro.
@interface GTMSessionSyncMonitorInternal : NSObject
- (instancetype)initWithSynchronizationObject:(id)object
allowRecursive:(BOOL)allowRecursive
functionName:(const char *)functionName;
// Return the names of the functions that hold sync on the object, or nil if none.
+ (nullable NSArray *)functionsHoldingSynchronizationOnObject:(id)object;
@end
#else
#define GTMSessionMonitorSynchronized(obj) \
do { \
} while (0)
#define GTMSessionMonitorRecursiveSynchronized(obj) \
do { \
} while (0)
#define GTMSessionCheckSynchronized(obj) \
do { \
} while (0)
#define GTMSessionCheckNotSynchronized(obj) \
do { \
} while (0)
#endif // !DEBUG
#endif // __OBJC__
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,105 @@
/* 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.
*/
#import "GTMSessionFetcher/GTMSessionFetcher.h"
// GTM HTTP Logging
//
// All traffic using GTMSessionFetcher can be easily logged. Call
//
// [GTMSessionFetcher setLoggingEnabled:YES];
//
// to begin generating log files.
//
// Unless explicitly set by the application using +setLoggingDirectory:,
// logs are put into a default directory, located at:
// * macOS: ~/Desktop/GTMHTTPDebugLogs
// * iOS simulator: ~/GTMHTTPDebugLogs (in application sandbox)
// * iOS device: ~/Documents/GTMHTTPDebugLogs (in application sandbox)
//
// Tip: use the Finder's "Sort By Date" to find the most recent logs.
//
// Each run of an application gets a separate set of log files. An html
// file is generated to simplify browsing the run's http transactions.
// The html file includes javascript links for inline viewing of uploaded
// and downloaded data.
//
// A symlink is created in the logs folder to simplify finding the html file
// for the latest run of the application; the symlink is called
//
// AppName_http_log_newest.html
//
// Each fetcher may be given a comment to be inserted as a label in the logs,
// such as
// [fetcher setCommentWithFormat:@"retrieve item %@", itemName];
//
// Projects may define STRIP_GTM_FETCH_LOGGING to remove logging code.
#if !STRIP_GTM_FETCH_LOGGING
@interface GTMSessionFetcher (GTMSessionFetcherLogging)
// Note: on macOS the default logs directory is ~/Desktop/GTMHTTPDebugLogs; on
// iOS simulators it will be the ~/GTMHTTPDebugLogs (in the app sandbox); on
// iOS devices it will be in ~/Documents/GTMHTTPDebugLogs (in the app sandbox).
// These directories will be created as needed, and are excluded from backups
// to iCloud and iTunes.
//
// If a custom directory is set, the directory should already exist. It is
// the application's responsibility to exclude any custom directory from
// backups, if desired.
+ (void)setLoggingDirectory:(NSString *)path;
+ (NSString *)loggingDirectory;
// client apps can turn logging on and off
+ (void)setLoggingEnabled:(BOOL)isLoggingEnabled;
+ (BOOL)isLoggingEnabled;
// client apps can turn off logging to a file if they want to only check
// the fetcher's log property
+ (void)setLoggingToFileEnabled:(BOOL)isLoggingToFileEnabled;
+ (BOOL)isLoggingToFileEnabled;
// client apps can optionally specify process name and date string used in
// log file names
+ (void)setLoggingProcessName:(NSString *)processName;
+ (NSString *)loggingProcessName;
+ (void)setLoggingDateStamp:(NSString *)dateStamp;
+ (NSString *)loggingDateStamp;
// client apps can specify the directory for the log for this specific run:
//
// [GTMSessionFetcher setLogDirectoryForCurrentRun:logDirectoryPath];
//
// Setting this overrides the logging directory, process name, and date stamp when writing
// the log file.
+ (void)setLogDirectoryForCurrentRun:(NSString *)logDirectoryForCurrentRun;
+ (NSString *)logDirectoryForCurrentRun;
// internal; called by fetcher
- (void)logFetchWithError:(NSError *)error;
- (NSInputStream *)loggedInputStreamForInputStream:(NSInputStream *)inputStream;
- (GTMSessionFetcherBodyStreamProvider)loggedStreamProviderForStreamProvider:
(GTMSessionFetcherBodyStreamProvider)streamProvider;
// internal; accessors useful for viewing logs
+ (NSString *)processNameLogPrefix;
+ (NSString *)symlinkNameSuffix;
+ (NSString *)htmlFileName;
@end
#endif // !STRIP_GTM_FETCH_LOGGING

View File

@@ -0,0 +1,230 @@
/* 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.
*/
// For best performance and convenient usage, fetchers should be generated by a common
// GTMSessionFetcherService instance, like
//
// _fetcherService = [[GTMSessionFetcherService alloc] init];
// GTMSessionFetcher* myFirstFetcher = [_fetcherService fetcherWithRequest:request1];
// GTMSessionFetcher* mySecondFetcher = [_fetcherService fetcherWithRequest:request2];
#import "GTMSessionFetcher/GTMSessionFetcher.h"
NS_ASSUME_NONNULL_BEGIN
// Notifications.
// This notification indicates a reusable session has become invalid. It is intended mainly for the
// service's unit tests.
//
// The notification object is the fetcher service.
// The invalid session is provided via the userInfo kGTMSessionFetcherServiceSessionKey key.
extern NSString *const kGTMSessionFetcherServiceSessionBecameInvalidNotification;
extern NSString *const kGTMSessionFetcherServiceSessionKey;
@interface GTMSessionFetcherService : NSObject <GTMSessionFetcherServiceProtocol>
// Queues of delayed and running fetchers. Each dictionary contains arrays
// of GTMSessionFetcher *fetchers, keyed by NSString *host
@property(atomic, strong, readonly, nullable)
NSDictionary<NSString *, NSArray *> *delayedFetchersByHost;
@property(atomic, strong, readonly, nullable)
NSDictionary<NSString *, NSArray *> *runningFetchersByHost;
// A max value of 0 means no fetchers should be delayed.
// The default limit is 10 simultaneous fetchers targeting each host.
// This does not apply to fetchers whose useBackgroundSession property is YES. Since services are
// not resurrected on an app relaunch, delayed fetchers would effectively be abandoned.
@property(atomic, assign) NSUInteger maxRunningFetchersPerHost;
// Properties to be applied to each fetcher; see GTMSessionFetcher.h for descriptions
@property(atomic, strong, nullable) NSURLSessionConfiguration *configuration;
@property(atomic, copy, nullable) GTMSessionFetcherConfigurationBlock configurationBlock;
@property(atomic, strong, nullable) NSHTTPCookieStorage *cookieStorage;
@property(atomic, strong, null_resettable) dispatch_queue_t callbackQueue;
@property(atomic, copy, nullable) GTMSessionFetcherChallengeBlock challengeBlock;
@property(atomic, strong, nullable) NSURLCredential *credential;
@property(atomic, strong) NSURLCredential *proxyCredential;
@property(atomic, copy, nullable) NSArray<NSString *> *allowedInsecureSchemes;
@property(atomic, assign) BOOL allowLocalhostRequest;
@property(atomic, assign) BOOL allowInvalidServerCertificates;
@property(atomic, assign, getter=isRetryEnabled) BOOL retryEnabled;
@property(atomic, copy, nullable) GTMSessionFetcherRetryBlock retryBlock;
@property(atomic, assign) NSTimeInterval maxRetryInterval;
@property(atomic, assign) NSTimeInterval minRetryInterval;
@property(atomic, copy, nullable) NSDictionary<NSString *, id> *properties;
@property(atomic, copy, nullable)
GTMSessionFetcherMetricsCollectionBlock metricsCollectionBlock API_AVAILABLE(
ios(10.0), macosx(10.12), tvos(10.0), watchos(6.0));
#if GTM_BACKGROUND_TASK_FETCHING
@property(atomic, assign) BOOL skipBackgroundTask;
#endif
// An optional provider to calculate the User-Agent string on demand. If non-nil and
// an HTTP header field for User-Agent is not set, this is queried before sending out
// the network request for the User-Agent string.
@property(atomic, strong, nullable) id<GTMUserAgentProvider> userAgentProvider;
// A default useragent of GTMFetcherStandardUserAgentString(nil) will be given to each fetcher
// created by this service unless the request already has a user-agent header set.
// This default will be added starting with builds with the SDKs for OS X 10.11 and iOS 9.
//
// To use the configuration's default user agent, set this property to nil.
@property(atomic, copy, nullable) NSString *userAgent;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated"
// The authorizer to attach to the created fetchers. If a specific fetcher should
// not authorize its requests, the fetcher's authorizer property may be set to nil
// before the fetch begins.
@property(atomic, strong, nullable) id<GTMFetcherAuthorizationProtocol> authorizer;
#pragma clang diagnostic pop
@property(atomic, readonly, strong, nullable) NSOperationQueue *delegateQueue;
// Delegate queue used by the session when calling back to the fetcher. The default
// is the main queue. Changing this does not affect the queue used to call back to the
// application; that is specified by the callbackQueue property above.
@property(atomic, strong, null_resettable) NSOperationQueue *sessionDelegateQueue;
// When enabled, indicates the same session should be used by subsequent fetchers.
//
// This is enabled by default.
@property(atomic, assign) BOOL reuseSession;
// Sets the delay until an unused session is invalidated.
// The default interval is 60 seconds.
//
// If the interval is set to 0, then any reused session is not invalidated except by
// explicitly invoking -resetSession. Be aware that setting the interval to 0 thus
// causes the session's delegate to be retained until the session is explicitly reset.
@property(atomic, assign) NSTimeInterval unusedSessionTimeout;
// If shouldReuseSession is enabled, this will force creation of a new session when future
// fetchers begin.
- (void)resetSession;
// Sets the callback queue, specifying that the provided queue is a concurrent queue.
//
// When a concurrent queue is explicitly provided via this setter, then each new fetcher
// instance created by the service will be provided a new serial queue targeting the
// concurrent callback queue; this will ensure callbacks for each instance are executed
// in order, while callbacks from separate fetcher instances are not blocked by each other.
//
// The service behavior when resetting the callback queue after providing a concurrent
// queue is unspecified.
- (void)setConcurrentCallbackQueue:(dispatch_queue_t)queue;
// Create a fetcher
//
// These methods will return a fetcher. If successfully created, the connection
// will hold a strong reference to it for the life of the connection as well.
// So the caller doesn't have to hold onto the fetcher explicitly unless they
// want to be able to monitor or cancel it.
- (GTMSessionFetcher *)fetcherWithRequest:(NSURLRequest *)request;
- (GTMSessionFetcher *)fetcherWithURL:(NSURL *)requestURL;
- (GTMSessionFetcher *)fetcherWithURLString:(NSString *)requestURLString;
// Common method for fetcher creation.
//
// -fetcherWithRequest:fetcherClass: may be overridden to customize creation of
// fetchers. This is the ONLY method in the GTMSessionFetcher library intended to
// be overridden.
- (id)fetcherWithRequest:(NSURLRequest *)request fetcherClass:(Class)fetcherClass;
- (BOOL)isDelayingFetcher:(GTMSessionFetcher *)fetcher;
- (NSUInteger)numberOfFetchers; // running + delayed fetchers
- (NSUInteger)numberOfRunningFetchers;
- (NSUInteger)numberOfDelayedFetchers;
// Return a list of all running or delayed fetchers. This includes fetchers created
// by the service which have been started and have not yet stopped.
//
// Returns an array of fetcher objects, or nil if none.
- (nullable NSArray<GTMSessionFetcher *> *)issuedFetchers;
// Search for running or delayed fetchers with the specified URL.
//
// Returns an array of fetcher objects found, or nil if none found.
- (nullable NSArray<GTMSessionFetcher *> *)issuedFetchersWithRequestURL:(NSURL *)requestURL;
- (void)stopAllFetchers;
// All decorators added to the service.
@property(atomic, readonly, strong, nullable) NSArray<id<GTMFetcherDecoratorProtocol>> *decorators;
// Holds a weak reference to `decorator`. When creating a fetcher via
// `-fetcherWithRequest:fetcherClass:`, each registered `decorator` can inspect and potentially
// change the fetcher's request before it starts. Decorators are invoked in the order in which
// they are passed to this method.
- (void)addDecorator:(id<GTMFetcherDecoratorProtocol>)decorator;
// Removes a `decorator` previously passed to `-removeDecorator:`.
- (void)removeDecorator:(id<GTMFetcherDecoratorProtocol>)decorator;
// The testBlock can inspect its fetcher parameter's request property to
// determine which fetcher is being faked.
@property(atomic, copy, nullable) GTMSessionFetcherTestBlock testBlock;
@end
@interface GTMSessionFetcherService (FetcherCallbacks)
// Checks whether the fetcher should delay starting to avoid overloading the host.
- (BOOL)fetcherShouldBeginFetching:(nonnull GTMSessionFetcher *)fetcher;
// Notifies the service that the fetcher did begin fetching.
- (void)fetcherDidBeginFetching:(nonnull GTMSessionFetcher *)fetcher;
// Notifies the service that the fetcher has stopped fetching.
- (void)fetcherDidStop:(nonnull GTMSessionFetcher *)fetcher;
@end
@interface GTMSessionFetcherService (TestingSupport)
// Convenience methods to create a fetcher service for testing.
//
// Fetchers generated by this mock fetcher service will not perform any
// network operation, but will invoke callbacks and provide the supplied data
// or error to the completion handler.
//
// You can make more customized mocks by setting the test block property of the service
// or fetcher; the test block can inspect the fetcher's request or other properties.
//
// See the description of the testBlock property below.
+ (instancetype)mockFetcherServiceWithFakedData:(nullable NSData *)fakedDataOrNil
fakedError:(nullable NSError *)fakedErrorOrNil;
+ (instancetype)mockFetcherServiceWithFakedData:(nullable NSData *)fakedDataOrNil
fakedResponse:(NSHTTPURLResponse *)fakedResponse
fakedError:(nullable NSError *)fakedErrorOrNil;
// DEPRECATED: Callers should use XCTestExpectation instead.
//
// Spin the run loop and discard events (or, if not on the main thread, just sleep the thread)
// until all running and delayed fetchers have completed.
//
// This is only for use in testing or in tools without a user interface.
//
// Synchronous fetches should never be done by shipping apps; they are
// sufficient reason for rejection from the app store.
//
// Returns NO if timed out.
- (BOOL)waitForCompletionOfAllFetchersWithTimeout:(NSTimeInterval)timeoutInSeconds
__deprecated_msg("Use XCTestExpectation instead");
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,179 @@
/* 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.
*/
// GTMSessionUploadFetcher implements Google's resumable upload protocol.
//
// This subclass of GTMSessionFetcher simulates the series of fetches
// needed for chunked upload as a single fetch operation.
//
// Protocol document: TBD
//
// To the client, the only fetcher that exists is this class; the subsidiary
// fetchers needed for uploading chunks are not visible (though the most recent
// chunk fetcher may be accessed via the -activeFetcher or -chunkFetcher methods, and
// -responseHeaders and -statusCode reflect results from the most recent chunk
// fetcher.)
//
// Chunk fetchers are discarded as soon as they have completed.
//
// The protocol also allows for a cancellation notification request to be sent to the
// server to allow discarding of the currently uploaded data and this will be sent
// automatically upon calling stopFetching if the upload has already started.
//
// Note: Unlike the fetcher superclass, the methods of GTMSessionUploadFetcher should
// only be used from the main thread until further work is done to make this subclass
// thread-safe.
#import "GTMSessionFetcher/GTMSessionFetcher.h"
#import "GTMSessionFetcher/GTMSessionFetcherService.h"
NS_ASSUME_NONNULL_BEGIN
// The value to use for file size parameters when the file size is not yet known.
extern int64_t const kGTMSessionUploadFetcherUnknownFileSize;
// Unless an application knows it needs a smaller chunk size, it should use the standard
// chunk size, which sends the entire file as a single chunk to minimize upload overhead.
// Setting an explicit chunk size that comfortably fits in memory is advisable for large
// uploads.
extern int64_t const kGTMSessionUploadFetcherStandardChunkSize;
// When uploading requires data buffer allocations (such as uploading from an NSData or
// an NSFileHandle) this is the maximum buffer size that will be created by the fetcher.
extern int64_t const kGTMSessionUploadFetcherMaximumDemandBufferSize;
// Notification that the upload location URL was provided by the server.
extern NSString *const kGTMSessionFetcherUploadLocationObtainedNotification;
// Notification that the exponential backoff for upload has started.
extern NSString *const kGTMSessionFetcherUploadInitialBackoffStartedNotification;
// Block to provide data during uploads.
//
// Response data may be allocated with dataWithBytesNoCopy:length:freeWhenDone: for efficiency,
// and released after the response block returns.
//
// If the length of the file being uploaded is unknown or already set, send
// kGTMSessionUploadFetcherUnknownFileSize for |fullUploadLength|. Otherwise, set |fullUploadLength|
// to its proper value.
//
// Pass nil as the data (and optionally an NSError) for a failure.
typedef void (^GTMSessionUploadFetcherDataProviderResponse)(NSData *_Nullable data,
int64_t fullUploadLength,
NSError *_Nullable error);
// Do not call the response with an NSData object with less data than the requested length unless
// you are passing the fullUploadLength to the fetcher for the first time and it is the last chunk
// of data in the file being uploaded.
typedef void (^GTMSessionUploadFetcherDataProvider)(
int64_t offset, int64_t length, GTMSessionUploadFetcherDataProviderResponse response);
// Block to be notified about the final status of the cancellation request started in stopFetching.
//
// |fetcher| will be the cancel request that was sent to the server, or nil if stopFetching is not
// going to send a cancel request. If |fetcher| is provided, the other parameters correspond to the
// completion handler of the cancellation request fetcher.
typedef void (^GTMSessionUploadFetcherCancellationHandler)(GTMSessionFetcher *_Nullable fetcher,
NSData *_Nullable data,
NSError *_Nullable error);
@interface GTMSessionUploadFetcher : GTMSessionFetcher
// Create an upload fetcher specifying either the request or the resume location URL,
// then set an upload data source using one of these:
//
// setUploadFileURL:
// setUploadDataLength:provider:
// setUploadFileHandle:
// setUploadData:
+ (instancetype)uploadFetcherWithRequest:(NSURLRequest *)request
uploadMIMEType:(NSString *)uploadMIMEType
chunkSize:(int64_t)chunkSize
fetcherService:(nullable GTMSessionFetcherService *)fetcherServiceOrNil;
// Allows cellular access.
+ (instancetype)uploadFetcherWithLocation:(nullable NSURL *)uploadLocationURL
uploadMIMEType:(NSString *)uploadMIMEType
chunkSize:(int64_t)chunkSize
fetcherService:(nullable GTMSessionFetcherService *)fetcherServiceOrNil;
+ (instancetype)uploadFetcherWithLocation:(nullable NSURL *)uploadLocationURL
uploadMIMEType:(NSString *)uploadMIMEType
chunkSize:(int64_t)chunkSize
allowsCellularAccess:(BOOL)allowsCellularAccess
fetcherService:(nullable GTMSessionFetcherService *)fetcherServiceOrNil;
// Allows dataProviders for files of unknown length. Pass kGTMSessionUploadFetcherUnknownFileSize as
// |fullLength| if the length is unknown.
- (void)setUploadDataLength:(int64_t)fullLength
provider:(nullable GTMSessionUploadFetcherDataProvider)block;
+ (NSArray *)uploadFetchersForBackgroundSessions;
+ (nullable instancetype)uploadFetcherForSessionIdentifier:(NSString *)sessionIdentifier;
- (void)pauseFetching;
- (void)resumeFetching;
- (BOOL)isPaused;
@property(atomic, strong, nullable) NSURL *uploadLocationURL;
@property(atomic, strong, nullable) NSData *uploadData;
@property(atomic, strong, nullable) NSURL *uploadFileURL;
@property(atomic, strong, nullable) NSFileHandle *uploadFileHandle;
@property(atomic, copy, readonly, nullable) GTMSessionUploadFetcherDataProvider uploadDataProvider;
@property(atomic, copy) NSString *uploadMIMEType;
@property(atomic, readonly, assign) int64_t chunkSize;
@property(atomic, readonly, assign) int64_t currentOffset;
@property(atomic, assign) double uploadRetryFactor;
@property(atomic, assign) NSTimeInterval maxUploadRetryInterval;
@property(atomic, assign) NSTimeInterval minUploadRetryInterval;
// Reflects the original NSURLRequest's @c allowCellularAccess property.
@property(atomic, readonly, assign) BOOL allowsCellularAccess;
// The fetcher for the current data chunk, if any
@property(atomic, strong, nullable) GTMSessionFetcher *chunkFetcher;
// The active fetcher is the current chunk fetcher, or the upload fetcher itself
// if no chunk fetcher has yet been created.
@property(atomic, readonly) GTMSessionFetcher *activeFetcher;
// The last request made by an active fetcher. Useful for testing.
@property(atomic, readonly, nullable) NSURLRequest *lastChunkRequest;
// The status code from the most recently-completed fetch.
@property(atomic, assign) NSInteger statusCode;
// Invoked as part of the stop fetching process. Invoked immediately if there is no upload in
// progress, otherwise invoked with the results of the attempt to notify the server that the
// upload will not continue.
//
// Unlike other callbacks, since this is related specifically to the stopFetching flow it is not
// cleared by stopFetching. It will instead clear itself after it is invoked or if the completion
// has occured before stopFetching is called.
@property(atomic, copy, nullable) GTMSessionUploadFetcherCancellationHandler cancellationHandler;
// Exposed for testing only.
@property(atomic, readonly, nullable) dispatch_queue_t delegateCallbackQueue;
@property(atomic, readonly, nullable) GTMSessionFetcherCompletionHandler delegateCompletionHandler;
@end
@interface GTMSessionFetcher (GTMSessionUploadFetcherMethods)
@property(readonly, nullable) GTMSessionUploadFetcher *parentUploadFetcher;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
</array>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
</dict>
</plist>