Source/TNLURLSessionTaskOperation.m (2,047 lines of code) (raw):
//
// TNLURLSessionTaskOperation.m
// TwitterNetworkLayer
//
// Created on 6/11/14.
// Copyright © 2020 Twitter. All rights reserved.
//
#include <mach/mach_time.h>
#include <objc/runtime.h>
#include <stdatomic.h>
#import <CommonCrypto/CommonDigest.h>
#import "NSCachedURLResponse+TNLAdditions.h"
#import "NSData+TNLAdditions.h"
#import "NSDictionary+TNLAdditions.h"
#import "NSURLResponse+TNLAdditions.h"
#import "NSURLSessionConfiguration+TNLAdditions.h"
#import "NSURLSessionTaskMetrics+TNLAdditions.h"
#import "TNL_Project.h"
#import "TNLAttemptMetaData_Project.h"
#import "TNLAttemptMetrics.h"
#import "TNLBackoff.h"
#import "TNLContentCoding.h"
#import "TNLError.h"
#import "TNLGlobalConfiguration.h"
#import "TNLHTTPHeaderProvider.h"
#import "TNLNetwork.h"
#import "TNLPriority.h"
#import "TNLRequest.h"
#import "TNLRequestConfiguration_Project.h"
#import "TNLRequestOperation_Project.h"
#import "TNLRequestOperationQueue_Project.h"
#import "TNLResponse_Project.h"
#import "TNLTemporaryFile_Project.h"
#import "TNLTiming.h"
#import "TNLURLSessionTaskOperation.h"
NS_ASSUME_NONNULL_BEGIN
#define EXTRA_DOWNLOAD_BYTES_BUFFER (16)
#define kTaskMetricsNotSeenOnCompletionDelayCompletionDuration (0.300)
static NSString * const kTempFilePrefix = @"com.tnl.temp.";
static NSString *TNLWriteDataToTemporaryFile(NSData *data);
static BOOL TNLURLRequestHasBody(NSURLRequest *request, id<TNLRequest> requestPrototype);
static NSArray<NSString *> *TNLSecTrustGetCertificateChainDescriptions(SecTrustRef trust);
static NSString *TNLSecCertificateDescription(SecCertificateRef cert);
TNL_OBJC_FINAL TNL_OBJC_DIRECT_MEMBERS
@interface TNLFakeRequestOperation : TNLRequestOperation
- (instancetype)initWithURLSessionTaskOperation:(TNLURLSessionTaskOperation *)op NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithRequest:(nullable id<TNLRequest>)request
configuration:(nullable TNLRequestConfiguration *)config
delegate:(nullable id<TNLRequestDelegate>)delegate NS_UNAVAILABLE;
@end
TNL_OBJC_DIRECT_MEMBERS
@interface TNLURLSessionTaskOperation () <TNLContentDecoderClient>
@property (nonatomic, readonly) TNLRequestOperationState state;
@property (nonatomic, readonly, getter=isComplete) BOOL complete;
@property (nonatomic, readonly, getter=isFinalizing) BOOL finalizing;
@property (nonatomic, readonly, nullable) NSDictionary<NSString *, id<TNLContentDecoder>> *additionalDecoders;
@property (nonatomic, readonly, nullable) id<TNLContentDecoder> contentDecoder;
@property (nonatomic, readonly, nullable) id<TNLContentDecoderContext> contentDecoderContext;
@property (nonatomic, readonly, nullable) NSMutableData *contentDecoderRecentData;
@property (nonatomic) id<TNLRequest> originalRequest;
- (BOOL)_currentRequestHasBody;
#pragma mark Decoding
- (void)_decodeData:(NSData *)data
completion:(void(^)(NSData * __nullable decodedData, NSError * __nullable decodeError))blockName; // completion called on bg queue
- (void)_finishDecodingWithURLSession:(NSURLSession *)completedURLSession
dataTask:(NSURLSessionDataTask *)dataTask;
- (void)_network_flushDecoding:(nullable NSError *)error
completion:(void(^)(NSData * __nullable decodedData, NSError * __nullable decodeError))completion;
- (void)_network_didDecodeData:(nullable NSData *)decodedData
URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
error:(nullable NSError *)decodeError;
#pragma mark Network
// TODO: move these all to (Network) category
- (void)_network_finalizeDidCompleteTask:(NSURLSessionTask *)task
URLSession:(NSURLSession *)session
error:(nullable NSError *)error;
// NSURLSession Events
- (void)_network_willPerformHTTPRedirectionFromRequest:(NSURLRequest *)fromRequest
response:(NSHTTPURLResponse *)response
originalRequest:(NSURLRequest *)originalRequest
suggestedRequest:(NSURLRequest *)suggestedRequest
chosenRequest:(nullable id<TNLRequest>)chosenRequest
completion:(void(^)(NSURLRequest * __nullable))completionHandler;
- (void)_network_handleRedirectFromRequest:(NSURLRequest *)fromRequest
response:(NSHTTPURLResponse *)response
toRequest:(nullable NSURLRequest *)toRequest
completion:(void(^)(NSURLRequest * __nullable))completionHandler;
@end
TNL_OBJC_DIRECT_MEMBERS
@interface TNLURLSessionTaskOperation (Network)
// Methods that can only be called from the tnl_network_queue()
#pragma mark Properties
- (float)_network_uploadProgress;
- (float)_network_downloadProgress;
#pragma mark NSOperation
- (BOOL)_network_shouldCancel;
#pragma mark Update State
- (void)_network_updatePriorities;
- (void)_network_updateUploadProgress:(float)progress;
- (void)_network_updateDownloadProgress:(float)progress;
- (void)_network_transitionToState:(TNLRequestOperationState)state;
- (void)_network_updateTimestampsWithState:(TNLRequestOperationState)state;
- (void)_network_buildResponseInfo;
- (void)_network_buildInternalResponse;
- (void)_network_finalizeWithState:(TNLRequestOperationState)state;
- (void)_network_finalizeWithResponseCompletion:(TNLRequestMakeFinalResponseCompletionBlock)completion;
#pragma mark Completion/Failure/Cancel
- (void)_network_fail:(NSError *)error;
- (void)_network_cancel;
- (void)_network_complete;
- (void)_network_completeCachedCompletionIfPossible;
#pragma mark NSURLSession Events
- (void)_network_didUpdateTotalBytesReceived:(int64_t)bytesReceived
expectedBytes:(int64_t)totalBytesExpectedToReceive
URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask;
- (void)_network_captureResponseFromTaskIfNeeded:(NSURLSessionTask *)task
URLSession:(NSURLSession *)session;
- (void)_network_didReceiveResponse:(NSURLResponse *)response
URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task;
#pragma mark NSURLSessionTask Methods
- (void)_network_willResumeSessionTaskWithRequest:(NSURLRequest *)resumeRequest;
- (void)_network_resumeSessionTask:(NSURLSessionTask *)task;
- (void)_network_createTaskWithRequest:(NSURLRequest *)request
prototype:(id<TNLRequest>)requestPrototype
completion:(void(^)(NSURLSessionTask *createdTask, NSError *error))complete;
- (nullable NSURLSessionTask *)_network_populateURLSessionTaskWithRequest:(NSURLRequest *)request
prototype:(id<TNLRequest>)requestPrototype
error:(out NSError * __nullable * __nonnull)errorOut;
#pragma mark Other Methods
- (nullable NSError *)_network_appendDecodedData:(nullable NSData *)data;
- (void)_network_didStartTask:(BOOL)isBackgroundRequest;
- (void)_network_updateHashWithData:(NSData *)data;
- (void)_network_finishHashWithSuccess:(BOOL)success;
#pragma mark Idle Timeout
- (void)_network_startIdleTimerWithDeferralDuration:(NSTimeInterval)deferral;
- (void)_network_stopIdleTimer;
- (void)_network_restartIdleTimer;
- (void)_network_idleTimerFired;
@end
#pragma mark - TNLURLSessionTaskOperation
@implementation TNLURLSessionTaskOperation
{
// Session/Task State
__unsafe_unretained id<TNLURLSessionManager> _sessionManager;
NSURLSession *_URLSession;
NSURLSessionDataTask *_dataTask;
NSURLSessionDownloadTask *_downloadTask;
NSURLSessionUploadTask *_uploadTask;
NSURLRequest *_taskRequest;
NSData *_uploadData;
NSString *_uploadFilePath;
NSData *_resumeData; // TODO:[nobrien] - utilize
// Request/Response iVars
id<TNLRequest> _hydratedRequest;
Class _responseClass;
// Timings
NSDate *_startDate;
uint64_t _startMachTime; // deprecated
NSDate *_endDate;
uint64_t _endMachTime; // deprecated
NSDate *_completeDate;
uint64_t _completeMachTime; // deprecated
TNLPriority _taskResumePriority;
NSDate *_taskResumeDate;
NSDate *_responseBodyStartDate;
NSDate *_responseBodyEndDate;
NSDate *_completionCallbackDate;
NSDate *_taskMetricsCallbackDate;
// Timers
dispatch_source_t _idleTimer;
// Cached State
NSError *_cachedFailure;
NSHTTPURLResponse *_cancelledRedirectResponse;
NSURLSession *_cachedCompletionSession;
NSURLSessionTask *_cachedCompletionTask;
NSError *_cachedCompletionError;
// Gathered State
void *_hashContextRef;
NSData *_hashData;
TNLResponseHashComputeAlgorithm _hashAlgo;
NSDictionary *_authChallengeCancelledUserInfo;
NSMutableData *_storedData;
TNLTemporaryFile *_tempFile;
SInt64 _layer8BodyBytesReceived; // count after uncompressing
// Metrics
NSTimeInterval _responseDecodeLatency;
NSURLSessionTaskMetrics *_taskMetrics;
// State
TNLRequestOperationState_AtomicT _internalState;
struct {
BOOL didCancel:1;
BOOL didStart:1;
BOOL didIncrementExecutionCount:1;
BOOL shouldComputeHash:1;
BOOL isComputingHash:1;
BOOL isFinalizing:1;
BOOL useIdleTimeout:1;
BOOL useIdleTimeoutForInitialConnection:1;
BOOL shouldCaptureResponse:1;
BOOL encounteredCompletionBeforeTaskMetrics:1;
BOOL shouldDeleteUploadFile:1;
} _flags;
volatile BOOL _isObservingURLSessionTask;
}
#pragma mark Description
- (NSString *)description
{
return [NSString stringWithFormat:@"<%@ %p: originalURL='%@'>", NSStringFromClass([self class]), self, self.originalRequest.URL];
}
#pragma mark init/dealloc
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
- (instancetype)init
#pragma clang diagnostic pop
{
[self doesNotRecognizeSelector:_cmd];
abort();
return nil;
}
- (instancetype)initWithRequestOperation:(TNLRequestOperation *)op
sessionManager:(id<TNLURLSessionManager>)sessionManager
{
if (self = [super init]) {
TNLIncrementObjectCount([self class]);
_sessionManager = sessionManager;
_originalRequest = op.originalRequest;
_hydratedRequest = op.hydratedRequest;
_hydratedURLRequest = op.hydratedURLRequest;
_requestConfiguration = op.requestConfiguration;
_additionalDecoders = op.additionalDecoders;
_executionMode = _requestConfiguration.executionMode;
_requestOperation = op;
_responseClass = op.responseClass;
_hashAlgo = op.requestConfiguration.responseComputeHashAlgorithm;
_flags.shouldComputeHash = (_hashAlgo != TNLResponseHashComputeAlgorithmNone);
_flags.shouldCaptureResponse = 1;
if (Nil == [NSURLSessionTaskMetrics class]) {
// If task metrics don't exist (pre iOS 10 / macOS 10.12) we should not worry about collecting the metrics.
// If there is no task metrics class, we can set that we've already "encountered" the metrics.
_flags.encounteredCompletionBeforeTaskMetrics = 1;
}
TNLAssert(_hydratedRequest != nil);
TNLAssert(_hydratedURLRequest != nil);
TNLAssert(_requestConfiguration != nil);
TNLAssert(op.URLSessionTaskOperation == nil);
TNLAssert(![_requestConfiguration respondsToSelector:@selector(setExecutionMode:)] && "MUST be immutable");
[self _network_updatePriorities];
}
return self;
}
- (void)dealloc
{
TNLAssert(!_hashContextRef);
[self _network_stopIdleTimer];
if (!self.isComplete) {
[self.URLSessionTask cancel];
}
TNLDecrementObjectCount([self class]);
}
#pragma mark Properties
- (TNLRequestOperationState)state
{
return atomic_load(&_internalState);
}
- (BOOL)_currentRequestHasBody
{
NSURLSessionTask *task = self.URLSessionTask;
if ([task isKindOfClass:[NSURLSessionUploadTask class]]) {
return YES;
}
return TNLURLRequestHasBody(task.currentRequest, _hydratedRequest);
}
- (nullable NSURLSessionTask *)URLSessionTask
{
return _dataTask ?: _downloadTask ?: _uploadTask;
}
- (nullable NSHTTPURLResponse *)URLResponse
{
return (NSHTTPURLResponse *)self.URLSessionTask.response ?: _cancelledRedirectResponse;
}
- (nullable NSURLRequest *)originalURLRequest
{
NSURLRequest *request = self.URLSessionTask.originalRequest ?: _taskRequest;
if (_uploadData && !request.HTTPBody) {
NSMutableURLRequest *mRequest = [request mutableCopy];
mRequest.HTTPBody = _uploadData;
request = mRequest;
}
return [request copy];
}
- (nullable NSURL *)originalURL
{
return self.URLSessionTask.originalRequest.URL;
}
- (nullable NSURLRequest *)currentURLRequest
{
NSURLRequest *request = self.URLSessionTask.currentRequest;
if (!request) {
return self.originalURLRequest;
}
if (_uploadData && !request.HTTPBody) {
NSMutableURLRequest *mRequest = [request mutableCopy];
mRequest.HTTPBody = _uploadData;
request = mRequest;
}
return [request copy];
}
- (nullable NSURL *)currentURL
{
return self.URLSessionTask.currentRequest.URL;
}
- (TNLResponseSource)responseSource
{
if (!self.URLSessionTask) {
return TNLResponseSourceUnknown;
}
NSURLResponse *URLResponse = self.URLResponse;
return (URLResponse.tnl_wasCachedResponse) ? TNLResponseSourceLocalCache : TNLResponseSourceNetworkRequest;
}
- (nullable NSURLSession *)URLSession
{
return _URLSession;
}
- (void)setURLSession:(NSURLSession *)URLSession supportsTaskMetrics:(BOOL)taskMetrics
{
TNLAssert(URLSession != nil);
_URLSession = URLSession;
if (!taskMetrics) {
tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{
// Task metrics are explicity not supported
self->_flags.encounteredCompletionBeforeTaskMetrics = 1;
});
}
}
#pragma mark Association
- (void)enqueueToOperationQueueIfNeeded:(TNLRequestOperationQueue *)requestOperationQueue
{
if (_requestOperationQueue) {
TNLAssert(_requestOperationQueue == requestOperationQueue);
return;
}
tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{
if (!self->_requestOperationQueue) {
if (gTwitterNetworkLayerAssertEnabled) {
TNLAssert(!self->_requestOperationQueue || self->_requestOperationQueue == requestOperationQueue);
TNLRequestOperation *strongRequestOp = self->_requestOperation;
if (strongRequestOp) {
TNLAssert(strongRequestOp.requestOperationQueue == requestOperationQueue);
}
}
self->_requestOperationQueue = requestOperationQueue;
if (!self.isExecuting && !self.isCancelled && !self.isFinished) {
[self->_sessionManager syncAddURLSessionTaskOperation:self];
}
} else {
TNLAssert(self->_requestOperationQueue == requestOperationQueue);
}
});
}
- (void)cancelWithSource:(nullable id)optionalSource
underlyingError:(nullable NSError *)optionalUnderlyingError
{
tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{
TNLRequestOperation *strongRequestOp = self->_requestOperation;
if (strongRequestOp) {
[strongRequestOp cancelWithSource:optionalSource
underlyingError:optionalUnderlyingError];
}
});
}
- (void)dissassociateRequestOperation:(TNLRequestOperation *)op
{
tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{
TNLRequestOperation *strongRequestOp = self->_requestOperation;
if (strongRequestOp == op) {
TNLResponse *response = strongRequestOp.response;
self->_requestOperation = nil;
if (response) {
self->_finalResponse = response;
}
if ([self _network_shouldCancel]) {
[self _network_cancel];
} else {
[self _network_updatePriorities];
}
}
});
}
- (TNLRequestOperation *)synthesizeRequestOperation
{
return [[TNLFakeRequestOperation alloc] initWithURLSessionTaskOperation:self];
}
#pragma mark Helpers
- (void)network_priorityDidChangeForRequestOperation:(TNLRequestOperation *)op
{
TNLRequestOperation *requestOperation = _requestOperation;
if (requestOperation == op) {
[self _network_updatePriorities];
}
}
#pragma mark NSOperation
- (BOOL)isConcurrent
{
return YES;
}
- (BOOL)isAsynchronous
{
return YES;
}
- (BOOL)isFinalizing
{
return _flags.isFinalizing;
}
- (BOOL)isComplete
{
return TNLRequestOperationStateIsFinal(atomic_load(&_internalState));
}
- (BOOL)isFinished
{
return TNLRequestOperationStateIsFinal(atomic_load(&_internalState));
}
- (BOOL)isCancelled
{
return TNLRequestOperationStateCancelled == atomic_load(&_internalState);
}
- (BOOL)isExecuting
{
return TNLRequestOperationStateIsActive(atomic_load(&_internalState));
}
- (void)start
{
tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{
TNLAssert(!self->_flags.didStart);
self->_flags.didStart = YES;
if (self.isComplete || self.isFinalizing) {
// Complete or completing
return;
}
if (self->_cachedFailure) {
// Already failed
[self _network_fail:self->_cachedFailure];
return;
}
if ([self _network_shouldCancel]) {
// Should cancel
[self _network_cancel];
return;
}
// Starting
[self _network_transitionToState:TNLRequestOperationStateStarting];
TNLAssert(self->_URLSession != nil);
NSURLRequest *taskRequest = self.hydratedURLRequest;
// Assert session is correct
if (gTwitterNetworkLayerAssertEnabled) {
if (TNLRequestExecutionModeBackground == self->_executionMode) {
// Background
TNLAssert([self->_URLSession.sessionDescription rangeOfString:@"/Background?"].location != NSNotFound);
} else {
// InApp
TNLAssert([self->_URLSession.sessionDescription rangeOfString:@"/InApp?"].location != NSNotFound);
}
}
// Create task and start
[self _network_createTaskWithRequest:taskRequest
prototype:self.originalRequest
completion:^(NSURLSessionTask *createdTask, NSError *error) {
TNLAssert(createdTask == self.URLSessionTask);
if (error) {
TNLAssert(!createdTask);
[self _network_fail:error];
} else {
NSURLRequest *currentURLRequest = self.currentURLRequest;
TNLAssert(createdTask);
TNLAssert(currentURLRequest);
TNLAssert(self.originalURLRequest);
if (TNLRequestExecutionModeBackground != self->_executionMode) {
TNLGlobalConfigurationIdleTimeoutMode mode = [TNLGlobalConfiguration sharedInstance].idleTimeoutMode;
self->_flags.useIdleTimeout = (mode != TNLGlobalConfigurationIdleTimeoutModeDisabled);
if (self->_flags.useIdleTimeout) {
self->_flags.useIdleTimeoutForInitialConnection = (mode == TNLGlobalConfigurationIdleTimeoutModeEnabledIncludingInitialConnection);
}
}
[self _network_willResumeSessionTaskWithRequest:currentURLRequest];
[self _network_resumeSessionTask:createdTask];
[self _network_didStartTask:(TNLRequestExecutionModeBackground == self->_executionMode) /*isBackgroundRequest*/];
if (self->_flags.shouldDeleteUploadFile) {
[[NSFileManager defaultManager] removeItemAtPath:self->_uploadFilePath error:NULL];
self->_flags.shouldDeleteUploadFile = NO;
}
if (self->_flags.useIdleTimeoutForInitialConnection) {
[self _network_restartIdleTimer];
}
}
}];
});
}
#pragma mark NSURLSessionDelegate
- (void)URLSession:(NSURLSession *)session
didBecomeInvalidWithError:(nullable NSError *)error
{
tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{
if (self.isComplete || self.isFinalizing) {
return;
}
// If error is nil, the session was explicitely invalidated
TNLLogError(@"%@ %@", NSStringFromSelector(_cmd), error);
[self _network_fail:TNLErrorCreateWithCodeAndUnderlyingError(TNLErrorCodeRequestOperationURLSessionInvalidated, error)];
});
}
- (void)handler:(nullable id<TNLAuthenticationChallengeHandler>)handler
didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
forURLSession:(NSURLSession *)session
context:(nullable id)cancelContext
{
NSURLProtectionSpace *protectionSpace = challenge.protectionSpace;
NSString *protectionSpaceHost = protectionSpace.host;
NSURLRequest *currentRequest = self.currentURLRequest;
NSArray<NSString *> *certDescriptions = TNLSecTrustGetCertificateChainDescriptions(protectionSpace.serverTrust);
NSString *authMethod = protectionSpace.authenticationMethod;
NSHTTPURLResponse *failedResponse = (id)challenge.failureResponse;
if (failedResponse && ![failedResponse isKindOfClass:[NSHTTPURLResponse class]]) {
failedResponse = nil;
}
tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{
NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] init];
if (protectionSpaceHost) {
userInfo[TNLErrorProtectionSpaceHostKey] = protectionSpaceHost;
}
if (currentRequest) {
userInfo[TNLErrorRequestKey] = currentRequest;
}
if (authMethod) {
userInfo[TNLErrorAuthenticationChallengeMethodKey] = authMethod;
}
if (challenge.protectionSpace.realm != nil) {
userInfo[TNLErrorAuthenticationChallengeRealmKey] = challenge.protectionSpace.realm;
}
if (certDescriptions) {
userInfo[TNLErrorCertificateChainDescriptionsKey] = certDescriptions;
}
if (cancelContext) {
userInfo[TNLErrorAuthenticationChallengeCancelContextKey] = cancelContext;
}
if (failedResponse) {
userInfo[TNLErrorResponseKey] = failedResponse;
// ... and, if we have the cancelled response, cache it for completion handling
self->_cancelledRedirectResponse = failedResponse;
}
self->_authChallengeCancelledUserInfo = [userInfo copy];
});
}
#pragma mark NSURLSessionTaskDelegate
- (void)URLSession:(NSURLSession *)session
taskIsWaitingForConnectivity:(NSURLSessionTask *)task
{
tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{
[self _network_restartIdleTimer];
const TNLRequestConnectivityOptions options = self->_requestConfiguration.connectivityOptions;
if (TNL_BITMASK_INTERSECTS_FLAGS(options, TNLRequestConnectivityOptionWaitForConnectivity)) {
// continue
} else if (TNL_BITMASK_INTERSECTS_FLAGS(options, TNLRequestConnectivityOptionWaitForConnectivityWhenRetryPolicyProvided) && self->_requestConfiguration.retryPolicyProvider != nil) {
// continue
} else {
// force failure - not waiting for connectivity
// this is the same error that would trigger if we didn't have waitsForConnectivity set
[self _network_fail:[NSError errorWithDomain:NSURLErrorDomain
code:NSURLErrorNotConnectedToInternet
userInfo:nil]];
return;
}
TNLRequestOperation *requestOperation = self->_requestOperation;
if (requestOperation) {
[requestOperation network_URLSessionTaskOperationIsWaitingForConnectivity:self];
}
});
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
newRequest:(NSURLRequest *)toRequest
completionHandler:(void (^)(NSURLRequest * __nullable))completionHandler
{
NSURLRequest *fromRequest = task.currentRequest;
NSURLRequest *originalRequest = task.originalRequest;
tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{
// redirects yield either a completion or a new attempt,
// stop our idle timer (if we have one running)
[self _network_stopIdleTimer];
TNLRequestOperation *requestOperation = self->_requestOperation;
if (requestOperation) {
[requestOperation network_URLSessionTaskOperation:self
willPerformRedirectFromRequest:fromRequest
withHTTPResponse:response
toRequest:toRequest
completion:^(id<TNLRequest> __nullable callbackRequest) {
TNLAssertIsNetworkQueue();
[self _network_willPerformHTTPRedirectionFromRequest:fromRequest
response:response
originalRequest:originalRequest
suggestedRequest:toRequest
chosenRequest:callbackRequest
completion:completionHandler];
}];
} else {
[self _network_willPerformHTTPRedirectionFromRequest:fromRequest
response:response
originalRequest:originalRequest
suggestedRequest:toRequest
chosenRequest:toRequest
completion:completionHandler];
}
});
}
- (void)_network_willPerformHTTPRedirectionFromRequest:(NSURLRequest *)fromRequest
response:(NSHTTPURLResponse *)response
originalRequest:(NSURLRequest *)originalRequest
suggestedRequest:(NSURLRequest *)suggestedRequest
chosenRequest:(nullable id<TNLRequest>)chosenRequest
completion:(void(^)(NSURLRequest * __nullable))completionHandler
{
NSURLRequest *toRequest = nil;
if (chosenRequest) {
if (chosenRequest == suggestedRequest) {
toRequest = suggestedRequest;
} else {
NSError *error = nil;
toRequest = TNLRequestToNSURLRequest(chosenRequest, nil /*config*/, &error);
if (!toRequest) {
TNLLogError(@"Provided TNLHTTPRequest (%@) for redirect cannot be converted into an NSURLRequest! %@", chosenRequest, error);
}
}
}
[self _network_handleRedirectFromRequest:fromRequest
response:response
toRequest:toRequest
completion:completionHandler];
}
- (void)_network_handleRedirectFromRequest:(NSURLRequest *)fromRequest
response:(NSHTTPURLResponse *)response
toRequest:(nullable NSURLRequest *)toRequest
completion:(void(^)(NSURLRequest * __nullable))completionHandler
{
TNLRequestOperation *requestOperation = _requestOperation;
void (^block)(NSURLRequest * __nullable, NSError * __nullable);
block = ^void(NSURLRequest * __nullable sanitizedRequest, NSError * __nullable sanitiziationError) {
TNLAssertIsNetworkQueue();
NSURLRequest *finalToRequest = (sanitiziationError) ? nil : [sanitizedRequest copy];
if (!finalToRequest) {
// IDYN-419
//
// Traditionally (and per Apple docs), we should be able to just
// call the completionHandler with nil. However, due to a bug in
// the NSURL framework, doing so when a custom NSURLProtocol is in
// use will result in the session task becoming impotent and
// hanging until the operation times out.
//
// As a workaround, we will cache the response since the task
// won't be retaining the response as a property, and cancel the
// task ourselves. Our task completion callback will handle the
// cancellation noting the cached response permitting us to simulate
// the behavior we would expect.
self->_cancelledRedirectResponse = response;
[self.URLSessionTask cancel]; // will trigger the completion callback
return;
} else {
// associate the request config with the new request we redirected to
TNLRequestConfigurationAssociateWithRequest(self.requestConfiguration, finalToRequest);
}
completionHandler(finalToRequest);
if (sanitiziationError) {
[self _network_fail:sanitiziationError];
return;
} else if (self.isComplete || self.finalizing) {
return;
} else if ([self _network_shouldCancel]) {
[self _network_cancel];
return;
}
if (self->_flags.useIdleTimeoutForInitialConnection) {
// only restart if we want the idle timer to also be for connection time
[self _network_restartIdleTimer];
}
[self _network_transitionToState:TNLRequestOperationStateRunning];
TNLAttemptMetaData *metadata = [self network_metaDataWithLowerCaseHeaderFields:[response.allHeaderFields tnl_copyWithLowercaseKeys]];
[requestOperation network_URLSessionTaskOperation:self
redirectedFrom:fromRequest
withHTTPResponse:response
to:finalToRequest
metaData:metadata];
};
if (toRequest && requestOperation) {
[requestOperation network_URLSessionTaskOperation:self
redirectFromRequest:fromRequest
withHTTPResponse:response
to:toRequest
completionHandler:block];
} else {
block(toRequest, nil);
}
}
// don't support per request operation auth challenges yet
//- (void)URLSession:(NSURLSession *)session
// task:(NSURLSessionTask *)task
// didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
// completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
//{
// [self URLSession:session didReceiveChallenge:challenge completionHandler:completionHandler];
//}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
needNewBodyStream:(void (^)(NSInputStream * __nullable bodyStream))completionHandler
{
tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{
if (self->_flags.useIdleTimeoutForInitialConnection) {
[self _network_restartIdleTimer];
} else {
[self _network_stopIdleTimer];
}
id<TNLRequest> request = self->_hydratedRequest;
NSInputStream *stream = nil;
if ([request respondsToSelector:@selector(HTTPBodyStream)]) {
stream = request.HTTPBodyStream;
}
completionHandler(stream);
});
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didSendBodyData:(int64_t)bytesSent
totalBytesSent:(int64_t)totalBytesSent
totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend
{
tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{
if (self.isComplete || self.finalizing) {
return;
} else if ([self _network_shouldCancel]) {
[self _network_cancel];
return;
}
[self _network_transitionToState:TNLRequestOperationStateRunning];
const float progress = [self _network_uploadProgress];
[self _network_updateUploadProgress:progress];
[self _network_restartIdleTimer];
});
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)theError
{
TNLAssertMessage(theError != nil || task.response != nil, @"task: %@\n%@", task, task.currentRequest);
tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{
if (!self->_completionCallbackDate && !self->_flags.encounteredCompletionBeforeTaskMetrics) {
self->_completionCallbackDate = [NSDate date];
}
if (!self->_taskMetrics && !self->_flags.encounteredCompletionBeforeTaskMetrics) {
TNLAssert([NSURLSessionTaskMetrics class] != Nil);
/*
Radar #27098270 - filed with iOS 10 beta 1
iOS 10 GM and macOS 10.12 GM released with a bug that the completion callback is
called BEFORE the task metrics callback is called. This directly contradicts the
Apple documentation which can be seen clearly in `NSURLSession.h`:
// 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:(nullable NSError *)error;
To work around this issue, we will "cache" the completion and retrigger it once we
finally get the task metrics. As a failsafe, we will set a short timer to force
completion even if the task metrics don't come through.
*/
self->_flags.encounteredCompletionBeforeTaskMetrics = 1;
self->_cachedCompletionSession = session;
self->_cachedCompletionTask = task;
self->_cachedCompletionError = theError;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kTaskMetricsNotSeenOnCompletionDelayCompletionDuration * NSEC_PER_SEC)), tnl_network_queue(), ^{
@autoreleasepool {
[self _network_completeCachedCompletionIfPossible];
}
});
return;
}
NSError *error = theError;
if (self.isComplete || self.finalizing) {
return;
} else if ([self _network_shouldCancel]) {
[self _network_cancel];
return;
}
[self _network_transitionToState:TNLRequestOperationStateRunning];
[self _network_captureResponseFromTaskIfNeeded:task URLSession:session];
TNLAssertMessage(task == self.URLSessionTask, @"task[%tu]:%@ != task[%tu]:%@", task.taskIdentifier, task, self.URLSessionTask.taskIdentifier, self.URLSessionTask);
TNLAssertMessage(error != nil || task.response != nil, @"task: %@\n%@", task, task.currentRequest);
if (!error && self->_contentDecoderContext) {
[self _network_stopIdleTimer];
[self _finishDecodingWithURLSession:session dataTask:(NSURLSessionDataTask *)task];
return;
}
[self _network_finalizeDidCompleteTask:task URLSession:session error:error];
});
}
- (void)_finishDecodingWithURLSession:(NSURLSession *)completedURLSession
dataTask:(NSURLSessionDataTask *)dataTask
{
tnl_dispatch_async_autoreleasing(tnl_coding_queue(), ^{
NSError *decodingError = nil;
if (![self->_contentDecoder tnl_finalizeDecoding:self->_contentDecoderContext error:&decodingError]) {
decodingError = TNLErrorCreateWithCodeAndUnderlyingError(TNLErrorCodeRequestOperationRequestContentDecodingFailed, decodingError);
}
tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{
const BOOL hasRecentData = self->_contentDecoderRecentData.length > 0;
if (hasRecentData) {
// flush is synchronous
[self _network_flushDecoding:decodingError
completion:^(NSData * __nullable decodedData, NSError * __nullable flushDecodingError) {
[self _network_didDecodeData:decodedData
URLSession:completedURLSession
dataTask:dataTask
error:flushDecodingError];
}];
}
// error would be triggered in the flush which would yield the op to fail before this point
[self _network_finalizeDidCompleteTask:dataTask
URLSession:completedURLSession
error:nil];
});
});
}
- (void)_network_finalizeDidCompleteTask:(NSURLSessionTask *)task
URLSession:(NSURLSession *)session
error:(nullable NSError *)error
{
if (self.isComplete || self.finalizing) {
return;
} else if ([self _network_shouldCancel]) {
[self _network_cancel];
return;
}
_contentDecoderContext = nil;
_contentDecoderRecentData = nil;
[self _network_stopIdleTimer];
BOOL success = YES;
if (error) {
success = NO;
if ([error.domain isEqualToString:NSURLErrorDomain]) {
switch (error.code) {
case NSURLErrorCancelled:
{
if (_flags.didCancel) {
// cancel, not an error
error = nil;
if (_cancelledRedirectResponse) {
success = YES;
}
} else {
// other cancellation
NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] init];
if (_authChallengeCancelledUserInfo) {
// definitely auth challenge
[userInfo addEntriesFromDictionary:self->_authChallengeCancelledUserInfo];
} else {
// unknown, but treat as auth challenge anyway
}
userInfo[NSUnderlyingErrorKey] = error;
error = TNLErrorCreateWithCodeAndUserInfo(TNLErrorCodeRequestOperationAuthenticationChallengeCancelled, userInfo);
}
break;
}
case NSURLErrorBadServerResponse:
{
// Log the error
TNLLogError(@"TNLURLSessionTaskOperation completed with bad server response error! %@", error);
break;
}
case NSURLErrorTimedOut:
{
// Replace the generic NSURL timeout with the specific TNL timeout
error = TNLErrorCreateWithCodeAndUnderlyingError(TNLErrorCodeRequestOperationAttemptTimedOut, error);
break;
}
default:
break;
}
}
}
if (success) {
[self _network_complete];
} else {
if (error) {
[self _network_fail:error];
} else {
[self _network_cancel];
}
}
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics
{
tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{
self->_taskMetricsCallbackDate = [NSDate date];
self->_taskMetrics = metrics;
/*
Radar #27098270 - filed with iOS 10 beta 1
iOS 10 GM and macOS 10.12 GM released with a bug that the completion callback is
called BEFORE the task metrics callback is called. This directly contradicts the
Apple documentation which can be seen clearly in `NSURLSession.h`:
// 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:(nullable NSError *)error;
To work around this issue, we will "cache" the completion and retrigger it once we
finally get the task metrics. As a failsafe, we will set a short timer to force
completion even if the task metrics don't come through.
*/
[self _network_completeCachedCompletionIfPossible];
});
}
#pragma mark NSURLSessionDataTaskDelegate
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
{
tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{
[self _network_didReceiveResponse:response URLSession:session task:dataTask];
completionHandler(NSURLSessionResponseAllow);
});
}
// Not implemented a.t.m.
/*
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask
*/
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data
{
// callback called on bg queue
[self _decodeData:data
completion:^(NSData * __nullable decodedData, NSError * __nullable decodeError) {
[self _network_didDecodeData:decodedData
URLSession:session
dataTask:dataTask
error:decodeError];
}];
}
- (void)_network_didDecodeData:(nullable NSData *)decodedData
URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
error:(nullable NSError *)decodeError
{
if (self.isComplete || self.isFinalizing) {
return;
} else if ([self _network_shouldCancel]) {
[self _network_cancel];
return;
}
[self _network_captureResponseFromTaskIfNeeded:dataTask URLSession:session];
NSError *error = decodeError ?: [self _network_appendDecodedData:decodedData];
if (error) {
[self _network_fail:error];
} else {
[self _network_updateDownloadProgress:[self _network_downloadProgress]];
[self _network_restartIdleTimer];
}
}
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
willCacheResponse:(NSCachedURLResponse *)proposedResponse
completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler
{
// TODO:[nobrien] - expose this via one of the request delegates or configuration (NSURLCacheStoragePolicy)
tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{
[self _network_restartIdleTimer];
completionHandler(proposedResponse);
});
}
#pragma mark NSURLSessionDownloadDelegate
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
// Capture the temp file immediately
NSError *error;
TNLTemporaryFile *tempFile = [TNLTemporaryFile temporaryFileWithExistingFilePath:location.path
error:&error];
TNLAssert(tempFile != nil || error != nil);
tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{
self->_tempFile = tempFile;
if (!self->_tempFile) {
[self _network_fail:error];
} else {
[self _network_captureResponseFromTaskIfNeeded:downloadTask URLSession:session];
[self _network_restartIdleTimer];
}
});
}
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{
[self _network_didUpdateTotalBytesReceived:totalBytesWritten
expectedBytes:totalBytesExpectedToWrite
URLSession:session
downloadTask:downloadTask];
});
}
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes
{
tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{
[self _network_didUpdateTotalBytesReceived:fileOffset
expectedBytes:expectedTotalBytes
URLSession:session
downloadTask:downloadTask];
});
}
#pragma mark - Decoding
- (void)_decodeData:(NSData *)data
completion:(void(^)(NSData * __nullable decodedData, NSError * __nullable decodeError))completion
{
if (!_contentDecoderContext) {
tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{
completion(data, nil);
});
return;
}
const uint64_t decodeStartMachTime = mach_absolute_time();
tnl_dispatch_async_autoreleasing(tnl_coding_queue(), ^{
NSError *error = nil;
const BOOL decodeSuccess = [self->_contentDecoder tnl_decode:self->_contentDecoderContext
additionalData:data
error:&error];
if (!decodeSuccess) {
error = TNLErrorCreateWithCodeAndUnderlyingError(TNLErrorCodeRequestOperationRequestContentDecodingFailed, error);
}
tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{
const NSTimeInterval decodeLatency = TNLComputeDuration(decodeStartMachTime, mach_absolute_time());
self->_responseDecodeLatency += decodeLatency;
[self _network_flushDecoding:error completion:completion];
});
});
}
- (void)_network_flushDecoding:(nullable NSError *)error
completion:(void(^)(NSData * __nullable decodedData, NSError * __nullable decodeError))completion
{
if (error) {
_contentDecoderContext = nil;
_contentDecoderRecentData = nil;
completion(nil, error);
} else {
// flush the recent data
NSData *recentData = _contentDecoderRecentData;
_contentDecoderRecentData = nil;
completion(recentData, nil);
}
}
- (BOOL)tnl_dataWasDecoded:(NSData *)data error:(out NSError **)error
{
tnl_dispatch_async_autoreleasing(tnl_network_queue(), ^{
if (!self->_contentDecoderRecentData) {
self->_contentDecoderRecentData = [data mutableCopy];
} else {
[self->_contentDecoderRecentData appendData:data];
}
});
if (error) {
*error = nil;
}
return YES; // defer the error to the append data handler
}
@end
#pragma mark - TNLURLSessionTaskOperation (Network)
@implementation TNLURLSessionTaskOperation (Network)
#pragma mark Properties
- (TNLAttemptMetaData *)network_metaDataWithLowerCaseHeaderFields:(nullable NSDictionary *)lowerCaseHeaderFields
{
NSURLSessionTask *task = self.URLSessionTask;
TNLAttemptMetaData *metaData = [[TNLAttemptMetaData alloc] init];
metaData.HTTPVersion = @"1.1";
metaData.sessionId = _URLSession.sessionDescription;
if (task) {
NSHTTPURLResponse *response = self.URLResponse;
TNLAssert(!response || response == task.response || response == _cancelledRedirectResponse);
metaData.responseLowercaseHeaders = lowerCaseHeaderFields;
if (_layer8BodyBytesReceived >= 0) {
metaData.layer8BodyBytesReceived = _layer8BodyBytesReceived;
}
if (task.countOfBytesSent >= 0) {
metaData.layer8BodyBytesTransmitted = task.countOfBytesSent;
}
if (task.countOfBytesExpectedToSend >= 0) {
metaData.requestContentLength = task.countOfBytesExpectedToSend;
}
const long long contentLength = [response tnl_expectedResponseBodySize];
if (contentLength >= 0) {
metaData.responseContentLength = contentLength;
} else if (task.countOfBytesExpectedToReceive >= 0) {
metaData.responseContentLength = task.countOfBytesExpectedToReceive;
}
NSString *responseTime = lowerCaseHeaderFields[@"x-response-time"];
if (responseTime) {
metaData.serverResponseTime = [responseTime longLongValue];
}
if (_hashData) {
metaData.responseBodyHashAlgorithm = _hashAlgo;
metaData.responseBodyHash = _hashData;
}
metaData.localCacheHit = response.tnl_wasCachedResponse;
if (_responseBodyEndDate && _responseBodyStartDate) {
metaData.responseContentDownloadDuration = [_responseBodyEndDate timeIntervalSinceDate:_responseBodyStartDate];
}
if (_responseDecodeLatency > 0) {
metaData.responseDecodingLatency = _responseDecodeLatency;
}
if (_layer8BodyBytesReceived > 0) {
metaData.responseDecodedContentLength = _layer8BodyBytesReceived;
}
if (_taskResumeDate) {
// indicates we also have task resume priority
metaData.taskResumePriority = _taskResumePriority;
}
if (_taskMetrics && _taskResumeDate) {
const NSTimeInterval fetchResumeDelta = [_taskMetrics.transactionMetrics.firstObject.fetchStartDate timeIntervalSinceDate:_taskResumeDate];
if (fetchResumeDelta > 1.0 || fetchResumeDelta < -1.0) {
metaData.taskResumeLatency = fetchResumeDelta;
}
}
if (_completionCallbackDate) {
// we were going to capture task metrics, see if there's any discrepency
if (_taskMetricsCallbackDate) {
const NSTimeInterval taskMetricsLatency = [_taskMetricsCallbackDate timeIntervalSinceDate:_completionCallbackDate];
if (taskMetricsLatency > 0.0) {
// should not get task metrics after completion!
metaData.taskMetricsAfterCompletionLatency = taskMetricsLatency;
}
} else {
NSDate *dateNow = [NSDate date];
const NSTimeInterval latencySinceCompletion = [dateNow timeIntervalSinceDate:_completionCallbackDate];
if (latencySinceCompletion >= 0.100) {
// should really be no latency, so capture any meaningful latency
metaData.taskWithoutMetricsCompletionLatency = latencySinceCompletion;
}
}
}
}
return metaData;
}
- (nullable NSURLSessionTaskMetrics *)network_taskMetrics
{
return _taskMetrics;
}
- (float)_network_uploadProgress
{
const int64_t bytesToSend = self.URLSessionTask.countOfBytesExpectedToSend;
const int64_t bytesSent = self.URLSessionTask.countOfBytesSent;
NSURLResponse *response = self.URLResponse;
if (TNLRequestOperationStateSucceeded == atomic_load(&_internalState) || response != nil) {
return 1.0f;
} else if (bytesToSend <= 0) {
// Non-deterministic
return 0.0f;
}
const double doubleProgress = (double)bytesSent / (double)(bytesToSend);
// progress can be > 1.0 if bytesSent is > bytesToSend
return (float)doubleProgress;
}
- (float)_network_downloadProgress
{
const int64_t bytesToReceive = self.URLSessionTask.countOfBytesExpectedToReceive;
const int64_t bytesReceived = self.URLSessionTask.countOfBytesReceived;
NSHTTPURLResponse *response = self.URLResponse;
if (nil != [response tnl_contentEncoding]) {
// Non-deterministic
return 0.0f;
}
if (TNLRequestOperationStateSucceeded == atomic_load(&_internalState)) {
return 1.0f;
} else if (bytesToReceive <= 0) {
// Non-deterministic
return 0.0f;
}
const double doubleProgress = (double)(bytesReceived) / (double)(bytesToReceive);
// progress can be > 1.0 if bytesReceived is > bytesToReceive
return (float)doubleProgress;
}
#pragma mark NSOperation
- (BOOL)_network_shouldCancel
{
if (TNLRequestOperationStateIsFinal(atomic_load(&_internalState))) {
return NO;
}
TNLRequestOperation *requestOperation = _requestOperation;
if (requestOperation != nil) {
return NO;
}
if ([_error.domain isEqualToString:TNLErrorDomain] && _error.code == TNLErrorCodeRequestOperationCancelled) {
// Already cancelling
return NO;
}
if (_requestConfiguration.URLCache != nil) {
// Has a cache, should we permit it to finish?
// TODO:[nobrien] - use better heuristics here
if ([self _currentRequestHasBody]) {
return [self _network_uploadProgress] < 0.9;
} else {
return [self _network_downloadProgress] < 0.9;
}
}
return YES;
}
#pragma mark Methods
- (void)_network_willResumeSessionTaskWithRequest:(NSURLRequest *)resumeRequest
{
[_requestOperation network_URLSessionTaskOperation:self
didStartSessionTaskWithRequest:resumeRequest];
}
- (void)_network_resumeSessionTask:(NSURLSessionTask *)task
{
// NSURLSessionTask's `resume` will capture the QOS of the calling queue for
// reusing the same QOS for the execution of the task
const TNLPriority requestPriority = self.requestPriority;
dispatch_sync(dispatch_get_global_queue((long)TNLConvertTNLPriorityToGCDQOS(requestPriority), 0), ^{
self->_taskResumeDate = [NSDate date];
self->_taskResumePriority = requestPriority;
[task resume];
});
}
- (void)_network_didStartTask:(BOOL)isBackgroundRequest
{
NSUInteger taskId = self.URLSessionTask.taskIdentifier;
NSString *configId = _URLSession.configuration.identifier;
NSString *sharedContainerIdentifier = _URLSession.configuration.sharedContainerIdentifier;
[_requestOperation network_URLSessionTaskOperation:self
didStartTaskWithTaskIdentifier:taskId
configIdentifier:configId
sharedContainerIdentifier:sharedContainerIdentifier
isBackgroundRequest:isBackgroundRequest];
}
- (void)_network_updatePriorities
{
TNLPriority pri = TNLPriorityVeryLow;
TNLRequestOperation *strongRequestOp = _requestOperation;
if (strongRequestOp) {
TNLPriority opPri = strongRequestOp.priority;
if (opPri > pri) {
pri = opPri;
}
}
_requestPriority = pri;
self.URLSessionTask.priority = TNLConvertTNLPriorityToURLSessionTaskPriority(self->_requestPriority);
// Apple discourages modifying NSOperation properties once an operation has been added to an
// NSOperationQueue. Crashing has been reproduced when modifying the queuePriority while an
// NSOperation is executing so we will prevent mutating these priorities if that request has
// started.
if (!_requestOperationQueue && !self.isReady) {
self.queuePriority = TNLConvertTNLPriorityToQueuePriority(self->_requestPriority);
self.qualityOfService = TNLConvertTNLPriorityToQualityOfService(self->_requestPriority);
}
}
- (void)_network_buildResponseInfo
{
if (!_responseInfo) {
_responseInfo = [[TNLResponseInfo alloc] initWithFinalURLRequest:self.currentURLRequest
URLResponse:self.URLResponse
source:self.responseSource
data:_storedData
temporarySavedFile:_tempFile];
}
}
- (void)_network_buildInternalResponse
{
TNLAttemptMetaData *metadata = [self network_metaDataWithLowerCaseHeaderFields:_responseInfo.allHTTPHeaderFieldsWithLowerCaseKeys];
TNLAttemptMetrics *attemptMetrics = [[TNLAttemptMetrics alloc] initWithType:TNLAttemptTypeInitial
startDate:_startDate
startMachTime:_startMachTime
endDate:_endDate
endMachTime:_endMachTime
metaData:metadata
URLRequest:self.currentURLRequest
URLResponse:self.URLResponse
operationError:self.error];
TNLResponseMetrics *metrics = [[TNLResponseMetrics alloc] initWithEnqueueDate:_startDate
enqueueTime:_startMachTime
completeDate:_completeDate
completeTime:_completeMachTime
attemptMetrics:@[attemptMetrics]];
TNLResponse *response = [_responseClass responseWithRequest:self.originalRequest
operationError:_error
info:_responseInfo
metrics:metrics];
_finalResponse = response;
}
- (void)_network_fail:(NSError *)error
{
if (self.isComplete || self.isFinalizing) {
return;
}
TNLAssert(error != nil);
_flags.shouldCaptureResponse = 0; // don't handle responses anymore
_contentDecoderContext = nil; // don't decode anymore
_contentDecoderRecentData = nil;
if (!_flags.didStart) {
_cachedFailure = error;
return;
}
const BOOL didCancel = [error.domain isEqualToString:TNLErrorDomain] && (error.code == TNLErrorCodeRequestOperationCancelled);
_flags.didCancel = didCancel;
// don't use network_cancel
[self.URLSessionTask cancel];
[self _network_stopIdleTimer];
_error = error;
TNLLogDebug(@"%@ error: %@", self, _error);
[self _network_finishHashWithSuccess:NO];
[self _network_buildResponseInfo];
TNLAssert(_responseInfo);
[self _network_finalizeWithState:(didCancel) ? TNLRequestOperationStateCancelled : TNLRequestOperationStateFailed];
// discard temporary file at the end (if needed)
if (_flags.shouldDeleteUploadFile) {
[[NSFileManager defaultManager] removeItemAtPath:_uploadFilePath error:NULL];
_flags.shouldDeleteUploadFile = NO;
}
}
- (void)_network_cancel
{
TNLAssert(!_flags.didCancel);
[self _network_fail:TNLErrorCreateWithCode(TNLErrorCodeRequestOperationCancelled)];
}
- (void)_network_complete
{
if (self.isComplete || self.isFinalizing) {
return;
}
_flags.shouldCaptureResponse = 0; // don't handle responses anymore
_contentDecoderContext = nil; // don't decode anymore
_contentDecoderRecentData = nil;
_responseBodyEndDate = [NSDate date];
[self _network_finishHashWithSuccess:YES];
[self _network_buildResponseInfo];
TNLAssert(_responseInfo);
[self _network_finalizeWithState:TNLRequestOperationStateSucceeded];
}
- (void)_network_completeCachedCompletionIfPossible
{
if (_flags.encounteredCompletionBeforeTaskMetrics) {
NSURLSession *session = _cachedCompletionSession;
if (session) {
NSURLSessionTask *task = _cachedCompletionTask;
NSError *error = _cachedCompletionError;
_cachedCompletionSession = nil;
_cachedCompletionTask = nil;
_cachedCompletionError = nil;
[self URLSession:session task:task didCompleteWithError:error];
} // else, already completed
}
}
- (void)_network_captureResponseFromTaskIfNeeded:(NSURLSessionTask *)task
URLSession:(NSURLSession *)session
{
if (_flags.shouldCaptureResponse && task) {
NSURLResponse *response = task.response;
if (response) {
[self _network_didReceiveResponse:response URLSession:session task:task];
}
}
}
- (void)_network_didReceiveResponse:(NSURLResponse *)response
URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
{
if (self.isComplete || self.isFinalizing) {
return;
} else if ([self _network_shouldCancel]) {
[self _network_cancel];
return;
}
[self _network_transitionToState:TNLRequestOperationStateRunning];
TNLAssertMessage([response isKindOfClass:[NSHTTPURLResponse class]], @"%@ is not an 'NSHTTPURLResponse'", response);
TNLAssert(task.response == self.URLResponse);
if (gTwitterNetworkLayerAssertEnabled) {
// instances might not match, ensure contents match
NSHTTPURLResponse *taskResponse = (NSHTTPURLResponse *)task.response;
TNLAssert([taskResponse.URL isEqual:response.URL]);
TNLAssert(taskResponse.allHeaderFields.count == [(NSHTTPURLResponse *)response allHeaderFields].count);
TNLAssert(taskResponse.statusCode == [(NSHTTPURLResponse *)response statusCode]);
}
NSError *decodingError = nil;
NSString *acceptEncoding = [[(NSHTTPURLResponse *)response allHeaderFields] tnl_objectForCaseInsensitiveKey:@"Content-Encoding"];
if (acceptEncoding) {
acceptEncoding = [acceptEncoding lowercaseString];
_contentDecoder = self->_additionalDecoders[acceptEncoding];
if (_contentDecoder) {
_contentDecoderContext = [_contentDecoder tnl_initializeDecodingWithContentEncoding:acceptEncoding
client:self
error:&decodingError];
}
}
_flags.shouldCaptureResponse = 0;
_responseBodyStartDate = [NSDate date];
[self _network_updateUploadProgress:[self _network_uploadProgress]];
[self _network_updateDownloadProgress:[self _network_downloadProgress]];
[_requestOperation network_URLSessionTaskOperation:self
didReceiveURLResponse:response];
if (decodingError) {
[self _network_fail:TNLErrorCreateWithCodeAndUnderlyingError(TNLErrorCodeRequestOperationRequestContentDecodingFailed, decodingError)];
return;
}
[self _network_restartIdleTimer];
}
- (void)_network_didUpdateTotalBytesReceived:(int64_t)bytesReceived
expectedBytes:(int64_t)totalBytesExpectedToReceive
URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
{
if (self.isComplete || self.isFinalizing) {
return;
} else if ([self _network_shouldCancel]) {
[self _network_cancel];
return;
}
[self _network_transitionToState:TNLRequestOperationStateRunning];
TNLAssert([[downloadTask response] isKindOfClass:[NSHTTPURLResponse class]]);
TNLAssert(downloadTask.response != nil);
[self _network_captureResponseFromTaskIfNeeded:downloadTask URLSession:session];
[self _network_updateDownloadProgress:[self _network_downloadProgress]];
[self _network_restartIdleTimer];
}
- (void)_network_updateUploadProgress:(float)progress
{
if (self.isComplete) {
return;
} else if ([self _network_shouldCancel]) {
[self _network_cancel];
return;
}
[_requestOperation network_URLSessionTaskOperation:self
didUpdateUploadProgress:progress];
}
- (void)_network_updateDownloadProgress:(float)progress
{
if (self.isComplete) {
return;
} else if ([self _network_shouldCancel]) {
[self _network_cancel];
return;
}
[_requestOperation network_URLSessionTaskOperation:self
didUpdateDownloadProgress:progress];
}
- (nullable NSError *)_network_appendDecodedData:(nullable NSData *)data
{
if (self.isComplete || self.isFinalizing) {
return nil;
} else if ([self _network_shouldCancel]) {
[self _network_cancel];
return nil;
}
NSError *error = nil;
[self _network_transitionToState:TNLRequestOperationStateRunning];
[self _network_updateHashWithData:data];
NSHTTPURLResponse *URLResponse = self.URLResponse;
_layer8BodyBytesReceived += data.length;
switch (_requestConfiguration.responseDataConsumptionMode) {
case TNLResponseDataConsumptionModeNone:
break;
case TNLResponseDataConsumptionModeStoreInMemory:
@try {
if (!_storedData) {
// We want the buffer of the mutable data to be the best guess we can offer to
// prevent continuous reallocs.
NSUInteger capacity = 0;
long long expectedDataSize = [URLResponse tnl_expectedResponseBodyExpandedDataSize];
// NOTE - it is possible that the expectedDataSize is the Content-Length which
// is the compressed size so it will be too small for the decompressed size.
// However, we know that the content will be at least as much as the
// Content-Length when decompressed so setting the capacity to the
// Content-Length will avoid the initial reallocs, but will still incur later
// reallocs so it is no worse than not setting the capacity.
if (expectedDataSize >= (LONG_MAX - EXTRA_DOWNLOAD_BYTES_BUFFER)) {
capacity = LONG_MAX;
} else if (expectedDataSize > 0) {
capacity = (NSUInteger)expectedDataSize + EXTRA_DOWNLOAD_BYTES_BUFFER;
}
_storedData = (capacity > 0) ? [NSMutableData dataWithCapacity:capacity] : [NSMutableData data];
}
if (data) {
[_storedData appendData:data];
}
} @catch (NSException *exception) {
TNLLogError(@"Append Data Exception: %@", exception);
error = TNLErrorCreateWithCodeAndUserInfo(TNLErrorCodeRequestOperationAppendResponseDataError,
@{ @"exception" : exception });
_storedData = nil;
}
break;
case TNLResponseDataConsumptionModeSaveToDisk:
TNLAssertNever(); // should be a download task
break;
case TNLResponseDataConsumptionModeChunkToDelegateCallback:
if (data) {
[_requestOperation network_URLSessionTaskOperation:self appendReceivedData:data];
}
break;
}
return error;
}
- (void)_network_finalizeWithState:(TNLRequestOperationState)state
{
[self _network_finalizeWithResponseCompletion:^(TNLResponse * __nullable finalResponse) {
TNLAssertIsNetworkQueue();
if (finalResponse) {
self->_finalResponse = finalResponse;
}
if (!self->_finalResponse) {
[self _network_buildInternalResponse];
}
TNLAssert(self->_finalResponse);
self->_responseInfo = self->_finalResponse.info;
[self _network_transitionToState:state];
self->_flags.isFinalizing = NO;
}];
}
- (void)_network_finalizeWithResponseCompletion:(TNLRequestMakeFinalResponseCompletionBlock)completion
{
if (self.isComplete || self.isFinalizing) {
return;
} else if ([self _network_shouldCancel]) {
[self _network_cancel];
return;
}
_flags.isFinalizing = YES;
_endMachTime = mach_absolute_time();
_endDate = [NSDate date];
TNLRequestOperation *strongRequestOp = _requestOperation;
if (strongRequestOp) {
NSURLSessionTaskMetrics *taskMetrics = _taskMetrics;
TNLAttemptMetaData *metaData = [self network_metaDataWithLowerCaseHeaderFields:_responseInfo.allHTTPHeaderFieldsWithLowerCaseKeys];
[strongRequestOp network_URLSessionTaskOperation:self
finalizeWithResponseInfo:_responseInfo
responseError:_error
metaData:metaData
taskMetrics:taskMetrics
completion:completion];
} else {
completion(nil);
}
}
- (void)_network_transitionToState:(TNLRequestOperationState)state
{
if (self.isComplete) {
return;
} else if (self.isFinalizing) {
TNLAssert(TNLRequestOperationStateIsFinal(state));
} else if ([self _network_shouldCancel]) {
// If we are finalizing or moving to be completed, no need to pre-emtively cancel
if (!TNLRequestOperationStateIsFinal(state)) {
[self _network_cancel];
return;
}
}
TNLRequestOperationState oldState = atomic_load(&_internalState);
if (oldState != state) {
if (TNLRequestOperationStateRunning == state && TNLRequestOperationStateStarting != oldState) {
return;
}
if (gTwitterNetworkLayerAssertEnabled) {
switch (state) {
case TNLRequestOperationStateIdle:
case TNLRequestOperationStatePreparingRequest:
case TNLRequestOperationStateWaitingToRetry:
TNLAssertNever();
break;
case TNLRequestOperationStateStarting:
case TNLRequestOperationStateRunning:
TNLAssertMessage(oldState < state, @"oldState (%ld) < newState (%ld)", (long)oldState, (long)state);
break;
case TNLRequestOperationStateCancelled:
case TNLRequestOperationStateFailed:
case TNLRequestOperationStateSucceeded:
TNLAssert(!TNLRequestOperationStateIsFinal(oldState));
TNLAssert(_finalResponse != nil);
break;
}
}
// KVO - Prep
TNLRequestOperation *strongRequestOp = _requestOperation;
BOOL cancelDidChange = NO;
BOOL finishedDidChange = NO;
BOOL executingDidChange = NO;
if (TNLRequestOperationStateIsFinal(state)) {
cancelDidChange = (TNLRequestOperationStateCancelled == state);
finishedDidChange = YES;
executingDidChange = YES;
} else if (TNLRequestOperationStateIdle == oldState) {
executingDidChange = YES;
}
// Will transition logic
if (finishedDidChange) {
// will transition to isFinished
// validate "didStart" flag
if (!_flags.didStart) {
TNLLogError(@"%@ changed stated to be final before being started!\n%@", NSStringFromClass([self class]), self.originalURLRequest);
}
TNLAssert(_flags.didStart);
// stop handling responses
_flags.shouldCaptureResponse = 0;
// check for backoff signal
TNLResponseInfo *info = _finalResponse.info;
const TNLHTTPStatusCode statusCode = info.statusCode;
NSDictionary *headers = info.allHTTPHeaderFieldsWithLowerCaseKeys;
NSString *host = nil;
NSURL *URL = _hydratedURLRequest.URL;
if ([TNLGlobalConfiguration sharedInstance].shouldBackoffUseOriginalRequestHost) {
host = [_originalRequest URL].host;
}
const BOOL shouldSignal = [[TNLGlobalConfiguration sharedInstance].backoffSignaler tnl_shouldSignalBackoffForURL:URL
host:host
statusCode:statusCode
responseHeaders:headers];
if (shouldSignal) {
[TNLNetwork backoffSignalEncounteredForURL:URL
host:host
responseHTTPHeaders:headers];
}
}
if (executingDidChange) {
if (TNLRequestOperationStateIdle == oldState && !finishedDidChange) {
// will transition to isExecuting
if (!_flags.didIncrementExecutionCount && _requestConfiguration.contributeToExecutingNetworkConnectionsCount) {
_flags.didIncrementExecutionCount = YES;
[TNLNetwork incrementExecutingNetworkConnections];
}
}
}
// Timestamps
[self _network_updateTimestampsWithState:state];
// KVO - transition
if (finishedDidChange) {
[self willChangeValueForKey:@"isFinished"];
}
if (cancelDidChange) {
[self willChangeValueForKey:@"isCancelled"];
}
if (executingDidChange) {
[self willChangeValueForKey:@"isExecuting"];
}
atomic_store(&_internalState, state);
if (finishedDidChange) {
// Last chance to update progress
[strongRequestOp network_URLSessionTaskOperation:self
didUpdateUploadProgress:[self _network_uploadProgress]];
[strongRequestOp network_URLSessionTaskOperation:self
didUpdateDownloadProgress:[self _network_downloadProgress]];
}
[strongRequestOp network_URLSessionTaskOperation:self
didTransitionToState:state
withResponse:_finalResponse];
if (executingDidChange) {
[self didChangeValueForKey:@"isExecuting"];
}
if (cancelDidChange) {
[self didChangeValueForKey:@"isCancelled"];
}
if (finishedDidChange) {
[self didChangeValueForKey:@"isFinished"];
}
// Log transition
TNLLogDebug(@"%@: %@ -> %@", self, TNLRequestOperationStateToString(oldState), TNLRequestOperationStateToString(state));
// Did transition logic
if (finishedDidChange) {
// Did transition to isFinished
// Decrement execution count
if (_flags.didIncrementExecutionCount) {
[TNLNetwork decrementExecutingNetworkConnections];
_flags.didIncrementExecutionCount = NO;
}
// Anonymous completion
if (!strongRequestOp) {
[self.requestOperationQueue taskOperation:self
didCompleteAttempt:_finalResponse];
}
}
// Background Completion
if (finishedDidChange && _executionMode == TNLRequestExecutionModeBackground && (_downloadTask != nil || _uploadTask != nil)) {
NSString *sharedContainerIdentifier = _URLSession.configuration.sharedContainerIdentifier;
TNLAssert(_finalResponse);
[_sessionManager URLSessionDidCompleteBackgroundTask:self.URLSessionTask.taskIdentifier
sessionConfigIdentifier:_URLSession.configuration.identifier
sharedContainerIdentifier:sharedContainerIdentifier
request:self.originalURLRequest
response:_finalResponse];
}
}
}
- (void)_network_updateTimestampsWithState:(TNLRequestOperationState)state
{
if (!_startMachTime) {
_startDate = [NSDate date];
_startMachTime = mach_absolute_time();
}
if (TNLRequestOperationStateIsFinal(state)) {
NSDate *dateNow = [NSDate date];
const uint64_t machTime = mach_absolute_time();
_completeMachTime = machTime;
_completeDate = dateNow;
if (!_endMachTime) {
_endDate = dateNow;
_endMachTime = machTime;
}
TNLAssert(_finalResponse);
TNLAssert(_completeMachTime > 0);
if (!_finalResponse.metrics.completeDate) {
[_finalResponse.metrics setCompleteDate:_completeDate machTime:_completeMachTime];
}
}
}
- (void)_network_createTaskWithRequest:(NSURLRequest *)request
prototype:(id<TNLRequest>)requestPrototype
completion:(void(^)(NSURLSessionTask *createdTask, NSError *error))complete
{
TNLAssert(!self.URLSessionTask);
TNLAssert(request);
TNLAssert(!self.originalURLRequest);
TNLAssert(_requestConfiguration);
NSError *error = nil;
NSURLSessionTask *task = nil;
task = [self _network_populateURLSessionTaskWithRequest:request
prototype:requestPrototype
error:&error];
if (!error) {
_resumeData = nil;
_taskRequest = request;
task.priority = TNLConvertTNLPriorityToURLSessionTaskPriority(self->_requestPriority);
TNLRequestConfigurationAssociateWithRequest(self.requestConfiguration, task.originalRequest ?: self->_taskRequest);
}
TNLAssert((nil == error) ^ (nil == task));
complete(task, error);
}
- (nullable NSURLSessionTask *)_network_populateURLSessionTaskWithRequest:(NSURLRequest *)request
prototype:(id<TNLRequest>)requestPrototype
error:(out NSError * __nullable * __nonnull)errorOut
{
TNLAssert(errorOut != NULL);
NSError *error = nil;
const BOOL hasBody = TNLURLRequestHasBody(request, requestPrototype);
const BOOL isDownload = TNLResponseDataConsumptionModeSaveToDisk == _requestConfiguration.responseDataConsumptionMode;
const BOOL isBackground = TNLRequestExecutionModeBackground == _requestConfiguration.executionMode;
@try {
if (isDownload) {
if (hasBody) {
TNLAssertNever(); // should have been caught by TNLRequestValidate(...)
error = TNLErrorCreateWithCode(TNLErrorCodeRequestHTTPBodyCannotBeSetForDownload);
} else {
// GET can support resume data
if (_resumeData && TNLHTTPMethodGET == TNLRequestGetHTTPMethodValue(request)) {
// resume data is easily invalidated, wrap our task creation in a try/catch so we can fallback to just a normal download task
@try {
_downloadTask = [_URLSession downloadTaskWithResumeData:_resumeData];
} @catch (NSException *exception) {
TNLLogWarning(@"%@", exception);
}
}
if (!_downloadTask) {
_downloadTask = [_URLSession downloadTaskWithRequest:request];
}
}
} else {
if (isBackground) {
if ([requestPrototype respondsToSelector:@selector(HTTPBodyFilePath)] && requestPrototype.HTTPBodyFilePath) {
// OK
} else if ([request respondsToSelector:@selector(HTTPBody)] && request.HTTPBody) {
// OK
} else {
TNLAssertNever(); // should have been caught by TNLRequestValidate(...)
error = TNLErrorCreateWithCode(TNLErrorCodeRequestInvalidBackgroundRequest);
}
}
if (hasBody && !error) {
if (request.HTTPBody) {
if (!isBackground) {
_uploadData = request.HTTPBody;
_uploadTask = [_URLSession uploadTaskWithRequest:request
fromData:_uploadData];
} else {
// NSURLSessionUploadTask cannot upload anything other than a file in the background.
// Let's help plug that hole by automatically writing the data to a file so
// NSURLSessionUploadTask won't fail
_uploadFilePath = TNLWriteDataToTemporaryFile(request.HTTPBody);
NSURL *uploadFileURL = [NSURL fileURLWithPath:_uploadFilePath isDirectory:NO];
_uploadTask = [_URLSession uploadTaskWithRequest:request
fromFile:uploadFileURL];
_flags.shouldDeleteUploadFile = YES;
}
} else if ([requestPrototype respondsToSelector:@selector(HTTPBodyFilePath)] && requestPrototype.HTTPBodyFilePath) {
_uploadFilePath = requestPrototype.HTTPBodyFilePath;
NSURL *uploadFileURL = [NSURL fileURLWithPath:_uploadFilePath isDirectory:NO];
_uploadTask = [_URLSession uploadTaskWithRequest:request
fromFile:uploadFileURL];
} else if ([requestPrototype respondsToSelector:@selector(HTTPBodyStream)] && requestPrototype.HTTPBodyStream) {
_uploadTask = [_URLSession uploadTaskWithStreamedRequest:request];
} else {
TNLAssertNever(); // where's the body?
}
}
// Not an upload task?
if (!_uploadTask && !error) {
if (isBackground) {
TNLAssertNever(); // should have been caught by TNLRequestValidate(...)
error = TNLErrorCreateWithCode(TNLErrorCodeRequestInvalidBackgroundRequest);
} else {
_dataTask = [_URLSession dataTaskWithRequest:request];
}
}
}
}
@catch (NSException *exception) {
if ([exception.name isEqualToString:NSInternalInconsistencyException]) {
// rethrow assertion exceptions
@throw exception;
}
// iOS 9 introduced a new exception.
// It used to be the case that when you created an NSURLSessionTask with an invalidated session
// you would just get the URLSession:didBecomeInvalidWithError: callback.
// Now it is possible for an exception to be thrown - so we need to handle it.
// The exception is an NSGenericException...so not exactly as concrete as a specific error.
// We'll just handle any exception (except assertion exceptions) and coerse it into an invalidated session error
// so that our session retry code will kick in.
TNLLogError(@"Exception creating NSURLSessionTask! %@", exception);
error = [NSError errorWithDomain:TNLErrorDomain
code:TNLErrorCodeRequestOperationURLSessionInvalidated
userInfo:@{ @"exception" : exception }];
}
if (!self.URLSessionTask && !error) {
NSString *reason = @"Underlying NSURLSessionTask was not populated and no error provided";
TNLAssertMessage(error || self.URLSessionTask, @"%@", reason);
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:reason
userInfo:nil];
}
if (error) {
*errorOut = error;
}
return self.URLSessionTask;
}
#pragma mark Timer
- (void)_network_startIdleTimerWithDeferralDuration:(NSTimeInterval)deferral
{
TNLAssert(!_idleTimer);
if (_flags.useIdleTimeout && _requestConfiguration.executionMode != TNLRequestExecutionModeBackground) {
const NSTimeInterval idleTimeout = _requestConfiguration.idleTimeout;
if (idleTimeout >= MIN_TIMER_INTERVAL) {
__weak typeof(self) weakSelf = self;
_idleTimer = tnl_dispatch_timer_create_and_start(tnl_network_queue(), idleTimeout, TIMER_LEEWAY_WITH_FIRE_INTERVAL(MAX(deferral, 0.0) + idleTimeout), NO, ^{
[weakSelf _network_idleTimerFired];
});
}
}
}
- (void)_network_stopIdleTimer
{
tnl_dispatch_timer_invalidate(_idleTimer);
_idleTimer = NULL;
}
- (void)_network_restartIdleTimer
{
[self _network_stopIdleTimer];
[self _network_startIdleTimerWithDeferralDuration:0.0];
}
- (void)_network_idleTimerFired
{
if (_idleTimer) {
[self _network_stopIdleTimer];
if (!self.isComplete && !self.isFinalizing) {
[self _network_fail:TNLErrorCreateWithCode(TNLErrorCodeRequestOperationIdleTimedOut)];
}
}
}
#pragma mark Hash
NS_INLINE void* __nullable _mallocAndInitHashContext(TNLResponseHashComputeAlgorithm algo)
{
#define INIT_HASH(hash) ({ \
contextRef = malloc(sizeof(CC_##hash##_CTX)); \
CC_##hash##_Init((CC_##hash##_CTX *)contextRef); \
})
void* contextRef = NULL;
switch (algo) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
case TNLResponseHashComputeAlgorithmMD2:
INIT_HASH(MD2);
#pragma clang diagnostic pop
break;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
case TNLResponseHashComputeAlgorithmMD4:
INIT_HASH(MD4);
#pragma clang diagnostic pop
break;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
case TNLResponseHashComputeAlgorithmMD5:
INIT_HASH(MD5);
#pragma clang diagnostic pop
break;
case TNLResponseHashComputeAlgorithmSHA1:
INIT_HASH(SHA1);
break;
case TNLResponseHashComputeAlgorithmSHA256:
INIT_HASH(SHA256);
break;
case TNLResponseHashComputeAlgorithmSHA512:
INIT_HASH(SHA512);
break;
case TNLResponseHashComputeAlgorithmNone:
default:
break;
}
#undef INIT_HASH
return contextRef;
}
NS_INLINE void _updateHash(TNLResponseHashComputeAlgorithm algo, void * __nullable contextRef, const void *data, CC_LONG len)
{
if (!contextRef) {
return;
}
#define UPDATE_HASH(hash) ({ \
CC_##hash##_Update((CC_##hash##_CTX *)contextRef, data, len); \
})
switch (algo) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
case TNLResponseHashComputeAlgorithmMD2:
UPDATE_HASH(MD2);
#pragma clang diagnostic pop
break;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
case TNLResponseHashComputeAlgorithmMD4:
UPDATE_HASH(MD4);
#pragma clang diagnostic pop
break;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
case TNLResponseHashComputeAlgorithmMD5:
UPDATE_HASH(MD5);
#pragma clang diagnostic pop
break;
case TNLResponseHashComputeAlgorithmSHA1:
UPDATE_HASH(SHA1);
break;
case TNLResponseHashComputeAlgorithmSHA256:
UPDATE_HASH(SHA256);
break;
case TNLResponseHashComputeAlgorithmSHA512:
UPDATE_HASH(SHA512);
break;
case TNLResponseHashComputeAlgorithmNone:
default:
break;
}
#undef UPDATE_HASH
}
NS_INLINE NSData * __nullable _finalizeHash(TNLResponseHashComputeAlgorithm algo, void * __nullable contextRef, BOOL success)
{
if (!contextRef) {
return nil;
}
unsigned char *hashBuffer = NULL;
NSData *hashData = nil;
#define FINALIZE_HASH(hash) ({ \
hashBuffer = (unsigned char *)malloc(CC_##hash##_DIGEST_LENGTH); \
hashData = [NSData dataWithBytesNoCopy:hashBuffer length:CC_##hash##_DIGEST_LENGTH freeWhenDone:YES]; \
CC_##hash##_Final(hashBuffer, (CC_##hash##_CTX *)contextRef); \
})
switch (algo) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
case TNLResponseHashComputeAlgorithmMD2:
FINALIZE_HASH(MD2);
#pragma clang diagnostic pop
break;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
case TNLResponseHashComputeAlgorithmMD4:
FINALIZE_HASH(MD4);
#pragma clang diagnostic pop
break;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
case TNLResponseHashComputeAlgorithmMD5:
FINALIZE_HASH(MD5);
#pragma clang diagnostic pop
break;
case TNLResponseHashComputeAlgorithmSHA1:
FINALIZE_HASH(SHA1);
break;
case TNLResponseHashComputeAlgorithmSHA256:
FINALIZE_HASH(SHA256);
break;
case TNLResponseHashComputeAlgorithmSHA512:
FINALIZE_HASH(SHA512);
break;
case TNLResponseHashComputeAlgorithmNone:
default:
break;
}
#undef FINALIZE_HASH
return (success) ? hashData : nil;
}
- (void)_network_updateHashWithData:(NSData *)data
{
if (!_flags.isComputingHash && _flags.shouldComputeHash) {
_hashContextRef = _mallocAndInitHashContext(_hashAlgo);
_flags.isComputingHash = YES;
}
if (_flags.isComputingHash) {
[data enumerateByteRangesUsingBlock:^(const void * _Nonnull bytes, NSRange byteRange, BOOL * _Nonnull stop) {
_updateHash(self->_hashAlgo, self->_hashContextRef, bytes, (CC_LONG)byteRange.length);
}];
}
}
- (void)_network_finishHashWithSuccess:(BOOL)success
{
if (_flags.isComputingHash) {
if (_hashContextRef) {
_hashData = _finalizeHash(_hashAlgo, _hashContextRef, success);
free(_hashContextRef);
_hashContextRef = NULL;
}
_flags.isComputingHash = NO;
}
}
@end
@implementation TNLFakeRequestOperation
{
TNLURLSessionTaskOperation *_ownedURLSessionTaskOperation;
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-designated-initializers"
- (instancetype)initWithURLSessionTaskOperation:(TNLURLSessionTaskOperation *)op
{
_ownedURLSessionTaskOperation = op;
return self;
}
#pragma clang pop
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
- (instancetype)init
#pragma clang diagnostic pop
{
[self doesNotRecognizeSelector:_cmd];
abort();
}
- (nullable TNLURLSessionTaskOperation *)URLSessionTaskOperation
{
return _ownedURLSessionTaskOperation;
}
- (int64_t)operationId
{
return 0;
}
- (nullable TNLRequestOperationQueue *)requestOperationQueue
{
return _ownedURLSessionTaskOperation.requestOperationQueue;
}
- (TNLRequestConfiguration *)requestConfiguration
{
return _ownedURLSessionTaskOperation.requestConfiguration;
}
- (nullable id<TNLRequestDelegate>)requestDelegate
{
return nil;
}
- (nullable id<TNLRequest>)originalRequest
{
return _ownedURLSessionTaskOperation.originalRequest;
}
- (nullable id<TNLRequest>)hydratedRequest
{
return _ownedURLSessionTaskOperation.originalURLRequest;
}
- (nullable TNLResponse *)response
{
return _ownedURLSessionTaskOperation.finalResponse;
}
- (nullable NSError *)error
{
return _ownedURLSessionTaskOperation.error;
}
- (TNLRequestOperationState)state
{
return _ownedURLSessionTaskOperation.state;
}
- (NSUInteger)attemptCount
{
return 1;
}
- (NSUInteger)retryCount
{
return 0;
}
- (NSUInteger)redirectCount
{
return 0;
}
- (float)downloadProgress
{
return [_ownedURLSessionTaskOperation _network_downloadProgress];
}
- (float)uploadProgress
{
return [_ownedURLSessionTaskOperation _network_uploadProgress];
}
- (nullable id)context
{
return nil;
}
- (TNLPriority)priority
{
return _ownedURLSessionTaskOperation.requestPriority;
}
- (void)cancelWithSource:(id<TNLRequestOperationCancelSource>)source
underlyingError:(nullable NSError *)optionalUnderlyingError
{
// no-op
}
- (void)waitUntilFinished
{
// no-op
}
- (void)waitUntilFinishedWithoutBlockingRunLoop
{
// no-op
}
@end
static BOOL TNLURLRequestHasBody(NSURLRequest *request, id<TNLRequest> requestPrototype)
{
if (request.HTTPBody) {
return YES;
}
if ([requestPrototype respondsToSelector:@selector(HTTPBodyFilePath)] && requestPrototype.HTTPBodyFilePath) {
return YES;
}
if ([requestPrototype respondsToSelector:@selector(HTTPBodyStream)] && requestPrototype.HTTPBodyStream) {
return YES;
}
TNLAssertMessage(!request.HTTPBodyStream, @"TNLURLSessionTaskOperation doesn't support HTTPBodyStream!");
return NO;
}
static NSString *TNLSecCertificateDescription(SecCertificateRef cert)
{
NSString *serialNumber = nil;
NSData *serialNumberData = nil;
if (tnl_available_ios_11) {
serialNumberData = (NSData *)CFBridgingRelease(SecCertificateCopySerialNumberData(cert, NULL));
} else {
#if TARGET_OS_OSX
serialNumberData = (NSData *)CFBridgingRelease(SecCertificateCopySerialNumber(cert, NULL));
#else
#if TARGET_OS_MACCATALYST
// this is not possible since TARGET_OS_MACCATALYST starts with iOS SDK 13,
// which will hit the above line
TNLAssertNever();
#else
serialNumberData = (NSData *)CFBridgingRelease(SecCertificateCopySerialNumber(cert));
#endif
#endif
}
if (serialNumberData) {
serialNumber = [serialNumberData tnl_hexStringValue];
}
NSString *description = (NSString *)CFBridgingRelease(CFCopyDescription(cert)) ?: @"<";
if ([description hasSuffix:@">"]) {
description = [description substringToIndex:description.length - 1];
}
return [NSString stringWithFormat:@"%@, sn: '%@'>", description, serialNumber];
}
static NSArray<NSString *> *TNLSecTrustGetCertificateChainDescriptions(SecTrustRef trust)
{
if (!trust) {
return nil;
}
const CFIndex count = SecTrustGetCertificateCount(trust);
NSMutableArray<NSString *> *certChain = [[NSMutableArray alloc] initWithCapacity:(NSUInteger)count];
for (CFIndex i = 0; i < count; i++) {
SecCertificateRef cert = SecTrustGetCertificateAtIndex(trust, i);
[certChain addObject:TNLSecCertificateDescription(cert)];
}
return [certChain copy];
}
static NSString *TNLWriteDataToTemporaryFile(NSData *data)
{
static NSString *temporaryFileDir;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
temporaryFileDir = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
#if !TARGET_OS_IPHONE || TARGET_OS_MACCATALYST
// platform may be non-sandboxed, or "sandbox" may contain sym-links outside of expected sandbox
// ensure unique path using bundle-id or a UUID for safety
NSString *extra = [[NSBundle mainBundle] bundleIdentifier] ?: [kTempFilePrefix stringByAppendingString:[[NSUUID UUID] UUIDString]];
temporaryFileDir = [temporaryFileDir stringByResolvingSymlinksInPath];
if (![temporaryFileDir containsString:[NSString stringWithFormat:@"/%@/", extra]]) {
temporaryFileDir = [temporaryFileDir stringByAppendingPathComponent:extra];
}
#endif
[[NSFileManager defaultManager] createDirectoryAtPath:temporaryFileDir withIntermediateDirectories:YES attributes:nil error:NULL];
});
NSString *temporaryFilePath = [temporaryFileDir stringByAppendingPathComponent:[kTempFilePrefix stringByAppendingString:[[NSUUID UUID] UUIDString]]];
[data writeToFile:temporaryFilePath atomically:YES];
return temporaryFilePath;
}
NS_ASSUME_NONNULL_END