#import "HttpdnsNWHTTPClient.h" #import "HttpdnsNWReusableConnection.h" #import "HttpdnsNWHTTPClient_Internal.h" #import #import #import #import #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 *> *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 *)pool referenceDate:(NSDate *)referenceDate; - (NSString *)buildHTTPRequestStringWithURL:(NSURL *)url userAgent:(NSString *)userAgent; - (BOOL)parseHTTPResponseData:(NSData *)data statusCode:(NSInteger *)statusCode headers:(NSDictionary *__autoreleasing *)headers body:(NSData *__autoreleasing *)body error:(NSError **)error; - (HttpdnsHTTPHeaderParseResult)tryParseHTTPHeadersInData:(NSData *)data headerEndIndex:(NSUInteger *)headerEndIndex statusCode:(NSInteger *)statusCode headers:(NSDictionary *__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 保证单例创建的线程安全与唯一性;复用连接池降低连接开销 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 *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 *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 *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 *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 *)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 *__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 *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 *statusParts = [statusLine componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; NSMutableArray *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 *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 *__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 *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); // 检查是否是无效的十六进制字符串 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 { // 测试专用:通过环境变量跳过 TLS 验证 // 仅在设置 HTTPDNS_SKIP_TLS_VERIFY 环境变量时生效(用于本地 mock server 测试? if (getenv("HTTPDNS_SKIP_TLS_VERIFY") != NULL) { return YES; } // 生产环境标准 TLS 验证流程 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 *pool = self.connectionPool[key]; count = pool ? pool.count : 0; }); return count; } - (NSArray *)allConnectionPoolKeys { __block NSArray *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; } // 获取指定池中的所有连接(用于状态检查) - (NSArray *)connectionsInPoolForKey:(NSString *)key { __block NSArray *connections = nil; dispatch_sync(self.poolQueue, ^{ NSMutableArray *pool = self.connectionPool[key]; connections = pool ? [pool copy] : @[]; }); return connections; } @end #endif