一直有用戶反映,不管通過(guò)通過(guò)手機(jī)端、還是PC端訪問(wèn)我們的產(chǎn)品都會(huì)不定時(shí)出現(xiàn)域名劫持的問(wèn)題。為了解決這個(gè)問(wèn)題,我們只能繞過(guò)傳統(tǒng)的運(yùn)營(yíng)商域名解析,通過(guò)IP直接訪問(wèn)服務(wù)。本文對(duì)App中集成HttpDNS作簡(jiǎn)要介紹。
一、HTTPDNS介紹:
httpDNS是阿里提供的面向移動(dòng)端的域名解析產(chǎn)品,提供了面向移動(dòng)端的SDK,客戶端可以通過(guò)傳入域名的方式調(diào)用,SDK會(huì)直接返回解析出的IP地址。
NSString *ip = [[HttpDnsService sharedInstance] getIpByHostAsync:Domain];
二、需求描述:
對(duì)項(xiàng)目中: 1.原生圖片的請(qǐng)求, 2.H5網(wǎng)頁(yè)中請(qǐng)求,分別做IP直連處理。
三、實(shí)現(xiàn)方案:
- 實(shí)現(xiàn)原理:
通過(guò)注冊(cè)NSURLProtocol,攔截所有請(qǐng)求,過(guò)濾出相應(yīng)的圖片請(qǐng)求及H5網(wǎng)頁(yè)請(qǐng)求,將請(qǐng)求的url中的域名替換為IP后,重新發(fā)起請(qǐng)求,獲取到響應(yīng)數(shù)據(jù)后,回調(diào)給URL Loading System。
- 實(shí)現(xiàn)過(guò)程:
1.攔截請(qǐng)求
由于原生圖片和H5網(wǎng)頁(yè)中的請(qǐng)求需要分開處理以便于實(shí)現(xiàn)通過(guò)降級(jí)開關(guān)分別控制,所以注冊(cè)了兩個(gè)NSURLProtocol分別處理這兩項(xiàng)業(yè)務(wù),具體策略為:
YH_ImageProtocol在攔截到請(qǐng)求后,按照URL后綴(是否包含:.jpg/.jpeg/.png/.gif)過(guò)濾出圖片的URL。
YH_WebProtocol在攔截到請(qǐng)求后,排除圖片的URL,則認(rèn)為是需要攔截的請(qǐng)求。
2. 手動(dòng)發(fā)起請(qǐng)求
攔截到請(qǐng)求后,需要根據(jù)協(xié)議分別做處理:如果是HTTP請(qǐng)求,使用NSURLSession重新發(fā)起請(qǐng)求,獲取到響應(yīng)的數(shù)據(jù)后,回調(diào)給URL Loaidng System。如果是HTTPS請(qǐng)求,由于當(dāng)前請(qǐng)求URL的域名被替換成了IP地址,請(qǐng)求URL中的host也會(huì)被替換成HTTPDNS解析出來(lái)的IP,導(dǎo)致服務(wù)器獲取到的域名為解析后的IP,無(wú)法找到匹配的證書,只能返回默認(rèn)的證書或者不返回,所以會(huì)出現(xiàn)SSL/TLS握手不成功的錯(cuò)誤。為了解決這個(gè)問(wèn)題,我們需要hook HTTPS訪問(wèn)前SSL連接過(guò)程,根據(jù)網(wǎng)絡(luò)請(qǐng)求頭部域中的HOST信息,設(shè)置SSL連接PeerHost的值,之后根據(jù)服務(wù)器返回的證書執(zhí)行驗(yàn)證過(guò)程。所以在攔截網(wǎng)絡(luò)請(qǐng)求后,使用CFHTTPMessageRef創(chuàng)建NSInputStream實(shí)例進(jìn)行Socket通信,并設(shè)置其kCFStreamSSLPeerName的值:
// 創(chuàng)建CFHTTPMessage對(duì)象的輸入流
CFReadStreamRef readStream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault,cfrequest);
inputStream = (__bridge_transfer NSInputStream *) readStream;
// 設(shè)置SNI host信息
NSString *host = [curRequest.allHTTPHeaderFields objectForKey:@"host"];
if (!host) {
host = curRequest.URL.host;
}
[inputStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL forKey:NSStreamSocketSecurityLevelKey];
NSDictionary *sslProperties = [[NSDictionary alloc] initWithObjectsAndKeys:
host, (__bridge id) kCFStreamSSLPeerName,
nil];
[inputStream setProperty:sslProperties forKey:(__bridge_transfer NSString *) kCFStreamPropertySSLSettings];
[inputStream setDelegate:self];
3.重定向
當(dāng)返回的StatusCode在300、400之間,且header中l(wèi)ocation字段中取出合法的URL時(shí),用該URL初始化新的請(qǐng)求,在protocol內(nèi)部重新執(zhí)行一遍之前的流程。
四、碰到的問(wèn)題及解決方法:
-
GZIP
之前在測(cè)試過(guò)程中發(fā)現(xiàn),用Webview加載官網(wǎng)時(shí),頁(yè)面顯示亂碼。經(jīng)排查,確認(rèn)是返回的content-type為gzip,因?yàn)槲唇鈮簩?dǎo)致頁(yè)面無(wú)法識(shí)別。為此,我們?cè)谑盏巾憫?yīng)后,先判斷content類型,如果為gzip,先進(jìn)行解壓再回調(diào)給相應(yīng)的client。
-
CSS文件中通過(guò)相對(duì)路徑的方式引用的靜態(tài)資源無(wú)法加載
該問(wèn)題發(fā)生的具體原因是:在WebView中的請(qǐng)求被攔截,域名改為IP直連后,CSS文件中通過(guò)相對(duì)路徑引用的靜態(tài)資源(包括iconfont和少量圖片)的url直接沿用了CSS文件URL中的IP地址作為域名,跳過(guò)了域名解析的步驟,且header中的HOST字段未設(shè)置為相應(yīng)的域名。最終導(dǎo)致無(wú)法通過(guò)SNI擴(kuò)展的方式獲取到SSL證書,建連失敗。我們的解決方案是保存好IP地址和域名的映射關(guān)系,碰到前述問(wèn)題時(shí),能夠獲取到IP地址對(duì)應(yīng)的域名,設(shè)置給HOST,以保證SSL握手成功。
五、待改進(jìn)的地方:
目前的業(yè)務(wù)需求是,攔截到的H5請(qǐng)求,全部強(qiáng)制轉(zhuǎn)為HTTPS方式請(qǐng)求。這種情況下會(huì)導(dǎo)致一些服務(wù)端不支持HTTPS的請(qǐng)求失敗,尤其跳轉(zhuǎn)到一些第三方網(wǎng)站的頁(yè)面。為避免該問(wèn)題,我們應(yīng)該提供一種容錯(cuò)機(jī)制,當(dāng)強(qiáng)制使用HTTPS的方式去打開頁(yè)面時(shí),如果SSL握手失敗,可以再改為HTTP的方式去請(qǐng)求。