784
阿裏雲
單IP多HTTPS域名場景下的解決方案__最佳實踐_HTTPDNS-阿裏雲
1.背景說明
在搭建支持HTTPS的前端代理服務器時候,通常會遇到讓人頭痛的證書問題。根據HTTPS的工作原理,瀏覽器在訪問一個HTTPS站點時,先與服務器建立SSL連接,建立連接的第一步就是請求服務器的證書。而服務器在發送證書的時候,是不知道瀏覽器訪問的是哪個域名的,所以不能根據不同域名發送不同的證書。
SNI(Server Name Indication)是為了解決一個服務器使用多個域名和證書的SSL/TLS擴展。它的工作原理如下:
- 在連接到服務器建立SSL鏈接之前先發送要訪問站點的域名(Hostname)。
- 服務器根據這個域名返回一個合適的證書。
目前,大多數操作係統和瀏覽器都已經很好地支持SNI擴展,OpenSSL 0.9.8也已經內置這一功能。
上述過程中,當客戶端使用HTTPDNS解析域名時,請求URL中的host會被替換成HTTPDNS解析出來的IP,導致服務器獲取到的域名為解析後的IP,無法找到匹配的證書,隻能返回默認的證書或者不返回,所以會出現SSL/TLS握手不成功的錯誤。
比如當你需要通過HTTPS訪問CDN資源時,CDN的站點往往服務了很多的域名,所以需要通過SNI指定具體的域名證書進行通信。
2 解決方案
針對以上問題,可以采用如下方案解決:hook HTTPS訪問前SSL連接過程,根據網絡請求頭部域中的HOST信息,設置SSL連接PeerHost的值,再根據服務器返回的證書執行驗證過程。
下麵分別列出Android和iOS平台的示例代碼。
2.1 Android示例
在HTTPDNS Android Demo Github中針對HttpsURLConnection接口,提供了在SNI業務場景下使用HTTPDNS的示例代碼。
定製SSLSocketFactory,在createSocket時替換為HTTPDNS的IP,並進行SNI/HostNameVerify配置。
class TlsSniSocketFactory extends SSLSocketFactory {
private final String TAG = TlsSniSocketFactory.class.getSimpleName();
HostnameVerifier hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
private HttpsURLConnection conn;
public TlsSniSocketFactory(HttpsURLConnection conn) {
this.conn = conn;
}
@Override
public Socket createSocket() throws IOException {
return null;
}
@Override
public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
return null;
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
return null;
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
return null;
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
return null;
}
// TLS layer
@Override
public String[] getDefaultCipherSuites() {
return new String[0];
}
@Override
public String[] getSupportedCipherSuites() {
return new String[0];
}
@Override
public Socket createSocket(Socket plainSocket, String host, int port, boolean autoClose) throws IOException {
String peerHost = this.conn.getRequestProperty("Host");
if (peerHost == null)
peerHost = host;
Log.i(TAG, "customized createSocket. host: " + peerHost);
InetAddress address = plainSocket.getInetAddress();
if (autoClose) {
// we don't need the plainSocket
plainSocket.close();
}
// create and connect SSL socket, but don't do hostname/certificate verification yet
SSLCertificateSocketFactory sslSocketFactory = (SSLCertificateSocketFactory) SSLCertificateSocketFactory.getDefault(0);
SSLSocket ssl = (SSLSocket) sslSocketFactory.createSocket(address, port);
// enable TLSv1.1/1.2 if available
ssl.setEnabledProtocols(ssl.getSupportedProtocols());
// set up SNI before the handshake
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
Log.i(TAG, "Setting SNI hostname");
sslSocketFactory.setHostname(ssl, peerHost);
} else {
Log.d(TAG, "No documented SNI support on Android <4.2, trying with reflection");
try {
java.lang.reflect.Method setHostnameMethod = ssl.getClass().getMethod("setHostname", String.class);
setHostnameMethod.invoke(ssl, peerHost);
} catch (Exception e) {
Log.w(TAG, "SNI not useable", e);
}
}
// verify hostname and certificate
SSLSession session = ssl.getSession();
if (!hostnameVerifier.verify(peerHost, session))
throw new SSLPeerUnverifiedException("Cannot verify hostname: " + peerHost);
Log.i(TAG, "Established " + session.getProtocol() + " connection with " + session.getPeerHost() +
" using " + session.getCipherSuite());
return ssl;
}
}
對於需要設置SNI的站點,通常需要重定向請求,示例中也給出了重定向請求的處理方法。
public void recursiveRequest(String path, String reffer) {
URL url = null;
try {
url = new URL(path);
conn = (HttpsURLConnection) url.openConnection();
// 同步接口獲取IP
String ip = httpdns.getIpByHostAsync(url.getHost());
if (ip != null) {
// 通過HTTPDNS獲取IP成功,進行URL替換和HOST頭設置
Log.d(TAG, "Get IP: " + ip + " for host: " + url.getHost() + " from HTTPDNS successfully!");
String newUrl = path.replaceFirst(url.getHost(), ip);
conn = (HttpsURLConnection) new URL(newUrl).openConnection();
// 設置HTTP請求頭Host域
conn.setRequestProperty("Host", url.getHost());
}
conn.setConnectTimeout(30000);
conn.setReadTimeout(30000);
conn.setInstanceFollowRedirects(false);
TlsSniSocketFactory sslSocketFactory = new TlsSniSocketFactory(conn);
conn.setSSLSocketFactory(sslSocketFactory);
conn.setHostnameVerifier(new HostnameVerifier() {
/*
* 關於這個接口的說明,官方有文檔描述:
* This is an extended verification option that implementers can provide.
* It is to be used during a handshake if the URL's hostname does not match the
* peer's identification hostname.
*
* 使用HTTPDNS後URL裏設置的hostname不是遠程的主機名(如:m.taobao.com),與證書頒發的域不匹配,
* Android HttpsURLConnection提供了回調接口讓用戶來處理這種定製化場景。
* 在確認HTTPDNS返回的源站IP與Session攜帶的IP信息一致後,您可以在回調方法中將待驗證域名替換為原來的真實域名進行驗證。
*
*/
@Override
public boolean verify(String hostname, SSLSession session) {
String host = conn.getRequestProperty("Host");
if (null == host) {
host = conn.getURL().getHost();
}
return HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session);
}
});
int code = conn.getResponseCode();// Network block
if (needRedirect(code)) {
//臨時重定向和永久重定向location的大小寫有區分
String location = conn.getHeaderField("Location");
if (location == null) {
location = conn.getHeaderField("location");
}
if (!(location.startsWith("https://") || location
.startsWith("https://"))) {
//某些時候會省略host,隻返回後麵的path,所以需要補全url
URL originalUrl = new URL(path);
location = originalUrl.getProtocol() + "://"
+ originalUrl.getHost() + location;
}
recursiveRequest(location, path);
} else {
// redirect finish.
DataInputStream dis = new DataInputStream(conn.getInputStream());
int len;
byte[] buff = new byte[4096];
StringBuilder response = new StringBuilder();
while ((len = dis.read(buff)) != -1) {
response.append(new String(buff, 0, len));
}
Log.d(TAG, "Response: " + response.toString());
}
} catch (MalformedURLException e) {
Log.w(TAG, "recursiveRequest MalformedURLException");
} catch (IOException e) {
Log.w(TAG, "recursiveRequest IOException");
} catch (Exception e) {
Log.w(TAG, "unknow exception");
} finally {
if (conn != null) {
conn.disconnect();
}
}
}
private boolean needRedirect(int code) {
return code >= 300 && code < 400;
}
2.2 iOS示例
由於iOS係統並沒有提供設置SNI的上層接口(NSURLConnection/NSURLSession),因此在HTTPDNS iOS Demo Github中,我們使用NSURLProtocol攔截網絡請求,然後使用CFHTTPMessageRef創建NSInputStream實例進行Socket通信,並設置其kCFStreamSSLPeerName的值。
需要注意的是,使用NSURLProtocol攔截NSURLSession發起的POST請求時,HTTPBody為空。解決方案有兩個:
- 使用NSURLConnection發POST請求。
- 先將HTTPBody放入HTTP Header field中,然後在NSURLProtocol中再取出來,Demo中主要演示該方案。
部分代碼如下:
在網絡請求前注冊NSURLProtocol子類,在示例的SNIViewController.m中。
// 注冊攔截請求的NSURLProtocol
[NSURLProtocol registerClass:[CFHttpMessageURLProtocol class]];
// 初始化HTTPDNS
HttpDnsService *httpdns = [HttpDnsService sharedInstance];
// 需要設置SNI的URL
NSString *originalUrl = @"your url";
NSURL *url = [NSURL URLWithString:originalUrl];
self.request = [[NSMutableURLRequest alloc] initWithURL:url];
NSString *ip = [httpdns getIpByHostAsync:url.host];
// 通過HTTPDNS獲取IP成功,進行URL替換和HOST頭設置
if (ip) {
NSLog(@"Get IP from HTTPDNS Successfully!");
NSRange hostFirstRange = [originalUrl rangeOfString:url.host];
if (NSNotFound != hostFirstRange.location) {
NSString *newUrl = [originalUrl stringByReplacingCharactersInRange:hostFirstRange withString:ip];
self.request.URL = [NSURL URLWithString:newUrl];
[_request setValue:url.host forHTTPHeaderField:@"host"];
}
}
// NSURLConnection例子
[[NSURLConnection alloc] initWithRequest:_request delegate:self startImmediately:YES];
// NSURLSession例子
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSArray *protocolArray = @[ [CFHttpMessageURLProtocol class] ];
configuration.protocolClasses = protocolArray;
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]];
NSURLSessionTask *task = [session dataTaskWithRequest:_request];
[task resume];
// 注*:使用NSURLProtocol攔截NSURLSession發起的POST請求時,HTTPBody為空。
// 解決方案有兩個:1. 使用NSURLConnection發POST請求。
// 2. 先將HTTPBody放入HTTP Header field中,然後在NSURLProtocol中再取出來。
// 下麵主要演示第二種解決方案
// NSString *postStr = [NSString stringWithFormat:@"param1=%@¶m2=%@", @"val1", @"val2"];
// [_request addValue:postStr forHTTPHeaderField:@"originalBody"];
// _request.HTTPMethod = @"POST";
// NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
// NSArray *protocolArray = @[ [CFHttpMessageURLProtocol class] ];
// configuration.protocolClasses = protocolArray;
// NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]];
// NSURLSessionTask *task = [session dataTaskWithRequest:_request];
// [task resume];
在NSURLProtocol子類中攔截網絡請求,在示例的CFHttpMessageURLProtocol.m中。
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
/* 防止無限循環,因為一個請求在被攔截處理過程中,也會發起一個請求,這樣又會走到這裏,如果不進行處理,就會造成無限循環 */
if ([NSURLProtocol propertyForKey:protocolKey inRequest:request]) {
return NO;
}
NSString *url = request.URL.absoluteString;
// 如果url以https開頭,則進行攔截處理,否則不處理
if ([url hasPrefix:@"https"]) {
return YES;
}
return NO;
}
使用CFHTTPMessageRef創建NSInputStream,並設置kCFStreamSSLPeerName,重新發起請求。
/**
* 開始加載,在該方法中,加載一個請求
*/
- (void)startLoading {
NSMutableURLRequest *request = [self.request mutableCopy];
// 表示該請求已經被處理,防止無限循環
[NSURLProtocol setProperty:@(YES) forKey:protocolKey inRequest:request];
curRequest = request;
[self startRequest];
}
/**
* 取消請求
*/
- (void)stopLoading {
if (inputStream.streamStatus == NSStreamStatusOpen) {
[inputStream removeFromRunLoop:curRunLoop forMode:NSRunLoopCommonModes];
[inputStream setDelegate:nil];
[inputStream close];
}
[self.client URLProtocol:self didFailWithError:[[NSError alloc] initWithDomain:@"stop loading" code:-1 userInfo:nil]];
}
/**
* 使用CFHTTPMessage轉發請求
*/
- (void)startRequest {
// 原請求的header信息
NSDictionary *headFields = curRequest.allHTTPHeaderFields;
// 添加http post請求所附帶的數據
CFStringRef requestBody = CFSTR("");
CFDataRef bodyData = CFStringCreateExternalRepresentation(kCFAllocatorDefault, requestBody, kCFStringEncodingUTF8, 0);
if (curRequest.HTTPBody) {
bodyData = (__bridge_retained CFDataRef) curRequest.HTTPBody;
} else if (headFields[@"originalBody"]) {
// 使用NSURLSession發POST請求時,將原始HTTPBody從header中取出
bodyData = (__bridge_retained CFDataRef) [headFields[@"originalBody"] dataUsingEncoding:NSUTF8StringEncoding];
}
CFStringRef url = (__bridge CFStringRef) [curRequest.URL absoluteString];
CFURLRef requestURL = CFURLCreateWithString(kCFAllocatorDefault, url, NULL);
// 原請求所使用的方法,GET或POST
CFStringRef requestMethod = (__bridge_retained CFStringRef) curRequest.HTTPMethod;
// 根據請求的url、方法、版本創建CFHTTPMessageRef對象
CFHTTPMessageRef cfrequest = CFHTTPMessageCreateRequest(kCFAllocatorDefault, requestMethod, requestURL, kCFHTTPVersion1_1);
CFHTTPMessageSetBody(cfrequest, bodyData);
// copy原請求的header信息
for (NSString *header in headFields) {
if (![header isEqualToString:@"originalBody"]) {
// 不包含POST請求時存放在header的body信息
CFStringRef requestHeader = (__bridge CFStringRef) header;
CFStringRef requestHeaderValue = (__bridge CFStringRef) [headFields valueForKey:header];
CFHTTPMessageSetHeaderFieldValue(cfrequest, requestHeader, requestHeaderValue);
}
}
// 創建CFHTTPMessage對象的輸入流
CFReadStreamRef readStream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault, cfrequest);
inputStream = (__bridge_transfer NSInputStream *) readStream;
// 設置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];
if (!curRunLoop)
// 保存當前線程的runloop,這對於重定向的請求很關鍵
curRunLoop = [NSRunLoop currentRunLoop];
// 將請求放入當前runloop的事件隊列
[inputStream scheduleInRunLoop:curRunLoop forMode:NSRunLoopCommonModes];
[inputStream open];
CFRelease(cfrequest);
CFRelease(requestURL);
CFRelease(url);
cfrequest = NULL;
CFRelease(bodyData);
CFRelease(requestBody);
CFRelease(requestMethod);
}
在NSStream的回調函數中,根據不同的eventCode通知原請求。此處eventCode主要有四種:
- NSStreamEventOpenCompleted:連接已打開。
- NSStreamEventHasBytesAvailable:響應頭部已下載完整,body中字節可讀。
- NSStreamEventErrorOccurred:連接發生錯誤。
- NSStreamEventEndEncountered:連接結束。
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
if (eventCode == NSStreamEventHasBytesAvailable) {
CFReadStreamRef readStream = (__bridge_retained CFReadStreamRef) aStream;
CFHTTPMessageRef message = (CFHTTPMessageRef) CFReadStreamCopyProperty(readStream, kCFStreamPropertyHTTPResponseHeader);
if (CFHTTPMessageIsHeaderComplete(message)) {
// 以防response的header信息不完整
UInt8 buffer[16 * 1024];
UInt8 *buf = NULL;
unsigned long length = 0;
NSInputStream *inputstream = (NSInputStream *) aStream;
NSNumber *alreadyAdded = objc_getAssociatedObject(aStream, kAnchorAlreadyAdded);
if (!alreadyAdded || ![alreadyAdded boolValue]) {
objc_setAssociatedObject(aStream, kAnchorAlreadyAdded, [NSNumber numberWithBool:YES], OBJC_ASSOCIATION_COPY);
// 通知client已收到response,隻通知一次
NSDictionary *headDict = (__bridge NSDictionary *) (CFHTTPMessageCopyAllHeaderFields(message));
CFStringRef httpVersion = CFHTTPMessageCopyVersion(message);
// 獲取響應頭部的狀態碼
CFIndex myErrCode = CFHTTPMessageGetResponseStatusCode(message);
NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:curRequest.URL statusCode:myErrCode HTTPVersion:(__bridge NSString *) httpVersion headerFields:headDict];
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
// 驗證證書
SecTrustRef trust = (__bridge SecTrustRef) [aStream propertyForKey:(__bridge NSString *) kCFStreamPropertySSLPeerTrust];
SecTrustResultType res = kSecTrustResultInvalid;
NSMutableArray *policies = [NSMutableArray array];
NSString *domain = [[curRequest 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) {
[aStream removeFromRunLoop:curRunLoop forMode:NSRunLoopCommonModes];
[aStream setDelegate:nil];
[aStream close];
[self.client URLProtocol:self didFailWithError:[[NSError alloc] initWithDomain:@"can not evaluate the server trust" code:-1 userInfo:nil]];
}
if (res != kSecTrustResultProceed && res != kSecTrustResultUnspecified) {
/* 證書驗證不通過,關閉input stream */
[aStream removeFromRunLoop:curRunLoop forMode:NSRunLoopCommonModes];
[aStream setDelegate:nil];
[aStream close];
[self.client URLProtocol:self didFailWithError:[[NSError alloc] initWithDomain:@"fail to evaluate the server trust" code:-1 userInfo:nil]];
} else {
// 證書通過,返回數據
if (![inputstream getBuffer:&buf length:&length]) {
NSInteger amount = [inputstream read:buffer maxLength:sizeof(buffer)];
buf = buffer;
length = amount;
}
NSData *data = [[NSData alloc] initWithBytes:buf length:length];
[self.client URLProtocol:self didLoadData:data];
}
} else {
// 證書已驗證過,返回數據
if (![inputstream getBuffer:&buf length:&length]) {
NSInteger amount = [inputstream read:buffer maxLength:sizeof(buffer)];
buf = buffer;
length = amount;
}
NSData *data = [[NSData alloc] initWithBytes:buf length:length];
[self.client URLProtocol:self didLoadData:data];
}
}
} else if (eventCode == NSStreamEventErrorOccurred) {
[aStream removeFromRunLoop:curRunLoop forMode:NSRunLoopCommonModes];
[aStream setDelegate:nil];
[aStream close];
// 通知client發生錯誤了
[self.client URLProtocol:self didFailWithError:[aStream streamError]];
} else if (eventCode == NSStreamEventEndEncountered) {
[self handleResponse];
}
}
- (void)handleResponse {
// 獲取響應頭部信息
CFReadStreamRef readStream = (__bridge_retained CFReadStreamRef) inputStream;
CFHTTPMessageRef message = (CFHTTPMessageRef) CFReadStreamCopyProperty(readStream, kCFStreamPropertyHTTPResponseHeader);
if (CFHTTPMessageIsHeaderComplete(message)) {
// 確保response頭部信息完整
NSDictionary *headDict = (__bridge NSDictionary *) (CFHTTPMessageCopyAllHeaderFields(message));
// 獲取響應頭部的狀態碼
CFIndex myErrCode = CFHTTPMessageGetResponseStatusCode(message);
// 把當前請求關閉
[inputStream removeFromRunLoop:curRunLoop forMode:NSRunLoopCommonModes];
[inputStream setDelegate:nil];
[inputStream close];
if (myErrCode >= 200 && myErrCode < 300) {
// 返回碼為2xx,直接通知client
[self.client URLProtocolDidFinishLoading:self];
} else if (myErrCode >= 300 && myErrCode < 400) {
// 返回碼為3xx,需要重定向請求,繼續訪問重定向頁麵
NSString *location = headDict[@"Location"];
NSURL *url = [[NSURL alloc] initWithString:location];
curRequest = [[NSMutableURLRequest alloc] initWithURL:url];
/***********重定向通知client處理或內部處理*************/
// client處理
// NSURLResponse* response = [[NSURLResponse alloc] initWithURL:curRequest.URL MIMEType:headDict[@"Content-Type"] expectedContentLength:[headDict[@"Content-Length"] integerValue] textEncodingName:@"UTF8"];
// [self.client URLProtocol:self wasRedirectedToRequest:curRequest redirectResponse:response];
// 內部處理,將url中的host通過HTTPDNS轉換為IP,不能在startLoading線程中進行同步網絡請求,會被阻塞
NSString *ip = [[HttpDnsService sharedInstance] getIpByHostAsync:url.host];
if (ip) {
NSLog(@"Get IP from HTTPDNS Successfully!");
NSRange hostFirstRange = [location rangeOfString:url.host];
if (NSNotFound != hostFirstRange.location) {
NSString *newUrl = [location stringByReplacingCharactersInRange:hostFirstRange withString:ip];
curRequest = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:newUrl]];
[curRequest setValue:url.host forHTTPHeaderField:@"host"];
}
}
[self startRequest];
} else {
// 其他情況,直接返回響應信息給client
[self.client URLProtocolDidFinishLoading:self];
}
} else {
// 頭部信息不完整,關閉inputstream,通知client
[inputStream removeFromRunLoop:curRunLoop forMode:NSRunLoopCommonModes];
[inputStream setDelegate:nil];
[inputStream close];
[self.client URLProtocolDidFinishLoading:self];
}
}
立即試用HTTPDNS服務,點擊此處。
最後更新:2016-11-23 16:04:14
上一篇:
HTTPDNS域名解析場景下如何使用Cookie?__最佳實踐_HTTPDNS-阿裏雲
下一篇:
金融雲特性__金融雲介紹_金融雲-阿裏雲
常見錯誤說明__附錄_大數據計算服務-阿裏雲
發送短信接口__API使用手冊_短信服務-阿裏雲
接口文檔__Android_安全組件教程_移動安全-阿裏雲
運營商錯誤碼(聯通)__常見問題_短信服務-阿裏雲
設置短信模板__使用手冊_短信服務-阿裏雲
OSS 權限問題及排查__常見錯誤及排除_最佳實踐_對象存儲 OSS-阿裏雲
消息通知__操作指南_批量計算-阿裏雲
設備端快速接入(MQTT)__快速開始_阿裏雲物聯網套件-阿裏雲
查詢API調用流量數據__API管理相關接口_API_API 網關-阿裏雲
使用STS訪問__JavaScript-SDK_SDK 參考_對象存儲 OSS-阿裏雲