阿里巴巴是這樣說的
HTTPDNS使用HTTP協議進行域名解析,代替現有基于UDP的DNS協議,域名解析請求直接發送到阿里云的HTTPDNS服務器,從而繞過運營商的Local DNS,能夠避免Local DNS造成的域名劫持問題和調度不精準問題。
分析demo
https://github.com/aliyun/alicloud-android-demo.git
普通場景 就是普通的http請求
sni場景 就是 server name Indication 場景
選擇里面的httpdns_android_demo
打開MainActivity。
private static final String APPLE_URL = "www.apple.com";
private static final String TAOBAO_URL = "m.taobao.com";
private static final String DOUBAN_URL = "dou.bz";
private static final String ALIYUN_URL = "aliyun.com";
private static final String HTTP_SCHEMA = "http://";
private static final String HTTPS_SCHEMA = "https://";
private static final String TAG = "httpdns_android_demo";
先看看普通請求,
/**
* 通過IP直連方式發起普通請求示例
* 1. 通過IP直連時,為繞開服務域名檢查,需要在Http報頭中設置Host字段
*/
private void normalReqeust() {
pool.execute(new Runnable() {
@Override
public void run() {
try {
// 發送網絡請求
String originalUrl = HTTP_SCHEMA + TAOBAO_URL;
URL url = new URL(originalUrl);
HttpURLConnection conn = (HttpURLConnection) 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!");
sendConsoleMessage("Get IP: " + ip + " for host: " + url.getHost() + " from HTTPDNS successfully!");
String newUrl = originalUrl.replaceFirst(url.getHost(), ip);
conn = (HttpURLConnection) new URL(newUrl).openConnection();
// 設置HTTP請求頭Host域
conn.setRequestProperty("Host", url.getHost());
}
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());
sendConsoleMessage("Get response from " + url.getHost() + ". Please check response detail from log.");
} catch (Throwable throwable) {
Log.e(TAG, "normal request failed.", throwable);
sendConsoleMessage("Normal request failed." + " Please check error detail from log.");
}
}
});
}
從這例子不難看出,阿里的demo首先是創建一個url連接,獲取host
host就是不包含http 的域名(比如s.taobao.com
)然后調用通過sdk中的String ip = httpdns.getIpByHostAsync(url.getHost());
也就是通過阿里自己的http請求查詢這個host對應的ip地址,如果查詢成功,那么HttpURLConnection會被重新創建,而且是通過ip進行創建,另外為了不丟失域名,所以這里做了一個操作就是設置請求頭"Host"
也就是調用conn.setRequestProperty("Host", url.getHost());
整個過程就是通過 域名查詢ip,然后通過ip進行請求的操作,
但是這個業務邏輯本身是dns自身做的事情,現在已經繞過了,直接交給阿里的http dns服務器進行操作。
不過不管怎么操作,這httpdns自身還是得通過運營商的dns進行請求,當然他們自己的也可以做緩存,或者ip地址可靠也可以直接訪問比如
http://203.107.1.33/100000/d?host=www.aliyun.com
他們是這樣說的
考慮到服務IP防攻擊之類的安全風險,為保障服務可用性,HTTPDNS同時提供多個服務IP,當某個服務IP在異常情況下不可用時,可以使用其它服務IP進行重試。上述文檔中使用的203.107.1.33
是其中一個服務IP。
HTTPDNS提供Android SDK和iOS SDK,兩個平臺的SDK中已經做了多IP輪轉和出錯重試的策略,通常情況下,建議開發者直接集成SDK即可,不需要自己手動調用HTTP API接口。
如果使用場景特殊,無法使用SDK,需要直接訪問HTTP API接口,請提工單聯系我們,我們將根據您的具體使用場景,為您提供多個服務IP和相關的安全建議。
具體參考https://help.aliyun.com/document_detail/52679.html?spm=a2c4g.11186623.2.21.11321d22lF9Vbp#1.1 訪問方式
再看看https
/**
* 通過IP直連方式發起https請求示例
* 1. 通過IP直連時,為繞開服務域名檢查,需要在Http報頭中設置Host字段
* 2. 為通過證書檢查,需要自定義證書驗證邏輯
*/
private void httpsRequest() {
pool.execute(new Runnable() {
@Override
public void run() {
try {
String originalUrl = HTTPS_SCHEMA + TAOBAO_URL + "/?sprefer=sypc00";
URL url = new URL(originalUrl);
HttpsURLConnection 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!");
sendConsoleMessage("Get IP: " + ip + " for host: " + url.getHost() + " from HTTPDNS successfully!");
String newUrl = originalUrl.replaceFirst(url.getHost(), ip);
conn = (HttpsURLConnection) new URL(newUrl).openConnection();
// 設置HTTP請求頭Host域
conn.setRequestProperty("Host", url.getHost());
}
final HttpsURLConnection finalConn = conn;
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 = finalConn.getRequestProperty("Host");
if (null == host) {
host = finalConn.getURL().getHost();
}
return HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session);
}
});
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());
sendConsoleMessage("Get reponse from " + url.getHost() + ". Please check response detail from log.");
} catch (Exception e) {
e.printStackTrace();
sendConsoleMessage("Get reponse failed. Please check error detail from log.");
}
}
});
}
處理重定向
private void recursiveRequest(String path, String reffer) {
URL url;
HttpsURLConnection conn = 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!");
sendConsoleMessage("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);
HttpDnsTLSSniSocketFactory sslSocketFactory = new HttpDnsTLSSniSocketFactory(conn);
conn.setSSLSocketFactory(sslSocketFactory);
final HttpsURLConnection finalConn = conn;
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 = finalConn.getRequestProperty("Host");
if (null == host) {
host = finalConn.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(HTTP_SCHEMA) || location
.startsWith(HTTPS_SCHEMA))) {
//某些時候會省略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());
sendConsoleMessage("Get reponse from " + url.getHost() + ". Please check response detail from log.");
}
} catch (MalformedURLException e) {
Log.w(TAG, "recursiveRequest MalformedURLException", e);
} 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;
}
預解析域名
顧名思義,在請求某個東西之前先請求,比如app剛打開的時候。
這樣通過sdk進行查詢就會直接從緩存中取出。
/**
* 設置預解析域名列表代碼示例
*/
private void setPreResoveHosts() {
// 設置預解析域名列表
// 可以替換成您在后臺配置的域名
httpdns.setPreResolveHosts(new ArrayList<>(Arrays.asList(APPLE_URL, ALIYUN_URL, TAOBAO_URL, DOUBAN_URL)));
sendConsoleMessage("設置預解析域名成功");
}
降級解析
/**
* 自定義降級邏輯代碼示例
*/
private void setDegrationFilter() {
DegradationFilter filter = new DegradationFilter() {
@Override
public boolean shouldDegradeHttpDNS(String hostName) {
// 此處可以自定義降級邏輯,例如www.taobao.com不使用HttpDNS解析
// 參照HttpDNS API文檔,當存在中間HTTP代理時,應選擇降級,使用Local DNS
return hostName.equals(DOUBAN_URL);
}
};
// 將filter傳進httpdns,解析時會回調shouldDegradeHttpDNS方法,判斷是否降級
httpdns.setDegradationFilter(filter);
sendConsoleMessage("自定義降級邏輯成功");
}
降級解析就是不用他們的dns,使用運營商的。
處理webview
class WebviewTlsSniSocketFactory extends SSLSocketFactory {
private final String TAG = WebviewTlsSniSocketFactory.class.getSimpleName();
HostnameVerifier hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
private HttpsURLConnection conn;
public WebviewTlsSniSocketFactory(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;
}
}
webView.setWebViewClient(new WebViewClient() {
@SuppressLint("NewApi")
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
String scheme = request.getUrl().getScheme().trim();
String method = request.getMethod();
Map<String, String> headerFields = request.getRequestHeaders();
String url = request.getUrl().toString();
Log.e(TAG, "url:" + url);
// 無法攔截body,攔截方案只能正常處理不帶body的請求;
if ((scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https"))
&& method.equalsIgnoreCase("get")) {
try {
URLConnection connection = recursiveRequest(url, headerFields, null);
if (connection == null) {
Log.e(TAG, "connection null");
return super.shouldInterceptRequest(view, request);
}
// 注*:對于POST請求的Body數據,WebResourceRequest接口中并沒有提供,這里無法處理
String contentType = connection.getContentType();
String mime = getMime(contentType);
String charset = getCharset(contentType);
HttpURLConnection httpURLConnection = (HttpURLConnection)connection;
int statusCode = httpURLConnection.getResponseCode();
String response = httpURLConnection.getResponseMessage();
Map<String, List<String>> headers = httpURLConnection.getHeaderFields();
Set<String> headerKeySet = headers.keySet();
Log.e(TAG, "code:" + httpURLConnection.getResponseCode());
Log.e(TAG, "mime:" + mime + "; charset:" + charset);
// 無mime類型的請求不攔截
if (TextUtils.isEmpty(mime)) {
Log.e(TAG, "no MIME");
return super.shouldInterceptRequest(view, request);
} else {
// 二進制資源無需編碼信息
if (!TextUtils.isEmpty(charset) || (isBinaryRes(mime))) {
WebResourceResponse resourceResponse = new WebResourceResponse(mime, charset, httpURLConnection.getInputStream());
resourceResponse.setStatusCodeAndReasonPhrase(statusCode, response);
Map<String, String> responseHeader = new HashMap<String, String>();
for (String key: headerKeySet) {
// HttpUrlConnection可能包含key為null的報頭,指向該http請求狀態碼
responseHeader.put(key, httpURLConnection.getHeaderField(key));
}
resourceResponse.setResponseHeaders(responseHeader);
return resourceResponse;
} else {
Log.e(TAG, "non binary resource for " + mime);
return super.shouldInterceptRequest(view, request);
}
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
return super.shouldInterceptRequest(view, request);
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
// API < 21 只能攔截URL參數
return super.shouldInterceptRequest(view, url);
}
});
webView.loadUrl(targetUrl);
}
public URLConnection recursiveRequest(String path, Map<String, String> headers, String reffer) {
HttpURLConnection conn;
URL url = null;
try {
url = new URL(path);
conn = (HttpURLConnection) 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 = (HttpURLConnection) new URL(newUrl).openConnection();
if (headers != null) {
for (Map.Entry<String, String> field : headers.entrySet()) {
conn.setRequestProperty(field.getKey(), field.getValue());
}
}
// 設置HTTP請求頭Host域
conn.setRequestProperty("Host", url.getHost());
} else {
return null;
}
conn.setConnectTimeout(30000);
conn.setReadTimeout(30000);
conn.setInstanceFollowRedirects(false);
if (conn instanceof HttpsURLConnection) {
final HttpsURLConnection httpsURLConnection = (HttpsURLConnection)conn;
WebviewTlsSniSocketFactory sslSocketFactory = new WebviewTlsSniSocketFactory((HttpsURLConnection) conn);
// sni場景,創建SSLScocket
httpsURLConnection.setSSLSocketFactory(sslSocketFactory);
// https場景,證書校驗
httpsURLConnection.setHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
String host = httpsURLConnection.getRequestProperty("Host");
if (null == host) {
host = httpsURLConnection.getURL().getHost();
}
return HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session);
}
});
}
int code = conn.getResponseCode();// Network block
if (needRedirect(code)) {
// 原有報頭中含有cookie,放棄攔截
if (containCookie(headers)) {
return null;
}
String location = conn.getHeaderField("Location");
if (location == null) {
location = conn.getHeaderField("location");
}
if (location != null) {
if (!(location.startsWith("http://") || location
.startsWith("https://"))) {
//某些時候會省略host,只返回后面的path,所以需要補全url
URL originalUrl = new URL(path);
location = originalUrl.getProtocol() + "://"
+ originalUrl.getHost() + location;
}
Log.e(TAG, "code:" + code + "; location:" + location + "; path" + path);
return recursiveRequest(location, headers, path);
} else {
// 無法獲取location信息,讓瀏覽器獲取
return null;
}
} else {
// redirect finish.
Log.e(TAG, "redirect finish");
return conn;
}
} catch (MalformedURLException e) {
Log.w(TAG, "recursiveRequest MalformedURLException");
} catch (IOException e) {
Log.w(TAG, "recursiveRequest IOException");
} catch (Exception e) {
Log.w(TAG, "unknow exception");
}
return null;
}
其他demo
public class NetworkRequestUsingHttpDNS {
private static HttpDnsService httpdns;
// 填入您的HTTPDNS accoutID信息,您可以從HTTPDNS控制臺獲取該信息
private static String accountID = "100000";
// 您的熱點域名
private static final String[] TEST_URL = {"http://www.aliyun.com", "http://www.taobao.com"};
public static void main(final Context ctx) {
try {
// 設置APP Context和Account ID,并初始化HTTPDNS
httpdns = HttpDns.getService(ctx, accountID);
// DegradationFilter用于自定義降級邏輯
// 通過實現shouldDegradeHttpDNS方法,可以根據需要,選擇是否降級
DegradationFilter filter = new DegradationFilter() {
@Override
public boolean shouldDegradeHttpDNS(String hostName) {
// 此處可以自定義降級邏輯,例如www.taobao.com不使用HttpDNS解析
// 參照HttpDNS API文檔,當存在中間HTTP代理時,應選擇降級,使用Local DNS
return hostName.equals("www.taobao.com") || detectIfProxyExist(ctx);
}
};
// 將filter傳進httpdns,解析時會回調shouldDegradeHttpDNS方法,判斷是否降級
httpdns.setDegradationFilter(filter);
// 設置預解析域名列表,真正使用時,建議您將預解析操作放在APP啟動函數中執行。預解析操作為異步行為,不會阻塞您的啟動流程
httpdns.setPreResolveHosts(new ArrayList<>(Arrays.asList("www.aliyun.com", "www.taobao.com")));
// 允許返回過期的IP,通過設置允許返回過期的IP,配合異步查詢接口,我們可以實現DNS懶更新策略
httpdns.setExpiredIPEnabled(true);
// 發送網絡請求
String originalUrl = "http://www.aliyun.com";
URL url = new URL(originalUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
// 異步接口獲取IP,當IP TTL過期時,由于采用DNS懶更新策略,我們可以直接從內存獲得最近的DNS解析結果,同時HTTPDNS SDK在后臺自動更新對應域名的解析結果
ip = httpdns.getIpByHostAsync(url.getHost());
if (ip != null) {
// 通過HTTPDNS獲取IP成功,進行URL替換和HOST頭設置
Log.d("HTTPDNS Demo", "Get IP: " + ip + " for host: " + url.getHost() + " from HTTPDNS successfully!");
String newUrl = originalUrl.replaceFirst(url.getHost(), ip);
conn = (HttpURLConnection) new URL(newUrl).openConnection();
}
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.e("HTTPDNS Demo", "Response: " + response.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 檢測系統是否已經設置代理,請參考HttpDNS API文檔。
*/
public static boolean detectIfProxyExist(Context ctx) {
boolean IS_ICS_OR_LATER = Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH;
String proxyHost;
int proxyPort;
if (IS_ICS_OR_LATER) {
proxyHost = System.getProperty("http.proxyHost");
String port = System.getProperty("http.proxyPort");
proxyPort = Integer.parseInt(port != null ? port : "-1");
} else {
proxyHost = android.net.Proxy.getHost(ctx);
proxyPort = android.net.Proxy.getPort(ctx);
}
return proxyHost != null && proxyPort != -1;
}
}
參考
https://help.aliyun.com/document_detail/30143.html
okhttp接入httpdns最佳實踐
https://help.aliyun.com/document_detail/52008.html