EMASCurl/EMASCurlProtocol.m (687 lines of code) (raw):
//
// EMASCurlProtocol.m
// EMASCurl
//
// Created by xin yu on 2024/10/29.
//
#import "EMASCurlProtocol.h"
#import "EMASCurlManager.h"
#import "EMASCurlCookieStorage.h"
#import <curl/curl.h>
#define HTTP_METHOD_GET @"GET"
#define HTTP_METHOD_PUT @"PUT"
#define HTTP_METHOD_POST @"POST"
#define HTTP_METHOD_HEAD @"HEAD"
#define HTTP_METHOD_DELETE @"DELETE"
#define HTTP_METHOD_PATCH @"PATCH"
#define HTTP_METHOD_OPTIONS @"OPTIONS"
#define HTTP_METHOD_TRACE @"TRACE"
#define HTTP_METHOD_CONNECT @"CONNECT"
static NSString * _Nonnull const kEMASCurlUploadProgressUpdateBlockKey = @"kEMASCurlUploadProgressUpdateBlockKey";
static NSString * _Nonnull const kEMASCurlMetricsObserverBlockKey = @"kEMASCurlMetricsObserverBlockKey";
static NSString * _Nonnull const kEMASCurlConnectTimeoutIntervalKey = @"kEMASCurlConnectTimeoutIntervalKey";
static NSString * _Nonnull const kEMASCurlHandledKey = @"kEMASCurlHandledKey";
@interface CurlHTTPResponse : NSObject
@property (nonatomic, assign) NSInteger statusCode;
@property (nonatomic, strong) NSString *httpVersion;
@property (nonatomic, strong) NSString *reasonPhrase;
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSString *> *headers;
@property (nonatomic, assign) BOOL isFinalResponse;
@end
@implementation CurlHTTPResponse
- (instancetype)init {
self = [super init];
if (self) {
[self reset];
}
return self;
}
- (void)reset {
_statusCode = 0;
_httpVersion = nil;
_reasonPhrase = nil;
_headers = [NSMutableDictionary new];
_isFinalResponse = NO;
}
@end
@interface EMASCurlProtocol()
@property (nonatomic, assign) CURL *easyHandle;
@property (nonatomic, strong) NSInputStream *inputStream;
@property (nonatomic, assign) struct curl_slist *requestHeaderFields;
@property (nonatomic, assign) struct curl_slist *resolveList;
@property (nonatomic, assign) int64_t totalBytesSent;
@property (nonatomic, assign) int64_t totalBytesExpected;
@property (nonatomic, strong) CurlHTTPResponse *currentResponse;
@property (atomic, assign) BOOL shouldCancel;
@property (atomic, strong) dispatch_semaphore_t cleanupSemaphore;
@property (nonatomic, copy) EMASCurlUploadProgressUpdateBlock uploadProgressUpdateBlock;
@property (nonatomic, copy) EMASCurlMetricsObserverBlock metricsObserverBlock;
@property (nonatomic, assign) double resolveDomainTimeInterval;
@end
static HTTPVersion s_httpVersion;
// runtime 的libcurl xcframework是否支持HTTP2
static bool curlFeatureHttp2;
// runtime 的libcurl xcframework是否支持HTTP3
static bool curlFeatureHttp3;
static bool s_enableBuiltInGzip;
static NSString *s_caFilePath;
static BOOL s_enableBuiltInRedirection;
static NSString *s_proxyServer;
static dispatch_queue_t s_serialQueue;
static Class<EMASCurlProtocolDNSResolver> s_dnsResolverClass;
static bool s_enableDebugLog;
// 标记是否启用了手动代理
static BOOL s_manualProxyEnabled;
// 定时更新系统代理设置的定时器
static NSTimer *s_proxyUpdateTimer;
// 拦截域名白名单
static NSArray<NSString *> *s_domainWhiteList;
static NSArray<NSString *> *s_domainBlackList;
// 公钥固定(Public Key Pinning)的公钥文件路径
static NSString *s_publicKeyPinningKeyPath;
@implementation EMASCurlProtocol
#pragma mark * user API
+ (void)installIntoSessionConfiguration:(nonnull NSURLSessionConfiguration*)sessionConfiguration {
NSMutableArray *protocolsArray = [NSMutableArray arrayWithArray:sessionConfiguration.protocolClasses];
[protocolsArray insertObject:self atIndex:0];
[sessionConfiguration setProtocolClasses:protocolsArray];
}
+ (void)registerCurlProtocol {
[NSURLProtocol registerClass:self];
}
+ (void)unregisterCurlProtocol {
[NSURLProtocol unregisterClass:self];
}
+ (void)setHTTPVersion:(HTTPVersion)version {
s_httpVersion = version;
}
+ (void)setBuiltInGzipEnabled:(BOOL)enabled {
s_enableBuiltInGzip = enabled;
}
+ (void)setSelfSignedCAFilePath:(nonnull NSString *)selfSignedCAFilePath {
s_caFilePath = selfSignedCAFilePath;
}
+ (void)setBuiltInRedirectionEnabled:(BOOL)enabled {
s_enableBuiltInRedirection = enabled;
}
+ (void)setDebugLogEnabled:(BOOL)debugLogEnabled {
s_enableDebugLog = debugLogEnabled;
}
+ (void)setDNSResolver:(nonnull Class<EMASCurlProtocolDNSResolver>)dnsResolver {
s_dnsResolverClass = dnsResolver;
}
+ (void)setUploadProgressUpdateBlockForRequest:(nonnull NSMutableURLRequest *)request uploadProgressUpdateBlock:(nonnull EMASCurlUploadProgressUpdateBlock)uploadProgressUpdateBlock {
[NSURLProtocol setProperty:[uploadProgressUpdateBlock copy] forKey:kEMASCurlUploadProgressUpdateBlockKey inRequest:request];
}
+ (void)setMetricsObserverBlockForRequest:(nonnull NSMutableURLRequest *)request metricsObserverBlock:(nonnull EMASCurlMetricsObserverBlock)metricsObserverBlock {
[NSURLProtocol setProperty:[metricsObserverBlock copy] forKey:kEMASCurlMetricsObserverBlockKey inRequest:request];
}
+ (void)setConnectTimeoutIntervalForRequest:(nonnull NSMutableURLRequest *)request connectTimeoutInterval:(NSTimeInterval)timeoutInterval {
[NSURLProtocol setProperty:@(timeoutInterval) forKey:kEMASCurlConnectTimeoutIntervalKey inRequest:request];
}
+ (void)setHijackDomainWhiteList:(nullable NSArray<NSString *> *)domainWhiteList {
s_domainWhiteList = domainWhiteList;
}
+ (void)setHijackDomainBlackList:(nullable NSArray<NSString *> *)domainBlackList {
s_domainBlackList = domainBlackList;
}
+ (void)setPublicKeyPinningKeyPath:(nullable NSString *)publicKeyPath {
s_publicKeyPinningKeyPath = [publicKeyPath copy];
}
+ (void)setManualProxyServer:(nullable NSString *)proxyServerURL {
__block BOOL shouldInvalidateTimer = NO;
__block BOOL shouldStartTimer = NO;
dispatch_sync(s_serialQueue, ^{
if (proxyServerURL && proxyServerURL.length > 0) {
s_manualProxyEnabled = YES;
s_proxyServer = [proxyServerURL copy];
shouldInvalidateTimer = YES;
} else {
s_manualProxyEnabled = NO;
s_proxyServer = nil;
shouldStartTimer = YES;
}
});
dispatch_async(dispatch_get_main_queue(), ^{
if (shouldInvalidateTimer && s_proxyUpdateTimer) {
[s_proxyUpdateTimer invalidate];
s_proxyUpdateTimer = nil;
NSLog(@"[EMASCurlProtocol] Manual proxy enabled: %@", proxyServerURL);
} else if (shouldStartTimer && !s_proxyUpdateTimer) {
[self startProxyUpdatingTimer];
NSLog(@"[EMASCurlProtocol] Manual proxy disabled, reverting to system settings.");
}
});
}
#pragma mark * NSURLProtocol overrides
- (instancetype)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client {
self = [super initWithRequest:request cachedResponse:cachedResponse client:client];
if (self) {
_shouldCancel = NO;
_cleanupSemaphore = dispatch_semaphore_create(0);
_totalBytesSent = 0;
_totalBytesExpected = 0;
_currentResponse = [CurlHTTPResponse new];
_resolveDomainTimeInterval = -1;
_uploadProgressUpdateBlock = [NSURLProtocol propertyForKey:kEMASCurlUploadProgressUpdateBlockKey inRequest:request];
_metricsObserverBlock = [NSURLProtocol propertyForKey:kEMASCurlMetricsObserverBlockKey inRequest:request];
}
return self;
}
// 在类加载方法中初始化libcurl
+ (void)load {
curl_global_init(CURL_GLOBAL_DEFAULT);
// 读取runtime libcurl对于http2/3的支持
curl_version_info_data *version_info = curl_version_info(CURLVERSION_NOW);
curlFeatureHttp2 = (version_info->features & CURL_VERSION_HTTP2) ? YES : NO;
curlFeatureHttp3 = (version_info->features & CURL_VERSION_HTTP3) ? YES : NO;
s_httpVersion = HTTP1;
s_enableDebugLog = NO;
s_enableBuiltInGzip = YES;
s_enableBuiltInRedirection = YES;
s_proxyServer = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
s_serialQueue = dispatch_queue_create("com.alicloud.emascurl.serialQueue", DISPATCH_QUEUE_SERIAL);
});
// 设置定时任务读取proxy
[self startProxyUpdatingTimer];
}
+ (void)startProxyUpdatingTimer {
// 确保在主线程上操作定时器
dispatch_async(dispatch_get_main_queue(), ^{
// 如果定时器已存在,先停止旧的
if (s_proxyUpdateTimer) {
[s_proxyUpdateTimer invalidate];
s_proxyUpdateTimer = nil;
}
// 设置一个定时器,10s更新一次proxy设置
NSTimer *timer = [NSTimer timerWithTimeInterval:10.0
target:self
selector:@selector(updateProxySettings)
userInfo:nil
repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
s_proxyUpdateTimer = timer; // 保存定时器实例
[self updateProxySettings]; // 立即执行一次更新
});
}
+ (void)updateProxySettings {
dispatch_sync(s_serialQueue, ^{
// If manual proxy is enabled, don't update anything
if (s_manualProxyEnabled) {
return;
}
// Get and process system proxy within the locked section
CFDictionaryRef proxySettings = CFNetworkCopySystemProxySettings();
if (!proxySettings) {
s_proxyServer = nil;
return;
}
NSDictionary *proxyDict = (__bridge NSDictionary *)(proxySettings);
if (!(proxyDict[(NSString *)kCFNetworkProxiesHTTPEnable])) {
s_proxyServer = nil;
CFRelease(proxySettings);
return;
}
NSString *httpProxy = proxyDict[(NSString *)kCFNetworkProxiesHTTPProxy];
NSNumber *httpPort = proxyDict[(NSString *)kCFNetworkProxiesHTTPPort];
if (httpProxy && httpPort) {
s_proxyServer = [NSString stringWithFormat:@"http://%@:%@", httpProxy, httpPort];
} else {
s_proxyServer = nil;
}
CFRelease(proxySettings);
});
}
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
if ([[request.URL absoluteString] isEqual:@"about:blank"]) {
return NO;
}
// 不拦截已经处理过的请求
if ([NSURLProtocol propertyForKey:kEMASCurlHandledKey inRequest:request]) {
return NO;
}
// 不是http或https,则不拦截
if (!([request.URL.scheme caseInsensitiveCompare:@"http"] == NSOrderedSame ||
[request.URL.scheme caseInsensitiveCompare:@"https"] == NSOrderedSame)) {
return NO;
}
// 检查请求的host是否在白名单或黑名单中
NSString *host = request.URL.host;
if (!host) {
return NO;
}
if (s_domainBlackList && s_domainBlackList.count > 0) {
for (NSString *blacklistDomain in s_domainBlackList) {
if ([host hasSuffix:blacklistDomain]) {
return NO;
}
}
}
if (s_domainWhiteList && s_domainWhiteList.count > 0) {
for (NSString *whitelistDomain in s_domainWhiteList) {
if ([host hasSuffix:whitelistDomain]) {
return YES;
}
}
return NO;
}
NSString *userAgent = [request valueForHTTPHeaderField:@"User-Agent"];
if (userAgent && [userAgent containsString:@"HttpdnsSDK"]) {
// 不拦截来自Httpdns SDK的请求
return NO;
}
return YES;
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
NSMutableURLRequest *mutableRequest = [request mutableCopy];
[NSURLProtocol setProperty:@YES forKey:kEMASCurlHandledKey inRequest:mutableRequest];
return mutableRequest;
}
- (void)startLoading {
CURL *easyHandle = curl_easy_init();
self.easyHandle = easyHandle;
if (!easyHandle) {
NSError *error = [NSError errorWithDomain:@"fail to init easy handle." code:-1 userInfo:nil];
[self reportNetworkMetric:NO error:error];
[self.client URLProtocol:self didFailWithError:error];
return;
}
[self populateRequestHeader:easyHandle];
[self populateRequestBody:easyHandle];
NSError *error = nil;
[self configEasyHandle:easyHandle error:&error];
if (error) {
[self reportNetworkMetric:NO error:error];
[self.client URLProtocol:self didFailWithError:error];
return;
}
[[EMASCurlManager sharedInstance] enqueueNewEasyHandle:easyHandle completion:^(BOOL succeed, NSError *error) {
[self reportNetworkMetric:succeed error:error];
if (succeed) {
[self.client URLProtocolDidFinishLoading:self];
} else {
[self.client URLProtocol:self didFailWithError:error];
}
dispatch_semaphore_signal(self.cleanupSemaphore);
}];
}
- (void)stopLoading {
self.shouldCancel = YES;
dispatch_semaphore_wait(self.cleanupSemaphore, DISPATCH_TIME_FOREVER);
if (self.inputStream) {
if ([self.inputStream streamStatus] == NSStreamStatusOpen) {
[self.inputStream close];
}
self.inputStream = nil;
}
if (self.requestHeaderFields) {
curl_slist_free_all(self.requestHeaderFields);
self.requestHeaderFields = nil;
}
if (self.resolveList) {
curl_slist_free_all(self.resolveList);
self.resolveList = nil;
}
if (self.easyHandle) {
curl_easy_cleanup(self.easyHandle);
self.easyHandle = nil;
}
}
- (void)reportNetworkMetric:(BOOL)success error:(NSError *)error {
if (!self.metricsObserverBlock || !self.easyHandle) {
return;
}
double nameLookupTime = 0;
double connectTime = 0;
double appConnectTime = 0;
double preTransferTime = 0;
double startTransferTime = 0;
double totalTime = 0;
if (self.resolveDomainTimeInterval > 0) {
nameLookupTime = self.resolveDomainTimeInterval;
} else {
curl_easy_getinfo(self.easyHandle, CURLINFO_NAMELOOKUP_TIME, &nameLookupTime);
}
curl_easy_getinfo(self.easyHandle, CURLINFO_CONNECT_TIME, &connectTime);
curl_easy_getinfo(self.easyHandle, CURLINFO_APPCONNECT_TIME, &appConnectTime);
curl_easy_getinfo(self.easyHandle, CURLINFO_PRETRANSFER_TIME, &preTransferTime);
curl_easy_getinfo(self.easyHandle, CURLINFO_STARTTRANSFER_TIME, &startTransferTime);
curl_easy_getinfo(self.easyHandle, CURLINFO_TOTAL_TIME, &totalTime);
self.metricsObserverBlock(self.request,
success,
error,
nameLookupTime * 1000,
connectTime * 1000,
appConnectTime * 1000,
preTransferTime * 1000,
startTransferTime * 1000,
totalTime * 1000);
}
#pragma mark * curl option setup
- (void)populateRequestHeader:(CURL *)easyHandle {
NSURLRequest *request = self.request;
// 配置HTTP METHOD
if ([HTTP_METHOD_GET isEqualToString:request.HTTPMethod]) {
curl_easy_setopt(easyHandle, CURLOPT_HTTPGET, 1);
} else if ([HTTP_METHOD_POST isEqualToString:request.HTTPMethod]) {
curl_easy_setopt(easyHandle, CURLOPT_POST, 1);
} else if ([HTTP_METHOD_PUT isEqualToString:request.HTTPMethod]) {
curl_easy_setopt(easyHandle, CURLOPT_UPLOAD, 1);
} else if ([HTTP_METHOD_HEAD isEqualToString:request.HTTPMethod]) {
curl_easy_setopt(easyHandle, CURLOPT_NOBODY, 1);
} else {
curl_easy_setopt(easyHandle, CURLOPT_CUSTOMREQUEST, [request.HTTPMethod UTF8String]);
}
// 配置URL
curl_easy_setopt(easyHandle, CURLOPT_URL, request.URL.absoluteString.UTF8String);
// 配置 http version
switch (s_httpVersion) {
case HTTP3:
// 仅https url能使用quic
if (curlFeatureHttp3 && [request.URL.scheme caseInsensitiveCompare:@"https"] == NSOrderedSame) {
// Use HTTP/3, fallback to HTTP/2 or HTTP/1 if needed. For HTTPS only. For HTTP, this option makes libcurl return error.
curl_easy_setopt(easyHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_3);
} else if (curlFeatureHttp2) {
// Attempt HTTP 2 requests. libcurl falls back to HTTP 1.1 if HTTP 2 cannot be negotiated with the server.
curl_easy_setopt(easyHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2);
} else {
// 仅使用http1.1
curl_easy_setopt(easyHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
}
break;
case HTTP2:
if (curlFeatureHttp2) {
curl_easy_setopt(easyHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2);
curl_easy_setopt(easyHandle, CURLOPT_PIPEWAIT, 1L);
} else {
curl_easy_setopt(easyHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
}
break;
default:
curl_easy_setopt(easyHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
break;
}
if (s_enableBuiltInGzip) {
// 配置支持的HTTP压缩算法,""代表自动检测内置的算法,目前zlib支持deflate与gzip
curl_easy_setopt(easyHandle, CURLOPT_ACCEPT_ENCODING, "");
}
// 将拦截到的request的header字段进行透传
self.requestHeaderFields = [self convertHeadersToCurlSlist:request.allHTTPHeaderFields];
curl_easy_setopt(easyHandle, CURLOPT_HTTPHEADER, self.requestHeaderFields);
}
- (void)populateRequestBody:(CURL *)easyHandle {
NSURLRequest *request = self.request;
if (!request.HTTPBodyStream) {
if ([HTTP_METHOD_PUT isEqualToString:request.HTTPMethod]) {
curl_easy_setopt(easyHandle, CURLOPT_INFILESIZE_LARGE, 0L);
} else if ([HTTP_METHOD_POST isEqualToString:request.HTTPMethod]) {
curl_easy_setopt(easyHandle, CURLOPT_POSTFIELDSIZE_LARGE, 0L);
} else {
// 其他情况无需处理
}
return;
}
self.inputStream = request.HTTPBodyStream;
// 用read_cb回调函数来读取需要传输的数据
curl_easy_setopt(easyHandle, CURLOPT_READFUNCTION, read_cb);
// self传给read_cb函数的void *userp参数
curl_easy_setopt(easyHandle, CURLOPT_READDATA, (__bridge void *)self);
NSString *contentLength = [request valueForHTTPHeaderField:@"Content-Length"];
if (!contentLength) {
// 未设置Content-Length的情况,即使是使用Transfer-Encoding: chunked,也把totalBytesExpected设置为-1
self.totalBytesExpected = -1;
return;
}
int64_t length = [contentLength longLongValue];
self.totalBytesExpected = length;
if ([HTTP_METHOD_PUT isEqualToString:request.HTTPMethod]) {
curl_easy_setopt(easyHandle, CURLOPT_INFILESIZE_LARGE, length);
return;
}
if ([HTTP_METHOD_GET isEqualToString:request.HTTPMethod]
|| [HTTP_METHOD_HEAD isEqualToString:request.HTTPMethod]) {
// GET/HEAD方法不需要设置body
return;
}
// 其他情况,都以POST的方式指定Content-Length
curl_easy_setopt(easyHandle, CURLOPT_POSTFIELDSIZE_LARGE, length);
curl_easy_setopt(easyHandle, CURLOPT_POSTFIELDSIZE, length);
}
- (void)configEasyHandle:(CURL *)easyHandle error:(NSError **)error {
// 假如是quic这个framework,由于使用的boringssl无法访问苹果native CA,需要从Bundle中读取CA
if (curlFeatureHttp3) {
NSBundle *mainBundle = [NSBundle mainBundle];
NSURL *bundleURL = [mainBundle URLForResource:@"EMASCAResource" withExtension:@"bundle"];
if (!bundleURL) {
*error = [NSError errorWithDomain:@"fail to load CA certificate." code:-3 userInfo:nil];
return;
}
NSBundle *resourceBundle = [NSBundle bundleWithURL:bundleURL];
NSString *filePath = [resourceBundle pathForResource:@"cacert" ofType:@"pem"];
curl_easy_setopt(easyHandle, CURLOPT_CAINFO, [filePath UTF8String]);
}
// 是否设置自定义根证书
if (s_caFilePath) {
curl_easy_setopt(easyHandle, CURLOPT_CAINFO, [s_caFilePath UTF8String]);
}
// 假如设置了自定义resolve,则使用
if (s_dnsResolverClass) {
NSTimeInterval startTime = [[NSDate date] timeIntervalSince1970];
if ([self preResolveDomain:easyHandle]) {
self.resolveDomainTimeInterval = [[NSDate date] timeIntervalSince1970] - startTime;
}
}
// 设置cookie
EMASCurlCookieStorage *cookieStorage = [EMASCurlCookieStorage sharedStorage];
NSString *cookieString = [cookieStorage cookieStringForURL:self.request.URL];
if (cookieString) {
curl_easy_setopt(easyHandle, CURLOPT_COOKIE, [cookieString UTF8String]);
}
dispatch_sync(s_serialQueue, ^{
// 设置proxy
if (s_proxyServer) {
curl_easy_setopt(easyHandle, CURLOPT_PROXY, [s_proxyServer UTF8String]);
}
});
// 设置debug回调函数以输出日志
if (s_enableDebugLog) {
curl_easy_setopt(easyHandle, CURLOPT_VERBOSE, 1L);
curl_easy_setopt(easyHandle, CURLOPT_DEBUGFUNCTION, debug_cb);
}
// 设置header回调函数处理收到的响应的header数据
// receivedHeader会被传给header_cb函数的void *userp参数
curl_easy_setopt(easyHandle, CURLOPT_HEADERFUNCTION, header_cb);
curl_easy_setopt(easyHandle, CURLOPT_HEADERDATA, (__bridge void *)self);
// 设置write回调函数处理收到的响应的body数据
// self会被传给write_cb函数的void *userp
curl_easy_setopt(easyHandle, CURLOPT_WRITEFUNCTION, write_cb);
curl_easy_setopt(easyHandle, CURLOPT_WRITEDATA, (__bridge void *)self);
// 设置progress_callback以响应任务取消
curl_easy_setopt(easyHandle, CURLOPT_NOPROGRESS, 0L);
curl_easy_setopt(easyHandle, CURLOPT_XFERINFOFUNCTION, progress_callback);
curl_easy_setopt(easyHandle, CURLOPT_XFERINFODATA, (__bridge void *)self);
// 开启TCP keep alive
curl_easy_setopt(easyHandle, CURLOPT_TCP_KEEPALIVE, 1L);
// 设置连接超时时间
NSNumber *connectTimeoutInterval = [NSURLProtocol propertyForKey:(NSString *)kEMASCurlConnectTimeoutIntervalKey inRequest:self.request];
if (connectTimeoutInterval) {
curl_easy_setopt(easyHandle, CURLOPT_CONNECTTIMEOUT, connectTimeoutInterval.longValue);
}
// 设置请求超时时间
NSTimeInterval requestTimeoutInterval = self.request.timeoutInterval;
if (requestTimeoutInterval) {
curl_easy_setopt(easyHandle, CURLOPT_TIMEOUT, requestTimeoutInterval);
}
// 开启重定向
if (s_enableBuiltInRedirection) {
curl_easy_setopt(easyHandle, CURLOPT_FOLLOWLOCATION, 1L);
} else {
curl_easy_setopt(easyHandle, CURLOPT_FOLLOWLOCATION, 0L);
}
// 为了线程安全,设置NOSIGNAL
curl_easy_setopt(easyHandle, CURLOPT_NOSIGNAL, 1L);
// 设置公钥固定
if (s_publicKeyPinningKeyPath) {
curl_easy_setopt(easyHandle, CURLOPT_PINNEDPUBLICKEY, [s_publicKeyPinningKeyPath UTF8String]);
}
}
- (BOOL)preResolveDomain:(CURL *)easyHandle {
NSURL *url = self.request.URL;
if (!url || !url.host) {
return NO;
}
NSString *host = url.host;
NSNumber *port = url.port;
NSString *scheme = url.scheme.lowercaseString;
NSInteger resolvedPort;
if (port) {
resolvedPort = port.integerValue;
} else {
if ([scheme isEqualToString:@"https"]) {
resolvedPort = 443;
} else if ([scheme isEqualToString:@"http"]) {
resolvedPort = 80;
} else {
return NO;
}
}
NSString *address = [s_dnsResolverClass resolveDomain:host];
if (!address) {
return NO;
}
// Format: +{host}:{port}:{ips}
NSString *hostPortAddressString = [NSString stringWithFormat:@"+%@:%ld:%@",
host,
(long)resolvedPort,
address];
self.resolveList = curl_slist_append(self.resolveList, [hostPortAddressString UTF8String]);
if (self.resolveList) {
curl_easy_setopt(easyHandle, CURLOPT_RESOLVE, self.resolveList);
return YES;
}
return NO;
}
// 将拦截到的request中的header字段,转换为一个curl list
- (struct curl_slist *)convertHeadersToCurlSlist:(NSDictionary<NSString *, NSString *> *)headers {
struct curl_slist *headerFields = NULL;
for (NSString *key in headers) {
// 对于Content-Length,使用CURLOPT_POSTFIELDSIZE_LARGE指定,不要在这里透传,否则POST重定向为GET时仍会保留Content-Length,导致错误
if ([key caseInsensitiveCompare:@"Content-Length"] == NSOrderedSame) {
continue;
}
NSString *value = headers[key];
NSString *header;
if ([[value stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet] length] == 0) {
header = [NSString stringWithFormat:@"%@;", key];
} else {
header = [NSString stringWithFormat:@"%@: %@", key, value];
}
headerFields = curl_slist_append(headerFields, [header UTF8String]);
}
return headerFields;
}
#pragma mark * libcurl callback function
// libcurl的header回调函数,用于处理收到的header
size_t header_cb(char *buffer, size_t size, size_t nitems, void *userdata) {
EMASCurlProtocol *protocol = (__bridge EMASCurlProtocol *)userdata;
size_t totalSize = size * nitems;
NSData *data = [NSData dataWithBytes:buffer length:size * nitems];
NSString *headerLine = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
headerLine = [headerLine stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if ([headerLine hasPrefix:@"HTTP/"]) {
// 头部首行,标识新的头部开始
[protocol.currentResponse reset];
NSArray<NSString *> *components = [headerLine componentsSeparatedByString:@" "];
if (components.count >= 3) {
protocol.currentResponse.httpVersion = components[0];
protocol.currentResponse.statusCode = [components[1] integerValue];
protocol.currentResponse.reasonPhrase = [[components subarrayWithRange:NSMakeRange(2, components.count - 2)] componentsJoinedByString:@" "];
} else if (components.count == 2) {
protocol.currentResponse.httpVersion = components[0];
protocol.currentResponse.statusCode = [components[1] integerValue];
protocol.currentResponse.reasonPhrase = @"";
}
} else {
NSRange delimiterRange = [headerLine rangeOfString:@": "];
if (delimiterRange.location != NSNotFound) {
NSString *key = [headerLine substringToIndex:delimiterRange.location];
NSString *value = [headerLine substringFromIndex:delimiterRange.location + delimiterRange.length];
// 设置cookie
if ([key caseInsensitiveCompare:@"set-cookie"] == NSOrderedSame) {
EMASCurlCookieStorage *cookieStorage = [EMASCurlCookieStorage sharedStorage];
[cookieStorage setCookieWithString:value forURL:protocol.request.URL];
}
if (protocol.currentResponse.headers[key]) {
NSString *existingValue = protocol.currentResponse.headers[key];
NSString *combinedValue = [existingValue stringByAppendingFormat:@", %@", value];
protocol.currentResponse.headers[key] = combinedValue;
} else {
protocol.currentResponse.headers[key] = value;
}
}
}
if ([headerLine length] == 0) {
// 尾行,标识当前头部读取结束
NSInteger statusCode = protocol.currentResponse.statusCode;
NSString *reasonPhrase = protocol.currentResponse.reasonPhrase;
NSHTTPURLResponse *httpResponse = [[NSHTTPURLResponse alloc] initWithURL:protocol.request.URL
statusCode:protocol.currentResponse.statusCode
HTTPVersion:protocol.currentResponse.httpVersion
headerFields:protocol.currentResponse.headers];
if (isRedirectionStatusCode(statusCode)) {
if (!s_enableBuiltInRedirection) {
// 关闭了重定向支持,则把重定向信息往外传递
__block NSString *location = nil;
[protocol.currentResponse.headers enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSString * _Nonnull obj, BOOL * _Nonnull stop) {
if ([key caseInsensitiveCompare:@"Location"] == NSOrderedSame) {
location = obj;
*stop = YES;
}
}];
if (location) {
NSURL *locationURL = [NSURL URLWithString:location relativeToURL:protocol.request.URL];
NSMutableURLRequest *redirectedRequest = [protocol.request mutableCopy];
[redirectedRequest setURL:locationURL];
[protocol.client URLProtocol:protocol wasRedirectedToRequest:redirectedRequest redirectResponse:httpResponse];
} else {
}
}
[protocol.currentResponse reset];
} else if (isInformationalStatusCode(statusCode)) {
[protocol.currentResponse reset];
} else if (isConnectEstablishedStatusCode(statusCode, reasonPhrase)) {
[protocol.currentResponse reset];
} else {
[protocol.client URLProtocol:protocol didReceiveResponse:httpResponse cacheStoragePolicy:NSURLCacheStorageAllowed];
protocol.currentResponse.isFinalResponse = YES;
}
}
return totalSize;
}
BOOL isRedirectionStatusCode(NSInteger statusCode) {
switch (statusCode) {
case 300: // Multiple Choices
case 301: // Moved Permanently
case 302: // Found
case 303: // See Other
case 307: // Temporary Redirect
case 308: // Permanent Redirect
return YES;
default:
return NO;
}
}
BOOL isInformationalStatusCode(NSInteger statusCode) {
return statusCode >= 100 && statusCode < 200;
}
BOOL isConnectEstablishedStatusCode(NSInteger statusCode, NSString *reasonPhrase) {
return statusCode == 200 && [reasonPhrase caseInsensitiveCompare:@"connection established"] == NSOrderedSame;
}
// libcurl的write回调函数,用于处理收到的body
static size_t write_cb(void *contents, size_t size, size_t nmemb, void *userp) {
EMASCurlProtocol *protocol = (__bridge EMASCurlProtocol *)userp;
NSMutableData *data = [[NSMutableData alloc] initWithBytes:contents length:size * nmemb];
// 只有确认获得已经读取了最后一个响应,接受的数据才视为有效数据
if (protocol.currentResponse.isFinalResponse) {
[protocol.client URLProtocol:protocol didLoadData:data];
}
return size * nmemb;
}
// libcurl的read回调函数,用于post等需要设置body数据的方法
static size_t read_cb(char *buffer, size_t size, size_t nitems, void *userp) {
EMASCurlProtocol *protocol = (__bridge EMASCurlProtocol *)userp;
if (!protocol || !protocol.inputStream) {
return CURL_READFUNC_ABORT;
}
if (protocol.shouldCancel) {
return CURL_READFUNC_ABORT;
}
if ([protocol.inputStream streamStatus] == NSStreamStatusNotOpen) {
[protocol.inputStream open];
}
NSInteger bytesRead = [protocol.inputStream read:(uint8_t *)buffer maxLength:size * nitems];
if (bytesRead < 0) {
return CURL_READFUNC_ABORT;
}
protocol.totalBytesSent += bytesRead;
if (protocol.uploadProgressUpdateBlock) {
protocol.uploadProgressUpdateBlock(protocol.request,
bytesRead,
protocol.totalBytesSent,
protocol.totalBytesExpected);
}
return bytesRead;
}
static int progress_callback(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) {
EMASCurlProtocol *protocol = (__bridge EMASCurlProtocol *)clientp;
// 检查是否取消传输
if (protocol.shouldCancel) {
return 1;
}
return 0;
}
// libcurl的debug回调函数,输出libcurl的运行日志
static int debug_cb(CURL *handle, curl_infotype type, char *data, size_t size, void *userptr) {
switch (type) {
case CURLINFO_TEXT:
NSLog(@"[CURLINFO_TEXT] %.*s", (int)size, data);
break;
case CURLINFO_HEADER_IN:
NSLog(@"[CURLINFO_HEADER_IN] %.*s", (int)size, data);
break;
case CURLINFO_HEADER_OUT:
NSLog(@"[CURLINFO_HEADER_OUT] %.*s", (int)size, data);
break;
case CURLINFO_DATA_IN:
NSLog(@"[CURLINFO_DATA_IN] %.*s", (int)size, data);
break;
case CURLINFO_DATA_OUT:
NSLog(@"[CURLINFO_DATA_OUT] %.*s", (int)size, data);
break;
case CURLINFO_SSL_DATA_IN:
NSLog(@"[CURLINFO_SSL_DATA_IN] %.*s", (int)size, data);
break;
case CURLINFO_SSL_DATA_OUT:
NSLog(@"[CURLINFO_SSL_DATA_OUT] %.*s", (int)size, data);
break;
case CURLINFO_END:
NSLog(@"[CURLINFO_END] %.*s", (int)size, data);
default:
break;
}
return 0;
}
@end