使用CFNetwork进行HTTP请求

背景

CFNetwork是比BSD套接字层级高,比Foundation的NSURLSession层级低的网络API。CFNetwork更侧重于网络协议,而Foundation级别API侧重于数据访问,例如通过HTTP或FTP传输数据。虽然NSURLSession使用起来更方便,但是对网络协议的可控性较低,这在iOS下使用HttpDNS进行IP直连避免DNS劫持中针对服务器使用多个域名和证书问题却没有解决办法,需要依靠低一层的CFNetwork去解决这个问题。

关键流程

创建请求

在握手之前设置SNI(iOS下使用HttpDNS进行IP直连避免DNS劫持第四个注意事项)。客户端在发起 SSL 握手请求时(具体说来,是客户端发出 SSL 请求中的 ClientHello 阶段),就提交请求的 Host 信息,使得服务器能够切换到正确的域并返回相应的证书。

// HTTPS请求处理SNI场景
if ([self isHTTPSScheme]) {
    // 设置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报文,查看其中的Server Name Indication extension字段的内容进行验证:


屏幕快照 2019-05-27 上午12.37.17.png

目前有疑问:
1> 使用Safari进行IP直连,SNI中是IP地址;使用Chrome进行IP直连,没有设置SNI。

读取数据流

使用CFNetwork与NSURLSession的的最大区别就是需要自己来维护数据的读取:

{
    // 创建CFHTTPMessage对象的输入流
    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];
}

在从流中读取数据的时候,可能会等待很长时间,如果使用同步读取,那么app会强制等待数据传输,因此需要使用非阻塞读取数据的方法,iOS推荐使用runLoop来实现非阻塞读取。“-scheduleInRunLoop:forMode:”就实现了通过runLoop来避免阻塞读取。
大致看一下"-scheduleInRunLoop:forMode:"实现了一个什么效果,runLoop是当前线程的runLoop,当前线程为:

(lldb) po [NSThread currentThread]
<NSThread: 0x600001ad9100>{number = 3, name = com.apple.CFNetwork.CustomProtocols}

通过观察"-scheduleInRunLoop:forMode:"执行前后runLoop中多出来的东西,就可以判断出该方法向runLoop中注册了什么内容,经过验证,是向runLoop中注册了一个source0:

<CFRunLoopSource 0x600003a53a80 [0x111416b68]>{signalled = Yes, valid = Yes, order = 0, context = (
    "<__NSCFInputStream: 0x600003d5b3c0>",
    "<__NSCFInputStream: 0x600003d53330>",
    "<__NSCFOutputStream: 0x600003d522e0>"
)

当有数据可读的时候,当前线程上的source0就会被激活,然后当前线程的runLoop被唤醒,执行source0的回调,这个回调中就会执行self.inputStream的
delegate的方法"-stream:handleEvent:"。在有数据可读的时候,读取数据,保存进本地缓存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;
    }
}
处理数据

在self.inputStream的代理delegate的方法"-stream:handleEvent:"中eventCode为NSStreamEventEndEncountered时,标识数据读取完成,这时需要处理数据,处理数据分为两部分,第一部分是响应头,第二部分是实体主体。

处理响应头

首先从self.inputStream中读取响应头

CFReadStreamRef readStream = (__bridge CFReadStreamRef) self.inputStream;
CFHTTPMessageRef message = (CFHTTPMessageRef) CFReadStreamCopyProperty(readStream, kCFStreamPropertyHTTPResponseHeader);
if (!message) {
    return NO;
}
result = CFHTTPMessageIsHeaderComplete(message);

然后判断是否需要进行重定向,如果返回状态码为301,302,303则进行重定向,

- (BOOL)needRedirection {
    BOOL needRedirect = NO;
    switch (self.response.statusCode) {
            // 永久重定向
        case 301:
            // 暂时重定向
        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重定向为GET
                    mRequest.HTTPMethod = @"GET";
                    mRequest.HTTPBody = nil;
                }
                [mRequest setValue:nil forHTTPHeaderField:@"host"];
                self.redirectRequest = mRequest;
                needRedirect = YES;
                break;
            }
        }
            // POST不重定向为GET,询问用户是否携带POST数据(很少使用)
            //case 307:
            //    break;
        default:
            break;
    }
    return needRedirect;
}

如果是HTTPS协议,则需要校验证书,校验证书的时候需要获取request的header中的host字段的值(iOS下IP直连避免DNS劫持第一个注意事项)来与服务器证书中的域名进行比较(iOS下IP直连避免DNS劫持第三个注意事项)。

// HTTPS校验证书
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()];
    }
    // 绑定校验策略到服务端的证书上
    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) {
        // 证书验证不通过
        [self.delegate task:self didCompleteWithError:[[NSError alloc] initWithDomain:@"fail to evaluate the server trust" code:-1 userInfo:nil]];
        result = NO;
    }
}
处理实体主体

处理实体主体需要注意的只有1点,就是当响应头中的"Content-Encoding"为"gzip"时,需要进行解压。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容