Source/TNLRequest.m (196 lines of code) (raw):
//
// TNLRequest.m
// TwitterNetworkLayer
//
// Created on 5/23/14.
// Copyright © 2020 Twitter, Inc. All rights reserved.
//
#import "NSDictionary+TNLAdditions.h"
#import "TNL_Project.h"
#import "TNLError.h"
#import "TNLRequest.h"
#import "TNLRequestConfiguration.h"
NS_ASSUME_NONNULL_BEGIN
static NSUInteger const kMaxBytesToCompare = 1024;
#pragma mark - TNLRequest Utilities
BOOL TNLRequestValidate(id<TNLRequest> __nullable request,
TNLRequestConfiguration * __nullable config,
NSError * __nullable * __nullable errorOut)
{
NSError *error = nil;
NSURL *url = [request respondsToSelector:@selector(URL)] ? request.URL : nil;
if (!url) {
error = TNLErrorCreateWithCode(TNLErrorCodeRequestInvalid);
} else if (!url.host || !url.scheme || url.isFileReferenceURL) {
error = TNLErrorCreateWithCode(TNLErrorCodeRequestInvalidURL);
} else {
TNLHTTPMethod method = TNLRequestGetHTTPMethodValue(request);
if (TNLHTTPMethodUnknown == method) {
error = TNLErrorCreateWithCode(TNLErrorCodeRequestInvalidHTTPMethod);
} else {
const BOOL isDownload = (TNLResponseDataConsumptionModeSaveToDisk == config.responseDataConsumptionMode);
const BOOL isBackground = (TNLRequestExecutionModeBackground == config.executionMode);
union {
struct {
BOOL data:1;
BOOL file:1;
BOOL stream:1;
char padding:5;
} body;
char hasBody;
} bodyUnion;
bodyUnion.hasBody = 0;
bodyUnion.body.data = [request respondsToSelector:@selector(HTTPBody)] && nil != request.HTTPBody;
bodyUnion.body.file = [request respondsToSelector:@selector(HTTPBodyFilePath)] && nil != request.HTTPBodyFilePath;
bodyUnion.body.stream = [request respondsToSelector:@selector(HTTPBodyStream)] && nil != request.HTTPBodyStream;
if (isBackground) {
if (!isDownload) {
if (!bodyUnion.body.file && !bodyUnion.body.data) {
// upload must have a file or data for the body.
// nil and stream are invalid in the background
error = TNLErrorCreateWithCode(TNLErrorCodeRequestInvalidBackgroundRequest);
}
}
}
if (isDownload && bodyUnion.hasBody) {
error = TNLErrorCreateWithCode(TNLErrorCodeRequestHTTPBodyCannotBeSetForDownload);
}
}
}
if (errorOut) {
*errorOut = error;
}
return !error;
}
NSURLRequest * __nullable TNLRequestToNSURLRequest(id<TNLRequest> __nullable request,
TNLRequestConfiguration * __nullable config,
NSError * __nullable * __nullable errorOut)
{
NSURLRequest *URLRequest;
if ([request isKindOfClass:[NSURLRequest class]] && !config) {
URLRequest = (NSURLRequest *)request;
} else {
URLRequest = TNLRequestToNSMutableURLRequest(request, config, errorOut);
}
return [URLRequest copy];
}
NSMutableURLRequest * __nullable TNLRequestToNSMutableURLRequest(id<TNLRequest> __nullable request,
TNLRequestConfiguration * __nullable config,
NSError * __nullable * __nullable errorOut)
{
if (![request respondsToSelector:@selector(URL)]) {
if (errorOut) {
*errorOut = TNLErrorCreateWithCode(TNLErrorCodeRequestInvalidURL);
}
return nil;
}
NSMutableURLRequest *urlRequest = nil;
NSURL *URL = request.URL;
urlRequest = (URL) ? [[NSMutableURLRequest alloc] initWithURL:URL] : [[NSMutableURLRequest alloc] init];
urlRequest.HTTPMethod = TNLRequestGetHTTPMethod(request);
if ([request respondsToSelector:@selector(HTTPBody)] && [request HTTPBody]) {
urlRequest.HTTPBody = [request HTTPBody];
} else if ([request respondsToSelector:@selector(HTTPBodyFilePath)] && [request HTTPBodyFilePath]) {
urlRequest.HTTPBodyStream = [NSInputStream inputStreamWithFileAtPath:[request HTTPBodyFilePath]];
} else if ([request respondsToSelector:@selector(HTTPBodyStream)] && [request HTTPBodyStream]) {
urlRequest.HTTPBodyStream = [request HTTPBodyStream];
}
if ([request respondsToSelector:@selector(allHTTPHeaderFields)]) {
urlRequest.allHTTPHeaderFields = [request allHTTPHeaderFields];
}
if (config) {
urlRequest.cachePolicy = config.cachePolicy;
urlRequest.allowsCellularAccess = config.allowsCellularAccess;
urlRequest.networkServiceType = config.networkServiceType;
urlRequest.HTTPShouldHandleCookies = config.shouldSetCookies;
// urlRequest.HTTPShouldUsePipelining -- move to HTTP/2 instead of worrying about pipelining
// urlRequest.timeoutInterval -- TNL controls timeout intervals
}
return urlRequest;
}
NSString *TNLRequestGetHTTPMethod(id<TNLRequest> __nullable request)
{
NSString *method = nil;
if ([request respondsToSelector:@selector(HTTPMethod)]) {
method = [request HTTPMethod];
} else if ([request respondsToSelector:@selector(HTTPMethodValue)]) {
method = TNLHTTPMethodToString([request HTTPMethodValue]);
}
return method ?: TNLHTTPMethodToString(TNLHTTPMethodGET);
}
TNLHTTPMethod TNLRequestGetHTTPMethodValue(id<TNLRequest> __nullable request)
{
TNLHTTPMethod method = TNLHTTPMethodGET;
if ([request respondsToSelector:@selector(HTTPMethod)]) {
method = TNLHTTPMethodFromString(request.HTTPMethod);
} else if ([request respondsToSelector:@selector(HTTPMethodValue)]) {
method = request.HTTPMethodValue;
}
return method;
}
BOOL TNLRequestHasBody(id<TNLRequest> __nullable request)
{
if ([request respondsToSelector:@selector(HTTPBody)] && request.HTTPBody != nil) {
return YES;
}
if ([request respondsToSelector:@selector(HTTPBodyFilePath)] && request.HTTPBodyFilePath != nil) {
return YES;
}
if ([request respondsToSelector:@selector(HTTPBodyStream)] && request.HTTPBodyStream != nil) {
return YES;
}
return NO;
}
BOOL TNLRequestEqualToRequest(id<TNLRequest> __nullable request1,
id<TNLRequest> __nullable request2,
BOOL quickBodyCheck)
{
if (request1 == request2) {
return YES;
}
if (![request1 respondsToSelector:@selector(URL)] || ![request2 respondsToSelector:@selector(URL)]) {
return NO;
}
TNLHTTPMethod method = TNLRequestGetHTTPMethodValue(request1);
if (TNLRequestGetHTTPMethodValue(request2) != method) {
return NO;
}
if (![request1.URL isEqual:request2.URL]) {
return NO;
}
NSDictionary *headers1 = [request1 respondsToSelector:@selector(allHTTPHeaderFields)] ? [request1 allHTTPHeaderFields] : nil;
NSDictionary *headers2 = [request2 respondsToSelector:@selector(allHTTPHeaderFields)] ? [request2 allHTTPHeaderFields] : nil;
if (headers1 != headers2) {
if (headers1.count != headers2.count) {
return NO;
}
NSDictionary *lowerCaseHeaders1 = [headers1 tnl_mutableCopyWithLowercaseKeys] ?: @{};
NSDictionary *lowerCaseHeaders2 = [headers2 tnl_mutableCopyWithLowercaseKeys] ?: @{};
if (![lowerCaseHeaders1 isEqualToDictionary:lowerCaseHeaders2]) {
return NO;
}
}
if (quickBodyCheck) {
return TNLRequestHasBody(request1) == TNLRequestHasBody(request2);
}
NSData *data1 = [request1 respondsToSelector:@selector(HTTPBody)] ? [request1 HTTPBody] : nil;
NSData *data2 = [request2 respondsToSelector:@selector(HTTPBody)] ? [request2 HTTPBody] : nil;
if (data1 != data2) {
// Compare Data
if (data1.length != data2.length) {
return NO;
} else if (!data1 || !data2) {
return NO;
} else if (data1.length > kMaxBytesToCompare) {
// If the body is too large, don't bother comparing as it would be too expensive
return NO;
} else if (![data1 isEqualToData:data2]) {
return NO;
}
} else {
if (!data1 /* both are nil */) {
NSString *file1 = [request1 respondsToSelector:@selector(HTTPBodyFilePath)] ? [request1 HTTPBodyFilePath] : nil;
NSString *file2 = [request2 respondsToSelector:@selector(HTTPBodyFilePath)] ? [request2 HTTPBodyFilePath] : nil;
if (!file1 && !file2) {
// Compare streams
NSInputStream *input1 = [request1 respondsToSelector:@selector(HTTPBodyStream)] ? [request1 HTTPBodyStream] : nil;
NSInputStream *input2 = [request2 respondsToSelector:@selector(HTTPBodyStream)] ? [request2 HTTPBodyStream] : nil;
if (input1 != input2) {
return NO;
}
} else {
// Compare files
if (!file1 || !file2 || ![file1 isEqualToString:file2]) {
return NO;
}
}
}
}
return YES;
}
NS_ASSUME_NONNULL_END