iOS中SNI 实现
1.SNI的概念与原理
SNI(Server Name Indication)是为了解决一个服务器使用多个域名和证书的SSL/TLS扩展。它的工作原理如下:
- 在连接到服务器建立SSL链接之前先发送要访问站点的域名(Hostname)。
- 服务器根据这个域名返回一个合适的证书。
上述过程中,当客户端直接用这个IP替换URL中域名发起请求,请求URL中头部的host字段会被替换成HttpDns解析出来的IP,导致服务器获取到的域名为解析后的IP。也就导致服务器不知道你想请求哪个域名,因为服务器里有多个域名,也就无法找到匹配的证书,只能返回默认的证书或者不返回,所以会出现SSL/TLS握手不成功的错误。而且如果你想强制把host头部字段改成域名,那也是无效的,请求发出去后,iOS会自动把host改成ip。
由于iOS上层网络库NSURLConnection/NSURLSession
没有提供接口进行SNI字段的配置,因此可以考虑使用NSURLProtocol
拦截网络请求,用更底层的网络库去修改,才能实现。
2.用libcurl实现 iOS SNI
之所以用libcurl,是因为它有一个CURLOPT_RESOLVE
函数可以解决SNI问题,下面就是我用libcurl写的发起http请求的函数,需要传入两个参数,一个是URL字符串,一个是形如Domin:port:IP
的一个字符串,解释下这个字符串的几个参数
- Domin就是URL的域名比如URL是https://www.baidu.com,那么Domin就是www.baidu.com
- port指的是这个域名对应的端口号,https一般是443,http是80,具体请求情况不同
- IP就是你需要让这个域名对应具体的那个IP
下面是我写的一个发起请求的范例,你也可以直接参照官方文档里的Example,更简洁些,会看官方文档很关键呐.
我自己写的代码如下:
//这是发起请求的函数
CURLcode sendRequestWithResolve(const char *resolve,const char *url,const char *postFile, const char * cerPath){
CURL *curl;
CURLcode res = CURLE_SEND_ERROR; //默认返回值为发送请求错误
struct curl_slist *headers = NULL;
struct curl_slist *host = NULL;
//增加resolve映射表
host = curl_slist_append(host, resolve);
//增加HTTP header
headers = curl_slist_append(headers, "Accept:application/json");
headers = curl_slist_append(headers, "Content-Type:application/json");
headers = curl_slist_append(headers, "charset:utf-8");
curl = curl_easy_init(); // 初始化
if (curl)
{
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);// 改协议头
curl_easy_setopt(curl, CURLOPT_RESOLVE, host);
if (postFile) {
//如果是POST请求的话,需要设置参数,具体如何发起POST请求自行Google吧
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postFile);
}
if(res != CURLE_OK){
fprintf(stderr, "curl_easy_perform() failed: %s\n",curl_easy_strerror(res));
}
curl_easy_setopt(curl, CURLOPT_URL,url);
//检查自签名证书路径是否存在,是否要进行自签名校验
if(cerPath) {
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, true);
curl_easy_setopt(curl, CURLOPT_CAINFO, cerPath);
}
res = curl_easy_perform(curl); // 执行curl请求
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
}
curl_slist_free_all(host);
return res;
}
//发起请求 因为这个函数也可以支持POST请求和自签名证书,如果不需要就传NULL好了
CURLcode code = sendRequestWithResolve("www.baidu.com:443:111.13.100.92", "https://www.baidu.com/",NULL,NULL);
NSLog(@"libCurl request code %i",code);
双向验证
我之前理解“双向验证”,就是自签名证书的验证并手动继续而已。但其实不是,双向认证和单向认证原理基本差不多,只是除了客户端需要认证服务端以外,增加了服务端对客户端的认证。
HTTPS 是支持双向认证的,不过那指的是客户端(浏览器或 APP)也像服务端一样,在发送请求给服务端的时候带上证书,再由服务端使用对应的私钥进行验证。一般 APP 不需要这么做。所以代码里我也注释掉了,如果有需要再具体研究。
https的请求用libcurl发起,就能解决SNI问题了。当然,最好是结合NSURLProtocol
来统一处理请求,具体实现请自己探索。
3.使用libcurl过程中遇到的一些问题
3.1 如何获取libcurl
libcurl官网并没有直接可供iOS使用的源码或者库,需要我们手动编译。我们要把openssl源码交叉编译进libcurl中,这样才能支持https请求,这也就要求我们先编译openssl,同样,openssl官网也是没有iOS可用的源码或者库的。看到这里你已经知道编译是件多么头痛的事情了吧,直接去网上下载支持https的libcurl库是一种方式,但基本上这些库里面的openssl或者libcurl版本都比较旧了,可能存在一些历史遗留问题。所以最好的方式就是自己去手动编译支持https的libcurl!网络上大部分都是用脚本编译的,但很遗憾很多脚本都是无效的,推荐看我自己写的这篇文章,编译支持iOS的libcurl+OpenSSL库(支持https IPv6),里面详细地介绍了如何编译libcurl的过程,我已经测试过libcurl 7.56.1 混编openssl 1.1.0g,可以顺利编译,并且能正常发起http/https请求。
3.2 libcurl的线程安全问题
浅析libcurl多线程安全问题这篇文章里面比较详细地探究了libcurl的线程安全问题,值得一看,文章提到在使用多线程libcurl发送请求,在未设置超时或长超时的情况下程序运行良好。但只要设置了较短超时(小于180s),程序就会出现随机的coredump。并且栈里面找不到任何有用的信息。解决方式是用easy_setopt(curl, CURLOPT_NOSIGNAL, (long)1);来避免。还有curl_global_init这个函数要放在程序入口执行,我的做法是放到APPDelegate.m文件的didFinishLaunch函数中执行。防止多次重复创建。
4.结语
这次SNI调研花了不少时间在熟悉libcurl上,但是结果还是令人满意的。