Source/TNLError.m (256 lines of code) (raw):

// // TNLError.m // TwitterNetworkLayer // // Created on 7/17/14. // Copyright © 2020 Twitter. All rights reserved. // #import "TNL_Project.h" #import "TNLError.h" #import "TNLRequestOperationCancelSource.h" NS_ASSUME_NONNULL_BEGIN NSErrorDomain const TNLErrorDomain = @"com.twitter.tnl.error.domain"; NSErrorDomain const TNLContentEncodingErrorDomain = @"com.twitter.tnl.content.encoding.error.domain"; TNLErrorInfoKey TNLErrorTimeoutTagsKey = @"timeoutTags"; TNLErrorInfoKey TNLErrorCancelSourceKey = @"cancelSource"; TNLErrorInfoKey TNLErrorCancelSourceDescriptionKey = @"cancelSourceDescription"; TNLErrorInfoKey TNLErrorCancelSourceLocalizedDescriptionKey = @"localizedCancelSourceDescription"; TNLErrorInfoKey TNLErrorCodeStringKey = @"TNLError.string"; TNLErrorInfoKey TNLErrorHostKey = @"host"; TNLErrorInfoKey TNLErrorRequestKey = @"request"; TNLErrorInfoKey TNLErrorResponseKey = @"response"; TNLErrorInfoKey TNLErrorProtectionSpaceHostKey = @"protectionSpaceHost"; TNLErrorInfoKey TNLErrorCertificateChainDescriptionsKey = @"certificateChainDescriptions"; TNLErrorInfoKey TNLErrorAuthenticationChallengeMethodKey = @"authChallengeMethod"; TNLErrorInfoKey TNLErrorAuthenticationChallengeRealmKey = @"authChallengeRealm"; TNLErrorInfoKey TNLErrorAuthenticationChallengeCancelContextKey = @"authChallengeCancelContext"; NSString *TNLErrorCodeToString(TNLErrorCode code) { #define ERROR_CASE(m) \ case TNLErrorCode##m : { return @"" #m ; } switch (code) { ERROR_CASE(RequestGenericError) ERROR_CASE(RequestInvalid) ERROR_CASE(RequestInvalidURL) ERROR_CASE(RequestInvalidHTTPMethod) ERROR_CASE(RequestHTTPBodyCannotBeSetForDownload) ERROR_CASE(RequestInvalidBackgroundRequest) ERROR_CASE(RequestOperationGenericError) ERROR_CASE(RequestOperationCancelled) ERROR_CASE(RequestOperationOperationTimedOut) ERROR_CASE(RequestOperationAttemptTimedOut) ERROR_CASE(RequestOperationIdleTimedOut) ERROR_CASE(RequestOperationCallbackTimedOut) ERROR_CASE(RequestOperationRequestNotProvided) ERROR_CASE(RequestOperationFailedToHydrateRequest) ERROR_CASE(RequestOperationInvalidHydratedRequest) ERROR_CASE(RequestOperationFileIOError) ERROR_CASE(RequestOperationAppendResponseDataError) ERROR_CASE(RequestOperationURLSessionInvalidated) ERROR_CASE(RequestOperationAuthenticationChallengeCancelled) ERROR_CASE(RequestOperationRequestContentEncodingTypeMissMatch) ERROR_CASE(RequestOperationRequestContentEncodingFailed) ERROR_CASE(RequestOperationRequestContentDecodingFailed) ERROR_CASE(RequestOperationFailedToAuthorizeRequest) ERROR_CASE(GlobalGenericError) ERROR_CASE(GlobalHostWasBlocked) ERROR_CASE(OtherGenericError) ERROR_CASE(OtherHostCannotBeEmpty) case TNLErrorCodeUnknown: return nil; } TNLAssertNever(); return nil; #undef ERROR_CASE } BOOL TNLErrorCodeIsTerminal(TNLErrorCode code) { if (TNLErrorCodeIsRequestError(code)) { return YES; } switch (code) { case TNLErrorCodeUnknown: return NO; case TNLErrorCodeRequestGenericError: case TNLErrorCodeRequestInvalid: case TNLErrorCodeRequestInvalidURL: case TNLErrorCodeRequestInvalidHTTPMethod: case TNLErrorCodeRequestHTTPBodyCannotBeSetForDownload: case TNLErrorCodeRequestInvalidBackgroundRequest: return YES; case TNLErrorCodeRequestOperationCancelled: case TNLErrorCodeRequestOperationOperationTimedOut: case TNLErrorCodeRequestOperationRequestNotProvided: case TNLErrorCodeRequestOperationFailedToHydrateRequest: case TNLErrorCodeRequestOperationInvalidHydratedRequest: case TNLErrorCodeRequestOperationFailedToAuthorizeRequest: return YES; case TNLErrorCodeOtherHostCannotBeEmpty: return YES; case TNLErrorCodeRequestOperationGenericError: case TNLErrorCodeRequestOperationAttemptTimedOut: case TNLErrorCodeRequestOperationIdleTimedOut: case TNLErrorCodeRequestOperationCallbackTimedOut: case TNLErrorCodeRequestOperationFileIOError: case TNLErrorCodeRequestOperationAppendResponseDataError: case TNLErrorCodeRequestOperationURLSessionInvalidated: case TNLErrorCodeRequestOperationAuthenticationChallengeCancelled: case TNLErrorCodeRequestOperationRequestContentEncodingFailed: case TNLErrorCodeRequestOperationRequestContentEncodingTypeMissMatch: case TNLErrorCodeRequestOperationRequestContentDecodingFailed: case TNLErrorCodeGlobalGenericError: case TNLErrorCodeGlobalHostWasBlocked: case TNLErrorCodeOtherGenericError: return NO; } return NO; } BOOL TNLErrorIsNetworkSecurityError(NSError * __nullable error) { NSString * const domain = error.domain; NSInteger code = error.code; if ([domain isEqualToString:TNLErrorDomain] && TNLErrorCodeRequestOperationAuthenticationChallengeCancelled == code) { // auth challenge failed return YES; } if (![domain isEqualToString:NSURLErrorDomain] && ![domain isEqualToString:(NSString *)kCFErrorDomainCFNetwork]) { // not an internet failure return NO; } // Conveniently, NSURL errors and CFNetwork errors share the same error code values if (-1022 == code) { // Insecure request violates ATS return YES; } else if (-1200 == code) { // skip! - this is SSL connection failure // which is a network failure, not a security failure } else if (code <= -1201 && code >= -1206) { // SSL error return YES; } else if (-2000 == code) { #if !TARGET_OS_WATCH // cannot load from network - is it due to SSL? if (nil != error.userInfo[(NSString *)kCFStreamPropertySSLPeerTrust]) { // short cut - we have a "trust" which implicates network security return YES; } // Some network errors can be underpinned by CFStream errors, // which are not wrapped in an `NSError`, // so we can look at the userInfo for specific keys // (namely _kCFStreamErrorDomainKey and _kCFStreamErrorDomainCode) id domainNumber = error.userInfo[@"_kCFStreamErrorDomainKey"]; if ([domainNumber respondsToSelector:@selector(intValue)]) { if (kCFStreamErrorDomainSSL == [domainNumber intValue]) { // It is an SSL error, treat as security error // NOTE: there is a more specific subset of error codes which we are ignoring (see Security/SecureTransport.h) return YES; } } #endif // !WATCH } NSError * const underlyingError = error.userInfo[NSUnderlyingErrorKey]; if (underlyingError) { // recurse to see if the underlying error is network security related return TNLErrorIsNetworkSecurityError(underlyingError); } return NO; } NSError * __nonnull TNLErrorFromCancelSource(id<TNLRequestOperationCancelSource> __nullable source, NSError * __nullable underlyingError) { NSError *error = [source respondsToSelector:@selector(tnl_cancelSourceOverrideError)] ? [source tnl_cancelSourceOverrideError] : nil; if (!error) { NSMutableDictionary *errorInfo = [NSMutableDictionary dictionary]; if (underlyingError) { errorInfo[NSUnderlyingErrorKey] = underlyingError; } errorInfo[TNLErrorCancelSourceKey] = source; errorInfo[TNLErrorCancelSourceDescriptionKey] = [source tnl_cancelSourceDescription]; if ([source respondsToSelector:@selector(tnl_localizedCancelSourceDescription)]) { NSString *localizedDescription = [source tnl_localizedCancelSourceDescription]; if (localizedDescription) { errorInfo[TNLErrorCancelSourceLocalizedDescriptionKey] = localizedDescription; } } error = TNLErrorCreateWithCodeAndUserInfo(TNLErrorCodeRequestOperationCancelled, errorInfo); } return error; } #pragma mark - NSError helpers NSArray<NSNumber *> *TNLStandardRetriableURLErrorCodes() { return @[ @(NSURLErrorUnknown), // -1 @(NSURLErrorTimedOut), // -1001 @(NSURLErrorCannotFindHost), // -1003 @(NSURLErrorCannotConnectToHost), // -1004 @(NSURLErrorNetworkConnectionLost), // -1005 @(NSURLErrorDNSLookupFailed), // -1006 @(NSURLErrorHTTPTooManyRedirects), // -1007 @(NSURLErrorResourceUnavailable), // -1008 @(NSURLErrorNotConnectedToInternet), // -1009 @(NSURLErrorRedirectToNonExistentLocation), // -1010 @(NSURLErrorInternationalRoamingOff), // -1018 @(NSURLErrorCallIsActive), // -1019 @(NSURLErrorDataNotAllowed), // -1020 @(NSURLErrorSecureConnectionFailed), // -1200 @(NSURLErrorCannotLoadFromNetwork), // -2000 ]; } NSArray<NSNumber *> *TNLStandardRetriablePOSIXErrorCodes() { return @[ @(EPIPE), // 32 /* Broken pipe */ @(ENETDOWN), // 50 /* Network is down */ @(ENETUNREACH), // 51 /* Network is unreachable */ @(ENETRESET), // 52 /* Network dropped connection on reset */ @(ECONNABORTED), // 53 /* Software caused connection abort */ @(ECONNRESET), // 54 /* Connection reset by peer */ @(ENOBUFS), // 55 /* No buffer space available */ @(EISCONN), // 56 /* Socket is already connected */ @(ENOTCONN), // 57 /* Socket is not connected */ @(ESHUTDOWN), // 58 /* Can't send after socket shutdown */ @(ETOOMANYREFS), // 59 /* Too many references: can't splice */ @(ETIMEDOUT), // 60 /* Connection timed out */ @(ECONNREFUSED), // 61 /* Connection refused */ @(EHOSTDOWN), // 64 /* Host is down */ @(EHOSTUNREACH), // 65 /* No route to host */ ]; } NSError * __nullable TNLErrorToSecureCodingError(NSError * __nullable error) { NSDictionary *userInfo = error.userInfo; if (0 == userInfo.count) { return error; } NSMutableDictionary *safeUserInfo = [[NSMutableDictionary alloc] init]; // A bunch of permitted string values NSArray *stringKeys = @[ NSLocalizedDescriptionKey, NSLocalizedFailureReasonErrorKey, NSLocalizedRecoverySuggestionErrorKey, NSHelpAnchorErrorKey, NSDebugDescriptionErrorKey, NSFilePathErrorKey ]; if (tnl_available_ios_11) { stringKeys = [stringKeys arrayByAddingObject:NSLocalizedFailureErrorKey]; } for (NSString *key in stringKeys) { NSString *value = userInfo[key]; if ([value isKindOfClass:[NSString class]]) { safeUserInfo[key] = [value copy]; } } // Underlying error value NSError *underlyingError = userInfo[NSUnderlyingErrorKey]; if ([underlyingError isKindOfClass:[NSError class]]) { safeUserInfo[NSUnderlyingErrorKey] = TNLErrorToSecureCodingError(underlyingError); } // Other specific keys that are OK NSURL *URL = userInfo[NSURLErrorKey]; if ([URL isKindOfClass:[NSURL class]]) { safeUserInfo[NSURLErrorKey] = URL; } NSNumber *stringEncoding = userInfo[NSStringEncodingErrorKey]; if ([stringEncoding isKindOfClass:[NSNumber class]]) { safeUserInfo[NSStringEncodingErrorKey] = stringEncoding; } return [NSError errorWithDomain:error.domain code:error.code userInfo:safeUserInfo]; } BOOL TNLSecureCodingErrorsAreEqual(NSError * __nullable error1, NSError * __nullable error2) { if (error1 == error2) { return YES; } if (!error1 || !error2) { return NO; } if (error1.code != error2.code) { return NO; } if (![error1.domain isEqualToString:error2.domain]) { return NO; } if (!TNLSecureCodingErrorsAreEqual(error1.userInfo[NSUnderlyingErrorKey], error2.userInfo[NSUnderlyingErrorKey])) { return NO; } return YES; } NS_ASSUME_NONNULL_END