背景
CFNetwork是比BSD套接字層級(jí)高,比Foundation的NSURLSession層級(jí)低的網(wǎng)絡(luò)API踢涌。CFNetwork更側(cè)重于網(wǎng)絡(luò)協(xié)議,而Foundation級(jí)別API側(cè)重于數(shù)據(jù)訪問序宦,例如通過HTTP或FTP傳輸數(shù)據(jù)睁壁。雖然NSURLSession使用起來更方便,但是對(duì)網(wǎng)絡(luò)協(xié)議的可控性較低互捌,這在iOS下使用HttpDNS進(jìn)行IP直連避免DNS劫持中針對(duì)服務(wù)器使用多個(gè)域名和證書問題卻沒有解決辦法潘明,需要依靠低一層的CFNetwork去解決這個(gè)問題。
關(guān)鍵流程
創(chuàng)建請(qǐng)求
在握手之前設(shè)置SNI(iOS下使用HttpDNS進(jìn)行IP直連避免DNS劫持第四個(gè)注意事項(xiàng))秕噪∏担客戶端在發(fā)起 SSL 握手請(qǐng)求時(shí)(具體說來,是客戶端發(fā)出 SSL 請(qǐng)求中的 ClientHello 階段)腌巾,就提交請(qǐng)求的 Host 信息遂填,使得服務(wù)器能夠切換到正確的域并返回相應(yīng)的證書铲觉。
// HTTPS請(qǐng)求處理SNI場(chǎng)景
if ([self isHTTPSScheme]) {
// 設(shè)置SNI host信息
NSString *host = [self.swizzleRequest.allHTTPHeaderFields objectForKey:@"host"];
if (!host) {
host = self.originalRequest.URL.host;
}
[self.inputStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL forKey:NSStreamSocketSecurityLevelKey];
NSDictionary *sslProperties = @{ (__bridge id) kCFStreamSSLPeerName : host };
[self.inputStream setProperty:sslProperties forKey:(__bridge_transfer NSString *) kCFStreamPropertySSLSettings];
}
然后通過wireshare抓取SSL握手中clientHello報(bào)文,查看其中的Server Name Indication extension字段的內(nèi)容進(jìn)行驗(yàn)證:
目前有疑問:
1> 使用Safari進(jìn)行IP直連城菊,SNI中是IP地址备燃;使用Chrome進(jìn)行IP直連,沒有設(shè)置SNI凌唬。
讀取數(shù)據(jù)流
使用CFNetwork與NSURLSession的的最大區(qū)別就是需要自己來維護(hù)數(shù)據(jù)的讀取:
{
// 創(chuàng)建CFHTTPMessage對(duì)象的輸入流
CFReadStreamRef readStream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault, cfRequest);
self.inputStream = (__bridge_transfer NSInputStream *) readStream;
// 打開流
__weak typeof(self) weakSelf = self;
self.runloop = [NSRunLoop currentRunLoop];
[self startTimer];
[self.inputStream setDelegate:weakSelf];
[self.inputStream scheduleInRunLoop:self.runloop forMode:[self runloopMode]];
[self.inputStream open];
}
在從流中讀取數(shù)據(jù)的時(shí)候漏麦,可能會(huì)等待很長時(shí)間客税,如果使用同步讀取,那么app會(huì)強(qiáng)制等待數(shù)據(jù)傳輸撕贞,因此需要使用非阻塞讀取數(shù)據(jù)的方法更耻,iOS推薦使用runLoop來實(shí)現(xiàn)非阻塞讀取∧笈颍“-scheduleInRunLoop:forMode:”就實(shí)現(xiàn)了通過runLoop來避免阻塞讀取秧均。
大致看一下"-scheduleInRunLoop:forMode:"實(shí)現(xiàn)了一個(gè)什么效果,runLoop是當(dāng)前線程的runLoop号涯,當(dāng)前線程為:
(lldb) po [NSThread currentThread]
<NSThread: 0x600001ad9100>{number = 3, name = com.apple.CFNetwork.CustomProtocols}
通過觀察"-scheduleInRunLoop:forMode:"執(zhí)行前后runLoop中多出來的東西目胡,就可以判斷出該方法向runLoop中注冊(cè)了什么內(nèi)容,經(jīng)過驗(yàn)證链快,是向runLoop中注冊(cè)了一個(gè)source0:
<CFRunLoopSource 0x600003a53a80 [0x111416b68]>{signalled = Yes, valid = Yes, order = 0, context = (
"<__NSCFInputStream: 0x600003d5b3c0>",
"<__NSCFInputStream: 0x600003d53330>",
"<__NSCFOutputStream: 0x600003d522e0>"
)
當(dāng)有數(shù)據(jù)可讀的時(shí)候誉己,當(dāng)前線程上的source0就會(huì)被激活,然后當(dāng)前線程的runLoop被喚醒域蜗,執(zhí)行source0的回調(diào)巨双,這個(gè)回調(diào)中就會(huì)執(zhí)行self.inputStream的
delegate的方法"-stream:handleEvent:"。在有數(shù)據(jù)可讀的時(shí)候霉祸,讀取數(shù)據(jù)筑累,保存進(jìn)本地緩存self.resultData中。
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
switch (eventCode) {
case NSStreamEventOpenCompleted:
//NSLog(@"InputStream opened success.");
break;
case NSStreamEventHasBytesAvailable:
{
if (![self analyseResponse]) {
return;
}
UInt8 buffer[BUFFER_SIZE];
NSInteger numBytesRead = 0;
NSInputStream *inputstream = (NSInputStream *) aStream;
// Read data
do {
numBytesRead = [inputstream read:buffer maxLength:sizeof(buffer)];
if (numBytesRead > 0) {
[self.resultData appendBytes:buffer length:numBytesRead];
}
} while (numBytesRead > 0);
}
break;
case NSStreamEventErrorOccurred:
self.completed = YES;
[self.delegate task:self didCompleteWithError:[aStream streamError]];
break;
case NSStreamEventEndEncountered:
self.completed = YES;
if (!self.responseAlreadyAnalysed) {
if (![self analyseResponse]) {
return;
}
}
[self handleResult];
break;
default:
break;
}
}
處理數(shù)據(jù)
在self.inputStream的代理delegate的方法"-stream:handleEvent:"中eventCode為NSStreamEventEndEncountered時(shí)丝蹭,標(biāo)識(shí)數(shù)據(jù)讀取完成慢宗,這時(shí)需要處理數(shù)據(jù),處理數(shù)據(jù)分為兩部分半夷,第一部分是響應(yīng)頭婆廊,第二部分是實(shí)體主體。
處理響應(yīng)頭
首先從self.inputStream中讀取響應(yīng)頭
CFReadStreamRef readStream = (__bridge CFReadStreamRef) self.inputStream;
CFHTTPMessageRef message = (CFHTTPMessageRef) CFReadStreamCopyProperty(readStream, kCFStreamPropertyHTTPResponseHeader);
if (!message) {
return NO;
}
result = CFHTTPMessageIsHeaderComplete(message);
然后判斷是否需要進(jìn)行重定向巫橄,如果返回狀態(tài)碼為301淘邻,302,303則進(jìn)行重定向湘换,
- (BOOL)needRedirection {
BOOL needRedirect = NO;
switch (self.response.statusCode) {
// 永久重定向
case 301:
// 暫時(shí)重定向
case 302:
// POST重定向GET
case 303:
{
NSString *location = self.response.headerFields[@"Location"];
if (location) {
NSURL *url = [[NSURL alloc] initWithString:location];
NSMutableURLRequest *mRequest = [self.swizzleRequest mutableCopy];
mRequest.URL = url;
if ([[self.swizzleRequest.HTTPMethod lowercaseString] isEqualToString:@"post"]) {
// POST重定向?yàn)镚ET
mRequest.HTTPMethod = @"GET";
mRequest.HTTPBody = nil;
}
[mRequest setValue:nil forHTTPHeaderField:@"host"];
self.redirectRequest = mRequest;
needRedirect = YES;
break;
}
}
// POST不重定向?yàn)镚ET宾舅,詢問用戶是否攜帶POST數(shù)據(jù)(很少使用)
//case 307:
// break;
default:
break;
}
return needRedirect;
}
如果是HTTPS協(xié)議统阿,則需要校驗(yàn)證書,校驗(yàn)證書的時(shí)候需要獲取request的header中的host字段的值(iOS下IP直連避免DNS劫持第一個(gè)注意事項(xiàng))來與服務(wù)器證書中的域名進(jìn)行比較(iOS下IP直連避免DNS劫持第三個(gè)注意事項(xiàng))筹我。
// HTTPS校驗(yàn)證書
if ([self isHTTPSScheme]) {
SecTrustRef trust = (__bridge SecTrustRef) [self.inputStream propertyForKey:(__bridge NSString *) kCFStreamPropertySSLPeerTrust];
SecTrustResultType res = kSecTrustResultInvalid;
NSMutableArray *policies = [NSMutableArray array];
NSString *domain = [[self.swizzleRequest allHTTPHeaderFields] valueForKey:@"host"];
if (domain) {
[policies addObject:(__bridge_transfer id) SecPolicyCreateSSL(true, (__bridge CFStringRef) domain)];
} else {
[policies addObject:(__bridge_transfer id) SecPolicyCreateBasicX509()];
}
// 綁定校驗(yàn)策略到服務(wù)端的證書上
SecTrustSetPolicies(trust, (__bridge CFArrayRef) policies);
if (SecTrustEvaluate(trust, &res) != errSecSuccess) {
[self.delegate task:self didCompleteWithError:[[NSError alloc] initWithDomain:@"can not evaluate the server trust" code:-1 userInfo:nil]];
result = NO;
} else if (res != kSecTrustResultProceed && res != kSecTrustResultUnspecified) {
// 證書驗(yàn)證不通過
[self.delegate task:self didCompleteWithError:[[NSError alloc] initWithDomain:@"fail to evaluate the server trust" code:-1 userInfo:nil]];
result = NO;
}
}
處理實(shí)體主體
處理實(shí)體主體需要注意的只有1點(diǎn)扶平,就是當(dāng)響應(yīng)頭中的"Content-Encoding"為"gzip"時(shí),需要進(jìn)行解壓蔬蕊。