-
Notifications
You must be signed in to change notification settings - Fork 150
/
GTMSessionFetcher.m
4928 lines (4253 loc) · 184 KB
/
GTMSessionFetcher.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* 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 TARGET_OS_IOS
# if defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_13_0
# define GTM_SDK_REQUIRES_TLSMINIMUMSUPPORTEDPROTOCOLVERSION 1
# else
# define GTM_SDK_REQUIRES_TLSMINIMUMSUPPORTEDPROTOCOLVERSION 0
# endif
#else // Not iOS
# define GTM_SDK_REQUIRES_TLSMINIMUMSUPPORTEDPROTOCOLVERSION 1
#endif
#if TARGET_OS_IOS
# if defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_13_0
# define GTM_SDK_REQUIRES_SECTRUSTEVALUATEWITHERROR 1
# else
# define GTM_SDK_REQUIRES_SECTRUSTEVALUATEWITHERROR 0
# endif
#else // Not iOS
# define GTM_SDK_REQUIRES_SECTRUSTEVALUATEWITHERROR 1
#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;
#else
if (@available(iOS 13, *)) {
_configuration.TLSMinimumSupportedProtocolVersion = tls_protocol_version_TLSv12;
} 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) {