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