Source/TNLHTTP.m (161 lines of code) (raw):

// // TNLHTTP.m // TwitterNetworkLayer // // Created on 6/9/14. // Copyright © 2020 Twitter. All rights reserved. // #import "TNL_Project.h" #import "TNLHTTP.h" NS_ASSUME_NONNULL_BEGIN static const size_t kMaxTimeFormattedStringLength = 80; typedef struct _TNLHTTPTimeFormatInfo { const char *readFormat; const char *writeFormat; BOOL usesHasTimezoneInfo; } TNLHTTPTimeFormatInfo; static TNLHTTPTimeFormatInfo kTimeFormatInfos[] = { { NULL, NULL, NO }, { "%a, %d %b %Y %H:%M:%S %Z", "%a, %d %b %Y %H:%M:%S GMT", YES }, // TNLHTTPDateFormatRFC822 { "%A, %d-%b-%y %H:%M:%S %Z", "%A, %d-%b-%y %H:%M:%S GMT", YES }, // TNLHTTPDateFormatRFC850 { "%a %b %e %H:%M:%S %Y", "%a %b %e %H:%M:%S %Y", NO }, // TNLHTTPDateFormatANSIC { "%a %b %d %H:%M:%S %z %Y", "%a %b %d %H:%M:%S %z %Y", YES }, // TNLHTTPDateFormatANSICExt }; TNLStaticAssert((sizeof(kTimeFormatInfos) / sizeof(kTimeFormatInfos[0])) == 5, MISALIGNED_TIME_FORMAT_STRUCT); NSString * const TNLHTTPContentTypeJPEGImage = @"image/jpeg"; NSString * const TNLHTTPContentTypeQuicktimeVideo = @"video/quicktime"; NSString * const TNLHTTPContentTypeJSON = @"application/json"; NSString * const TNLHTTPContentTypeTextPlain = @"text/plain"; NSString * const TNLHTTPContentTypeMultipartFormData = @"multipart/form-data"; NSString * const TNLHTTPContentTypeOctetStream = @"application/octet-stream"; NSString * const TNLHTTPContentTypeURLEncodedString = @"application/x-www-form-urlencoded"; NSString * const TNLHTTPContentTypeThriftBinary = @"application/vnd.apache.thrift.binary"; static BOOL TNLHTTPContentTypeIsTextualInternal(NSString * __nonnull contentType) { if ([contentType hasPrefix:@"text/"]) { return YES; } if ([contentType isEqualToString:TNLHTTPContentTypeURLEncodedString]) { return YES; } if ([contentType isEqualToString:TNLHTTPContentTypeJSON]) { return YES; } if ([contentType hasPrefix:@"application"]) { if ([contentType hasSuffix:@"/xml"]) { return YES; } if ([contentType hasSuffix:@"+xml"]) { return YES; } } return NO; } BOOL TNLHTTPContentTypeIsTextual(NSString * __nullable contentType) { if (!contentType) { return NO; } // Is this a componentized mimetype? e.g. "application/json;charset=utf-8" NSArray<NSString *> *components = [contentType componentsSeparatedByString:@";"]; if (components.count <= 1) { // nope, do the easy check return TNLHTTPContentTypeIsTextualInternal(contentType); } // It is componentized, get the content type and check it contentType = [components.firstObject stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; if (!TNLHTTPContentTypeIsTextualInternal(contentType)) { // content type is not textual return NO; } // Content type is textual, need to confirm the character set is acceptable (we restrict to utf-8 and ascii for simplicity) for (NSUInteger i = 1; i < components.count; i++) { NSString *extraInfo = components[i].lowercaseString; NSArray<NSString *> *extraComponents = [extraInfo componentsSeparatedByString:@"="]; NSString *key = [extraComponents.firstObject stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; if ([key isEqualToString:@"charset"]) { // charset was provided, so check it and return if the character set is utf8/ascii NSString *value = [extraComponents.lastObject stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; if ([value isEqualToString:@"utf-8"]) { return YES; } else if ([value isEqualToString:@"ascii"] || [value isEqualToString:@"us-ascii"]) { return YES; } return NO; } } // no charset provided, presume utf-8 return YES; } NSString *TNLHTTPMethodToString(TNLHTTPMethod method) { #define METHOD_CASE(m) \ case TNLHTTPMethod##m : { return @"" #m ; } switch (method) { METHOD_CASE(OPTIONS) METHOD_CASE(GET) METHOD_CASE(HEAD) METHOD_CASE(POST) METHOD_CASE(PUT) METHOD_CASE(DELETE) METHOD_CASE(TRACE) METHOD_CASE(CONNECT) case TNLHTTPMethodUnknown: return nil; } TNLAssertNever(); return nil; #undef METHOD_CASE } TNLHTTPMethod TNLHTTPMethodFromString(NSString *methodString) { #define METHOD_CASE(m) \ if (methodString && [methodString caseInsensitiveCompare:@"" #m ] == NSOrderedSame) { /* TWITTER_STYLE_CASE_INSENSITIVE_COMPARE_NIL_PRECHECKED */ \ return TNLHTTPMethod##m ; \ } else #define METHOD_CASE_UNKNOWN \ { return TNLHTTPMethodUnknown; } METHOD_CASE(OPTIONS) METHOD_CASE(GET) METHOD_CASE(HEAD) METHOD_CASE(POST) METHOD_CASE(PUT) METHOD_CASE(DELETE) METHOD_CASE(TRACE) METHOD_CASE(CONNECT) METHOD_CASE_UNKNOWN #undef METHOD_CASE } NSDate * __nullable TNLHTTPDateFromString(NSString * __nullable string, TNLHTTPDateFormat * __nullable detectedFormat) { NSDate *date = nil; TNLHTTPDateFormat format = TNLHTTPDateFormatUnknown; if (string) { struct tm parsedTime; const char *utf8String = [string UTF8String]; for (format = (TNLHTTPDateFormatUnknown + 1); (size_t)format < (sizeof(kTimeFormatInfos) / sizeof(kTimeFormatInfos[0])); format++) { TNLHTTPTimeFormatInfo info = kTimeFormatInfos[format]; bzero(&parsedTime, sizeof(parsedTime)); if (info.readFormat != NULL && strptime(utf8String, info.readFormat, &parsedTime)) { const NSTimeInterval ti = (info.usesHasTimezoneInfo ? mktime(&parsedTime) : timegm(&parsedTime)); date = [NSDate dateWithTimeIntervalSince1970:ti]; if (date) { break; } } } } if (detectedFormat) { *detectedFormat = (date != nil) ? format : TNLHTTPDateFormatUnknown; } return date; } NSString * __nullable TNLHTTPDateToString(NSDate * __nullable date, TNLHTTPDateFormat format) { NSString *string = nil; if (date) { if (format == 0 || ((size_t)format >= (sizeof(kTimeFormatInfos) / sizeof(kTimeFormatInfos[0])))) { format = TNLHTTPDateFormatRFC822; } time_t timeRaw = (long)date.timeIntervalSince1970; struct tm timeStruct; char buffer[kMaxTimeFormattedStringLength]; gmtime_r(&timeRaw, &timeStruct); size_t charCount = strftime(buffer, sizeof(buffer), kTimeFormatInfos[format].writeFormat, &timeStruct); if (0 != charCount) { string = [[NSString alloc] initWithCString:buffer encoding:NSASCIIStringEncoding]; } } return string; } NS_ASSUME_NONNULL_END