835 lines
34 KiB
Objective-C
835 lines
34 KiB
Objective-C
#import "HttpdnsNWHTTPClient.h"
|
||
#import "HttpdnsNWReusableConnection.h"
|
||
#import "HttpdnsNWHTTPClient_Internal.h"
|
||
|
||
#import <Network/Network.h>
|
||
#import <Security/SecCertificate.h>
|
||
#import <Security/SecPolicy.h>
|
||
#import <Security/SecTrust.h>
|
||
|
||
#import "HttpdnsInternalConstant.h"
|
||
#import "HttpdnsLog_Internal.h"
|
||
#import "HttpdnsPublicConstant.h"
|
||
#import "HttpdnsUtil.h"
|
||
|
||
@interface HttpdnsNWHTTPClientResponse ()
|
||
@end
|
||
|
||
@implementation HttpdnsNWHTTPClientResponse
|
||
@end
|
||
|
||
static const NSUInteger kHttpdnsNWHTTPClientMaxIdleConnectionsPerKey = 4;
|
||
static const NSTimeInterval kHttpdnsNWHTTPClientIdleConnectionTimeout = 30.0;
|
||
static const NSTimeInterval kHttpdnsNWHTTPClientDefaultTimeout = 10.0;
|
||
|
||
// decoupled reusable connection implementation moved to HttpdnsNWReusableConnection.{h,m}
|
||
|
||
@interface HttpdnsNWHTTPClient ()
|
||
|
||
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSMutableArray<HttpdnsNWReusableConnection *> *> *connectionPool;
|
||
@property (nonatomic, strong) dispatch_queue_t poolQueue;
|
||
|
||
#if DEBUG
|
||
// 测试专用统计计数�
|
||
@property (atomic, assign) NSUInteger connectionCreationCount;
|
||
@property (atomic, assign) NSUInteger connectionReuseCount;
|
||
#endif
|
||
|
||
- (NSString *)connectionPoolKeyForHost:(NSString *)host port:(NSString *)port useTLS:(BOOL)useTLS;
|
||
- (HttpdnsNWReusableConnection *)dequeueConnectionForHost:(NSString *)host
|
||
port:(NSString *)port
|
||
useTLS:(BOOL)useTLS
|
||
timeout:(NSTimeInterval)timeout
|
||
error:(NSError **)error;
|
||
- (void)returnConnection:(HttpdnsNWReusableConnection *)connection
|
||
forKey:(NSString *)key
|
||
shouldClose:(BOOL)shouldClose;
|
||
- (void)pruneConnectionPool:(NSMutableArray<HttpdnsNWReusableConnection *> *)pool
|
||
referenceDate:(NSDate *)referenceDate;
|
||
- (NSString *)buildHTTPRequestStringWithURL:(NSURL *)url userAgent:(NSString *)userAgent;
|
||
- (BOOL)parseHTTPResponseData:(NSData *)data
|
||
statusCode:(NSInteger *)statusCode
|
||
headers:(NSDictionary<NSString *, NSString *> *__autoreleasing *)headers
|
||
body:(NSData *__autoreleasing *)body
|
||
error:(NSError **)error;
|
||
- (HttpdnsHTTPHeaderParseResult)tryParseHTTPHeadersInData:(NSData *)data
|
||
headerEndIndex:(NSUInteger *)headerEndIndex
|
||
statusCode:(NSInteger *)statusCode
|
||
headers:(NSDictionary<NSString *, NSString *> *__autoreleasing *)headers
|
||
error:(NSError **)error;
|
||
- (HttpdnsHTTPChunkParseResult)checkChunkedBodyCompletionInData:(NSData *)data
|
||
headerEndIndex:(NSUInteger)headerEndIndex
|
||
error:(NSError **)error;
|
||
- (NSData *)decodeChunkedBody:(NSData *)bodyData error:(NSError **)error;
|
||
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain;
|
||
+ (NSError *)errorFromNWError:(nw_error_t)nwError description:(NSString *)description;
|
||
|
||
@end
|
||
|
||
@implementation HttpdnsNWHTTPClient
|
||
|
||
+ (instancetype)sharedInstance {
|
||
// 使用 dispatch_once ä¿<C3A4>è¯<C3A8>å<EFBFBD>•例创建的线程安全与唯一性;å¤<C3A5>ç”¨è¿žæŽ¥æ± é™<C3A9>低连接开销
|
||
static dispatch_once_t onceToken;
|
||
static HttpdnsNWHTTPClient *instance = nil;
|
||
dispatch_once(&onceToken, ^{
|
||
instance = [[self alloc] init];
|
||
});
|
||
return instance;
|
||
}
|
||
|
||
- (instancetype)init {
|
||
self = [super init];
|
||
if (self) {
|
||
_poolQueue = dispatch_queue_create("com.New.sdk.httpdns.network.pool", DISPATCH_QUEUE_SERIAL);
|
||
_connectionPool = [NSMutableDictionary dictionary];
|
||
}
|
||
return self;
|
||
}
|
||
|
||
- (nullable HttpdnsNWHTTPClientResponse *)performRequestWithURLString:(NSString *)urlString
|
||
userAgent:(NSString *)userAgent
|
||
timeout:(NSTimeInterval)timeout
|
||
error:(NSError **)error {
|
||
HttpdnsLogDebug("Send Network.framework request URL: %@", urlString);
|
||
NSURL *url = [NSURL URLWithString:urlString];
|
||
if (!url) {
|
||
if (error) {
|
||
*error = [NSError errorWithDomain:NEW_HTTPDNS_ERROR_DOMAIN
|
||
code:NEW_HTTPDNS_HTTPS_COMMON_ERROR_CODE
|
||
userInfo:@{NSLocalizedDescriptionKey: @"Invalid resolve URL"}];
|
||
}
|
||
return nil;
|
||
}
|
||
|
||
NSTimeInterval requestTimeout = timeout > 0 ? timeout : kHttpdnsNWHTTPClientDefaultTimeout;
|
||
|
||
NSString *host = url.host;
|
||
if (![HttpdnsUtil isNotEmptyString:host]) {
|
||
if (error) {
|
||
*error = [NSError errorWithDomain:NEW_HTTPDNS_ERROR_DOMAIN
|
||
code:NEW_HTTPDNS_HTTPS_COMMON_ERROR_CODE
|
||
userInfo:@{NSLocalizedDescriptionKey: @"Missing host in resolve URL"}];
|
||
}
|
||
return nil;
|
||
}
|
||
|
||
BOOL useTLS = [[url.scheme lowercaseString] isEqualToString:@"https"];
|
||
NSString *portString = url.port ? url.port.stringValue : (useTLS ? @"443" : @"80");
|
||
|
||
NSString *requestString = [self buildHTTPRequestStringWithURL:url userAgent:userAgent];
|
||
NSData *requestData = [requestString dataUsingEncoding:NSUTF8StringEncoding];
|
||
if (!requestData) {
|
||
if (error) {
|
||
*error = [NSError errorWithDomain:NEW_HTTPDNS_ERROR_DOMAIN
|
||
code:NEW_HTTP_PARSE_JSON_FAILED
|
||
userInfo:@{NSLocalizedDescriptionKey: @"Failed to encode HTTP request"}];
|
||
}
|
||
return nil;
|
||
}
|
||
|
||
NSError *connectionError = nil;
|
||
HttpdnsNWReusableConnection *connection = [self dequeueConnectionForHost:host
|
||
port:portString
|
||
useTLS:useTLS
|
||
timeout:requestTimeout
|
||
error:&connectionError];
|
||
if (!connection) {
|
||
if (error) {
|
||
*error = connectionError ?: [NSError errorWithDomain:NEW_HTTPDNS_ERROR_DOMAIN
|
||
code:NEW_HTTPDNS_HTTPS_COMMON_ERROR_CODE
|
||
userInfo:@{NSLocalizedDescriptionKey: @"Unable to obtain network connection"}];
|
||
}
|
||
return nil;
|
||
}
|
||
|
||
NSString *poolKey = [self connectionPoolKeyForHost:host port:portString useTLS:useTLS];
|
||
BOOL remoteClosed = NO;
|
||
NSError *exchangeError = nil;
|
||
NSData *rawResponse = [connection sendRequestData:requestData
|
||
timeout:requestTimeout
|
||
remoteConnectionClosed:&remoteClosed
|
||
error:&exchangeError];
|
||
|
||
if (!rawResponse) {
|
||
[self returnConnection:connection forKey:poolKey shouldClose:YES];
|
||
if (error) {
|
||
*error = exchangeError ?: [NSError errorWithDomain:NEW_HTTPDNS_ERROR_DOMAIN
|
||
code:NEW_HTTPDNS_HTTPS_COMMON_ERROR_CODE
|
||
userInfo:@{NSLocalizedDescriptionKey: @"Network request failed"}];
|
||
}
|
||
return nil;
|
||
}
|
||
|
||
NSInteger statusCode = 0;
|
||
NSDictionary<NSString *, NSString *> *headers = nil;
|
||
NSData *bodyData = nil;
|
||
NSError *parseError = nil;
|
||
if (![self parseHTTPResponseData:rawResponse statusCode:&statusCode headers:&headers body:&bodyData error:&parseError]) {
|
||
[self returnConnection:connection forKey:poolKey shouldClose:YES];
|
||
if (error) {
|
||
*error = parseError ?: [NSError errorWithDomain:NEW_HTTPDNS_ERROR_DOMAIN
|
||
code:NEW_HTTP_PARSE_JSON_FAILED
|
||
userInfo:@{NSLocalizedDescriptionKey: @"Failed to parse HTTP response"}];
|
||
}
|
||
return nil;
|
||
}
|
||
|
||
BOOL shouldClose = remoteClosed;
|
||
NSString *connectionHeader = headers[@"connection"];
|
||
if ([HttpdnsUtil isNotEmptyString:connectionHeader] && [connectionHeader rangeOfString:@"close" options:NSCaseInsensitiveSearch].location != NSNotFound) {
|
||
shouldClose = YES;
|
||
}
|
||
NSString *proxyConnectionHeader = headers[@"proxy-connection"];
|
||
if (!shouldClose && [HttpdnsUtil isNotEmptyString:proxyConnectionHeader] && [proxyConnectionHeader rangeOfString:@"close" options:NSCaseInsensitiveSearch].location != NSNotFound) {
|
||
shouldClose = YES;
|
||
}
|
||
|
||
[self returnConnection:connection forKey:poolKey shouldClose:shouldClose];
|
||
|
||
HttpdnsNWHTTPClientResponse *response = [HttpdnsNWHTTPClientResponse new];
|
||
response.statusCode = statusCode;
|
||
response.headers = headers ?: @{};
|
||
response.body = bodyData ?: [NSData data];
|
||
return response;
|
||
}
|
||
|
||
- (NSString *)connectionPoolKeyForHost:(NSString *)host port:(NSString *)port useTLS:(BOOL)useTLS {
|
||
NSString *safeHost = host ?: @"";
|
||
NSString *safePort = port ?: @"";
|
||
return [NSString stringWithFormat:@"%@:%@:%@", safeHost, safePort, useTLS ? @"tls" : @"tcp"];
|
||
}
|
||
|
||
- (HttpdnsNWReusableConnection *)dequeueConnectionForHost:(NSString *)host
|
||
port:(NSString *)port
|
||
useTLS:(BOOL)useTLS
|
||
timeout:(NSTimeInterval)timeout
|
||
error:(NSError **)error {
|
||
NSString *key = [self connectionPoolKeyForHost:host port:port useTLS:useTLS];
|
||
NSDate *now = [NSDate date];
|
||
__block HttpdnsNWReusableConnection *connection = nil;
|
||
|
||
dispatch_sync(self.poolQueue, ^{
|
||
NSMutableArray<HttpdnsNWReusableConnection *> *pool = self.connectionPool[key];
|
||
if (!pool) {
|
||
pool = [NSMutableArray array];
|
||
self.connectionPool[key] = pool;
|
||
}
|
||
[self pruneConnectionPool:pool referenceDate:now];
|
||
for (HttpdnsNWReusableConnection *candidate in pool) {
|
||
if (!candidate.inUse && [candidate isViable]) {
|
||
candidate.inUse = YES;
|
||
candidate.lastUsedDate = now;
|
||
connection = candidate;
|
||
break;
|
||
}
|
||
}
|
||
});
|
||
|
||
if (connection) {
|
||
#if DEBUG
|
||
self.connectionReuseCount++;
|
||
#endif
|
||
return connection;
|
||
}
|
||
|
||
HttpdnsNWReusableConnection *newConnection = [[HttpdnsNWReusableConnection alloc] initWithClient:self
|
||
host:host
|
||
port:port
|
||
useTLS:useTLS];
|
||
if (!newConnection) {
|
||
if (error) {
|
||
*error = [NSError errorWithDomain:NEW_HTTPDNS_ERROR_DOMAIN
|
||
code:NEW_HTTPDNS_HTTPS_COMMON_ERROR_CODE
|
||
userInfo:@{NSLocalizedDescriptionKey: @"Failed to create network connection"}];
|
||
}
|
||
return nil;
|
||
}
|
||
|
||
if (![newConnection openWithTimeout:timeout error:error]) {
|
||
[newConnection invalidate];
|
||
return nil;
|
||
}
|
||
|
||
#if DEBUG
|
||
self.connectionCreationCount++;
|
||
#endif
|
||
|
||
newConnection.inUse = YES;
|
||
newConnection.lastUsedDate = now;
|
||
|
||
dispatch_sync(self.poolQueue, ^{
|
||
NSMutableArray<HttpdnsNWReusableConnection *> *pool = self.connectionPool[key];
|
||
if (!pool) {
|
||
pool = [NSMutableArray array];
|
||
self.connectionPool[key] = pool;
|
||
}
|
||
[pool addObject:newConnection];
|
||
[self pruneConnectionPool:pool referenceDate:[NSDate date]];
|
||
});
|
||
|
||
return newConnection;
|
||
}
|
||
|
||
- (void)returnConnection:(HttpdnsNWReusableConnection *)connection
|
||
forKey:(NSString *)key
|
||
shouldClose:(BOOL)shouldClose {
|
||
if (!connection || !key) {
|
||
return;
|
||
}
|
||
|
||
NSDate *now = [NSDate date];
|
||
dispatch_async(self.poolQueue, ^{
|
||
NSMutableArray<HttpdnsNWReusableConnection *> *pool = self.connectionPool[key];
|
||
if (!pool) {
|
||
pool = [NSMutableArray array];
|
||
self.connectionPool[key] = pool;
|
||
}
|
||
|
||
if (shouldClose || connection.isInvalidated) {
|
||
[connection invalidate];
|
||
[pool removeObject:connection];
|
||
} else {
|
||
connection.inUse = NO;
|
||
connection.lastUsedDate = now;
|
||
if (![pool containsObject:connection]) {
|
||
[pool addObject:connection];
|
||
}
|
||
[self pruneConnectionPool:pool referenceDate:now];
|
||
}
|
||
|
||
if (pool.count == 0) {
|
||
[self.connectionPool removeObjectForKey:key];
|
||
}
|
||
});
|
||
}
|
||
|
||
- (void)pruneConnectionPool:(NSMutableArray<HttpdnsNWReusableConnection *> *)pool referenceDate:(NSDate *)referenceDate {
|
||
if (!pool || pool.count == 0) {
|
||
return;
|
||
}
|
||
|
||
NSTimeInterval idleLimit = kHttpdnsNWHTTPClientIdleConnectionTimeout;
|
||
for (NSInteger idx = (NSInteger)pool.count - 1; idx >= 0; idx--) {
|
||
HttpdnsNWReusableConnection *candidate = pool[(NSUInteger)idx];
|
||
if (!candidate) {
|
||
[pool removeObjectAtIndex:(NSUInteger)idx];
|
||
continue;
|
||
}
|
||
NSDate *lastUsed = candidate.lastUsedDate ?: [NSDate distantPast];
|
||
BOOL expired = !candidate.inUse && referenceDate && [referenceDate timeIntervalSinceDate:lastUsed] > idleLimit;
|
||
if (candidate.isInvalidated || expired) {
|
||
[candidate invalidate];
|
||
[pool removeObjectAtIndex:(NSUInteger)idx];
|
||
}
|
||
}
|
||
|
||
if (pool.count <= kHttpdnsNWHTTPClientMaxIdleConnectionsPerKey) {
|
||
return;
|
||
}
|
||
|
||
while (pool.count > kHttpdnsNWHTTPClientMaxIdleConnectionsPerKey) {
|
||
NSInteger removeIndex = NSNotFound;
|
||
NSDate *oldestDate = nil;
|
||
for (NSInteger idx = 0; idx < (NSInteger)pool.count; idx++) {
|
||
HttpdnsNWReusableConnection *candidate = pool[(NSUInteger)idx];
|
||
if (candidate.inUse) {
|
||
continue;
|
||
}
|
||
NSDate *candidateDate = candidate.lastUsedDate ?: [NSDate distantPast];
|
||
if (!oldestDate || [candidateDate compare:oldestDate] == NSOrderedAscending) {
|
||
oldestDate = candidateDate;
|
||
removeIndex = idx;
|
||
}
|
||
}
|
||
if (removeIndex == NSNotFound) {
|
||
break;
|
||
}
|
||
HttpdnsNWReusableConnection *candidate = pool[(NSUInteger)removeIndex];
|
||
[candidate invalidate];
|
||
[pool removeObjectAtIndex:(NSUInteger)removeIndex];
|
||
}
|
||
}
|
||
|
||
- (NSString *)buildHTTPRequestStringWithURL:(NSURL *)url userAgent:(NSString *)userAgent {
|
||
NSString *pathComponent = url.path.length > 0 ? url.path : @"/";
|
||
NSMutableString *path = [NSMutableString stringWithString:pathComponent];
|
||
if (url.query.length > 0) {
|
||
[path appendFormat:@"?%@", url.query];
|
||
}
|
||
|
||
BOOL isTLS = [[url.scheme lowercaseString] isEqualToString:@"https"];
|
||
NSInteger portValue = url.port ? url.port.integerValue : (isTLS ? 443 : 80);
|
||
BOOL isDefaultPort = (!url.port) || (isTLS && portValue == 443) || (!isTLS && portValue == 80);
|
||
|
||
NSMutableString *hostHeader = [NSMutableString stringWithString:url.host ?: @""];
|
||
if (!isDefaultPort && url.port) {
|
||
[hostHeader appendFormat:@":%@", url.port];
|
||
}
|
||
|
||
NSMutableString *request = [NSMutableString stringWithFormat:@"GET %@ HTTP/1.1\r\n", path];
|
||
[request appendFormat:@"Host: %@\r\n", hostHeader];
|
||
if ([HttpdnsUtil isNotEmptyString:userAgent]) {
|
||
[request appendFormat:@"User-Agent: %@\r\n", userAgent];
|
||
}
|
||
[request appendString:@"Accept: application/json\r\n"];
|
||
[request appendString:@"Accept-Encoding: identity\r\n"];
|
||
[request appendString:@"Connection: keep-alive\r\n\r\n"];
|
||
return request;
|
||
}
|
||
|
||
- (HttpdnsHTTPHeaderParseResult)tryParseHTTPHeadersInData:(NSData *)data
|
||
headerEndIndex:(NSUInteger *)headerEndIndex
|
||
statusCode:(NSInteger *)statusCode
|
||
headers:(NSDictionary<NSString *, NSString *> *__autoreleasing *)headers
|
||
error:(NSError **)error {
|
||
if (!data || data.length == 0) {
|
||
return HttpdnsHTTPHeaderParseResultIncomplete;
|
||
}
|
||
|
||
const uint8_t *bytes = data.bytes;
|
||
NSUInteger length = data.length;
|
||
NSUInteger headerEnd = NSNotFound;
|
||
for (NSUInteger idx = 0; idx + 3 < length; idx++) {
|
||
if (bytes[idx] == '\r' && bytes[idx + 1] == '\n' && bytes[idx + 2] == '\r' && bytes[idx + 3] == '\n') {
|
||
headerEnd = idx;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (headerEnd == NSNotFound) {
|
||
return HttpdnsHTTPHeaderParseResultIncomplete;
|
||
}
|
||
|
||
if (headerEndIndex) {
|
||
*headerEndIndex = headerEnd;
|
||
}
|
||
|
||
NSData *headerData = [data subdataWithRange:NSMakeRange(0, headerEnd)];
|
||
NSString *headerString = [[NSString alloc] initWithData:headerData encoding:NSUTF8StringEncoding];
|
||
if (![HttpdnsUtil isNotEmptyString:headerString]) {
|
||
if (error) {
|
||
*error = [NSError errorWithDomain:NEW_HTTPDNS_ERROR_DOMAIN
|
||
code:NEW_HTTP_PARSE_JSON_FAILED
|
||
userInfo:@{NSLocalizedDescriptionKey: @"Failed to decode HTTP headers"}];
|
||
}
|
||
return HttpdnsHTTPHeaderParseResultError;
|
||
}
|
||
|
||
NSArray<NSString *> *lines = [headerString componentsSeparatedByString:@"\r\n"];
|
||
if (lines.count == 0) {
|
||
if (error) {
|
||
*error = [NSError errorWithDomain:NEW_HTTPDNS_ERROR_DOMAIN
|
||
code:NEW_HTTP_PARSE_JSON_FAILED
|
||
userInfo:@{NSLocalizedDescriptionKey: @"Missing HTTP status line"}];
|
||
}
|
||
return HttpdnsHTTPHeaderParseResultError;
|
||
}
|
||
|
||
NSString *statusLine = lines.firstObject;
|
||
NSArray<NSString *> *statusParts = [statusLine componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||
NSMutableArray<NSString *> *filteredParts = [NSMutableArray array];
|
||
for (NSString *component in statusParts) {
|
||
if (component.length > 0) {
|
||
[filteredParts addObject:component];
|
||
}
|
||
}
|
||
|
||
if (filteredParts.count < 2) {
|
||
if (error) {
|
||
*error = [NSError errorWithDomain:NEW_HTTPDNS_ERROR_DOMAIN
|
||
code:NEW_HTTP_PARSE_JSON_FAILED
|
||
userInfo:@{NSLocalizedDescriptionKey: @"Invalid HTTP status line"}];
|
||
}
|
||
return HttpdnsHTTPHeaderParseResultError;
|
||
}
|
||
|
||
NSInteger localStatus = [filteredParts[1] integerValue];
|
||
if (localStatus <= 0) {
|
||
if (error) {
|
||
*error = [NSError errorWithDomain:NEW_HTTPDNS_ERROR_DOMAIN
|
||
code:NEW_HTTP_PARSE_JSON_FAILED
|
||
userInfo:@{NSLocalizedDescriptionKey: @"Invalid HTTP status code"}];
|
||
}
|
||
return HttpdnsHTTPHeaderParseResultError;
|
||
}
|
||
|
||
NSMutableDictionary<NSString *, NSString *> *headerDict = [NSMutableDictionary dictionary];
|
||
NSCharacterSet *trimSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
||
for (NSUInteger idx = 1; idx < lines.count; idx++) {
|
||
NSString *line = lines[idx];
|
||
if (line.length == 0) {
|
||
continue;
|
||
}
|
||
NSRange colonRange = [line rangeOfString:@":"];
|
||
if (colonRange.location == NSNotFound) {
|
||
continue;
|
||
}
|
||
NSString *key = [[line substringToIndex:colonRange.location] stringByTrimmingCharactersInSet:trimSet];
|
||
NSString *value = [[line substringFromIndex:colonRange.location + 1] stringByTrimmingCharactersInSet:trimSet];
|
||
if (key.length > 0) {
|
||
headerDict[[key lowercaseString]] = value ?: @"";
|
||
}
|
||
}
|
||
|
||
if (statusCode) {
|
||
*statusCode = localStatus;
|
||
}
|
||
if (headers) {
|
||
*headers = [headerDict copy];
|
||
}
|
||
return HttpdnsHTTPHeaderParseResultSuccess;
|
||
}
|
||
|
||
- (HttpdnsHTTPChunkParseResult)checkChunkedBodyCompletionInData:(NSData *)data
|
||
headerEndIndex:(NSUInteger)headerEndIndex
|
||
error:(NSError **)error {
|
||
if (!data || headerEndIndex == NSNotFound) {
|
||
return HttpdnsHTTPChunkParseResultIncomplete;
|
||
}
|
||
|
||
NSUInteger length = data.length;
|
||
NSUInteger cursor = headerEndIndex + 4;
|
||
if (cursor > length) {
|
||
return HttpdnsHTTPChunkParseResultIncomplete;
|
||
}
|
||
|
||
const uint8_t *bytes = data.bytes;
|
||
NSCharacterSet *trimSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
||
|
||
while (cursor < length) {
|
||
NSUInteger lineEnd = cursor;
|
||
while (lineEnd + 1 < length && !(bytes[lineEnd] == '\r' && bytes[lineEnd + 1] == '\n')) {
|
||
lineEnd++;
|
||
}
|
||
if (lineEnd + 1 >= length) {
|
||
return HttpdnsHTTPChunkParseResultIncomplete;
|
||
}
|
||
|
||
NSData *sizeData = [data subdataWithRange:NSMakeRange(cursor, lineEnd - cursor)];
|
||
NSString *sizeString = [[NSString alloc] initWithData:sizeData encoding:NSUTF8StringEncoding];
|
||
if (![HttpdnsUtil isNotEmptyString:sizeString]) {
|
||
if (error) {
|
||
*error = [NSError errorWithDomain:NEW_HTTPDNS_ERROR_DOMAIN
|
||
code:NEW_HTTP_PARSE_JSON_FAILED
|
||
userInfo:@{NSLocalizedDescriptionKey: @"Invalid chunk size"}];
|
||
}
|
||
return HttpdnsHTTPChunkParseResultError;
|
||
}
|
||
|
||
NSString *trimmed = [[sizeString componentsSeparatedByString:@";"] firstObject];
|
||
trimmed = [trimmed stringByTrimmingCharactersInSet:trimSet];
|
||
const char *cStr = trimmed.UTF8String;
|
||
char *endPtr = NULL;
|
||
unsigned long long chunkSize = strtoull(cStr, &endPtr, 16);
|
||
if (endPtr == NULL || endPtr == cStr) {
|
||
if (error) {
|
||
*error = [NSError errorWithDomain:NEW_HTTPDNS_ERROR_DOMAIN
|
||
code:NEW_HTTP_PARSE_JSON_FAILED
|
||
userInfo:@{NSLocalizedDescriptionKey: @"Invalid chunk size"}];
|
||
}
|
||
return HttpdnsHTTPChunkParseResultError;
|
||
}
|
||
|
||
if (chunkSize > NSUIntegerMax - cursor) {
|
||
if (error) {
|
||
*error = [NSError errorWithDomain:NEW_HTTPDNS_ERROR_DOMAIN
|
||
code:NEW_HTTP_PARSE_JSON_FAILED
|
||
userInfo:@{NSLocalizedDescriptionKey: @"Chunk size overflow"}];
|
||
}
|
||
return HttpdnsHTTPChunkParseResultError;
|
||
}
|
||
|
||
cursor = lineEnd + 2;
|
||
if (chunkSize == 0) {
|
||
NSUInteger trailerCursor = cursor;
|
||
while (YES) {
|
||
if (trailerCursor + 1 >= length) {
|
||
return HttpdnsHTTPChunkParseResultIncomplete;
|
||
}
|
||
NSUInteger trailerLineEnd = trailerCursor;
|
||
while (trailerLineEnd + 1 < length && !(bytes[trailerLineEnd] == '\r' && bytes[trailerLineEnd + 1] == '\n')) {
|
||
trailerLineEnd++;
|
||
}
|
||
if (trailerLineEnd + 1 >= length) {
|
||
return HttpdnsHTTPChunkParseResultIncomplete;
|
||
}
|
||
if (trailerLineEnd == trailerCursor) {
|
||
return HttpdnsHTTPChunkParseResultSuccess;
|
||
}
|
||
trailerCursor = trailerLineEnd + 2;
|
||
}
|
||
}
|
||
|
||
if (cursor + (NSUInteger)chunkSize > length) {
|
||
return HttpdnsHTTPChunkParseResultIncomplete;
|
||
}
|
||
cursor += (NSUInteger)chunkSize;
|
||
if (cursor + 1 >= length) {
|
||
return HttpdnsHTTPChunkParseResultIncomplete;
|
||
}
|
||
if (bytes[cursor] != '\r' || bytes[cursor + 1] != '\n') {
|
||
if (error) {
|
||
*error = [NSError errorWithDomain:NEW_HTTPDNS_ERROR_DOMAIN
|
||
code:NEW_HTTP_PARSE_JSON_FAILED
|
||
userInfo:@{NSLocalizedDescriptionKey: @"Invalid chunk terminator"}];
|
||
}
|
||
return HttpdnsHTTPChunkParseResultError;
|
||
}
|
||
cursor += 2;
|
||
}
|
||
|
||
return HttpdnsHTTPChunkParseResultIncomplete;
|
||
}
|
||
|
||
- (BOOL)parseHTTPResponseData:(NSData *)data
|
||
statusCode:(NSInteger *)statusCode
|
||
headers:(NSDictionary<NSString *, NSString *> *__autoreleasing *)headers
|
||
body:(NSData *__autoreleasing *)body
|
||
error:(NSError **)error {
|
||
if (!data || data.length == 0) {
|
||
if (error) {
|
||
*error = [NSError errorWithDomain:NEW_HTTPDNS_ERROR_DOMAIN
|
||
code:NEW_HTTP_PARSE_JSON_FAILED
|
||
userInfo:@{NSLocalizedDescriptionKey: @"Empty HTTP response"}];
|
||
}
|
||
return NO;
|
||
}
|
||
|
||
NSUInteger headerEnd = NSNotFound;
|
||
NSInteger localStatus = 0;
|
||
NSDictionary<NSString *, NSString *> *headerDict = nil;
|
||
NSError *headerError = nil;
|
||
HttpdnsHTTPHeaderParseResult headerResult = [self tryParseHTTPHeadersInData:data
|
||
headerEndIndex:&headerEnd
|
||
statusCode:&localStatus
|
||
headers:&headerDict
|
||
error:&headerError];
|
||
if (headerResult != HttpdnsHTTPHeaderParseResultSuccess) {
|
||
if (error) {
|
||
if (headerResult == HttpdnsHTTPHeaderParseResultIncomplete) {
|
||
*error = [NSError errorWithDomain:NEW_HTTPDNS_ERROR_DOMAIN
|
||
code:NEW_HTTP_PARSE_JSON_FAILED
|
||
userInfo:@{NSLocalizedDescriptionKey: @"Missing HTTP header terminator"}];
|
||
} else {
|
||
*error = headerError;
|
||
}
|
||
}
|
||
return NO;
|
||
}
|
||
|
||
NSUInteger bodyStart = headerEnd + 4;
|
||
NSData *bodyData = bodyStart <= data.length ? [data subdataWithRange:NSMakeRange(bodyStart, data.length - bodyStart)] : [NSData data];
|
||
|
||
NSString *transferEncoding = headerDict[@"transfer-encoding"];
|
||
if ([HttpdnsUtil isNotEmptyString:transferEncoding] && [transferEncoding rangeOfString:@"chunked" options:NSCaseInsensitiveSearch].location != NSNotFound) {
|
||
NSError *chunkError = nil;
|
||
NSData *decoded = [self decodeChunkedBody:bodyData error:&chunkError];
|
||
if (!decoded) {
|
||
HttpdnsLogDebug("Chunked decode failed, fallback to raw body, error: %@", chunkError);
|
||
decoded = bodyData;
|
||
}
|
||
bodyData = decoded;
|
||
} else {
|
||
NSString *contentLengthValue = headerDict[@"content-length"];
|
||
if ([HttpdnsUtil isNotEmptyString:contentLengthValue]) {
|
||
long long expected = [contentLengthValue longLongValue];
|
||
if (expected >= 0 && (NSUInteger)expected != bodyData.length) {
|
||
HttpdnsLogDebug("Content-Length mismatch, expected: %lld, actual: %lu", expected, (unsigned long)bodyData.length);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (statusCode) {
|
||
*statusCode = localStatus;
|
||
}
|
||
if (headers) {
|
||
*headers = headerDict ?: @{};
|
||
}
|
||
if (body) {
|
||
*body = bodyData;
|
||
}
|
||
return YES;
|
||
}
|
||
|
||
- (NSData *)decodeChunkedBody:(NSData *)bodyData error:(NSError **)error {
|
||
if (!bodyData) {
|
||
return [NSData data];
|
||
}
|
||
|
||
const uint8_t *bytes = bodyData.bytes;
|
||
NSUInteger length = bodyData.length;
|
||
NSUInteger cursor = 0;
|
||
NSMutableData *decoded = [NSMutableData data];
|
||
NSCharacterSet *trimSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
||
|
||
while (cursor < length) {
|
||
NSUInteger lineEnd = cursor;
|
||
while (lineEnd + 1 < length && !(bytes[lineEnd] == '\r' && bytes[lineEnd + 1] == '\n')) {
|
||
lineEnd++;
|
||
}
|
||
if (lineEnd + 1 >= length) {
|
||
if (error) {
|
||
*error = [NSError errorWithDomain:NEW_HTTPDNS_ERROR_DOMAIN
|
||
code:NEW_HTTP_PARSE_JSON_FAILED
|
||
userInfo:@{NSLocalizedDescriptionKey: @"Invalid chunked encoding"}];
|
||
}
|
||
return nil;
|
||
}
|
||
|
||
NSData *sizeData = [bodyData subdataWithRange:NSMakeRange(cursor, lineEnd - cursor)];
|
||
NSString *sizeString = [[NSString alloc] initWithData:sizeData encoding:NSUTF8StringEncoding];
|
||
if (![HttpdnsUtil isNotEmptyString:sizeString]) {
|
||
if (error) {
|
||
*error = [NSError errorWithDomain:NEW_HTTPDNS_ERROR_DOMAIN
|
||
code:NEW_HTTP_PARSE_JSON_FAILED
|
||
userInfo:@{NSLocalizedDescriptionKey: @"Invalid chunk size"}];
|
||
}
|
||
return nil;
|
||
}
|
||
NSString *trimmed = [[sizeString componentsSeparatedByString:@";"] firstObject];
|
||
trimmed = [trimmed stringByTrimmingCharactersInSet:trimSet];
|
||
const char *cStr = trimmed.UTF8String;
|
||
char *endPtr = NULL;
|
||
unsigned long chunkSize = strtoul(cStr, &endPtr, 16);
|
||
// 检查是å<C2AF>¦æ˜¯æ— 效的å<E2809E><C3A5>å…进制å—符串
|
||
if (endPtr == cStr) {
|
||
if (error) {
|
||
*error = [NSError errorWithDomain:NEW_HTTPDNS_ERROR_DOMAIN
|
||
code:NEW_HTTP_PARSE_JSON_FAILED
|
||
userInfo:@{NSLocalizedDescriptionKey: @"Invalid chunk size format"}];
|
||
}
|
||
return nil;
|
||
}
|
||
cursor = lineEnd + 2;
|
||
if (chunkSize == 0) {
|
||
if (cursor + 1 < length) {
|
||
cursor += 2;
|
||
}
|
||
break;
|
||
}
|
||
if (cursor + chunkSize > length) {
|
||
if (error) {
|
||
*error = [NSError errorWithDomain:NEW_HTTPDNS_ERROR_DOMAIN
|
||
code:NEW_HTTP_PARSE_JSON_FAILED
|
||
userInfo:@{NSLocalizedDescriptionKey: @"Chunk size exceeds buffer"}];
|
||
}
|
||
return nil;
|
||
}
|
||
[decoded appendBytes:bytes + cursor length:chunkSize];
|
||
cursor += chunkSize;
|
||
if (cursor + 1 >= length || bytes[cursor] != '\r' || bytes[cursor + 1] != '\n') {
|
||
if (error) {
|
||
*error = [NSError errorWithDomain:NEW_HTTPDNS_ERROR_DOMAIN
|
||
code:NEW_HTTP_PARSE_JSON_FAILED
|
||
userInfo:@{NSLocalizedDescriptionKey: @"Invalid chunk terminator"}];
|
||
}
|
||
return nil;
|
||
}
|
||
cursor += 2;
|
||
}
|
||
|
||
return decoded;
|
||
}
|
||
|
||
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain {
|
||
// 测试专用:通过环境å<C692>˜é‡<C3A9>跳过 TLS 验è¯<C3A8>
|
||
// 仅在设置 HTTPDNS_SKIP_TLS_VERIFY 环境å<C692>˜é‡<C3A9>时生效(用于本地 mock server 测试ï¼?
|
||
if (getenv("HTTPDNS_SKIP_TLS_VERIFY") != NULL) {
|
||
return YES;
|
||
}
|
||
|
||
// ç”Ÿäº§çŽ¯å¢ƒæ ‡å‡† TLS 验è¯<C3A8>æµ<C3A6>程
|
||
NSMutableArray *policies = [NSMutableArray array];
|
||
if (domain) {
|
||
[policies addObject:(__bridge_transfer id) SecPolicyCreateSSL(true, (__bridge CFStringRef) domain)];
|
||
} else {
|
||
[policies addObject:(__bridge_transfer id) SecPolicyCreateBasicX509()];
|
||
}
|
||
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef) policies);
|
||
SecTrustResultType result;
|
||
SecTrustEvaluate(serverTrust, &result);
|
||
if (result == kSecTrustResultRecoverableTrustFailure) {
|
||
CFDataRef errDataRef = SecTrustCopyExceptions(serverTrust);
|
||
SecTrustSetExceptions(serverTrust, errDataRef);
|
||
SecTrustEvaluate(serverTrust, &result);
|
||
}
|
||
return (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
|
||
}
|
||
|
||
+ (NSError *)errorFromNWError:(nw_error_t)nwError description:(NSString *)description {
|
||
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
|
||
if ([HttpdnsUtil isNotEmptyString:description]) {
|
||
userInfo[NSLocalizedDescriptionKey] = description;
|
||
}
|
||
if (nwError) {
|
||
CFErrorRef cfError = nw_error_copy_cf_error(nwError);
|
||
if (cfError) {
|
||
NSError *underlyingError = CFBridgingRelease(cfError);
|
||
if (underlyingError) {
|
||
userInfo[NSUnderlyingErrorKey] = underlyingError;
|
||
if (!userInfo[NSLocalizedDescriptionKey] && underlyingError.localizedDescription) {
|
||
userInfo[NSLocalizedDescriptionKey] = underlyingError.localizedDescription;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (!userInfo[NSLocalizedDescriptionKey]) {
|
||
userInfo[NSLocalizedDescriptionKey] = @"Network operation failed";
|
||
}
|
||
return [NSError errorWithDomain:NEW_HTTPDNS_ERROR_DOMAIN
|
||
code:NEW_HTTPDNS_HTTPS_COMMON_ERROR_CODE
|
||
userInfo:userInfo];
|
||
}
|
||
|
||
@end
|
||
|
||
#if DEBUG
|
||
// æµ‹è¯•ä¸“ç”¨ï¼šè¿žæŽ¥æ± æ£€æŸ?API 实现
|
||
@implementation HttpdnsNWHTTPClient (TestInspection)
|
||
|
||
- (NSUInteger)connectionPoolCountForKey:(NSString *)key {
|
||
__block NSUInteger count = 0;
|
||
dispatch_sync(self.poolQueue, ^{
|
||
NSMutableArray<HttpdnsNWReusableConnection *> *pool = self.connectionPool[key];
|
||
count = pool ? pool.count : 0;
|
||
});
|
||
return count;
|
||
}
|
||
|
||
- (NSArray<NSString *> *)allConnectionPoolKeys {
|
||
__block NSArray<NSString *> *keys = nil;
|
||
dispatch_sync(self.poolQueue, ^{
|
||
keys = [self.connectionPool.allKeys copy];
|
||
});
|
||
return keys ?: @[];
|
||
}
|
||
|
||
- (NSUInteger)totalConnectionCount {
|
||
__block NSUInteger total = 0;
|
||
dispatch_sync(self.poolQueue, ^{
|
||
for (NSMutableArray *pool in self.connectionPool.allValues) {
|
||
total += pool.count;
|
||
}
|
||
});
|
||
return total;
|
||
}
|
||
|
||
- (void)resetPoolStatistics {
|
||
self.connectionCreationCount = 0;
|
||
self.connectionReuseCount = 0;
|
||
}
|
||
|
||
// 获å<C2B7>–æŒ‡å®šæ± ä¸çš„æ‰€æœ‰è¿žæŽ¥ï¼ˆç”¨äºŽçжæ€<C3A6>检查)
|
||
- (NSArray<HttpdnsNWReusableConnection *> *)connectionsInPoolForKey:(NSString *)key {
|
||
__block NSArray<HttpdnsNWReusableConnection *> *connections = nil;
|
||
dispatch_sync(self.poolQueue, ^{
|
||
NSMutableArray<HttpdnsNWReusableConnection *> *pool = self.connectionPool[key];
|
||
connections = pool ? [pool copy] : @[];
|
||
});
|
||
return connections;
|
||
}
|
||
|
||
@end
|
||
#endif
|