/iOS/Pods/GTMSessionFetcher/Source/GTMSessionFetcher.m

https://bitbucket.org/bakke458/dvhndemoapp · Objective C · 4453 lines · 3331 code · 633 blank · 489 comment · 633 complexity · a6bdb28901cb9439b64982b72b991389 MD5 · raw file

Large files are truncated click here to view the full file

  1. /* Copyright 2014 Google Inc. All rights reserved.
  2. *
  3. * Licensed under the Apache License, Version 2.0 (the "License");
  4. * you may not use this file except in compliance with the License.
  5. * You may obtain a copy of the License at
  6. *
  7. * http://www.apache.org/licenses/LICENSE-2.0
  8. *
  9. * Unless required by applicable law or agreed to in writing, software
  10. * distributed under the License is distributed on an "AS IS" BASIS,
  11. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. * See the License for the specific language governing permissions and
  13. * limitations under the License.
  14. */
  15. #if !defined(__has_feature) || !__has_feature(objc_arc)
  16. #error "This file requires ARC support."
  17. #endif
  18. #import "GTMSessionFetcher.h"
  19. #import <sys/utsname.h>
  20. GTM_ASSUME_NONNULL_BEGIN
  21. NSString *const kGTMSessionFetcherStartedNotification = @"kGTMSessionFetcherStartedNotification";
  22. NSString *const kGTMSessionFetcherStoppedNotification = @"kGTMSessionFetcherStoppedNotification";
  23. NSString *const kGTMSessionFetcherRetryDelayStartedNotification = @"kGTMSessionFetcherRetryDelayStartedNotification";
  24. NSString *const kGTMSessionFetcherRetryDelayStoppedNotification = @"kGTMSessionFetcherRetryDelayStoppedNotification";
  25. NSString *const kGTMSessionFetcherCompletionInvokedNotification = @"kGTMSessionFetcherCompletionInvokedNotification";
  26. NSString *const kGTMSessionFetcherCompletionDataKey = @"data";
  27. NSString *const kGTMSessionFetcherCompletionErrorKey = @"error";
  28. NSString *const kGTMSessionFetcherErrorDomain = @"com.google.GTMSessionFetcher";
  29. NSString *const kGTMSessionFetcherStatusDomain = @"com.google.HTTPStatus";
  30. NSString *const kGTMSessionFetcherStatusDataKey = @"data"; // data returned with a kGTMSessionFetcherStatusDomain error
  31. NSString *const kGTMSessionFetcherNumberOfRetriesDoneKey = @"kGTMSessionFetcherNumberOfRetriesDoneKey";
  32. NSString *const kGTMSessionFetcherElapsedIntervalWithRetriesKey = @"kGTMSessionFetcherElapsedIntervalWithRetriesKey";
  33. static NSString *const kGTMSessionIdentifierPrefix = @"com.google.GTMSessionFetcher";
  34. static NSString *const kGTMSessionIdentifierDestinationFileURLMetadataKey = @"_destURL";
  35. static NSString *const kGTMSessionIdentifierBodyFileURLMetadataKey = @"_bodyURL";
  36. // The default max retry interview is 10 minutes for uploads (POST/PUT/PATCH),
  37. // 1 minute for downloads.
  38. static const NSTimeInterval kUnsetMaxRetryInterval = -1.0;
  39. static const NSTimeInterval kDefaultMaxDownloadRetryInterval = 60.0;
  40. static const NSTimeInterval kDefaultMaxUploadRetryInterval = 60.0 * 10.;
  41. #ifdef GTMSESSION_PERSISTED_DESTINATION_KEY
  42. // Projects using unique class names should also define a unique persisted destination key.
  43. static NSString * const kGTMSessionFetcherPersistedDestinationKey =
  44. GTMSESSION_PERSISTED_DESTINATION_KEY;
  45. #else
  46. static NSString * const kGTMSessionFetcherPersistedDestinationKey =
  47. @"com.google.GTMSessionFetcher.downloads";
  48. #endif
  49. GTM_ASSUME_NONNULL_END
  50. //
  51. // GTMSessionFetcher
  52. //
  53. #if 0
  54. #define GTM_LOG_BACKGROUND_SESSION(...) GTMSESSION_LOG_DEBUG(__VA_ARGS__)
  55. #else
  56. #define GTM_LOG_BACKGROUND_SESSION(...)
  57. #endif
  58. #ifndef GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY
  59. #if (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_11) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_11) \
  60. || (TARGET_OS_IPHONE && defined(__IPHONE_9_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_9_0)
  61. #define GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY 1
  62. #endif
  63. #endif
  64. @interface GTMSessionFetcher ()
  65. @property(atomic, strong, readwrite, GTM_NULLABLE) NSData *downloadedData;
  66. @property(atomic, strong, readwrite, GTM_NULLABLE) NSData *downloadResumeData;
  67. #if GTM_BACKGROUND_TASK_FETCHING
  68. @property(assign, atomic) UIBackgroundTaskIdentifier backgroundTaskIdentifier;
  69. #endif
  70. @property(atomic, readwrite, getter=isUsingBackgroundSession) BOOL usingBackgroundSession;
  71. @end
  72. #if !GTMSESSION_BUILD_COMBINED_SOURCES
  73. @interface GTMSessionFetcher (GTMSessionFetcherLoggingInternal)
  74. - (void)logFetchWithError:(NSError *)error;
  75. - (void)logNowWithError:(GTM_NULLABLE NSError *)error;
  76. - (NSInputStream *)loggedInputStreamForInputStream:(NSInputStream *)inputStream;
  77. - (GTMSessionFetcherBodyStreamProvider)loggedStreamProviderForStreamProvider:
  78. (GTMSessionFetcherBodyStreamProvider)streamProvider;
  79. @end
  80. #endif // !GTMSESSION_BUILD_COMBINED_SOURCES
  81. GTM_ASSUME_NONNULL_BEGIN
  82. static NSTimeInterval InitialMinRetryInterval(void) {
  83. return 1.0 + ((double)(arc4random_uniform(0x0FFFF)) / (double) 0x0FFFF);
  84. }
  85. static BOOL IsLocalhost(NSString * GTM_NULLABLE_TYPE host) {
  86. // We check if there's host, and then make the comparisons.
  87. if (host == nil) return NO;
  88. return ([host caseInsensitiveCompare:@"localhost"] == NSOrderedSame
  89. || [host isEqual:@"::1"]
  90. || [host isEqual:@"127.0.0.1"]);
  91. }
  92. static GTMSessionFetcherTestBlock GTM_NULLABLE_TYPE gGlobalTestBlock;
  93. @implementation GTMSessionFetcher {
  94. NSMutableURLRequest *_request; // after beginFetch, changed only in delegate callbacks
  95. BOOL _useUploadTask; // immutable after beginFetch
  96. NSURL *_bodyFileURL; // immutable after beginFetch
  97. GTMSessionFetcherBodyStreamProvider _bodyStreamProvider; // immutable after beginFetch
  98. NSURLSession *_session;
  99. BOOL _shouldInvalidateSession; // immutable after beginFetch
  100. NSURLSession *_sessionNeedingInvalidation;
  101. NSURLSessionConfiguration *_configuration;
  102. NSURLSessionTask *_sessionTask;
  103. NSString *_taskDescription;
  104. float _taskPriority;
  105. NSURLResponse *_response;
  106. NSString *_sessionIdentifier;
  107. BOOL _wasCreatedFromBackgroundSession;
  108. BOOL _didCreateSessionIdentifier;
  109. NSString *_sessionIdentifierUUID;
  110. BOOL _userRequestedBackgroundSession;
  111. BOOL _usingBackgroundSession;
  112. NSMutableData * GTM_NULLABLE_TYPE _downloadedData;
  113. NSError *_downloadFinishedError;
  114. NSData *_downloadResumeData; // immutable after construction
  115. NSURL *_destinationFileURL;
  116. int64_t _downloadedLength;
  117. NSURLCredential *_credential; // username & password
  118. NSURLCredential *_proxyCredential; // credential supplied to proxy servers
  119. BOOL _isStopNotificationNeeded; // set when start notification has been sent
  120. BOOL _isUsingTestBlock; // set when a test block was provided (remains set when the block is released)
  121. id _userData; // retained, if set by caller
  122. NSMutableDictionary *_properties; // more data retained for caller
  123. dispatch_queue_t _callbackQueue;
  124. dispatch_group_t _callbackGroup; // read-only after creation
  125. NSOperationQueue *_delegateQueue; // immutable after beginFetch
  126. id<GTMFetcherAuthorizationProtocol> _authorizer; // immutable after beginFetch
  127. // The service object that created and monitors this fetcher, if any.
  128. id<GTMSessionFetcherServiceProtocol> _service; // immutable; set by the fetcher service upon creation
  129. NSString *_serviceHost;
  130. NSInteger _servicePriority; // immutable after beginFetch
  131. BOOL _hasStoppedFetching; // counterpart to _initialBeginFetchDate
  132. BOOL _userStoppedFetching;
  133. BOOL _isRetryEnabled; // user wants auto-retry
  134. NSTimer *_retryTimer;
  135. NSUInteger _retryCount;
  136. NSTimeInterval _maxRetryInterval; // default 60 (download) or 600 (upload) seconds
  137. NSTimeInterval _minRetryInterval; // random between 1 and 2 seconds
  138. NSTimeInterval _retryFactor; // default interval multiplier is 2
  139. NSTimeInterval _lastRetryInterval;
  140. NSDate *_initialBeginFetchDate; // date that beginFetch was first invoked; immutable after initial beginFetch
  141. NSDate *_initialRequestDate; // date of first request to the target server (ignoring auth)
  142. BOOL _hasAttemptedAuthRefresh; // accessed only in shouldRetryNowForStatus:
  143. NSString *_comment; // comment for log
  144. NSString *_log;
  145. #if !STRIP_GTM_FETCH_LOGGING
  146. NSMutableData *_loggedStreamData;
  147. NSURL *_redirectedFromURL;
  148. NSString *_logRequestBody;
  149. NSString *_logResponseBody;
  150. BOOL _hasLoggedError;
  151. BOOL _deferResponseBodyLogging;
  152. #endif
  153. }
  154. #if !GTMSESSION_UNIT_TESTING
  155. + (void)load {
  156. [self fetchersForBackgroundSessions];
  157. }
  158. #endif
  159. + (instancetype)fetcherWithRequest:(GTM_NULLABLE NSURLRequest *)request {
  160. return [[self alloc] initWithRequest:request configuration:nil];
  161. }
  162. + (instancetype)fetcherWithURL:(NSURL *)requestURL {
  163. return [self fetcherWithRequest:[NSURLRequest requestWithURL:requestURL]];
  164. }
  165. + (instancetype)fetcherWithURLString:(NSString *)requestURLString {
  166. return [self fetcherWithURL:(NSURL *)[NSURL URLWithString:requestURLString]];
  167. }
  168. + (instancetype)fetcherWithDownloadResumeData:(NSData *)resumeData {
  169. GTMSessionFetcher *fetcher = [self fetcherWithRequest:nil];
  170. fetcher.comment = @"Resuming download";
  171. fetcher.downloadResumeData = resumeData;
  172. return fetcher;
  173. }
  174. + (GTM_NULLABLE instancetype)fetcherWithSessionIdentifier:(NSString *)sessionIdentifier {
  175. GTMSESSION_ASSERT_DEBUG(sessionIdentifier != nil, @"Invalid session identifier");
  176. NSMapTable *sessionIdentifierToFetcherMap = [self sessionIdentifierToFetcherMap];
  177. GTMSessionFetcher *fetcher = [sessionIdentifierToFetcherMap objectForKey:sessionIdentifier];
  178. if (!fetcher && [sessionIdentifier hasPrefix:kGTMSessionIdentifierPrefix]) {
  179. fetcher = [self fetcherWithRequest:nil];
  180. [fetcher setSessionIdentifier:sessionIdentifier];
  181. [sessionIdentifierToFetcherMap setObject:fetcher forKey:sessionIdentifier];
  182. fetcher->_wasCreatedFromBackgroundSession = YES;
  183. [fetcher setCommentWithFormat:@"Resuming %@",
  184. fetcher && fetcher->_sessionIdentifierUUID ? fetcher->_sessionIdentifierUUID : @"?"];
  185. }
  186. return fetcher;
  187. }
  188. + (NSMapTable *)sessionIdentifierToFetcherMap {
  189. // TODO: What if a service is involved in creating the fetcher? Currently, when re-creating
  190. // fetchers, if a service was involved, it is not re-created. Should the service maintain a map?
  191. static NSMapTable *gSessionIdentifierToFetcherMap = nil;
  192. static dispatch_once_t onceToken;
  193. dispatch_once(&onceToken, ^{
  194. gSessionIdentifierToFetcherMap = [NSMapTable strongToWeakObjectsMapTable];
  195. });
  196. return gSessionIdentifierToFetcherMap;
  197. }
  198. #if !GTM_ALLOW_INSECURE_REQUESTS
  199. + (BOOL)appAllowsInsecureRequests {
  200. // If the main bundle Info.plist key NSAppTransportSecurity is present, and it specifies
  201. // NSAllowsArbitraryLoads, then we need to explicitly enforce secure schemes.
  202. #if GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY
  203. static BOOL allowsInsecureRequests;
  204. static dispatch_once_t onceToken;
  205. dispatch_once(&onceToken, ^{
  206. NSBundle *mainBundle = [NSBundle mainBundle];
  207. NSDictionary *appTransportSecurity =
  208. [mainBundle objectForInfoDictionaryKey:@"NSAppTransportSecurity"];
  209. allowsInsecureRequests =
  210. [[appTransportSecurity objectForKey:@"NSAllowsArbitraryLoads"] boolValue];
  211. });
  212. return allowsInsecureRequests;
  213. #else
  214. // For builds targeting iOS 8 or 10.10 and earlier, we want to require fetcher
  215. // security checks.
  216. return YES;
  217. #endif // GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY
  218. }
  219. #else // GTM_ALLOW_INSECURE_REQUESTS
  220. + (BOOL)appAllowsInsecureRequests {
  221. return YES;
  222. }
  223. #endif // !GTM_ALLOW_INSECURE_REQUESTS
  224. - (instancetype)init {
  225. return [self initWithRequest:nil configuration:nil];
  226. }
  227. - (instancetype)initWithRequest:(NSURLRequest *)request {
  228. return [self initWithRequest:request configuration:nil];
  229. }
  230. - (instancetype)initWithRequest:(GTM_NULLABLE NSURLRequest *)request
  231. configuration:(GTM_NULLABLE NSURLSessionConfiguration *)configuration {
  232. self = [super init];
  233. if (self) {
  234. if (![NSURLSession class]) {
  235. Class oldFetcherClass = NSClassFromString(@"GTMHTTPFetcher");
  236. if (oldFetcherClass && request) {
  237. self = [[oldFetcherClass alloc] initWithRequest:(NSURLRequest *)request];
  238. } else {
  239. self = nil;
  240. }
  241. return self;
  242. }
  243. #if GTM_BACKGROUND_TASK_FETCHING
  244. _backgroundTaskIdentifier = UIBackgroundTaskInvalid;
  245. #endif
  246. _request = [request mutableCopy];
  247. _configuration = configuration;
  248. NSData *bodyData = request.HTTPBody;
  249. if (bodyData) {
  250. _bodyLength = (int64_t)bodyData.length;
  251. } else {
  252. _bodyLength = NSURLSessionTransferSizeUnknown;
  253. }
  254. _callbackQueue = dispatch_get_main_queue();
  255. _callbackGroup = dispatch_group_create();
  256. _delegateQueue = [NSOperationQueue mainQueue];
  257. _minRetryInterval = InitialMinRetryInterval();
  258. _maxRetryInterval = kUnsetMaxRetryInterval;
  259. _taskPriority = -1.0f; // Valid values if set are 0.0...1.0.
  260. #if !STRIP_GTM_FETCH_LOGGING
  261. // Encourage developers to set the comment property or use
  262. // setCommentWithFormat: by providing a default string.
  263. _comment = @"(No fetcher comment set)";
  264. #endif
  265. }
  266. return self;
  267. }
  268. - (id)copyWithZone:(NSZone *)zone {
  269. // disallow use of fetchers in a copy property
  270. [self doesNotRecognizeSelector:_cmd];
  271. return nil;
  272. }
  273. - (NSString *)description {
  274. NSString *requestStr = self.request.URL.description;
  275. if (requestStr.length == 0) {
  276. if (self.downloadResumeData.length > 0) {
  277. requestStr = @"<download resume data>";
  278. } else if (_wasCreatedFromBackgroundSession) {
  279. requestStr = @"<from bg session>";
  280. } else {
  281. requestStr = @"<no request>";
  282. }
  283. }
  284. return [NSString stringWithFormat:@"%@ %p (%@)", [self class], self, requestStr];
  285. }
  286. - (void)dealloc {
  287. GTMSESSION_ASSERT_DEBUG(!_isStopNotificationNeeded,
  288. @"unbalanced fetcher notification for %@", _request.URL);
  289. [self forgetSessionIdentifierForFetcherWithoutSyncCheck];
  290. // Note: if a session task or a retry timer was pending, then this instance
  291. // would be retained by those so it wouldn't be getting dealloc'd,
  292. // hence we don't need to stopFetch here
  293. }
  294. #pragma mark -
  295. // Begin fetching the URL (or begin a retry fetch). The delegate is retained
  296. // for the duration of the fetch connection.
  297. - (void)beginFetchWithCompletionHandler:(GTM_NULLABLE GTMSessionFetcherCompletionHandler)handler {
  298. GTMSessionCheckNotSynchronized(self);
  299. _completionHandler = [handler copy];
  300. // The user may have called setDelegate: earlier if they want to use other
  301. // delegate-style callbacks during the fetch; otherwise, the delegate is nil,
  302. // which is fine.
  303. [self beginFetchMayDelay:YES mayAuthorize:YES];
  304. }
  305. - (GTMSessionFetcherCompletionHandler)completionHandlerWithTarget:(GTM_NULLABLE_TYPE id)target
  306. didFinishSelector:(GTM_NULLABLE_TYPE SEL)finishedSelector {
  307. GTMSessionFetcherAssertValidSelector(target, finishedSelector, @encode(GTMSessionFetcher *),
  308. @encode(NSData *), @encode(NSError *), 0);
  309. GTMSessionFetcherCompletionHandler completionHandler = ^(NSData *data, NSError *error) {
  310. if (target && finishedSelector) {
  311. id selfArg = self; // Placate ARC.
  312. NSMethodSignature *sig = [target methodSignatureForSelector:finishedSelector];
  313. NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
  314. [invocation setSelector:(SEL)finishedSelector];
  315. [invocation setTarget:target];
  316. [invocation setArgument:&selfArg atIndex:2];
  317. [invocation setArgument:&data atIndex:3];
  318. [invocation setArgument:&error atIndex:4];
  319. [invocation invoke];
  320. }
  321. };
  322. return completionHandler;
  323. }
  324. - (void)beginFetchWithDelegate:(GTM_NULLABLE_TYPE id)target
  325. didFinishSelector:(GTM_NULLABLE_TYPE SEL)finishedSelector {
  326. GTMSessionCheckNotSynchronized(self);
  327. GTMSessionFetcherCompletionHandler handler = [self completionHandlerWithTarget:target
  328. didFinishSelector:finishedSelector];
  329. [self beginFetchWithCompletionHandler:handler];
  330. }
  331. - (void)beginFetchMayDelay:(BOOL)mayDelay
  332. mayAuthorize:(BOOL)mayAuthorize {
  333. // This is the internal entry point for re-starting fetches.
  334. GTMSessionCheckNotSynchronized(self);
  335. NSMutableURLRequest *fetchRequest = _request; // The request property is now externally immutable.
  336. NSURL *fetchRequestURL = fetchRequest.URL;
  337. NSString *priorSessionIdentifier = self.sessionIdentifier;
  338. // A utility block for creating error objects when we fail to start the fetch.
  339. NSError *(^beginFailureError)(NSInteger) = ^(NSInteger code){
  340. NSString *urlString = fetchRequestURL.absoluteString;
  341. NSDictionary *userInfo = @{
  342. NSURLErrorFailingURLStringErrorKey : (urlString ? urlString : @"(missing URL)")
  343. };
  344. return [NSError errorWithDomain:kGTMSessionFetcherErrorDomain
  345. code:code
  346. userInfo:userInfo];
  347. };
  348. // Catch delegate queue maxConcurrentOperationCount values other than 1, particularly
  349. // NSOperationQueueDefaultMaxConcurrentOperationCount (-1), to avoid the additional complexity
  350. // of simultaneous or out-of-order delegate callbacks.
  351. GTMSESSION_ASSERT_DEBUG(_delegateQueue.maxConcurrentOperationCount == 1,
  352. @"delegate queue %@ should support one concurrent operation, not %zd",
  353. _delegateQueue.name, _delegateQueue.maxConcurrentOperationCount);
  354. if (!_initialBeginFetchDate) {
  355. // This ivar is set only here on the initial beginFetch so need not be synchronized.
  356. _initialBeginFetchDate = [[NSDate alloc] init];
  357. }
  358. if (self.sessionTask != nil) {
  359. // If cached fetcher returned through fetcherWithSessionIdentifier:, then it's
  360. // already begun, but don't consider this a failure, since the user need not know this.
  361. if (self.sessionIdentifier != nil) {
  362. return;
  363. }
  364. GTMSESSION_ASSERT_DEBUG(NO, @"Fetch object %@ being reused; this should never happen", self);
  365. [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorDownloadFailed)];
  366. return;
  367. }
  368. if (fetchRequestURL == nil && !_downloadResumeData && !priorSessionIdentifier) {
  369. GTMSESSION_ASSERT_DEBUG(NO, @"Beginning a fetch requires a request with a URL");
  370. [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorDownloadFailed)];
  371. return;
  372. }
  373. // We'll respect the user's request for a background session (unless this is
  374. // an upload fetcher, which does its initial request foreground.)
  375. self.usingBackgroundSession = self.useBackgroundSession && [self canFetchWithBackgroundSession];
  376. NSURL *bodyFileURL = self.bodyFileURL;
  377. if (bodyFileURL) {
  378. NSError *fileCheckError;
  379. if (![bodyFileURL checkResourceIsReachableAndReturnError:&fileCheckError]) {
  380. // This assert fires when the file being uploaded no longer exists once
  381. // the fetcher is ready to start the upload.
  382. GTMSESSION_ASSERT_DEBUG_OR_LOG(0, @"Body file is unreachable: %@\n %@",
  383. bodyFileURL.path, fileCheckError);
  384. [self failToBeginFetchWithError:fileCheckError];
  385. return;
  386. }
  387. }
  388. NSString *requestScheme = fetchRequestURL.scheme;
  389. BOOL isDataRequest = [requestScheme isEqual:@"data"];
  390. if (isDataRequest) {
  391. // NSURLSession does not support data URLs in background sessions.
  392. #if DEBUG
  393. if (priorSessionIdentifier || self.sessionIdentifier) {
  394. GTMSESSION_LOG_DEBUG(@"Converting background to foreground session for %@",
  395. fetchRequest);
  396. }
  397. #endif
  398. [self setSessionIdentifierInternal:nil];
  399. self.useBackgroundSession = NO;
  400. }
  401. #if GTM_ALLOW_INSECURE_REQUESTS
  402. BOOL shouldCheckSecurity = NO;
  403. #else
  404. BOOL shouldCheckSecurity = (fetchRequestURL != nil
  405. && !isDataRequest
  406. && [[self class] appAllowsInsecureRequests]);
  407. #endif
  408. if (shouldCheckSecurity) {
  409. // Allow https only for requests, unless overridden by the client.
  410. //
  411. // Non-https requests may too easily be snooped, so we disallow them by default.
  412. //
  413. // file: and data: schemes are usually safe if they are hardcoded in the client or provided
  414. // by a trusted source, but since it's fairly rare to need them, it's safest to make clients
  415. // explicitly whitelist them.
  416. BOOL isSecure =
  417. requestScheme != nil && [requestScheme caseInsensitiveCompare:@"https"] == NSOrderedSame;
  418. if (!isSecure) {
  419. BOOL allowRequest = NO;
  420. NSString *host = fetchRequestURL.host;
  421. // Check schemes first. A file scheme request may be allowed here, or as a localhost request.
  422. for (NSString *allowedScheme in _allowedInsecureSchemes) {
  423. if (requestScheme != nil &&
  424. [requestScheme caseInsensitiveCompare:allowedScheme] == NSOrderedSame) {
  425. allowRequest = YES;
  426. break;
  427. }
  428. }
  429. if (!allowRequest) {
  430. // Check for localhost requests. Security checks only occur for non-https requests, so
  431. // this check won't happen for an https request to localhost.
  432. BOOL isLocalhostRequest = (host.length == 0 && [fetchRequestURL isFileURL]) || IsLocalhost(host);
  433. if (isLocalhostRequest) {
  434. if (self.allowLocalhostRequest) {
  435. allowRequest = YES;
  436. } else {
  437. GTMSESSION_ASSERT_DEBUG(NO, @"Fetch request for localhost but fetcher"
  438. @" allowLocalhostRequest is not set: %@", fetchRequestURL);
  439. }
  440. } else {
  441. GTMSESSION_ASSERT_DEBUG(NO, @"Insecure fetch request has a scheme (%@)"
  442. @" not found in fetcher allowedInsecureSchemes (%@): %@",
  443. requestScheme, _allowedInsecureSchemes ?: @" @[] ", fetchRequestURL);
  444. }
  445. }
  446. if (!allowRequest) {
  447. #if !DEBUG
  448. NSLog(@"Insecure fetch disallowed for %@", fetchRequestURL.description ?: @"nil request URL");
  449. #endif
  450. [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorInsecureRequest)];
  451. return;
  452. }
  453. } // !isSecure
  454. } // (requestURL != nil) && !isDataRequest
  455. if (self.cookieStorage == nil) {
  456. self.cookieStorage = [[self class] staticCookieStorage];
  457. }
  458. BOOL isRecreatingSession = (self.sessionIdentifier != nil) && (fetchRequest == nil);
  459. self.canShareSession = !isRecreatingSession && !self.usingBackgroundSession;
  460. if (!self.session && self.canShareSession) {
  461. self.session = [_service sessionForFetcherCreation];
  462. // If _session is nil, then the service's session creation semaphore will block
  463. // until this fetcher invokes fetcherDidCreateSession: below, so this *must* invoke
  464. // that method, even if the session fails to be created.
  465. }
  466. if (!self.session) {
  467. // Create a session.
  468. if (!_configuration) {
  469. if (priorSessionIdentifier || self.usingBackgroundSession) {
  470. NSString *sessionIdentifier = priorSessionIdentifier;
  471. if (!sessionIdentifier) {
  472. sessionIdentifier = [self createSessionIdentifierWithMetadata:nil];
  473. }
  474. NSMapTable *sessionIdentifierToFetcherMap = [[self class] sessionIdentifierToFetcherMap];
  475. [sessionIdentifierToFetcherMap setObject:self forKey:self.sessionIdentifier];
  476. #if (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_10) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_10) \
  477. || (TARGET_OS_IPHONE && defined(__IPHONE_8_0) && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0)
  478. // iOS 8/10.10 builds require the new backgroundSessionConfiguration method name.
  479. _configuration =
  480. [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessionIdentifier];
  481. #elif (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_10) && MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_10) \
  482. || (TARGET_OS_IPHONE && defined(__IPHONE_8_0) && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0)
  483. // Do a runtime check to avoid a deprecation warning about using
  484. // +backgroundSessionConfiguration: on iOS 8.
  485. if ([NSURLSessionConfiguration respondsToSelector:@selector(backgroundSessionConfigurationWithIdentifier:)]) {
  486. // Running on iOS 8+/OS X 10.10+.
  487. _configuration =
  488. [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessionIdentifier];
  489. } else {
  490. // Running on iOS 7/OS X 10.9.
  491. _configuration =
  492. [NSURLSessionConfiguration backgroundSessionConfiguration:sessionIdentifier];
  493. }
  494. #else
  495. // Building with an SDK earlier than iOS 8/OS X 10.10.
  496. _configuration =
  497. [NSURLSessionConfiguration backgroundSessionConfiguration:sessionIdentifier];
  498. #endif
  499. self.usingBackgroundSession = YES;
  500. self.canShareSession = NO;
  501. } else {
  502. _configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
  503. }
  504. #if !GTM_ALLOW_INSECURE_REQUESTS
  505. _configuration.TLSMinimumSupportedProtocol = kTLSProtocol12;
  506. #endif
  507. } // !_configuration
  508. _configuration.HTTPCookieStorage = self.cookieStorage;
  509. if (_configurationBlock) {
  510. _configurationBlock(self, _configuration);
  511. }
  512. id<NSURLSessionDelegate> delegate = [_service sessionDelegate];
  513. if (!delegate || !self.canShareSession) {
  514. delegate = self;
  515. }
  516. self.session = [NSURLSession sessionWithConfiguration:_configuration
  517. delegate:delegate
  518. delegateQueue:self.sessionDelegateQueue];
  519. GTMSESSION_ASSERT_DEBUG(self.session, @"Couldn't create session");
  520. // Tell the service about the session created by this fetcher. This also signals the
  521. // service's semaphore to allow other fetchers to request this session.
  522. [_service fetcherDidCreateSession:self];
  523. // If this assertion fires, the client probably tried to use a session identifier that was
  524. // already used. The solution is to make the client use a unique identifier (or better yet let
  525. // the session fetcher assign the identifier).
  526. GTMSESSION_ASSERT_DEBUG(self.session.delegate == delegate, @"Couldn't assign delegate.");
  527. if (self.session) {
  528. BOOL isUsingSharedDelegate = (delegate != self);
  529. if (!isUsingSharedDelegate) {
  530. _shouldInvalidateSession = YES;
  531. }
  532. }
  533. }
  534. if (isRecreatingSession) {
  535. _shouldInvalidateSession = YES;
  536. // Let's make sure there are tasks still running or if not that we get a callback from a
  537. // completed one; otherwise, we assume the tasks failed.
  538. // This is the observed behavior perhaps 25% of the time within the Simulator running 7.0.3 on
  539. // exiting the app after starting an upload and relaunching the app if we manage to relaunch
  540. // after the task has completed, but before the system relaunches us in the background.
  541. [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks,
  542. NSArray *downloadTasks) {
  543. if (dataTasks.count == 0 && uploadTasks.count == 0 && downloadTasks.count == 0) {
  544. double const kDelayInSeconds = 1.0; // We should get progress indication or completion soon
  545. dispatch_time_t checkForFeedbackDelay =
  546. dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kDelayInSeconds * NSEC_PER_SEC));
  547. dispatch_after(checkForFeedbackDelay, dispatch_get_main_queue(), ^{
  548. if (!self.sessionTask && !fetchRequest) {
  549. // If our task and/or request haven't been restored, then we assume task feedback lost.
  550. [self removePersistedBackgroundSessionFromDefaults];
  551. NSError *sessionError =
  552. [NSError errorWithDomain:kGTMSessionFetcherErrorDomain
  553. code:GTMSessionFetcherErrorBackgroundFetchFailed
  554. userInfo:nil];
  555. [self failToBeginFetchWithError:sessionError];
  556. }
  557. });
  558. }
  559. }];
  560. return;
  561. }
  562. self.downloadedData = nil;
  563. self.downloadedLength = 0;
  564. if (_servicePriority == NSIntegerMin) {
  565. mayDelay = NO;
  566. }
  567. if (mayDelay && _service) {
  568. BOOL shouldFetchNow = [_service fetcherShouldBeginFetching:self];
  569. if (!shouldFetchNow) {
  570. // The fetch is deferred, but will happen later.
  571. //
  572. // If this session is held by the fetcher service, clear the session now so that we don't
  573. // assume it's still valid after the fetcher is restarted.
  574. if (self.canShareSession) {
  575. self.session = nil;
  576. }
  577. return;
  578. }
  579. }
  580. NSString *effectiveHTTPMethod = [fetchRequest valueForHTTPHeaderField:@"X-HTTP-Method-Override"];
  581. if (effectiveHTTPMethod == nil) {
  582. effectiveHTTPMethod = fetchRequest.HTTPMethod;
  583. }
  584. BOOL isEffectiveHTTPGet = (effectiveHTTPMethod == nil
  585. || [effectiveHTTPMethod isEqual:@"GET"]);
  586. BOOL needsUploadTask = (self.useUploadTask || self.bodyFileURL || self.bodyStreamProvider);
  587. if (_bodyData || self.bodyStreamProvider || fetchRequest.HTTPBodyStream) {
  588. if (isEffectiveHTTPGet) {
  589. fetchRequest.HTTPMethod = @"POST";
  590. isEffectiveHTTPGet = NO;
  591. }
  592. if (_bodyData) {
  593. if (!needsUploadTask) {
  594. fetchRequest.HTTPBody = _bodyData;
  595. }
  596. #if !STRIP_GTM_FETCH_LOGGING
  597. } else if (fetchRequest.HTTPBodyStream) {
  598. if ([self respondsToSelector:@selector(loggedInputStreamForInputStream:)]) {
  599. fetchRequest.HTTPBodyStream =
  600. [self performSelector:@selector(loggedInputStreamForInputStream:)
  601. withObject:fetchRequest.HTTPBodyStream];
  602. }
  603. #endif
  604. }
  605. }
  606. // We authorize after setting up the http method and body in the request
  607. // because OAuth 1 may need to sign the request body
  608. if (mayAuthorize && _authorizer && !isDataRequest) {
  609. BOOL isAuthorized = [_authorizer isAuthorizedRequest:fetchRequest];
  610. if (!isAuthorized) {
  611. // Authorization needed.
  612. //
  613. // If this session is held by the fetcher service, clear the session now so that we don't
  614. // assume it's still valid after authorization completes.
  615. if (self.canShareSession) {
  616. self.session = nil;
  617. }
  618. // Authorizing the request will recursively call this beginFetch:mayDelay:
  619. // or failToBeginFetchWithError:.
  620. [self authorizeRequest];
  621. return;
  622. }
  623. }
  624. // set the default upload or download retry interval, if necessary
  625. if ([self isRetryEnabled] && self.maxRetryInterval <= 0) {
  626. if (isEffectiveHTTPGet || [effectiveHTTPMethod isEqual:@"HEAD"]) {
  627. [self setMaxRetryInterval:kDefaultMaxDownloadRetryInterval];
  628. } else {
  629. [self setMaxRetryInterval:kDefaultMaxUploadRetryInterval];
  630. }
  631. }
  632. // finally, start the connection
  633. NSURLSessionTask *newSessionTask;
  634. BOOL needsDataAccumulator = NO;
  635. if (_downloadResumeData) {
  636. newSessionTask = [_session downloadTaskWithResumeData:_downloadResumeData];
  637. GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask,
  638. @"Failed downloadTaskWithResumeData for %@, resume data %tu bytes",
  639. _session, _downloadResumeData.length);
  640. } else if (_destinationFileURL && !isDataRequest) {
  641. newSessionTask = [_session downloadTaskWithRequest:fetchRequest];
  642. GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask, @"Failed downloadTaskWithRequest for %@, %@",
  643. _session, fetchRequest);
  644. } else if (needsUploadTask) {
  645. if (bodyFileURL) {
  646. newSessionTask = [_session uploadTaskWithRequest:fetchRequest
  647. fromFile:bodyFileURL];
  648. GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask,
  649. @"Failed uploadTaskWithRequest for %@, %@, file %@",
  650. _session, fetchRequest, bodyFileURL.path);
  651. } else if (self.bodyStreamProvider) {
  652. newSessionTask = [_session uploadTaskWithStreamedRequest:fetchRequest];
  653. GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask,
  654. @"Failed uploadTaskWithStreamedRequest for %@, %@",
  655. _session, fetchRequest);
  656. } else {
  657. GTMSESSION_ASSERT_DEBUG_OR_LOG(_bodyData != nil,
  658. @"Upload task needs body data, %@", fetchRequest);
  659. newSessionTask = [_session uploadTaskWithRequest:fetchRequest
  660. fromData:(NSData * GTM_NONNULL_TYPE)_bodyData];
  661. GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask,
  662. @"Failed uploadTaskWithRequest for %@, %@, body data %tu bytes",
  663. _session, fetchRequest, _bodyData.length);
  664. }
  665. needsDataAccumulator = YES;
  666. } else {
  667. newSessionTask = [_session dataTaskWithRequest:fetchRequest];
  668. needsDataAccumulator = YES;
  669. GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask, @"Failed dataTaskWithRequest for %@, %@",
  670. _session, fetchRequest);
  671. }
  672. self.sessionTask = newSessionTask;
  673. if (!newSessionTask) {
  674. // We shouldn't get here; if we're here, an earlier assertion should have fired to explain
  675. // which session task creation failed.
  676. [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorTaskCreationFailed)];
  677. return;
  678. }
  679. if (needsDataAccumulator && _accumulateDataBlock == nil) {
  680. self.downloadedData = [NSMutableData data];
  681. }
  682. if (_taskDescription) {
  683. newSessionTask.taskDescription = _taskDescription;
  684. }
  685. if (_taskPriority >= 0) {
  686. #if (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_10) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_10) \
  687. || (TARGET_OS_IPHONE && defined(__IPHONE_8_0) && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0)
  688. BOOL hasTaskPriority = YES;
  689. #else
  690. BOOL hasTaskPriority = [newSessionTask respondsToSelector:@selector(setPriority:)];
  691. #endif
  692. if (hasTaskPriority) {
  693. newSessionTask.priority = _taskPriority;
  694. }
  695. }
  696. #if GTM_DISABLE_FETCHER_TEST_BLOCK
  697. GTMSESSION_ASSERT_DEBUG(_testBlock == nil && gGlobalTestBlock == nil, @"test blocks disabled");
  698. _testBlock = nil;
  699. #else
  700. if (!_testBlock) {
  701. if (gGlobalTestBlock) {
  702. // Note that the test block may pass nil for all of its response parameters,
  703. // indicating that the fetch should actually proceed. This is useful when the
  704. // global test block has been set, and the app is only testing a specific
  705. // fetcher. The block simulation code will then resume the task.
  706. _testBlock = gGlobalTestBlock;
  707. }
  708. }
  709. _isUsingTestBlock = (_testBlock != nil);
  710. #endif // GTM_DISABLE_FETCHER_TEST_BLOCK
  711. #if GTM_BACKGROUND_TASK_FETCHING
  712. // Background tasks seem to interfere with out-of-process uploads and downloads.
  713. if (!self.skipBackgroundTask && !self.useBackgroundSession) {
  714. // Tell UIApplication that we want to continue even when the app is in the
  715. // background.
  716. id<GTMUIApplicationProtocol> app = [[self class] fetcherUIApplication];
  717. #if DEBUG
  718. NSString *bgTaskName = [NSString stringWithFormat:@"%@-%@",
  719. [self class], fetchRequest.URL.host];
  720. #else
  721. NSString *bgTaskName = @"GTMSessionFetcher";
  722. #endif
  723. __block UIBackgroundTaskIdentifier bgTaskID = [app beginBackgroundTaskWithName:bgTaskName
  724. expirationHandler:^{
  725. // Background task expiration callback - this block is always invoked by
  726. // UIApplication on the main thread.
  727. if (bgTaskID != UIBackgroundTaskInvalid) {
  728. if (bgTaskID == self.backgroundTaskIdentifier) {
  729. self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
  730. }
  731. [app endBackgroundTask:bgTaskID];
  732. }
  733. }];
  734. self.backgroundTaskIdentifier = bgTaskID;
  735. }
  736. #endif
  737. if (!_initialRequestDate) {
  738. _initialRequestDate = [[NSDate alloc] init];
  739. }
  740. // We don't expect to reach here even on retry or auth until a stop notification has been sent
  741. // for the previous task, but we should ensure that we don't unbalance that.
  742. GTMSESSION_ASSERT_DEBUG(!_isStopNotificationNeeded, @"Start notification without a prior stop");
  743. [self sendStopNotificationIfNeeded];
  744. [self addPersistedBackgroundSessionToDefaults];
  745. [self setStopNotificationNeeded:YES];
  746. [self postNotificationOnMainThreadWithName:kGTMSessionFetcherStartedNotification
  747. userInfo:nil
  748. requireAsync:NO];
  749. // The service needs to know our task if it is serving as NSURLSession delegate.
  750. [_service fetcherDidBeginFetching:self];
  751. if (_testBlock) {
  752. #if !GTM_DISABLE_FETCHER_TEST_BLOCK
  753. [self simulateFetchForTestBlock];
  754. #endif
  755. } else {
  756. // We resume the session task after posting the notification since the
  757. // delegate callbacks may happen immediately if the fetch is started off
  758. // the main thread or the session delegate queue is on a background thread,
  759. // and we don't want to post a start notification after a premature finish
  760. // of the session task.
  761. [newSessionTask resume];
  762. }
  763. }
  764. NSData * GTM_NULLABLE_TYPE GTMDataFromInputStream(NSInputStream *inputStream, NSError **outError) {
  765. NSMutableData *data = [NSMutableData data];
  766. [inputStream open];
  767. NSInteger numberOfBytesRead = 0;
  768. while ([inputStream hasBytesAvailable]) {
  769. uint8_t buffer[512];
  770. numberOfBytesRead = [inputStream read:buffer maxLength:sizeof(buffer)];
  771. if (numberOfBytesRead > 0) {
  772. [data appendBytes:buffer length:(NSUInteger)numberOfBytesRead];
  773. } else {
  774. break;
  775. }
  776. }
  777. [inputStream close];
  778. NSError *streamError = inputStream.streamError;
  779. if (streamError) {
  780. data = nil;
  781. }
  782. if (outError) {
  783. *outError = streamError;
  784. }
  785. return data;
  786. }
  787. #if !GTM_DISABLE_FETCHER_TEST_BLOCK
  788. - (void)simulateFetchForTestBlock {
  789. // This is invoked on the same thread as the beginFetch method was.
  790. //
  791. // Callbacks will all occur on the callback queue.
  792. _testBlock(self, ^(NSURLResponse *response, NSData *responseData, NSError *error) {
  793. // Callback from test block.
  794. if (response == nil && responseData == nil && error == nil) {
  795. // Assume the fetcher should execute rather than be tested.
  796. _testBlock = nil;
  797. _isUsingTestBlock = NO;
  798. [_sessionTask resume];
  799. return;
  800. }
  801. GTMSessionFetcherBodyStreamProvider bodyStreamProvider = self.bodyStreamProvider;
  802. if (bodyStreamProvider) {
  803. bodyStreamProvider(^(NSInputStream *bodyStream){
  804. // Read from the input stream into an NSData buffer. We'll drain the stream
  805. // explicitly on a background queue.
  806. [self invokeOnCallbackQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)
  807. afterUserStopped:NO
  808. block:^{
  809. NSError *streamError;
  810. NSData *streamedData = GTMDataFromInputStream(bodyStream, &streamError);
  811. dispatch_async(dispatch_get_main_queue(), ^{
  812. // Continue callbacks on the main thread, since serial behavior
  813. // is more reliable for tests.
  814. [self simulateDataCallbacksForTestBlockWithBodyData:streamedData
  815. response:response
  816. responseData:responseData
  817. error:(error ?: streamError)];
  818. });
  819. }];
  820. });
  821. } else {
  822. // No input stream; use the supplied data or file URL.
  823. NSURL *bodyFileURL = self.bodyFileURL;
  824. if (bodyFileURL) {
  825. NSError *readError;
  826. _bodyData = [NSData dataWithContentsOfURL:bodyFileURL
  827. options:NSDataReadingMappedIfSafe
  828. error:&readError];
  829. error = readError;
  830. }
  831. // No stream provider.
  832. // In real fetches, nothing happens until the run loop spins, so apps have leeway to
  833. // set callbacks after they call beginFetch. We'll mirror that fetcher behavior by
  834. // delaying callbacks here at least to the next spin of the run loop. That keeps
  835. // immediate, synchronous setting of callback blocks after beginFetch working in tests.
  836. dispatch_async(dispatch_get_main_queue(), ^{
  837. [self simulateDataCallbacksForTestBlockWithBodyData:_bodyData
  838. response:response
  839. responseData:responseData
  840. error:error];
  841. });
  842. }
  843. });
  844. }
  845. - (void)simulateByteTransferReportWithDataLength:(int64_t)totalDataLength
  846. block:(GTMSessionFetcherSendProgressBlock)block {
  847. // This utility method simulates transfer progress with up to three callbacks.
  848. // It is used to call back to any of the progress blocks.
  849. int64_t sendReportSize = totalDataLength / 3 + 1;
  850. int64_t totalSent = 0;
  851. while (totalSent < totalDataLength) {
  852. int64_t bytesRemaining = totalDataLength - totalSent;
  853. sendReportSize = MIN(sendReportSize, bytesRemaining);
  854. totalSent += sendReportSize;
  855. [self invokeOnCallbackQueueUnlessStopped:^{
  856. block(sendReportSize, totalSent, totalDataLength);
  857. }];
  858. }
  859. }
  860. - (void)simulateDataCallbacksForTestBlockWithBodyData:(NSData * GTM_NULLABLE_TYPE)bodyData
  861. response:(NSURLResponse *)response
  862. responseData:(NSData *)suppliedData
  863. error:(NSError *)suppliedError {
  864. __block NSData *responseData = suppliedData;
  865. __block NSError *responseError = suppliedError;
  866. // This method does the test simulation of callbacks once the upload
  867. // and download data are known.
  868. @synchronized(self) {
  869. GTMSessionMonitorSynchronized(self);
  870. // Get copies of ivars we'll access in async invocations. This simulation assumes
  871. // they won't change during fetcher execution.
  872. NSURL *destinationFileURL = _destinationFileURL;
  873. GTMSessionFetcherWillRedirectBlock willRedirectBlock = _willRedirectBlock;
  874. GTMSessionFetcherDidReceiveResponseBlock didReceiveResponseBlock = _didReceiveResponseBlock;
  875. GTMSessionFetcherSendProgressBlock sendProgressBlock = _sendProgressBlock;
  876. GTMSessionFetcherDownloadProgressBlock downloadProgressBlock = _downloadProgressBlock;
  877. GTMSessionFetcherAccumulateDataBlock accumulateDataBlock = _accumulateDataBlock;
  878. GTMSessionFetcherReceivedProgressBlock receivedProgressBlock = _receivedProgressBlock;
  879. GTMSessionFetcherWillCacheURLResponseBlock willCacheURLResponseBlock =
  880. _willCacheURLResponseBlock;
  881. // Simulate receipt of redirection.
  882. if (willRedirectBlock) {
  883. [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES
  884. block:^{
  885. willRedirectBlock((NSHTTPURLResponse *)response, _request,
  886. ^(NSURLRequest *redirectRequest) {
  887. // For simulation, we'll assume the app will just continue.
  888. });
  889. }];
  890. }
  891. // If the fetcher has a challenge block, simulate a challenge.
  892. //
  893. // It might be nice to eventually let the user determine which testBlock
  894. // fetches get challenged rather than always executing the supplied
  895. // challenge block.
  896. if (_challengeBlock) {
  897. [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES
  898. block:^{
  899. if (_challengeBlock) {
  900. NSURL *requestURL = _request.URL;
  901. NSString *host = requestURL.host;
  902. NSURLProtectionSpace *pspace =
  903. [[NSURLProtectionSpace alloc] initWithHost:host
  904. port:requestURL.port.integerValue
  905. protocol:requestURL.scheme
  906. realm:nil
  907. authenticationMethod:NSURLAuthenticationMethodHTTPBasic];
  908. id<NSURLAuthenticationChallengeSender> unusedSender =
  909. (id<NSURLAuthenticationChallengeSender>)[NSNull null];
  910. NSURLAuthenticationChallenge *challenge =
  911. [[NSURLAuthenticationChallenge alloc] initWithProtectionSpace:pspace
  912. proposedCredential:nil
  913. previousFailureCount:0
  914. failureResponse:nil
  915. error:nil
  916. sender:unusedSender];
  917. _challengeBlock(self, challenge, ^(NSURLSessionAuthChallengeDisposition disposition,
  918. NSURLCredential * GTM_NULLABLE_TYPE credential){
  919. // We could change the responseData and responseError based on the disposition,
  920. // but it's easier for apps to just supply the expected data and error
  921. // directly to the test block. So this simulation ignores the disposition.
  922. });
  923. }
  924. }];
  925. }
  926. // Simulate receipt of an initial response.
  927. if (didReceiveResponseBlock) {
  928. [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES
  929. block:^{
  930. didReceiveResponseBlock(response, ^(NSURLSessionResponseDisposition desiredDisposition) {
  931. // For simulation, we'll assume the disposition is to continue.
  932. });
  933. }];
  934. }
  935. // Simulate reporting send progress.
  936. if (sendProgressBlock) {
  937. [self simulateByteTransferReportWithDataLength:(int64_t)bodyData.length
  938. block:^(int64_t bytesSent,
  939. int64_t totalBytesSent,
  940. int64_t totalBytesExpectedToSend) {
  941. // This is invoked on the callback queue unless stopped.
  942. sendProgressBlock(bytesSent, totalBytesSent, totalBytesExpectedToSend);
  943. }];
  944. }
  945. if (destinationFileURL) {
  946. // Simulate download to file progress.
  947. if (downloadProgressBlock) {
  948. [self simulateByteTransferReportWithDataLength:(int64_t)responseData.length
  949. block:^(int64_t bytesDownloaded,
  950. int64_t totalBytesDownloaded,
  951. int64_t totalBytesExpectedToDownload) {
  952. // This is invoked on the callback queue unless stopped.
  953. downloadProgressBlock(bytesDownloaded, totalBytesDownloaded,
  954. totalBytesExpectedToDownload);
  955. }];
  956. }
  957. NSError *writeError;
  958. [responseData writeToURL:destinationFileURL
  959. options:NSDataWritingAtomic
  960. error:&writeError];
  961. if (writeError) {
  962. // Tell the test code that writing failed.
  963. responseError = writeError;
  964. }
  965. } else {
  966. // Simulate download to NSData progress.
  967. if (accumulateDataBlock) {
  968. if (responseData) {
  969. [self invokeOnCallbackQueueUnlessStopped:^{
  970. accumulateDataBlock(responseData);
  971. }];
  972. }
  973. } else {
  974. _downloadedData = [responseData mutableCopy];
  975. }
  976. if (receivedProgressBlock) {
  977. [self simulateByteTransferReportWithDataLength:(int64_t)responseData.length
  978. block:^(int64_t bytesReceived,
  979. int64_t totalBytesReceived,
  980. int64_t totalBytesExpectedToReceive) {
  981. // This is invoked on the callback queue unless stopped.
  982. receivedProgressBlock(bytesReceived, totalBytesReceived);
  983. }];
  984. }
  985. if (willCacheURLResponseBlock) {
  986. // Simulate letting the client inspect and alter the cached response.
  987. NSData *cachedData = responseData ?: [[NSData alloc] init]; // Always have non-nil data.
  988. NSCachedURLResponse *cachedResponse =
  989. [[NSCachedURLResponse alloc] initWithResponse:response
  990. data:cachedData];
  991. [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES
  992. block:^{
  993. willCacheURLResponseBlock(cachedResponse, ^(NSCachedURLResponse *responseToCache){
  994. // The app may provide an alternative response, or nil to defeat caching.
  995. });
  996. }];
  997. }
  998. }
  999. _response = response;
  1000. } // @synchronized(self)
  1001. NSOperationQueue *queue = self.sessionDelegateQueue;
  1002. [queue addOperationWithBlock:^{
  1003. // Rather than invoke failToBeginFetchWithError: we want to simulate completion of
  1004. // a connection that started and ended, so we'll call down to finishWithError:
  1005. NSInteger status = responseError ? responseError.code : 200;
  1006. if (status >= 200 && status <= 399) {
  1007. [self finishWithError:nil shouldRetry:NO];
  1008. } else {
  1009. [self shouldRetryNowForStatus:status
  1010. error:responseError
  1011. forceAssumeRetry:NO
  1012. response:^(BOOL shouldRetry) {
  1013. [self finishWithError:responseError shouldRetry:shouldRetry];
  1014. }];
  1015. }
  1016. }];
  1017. }
  1018. #endif // !GTM_DISABLE_FETCHER_TEST_BLOCK
  1019. - (void)setSessionTask:(NSURLSessionTask *)sessionTask {
  1020. @synchronized(self) {
  1021. GTMSessionMonitorSynchronized(self);
  1022. if (_sessionTask != sessionTask) {
  1023. _sessionTask = sessionTask;
  1024. if (_sessionTask) {
  1025. // Request could be nil on restoring this fetcher from a background session.
  1026. if (!_request) {
  1027. _request = [_sessionTask.originalRequest mutableCopy];
  1028. }
  1029. }
  1030. }
  1031. } // @synchronized(self)
  1032. }
  1033. - (NSURLSessionTask * GTM_NULLABLE_TYPE)sessionTask {
  1034. @synchronized(self) {
  1035. GTMSessionMonitorSynchronize