如果您的 app 使用 HttpURLConnection 发送和接收网络请求,您需要按照以下步骤手动将请求改写成 IP 直连请求。
说明
HTTPS 请求使用 SSL/TLS 协议。SNI(Server Name Indication) 是 SSL/TLS 协议的扩展,在 RFC 6066 中定义。SNI 可以解决一个服务端 IP 地址对应多个主机名时,SSL 证书无法正常认证的问题。发送 SNI 请求时,您需要通过 SNI 将服务端的主机名传递到 SSL/TLS 握手进程。这样,SSL/TLS 握手进程可以生成正确的 SSL/TLS 证书。
注意
App 开启代理时,如果代理无法读取 Host header,您无法将请求改写成 IP 直连请求,这样会导致请求无法正常发送。因此,我们建议您针对这种情况增加一层异常处理逻辑。如果请求无法改写成 IP 直连请求,您可以直接通过 HttpURLConnection 发送请求。
警告
对于没有在控制台添加的域名,HTTPDNS 服务端的解析会失败,您只能获得 Local DNS 服务器的解析结果。参见 添加需要解析的域名了解如何添加域名。
注意
为了演示需要,示例代码仅提供了集成方案中最基本的逻辑。移动解析 HTTPDNS 仅保证 HTTPDNS SDK 本身的 可用性。在生产环境下,您需要自行保证集成方案的健壮性。
通过 getHttpDnsResult
方法获取目标域名对应的 IP 地址。然后,您需要将 URL 请求改写为 IP 直连请求。
说明
SDK 提供以下类型的 getHttpDnsResult
方法。示例代码中使用了 getHttpDnsResultForHostSyncBlock 方法。该方法会阻塞后续代码的运行,直到 SDK 获取到域名解析结果。您也可以根据需求使用其他类型的 getHttpDnsResult
方法。
// 调用 getHttpDnsResultForHostSyncBlock 获取目标域名对应的 IP 地址 long beforeResolve = System.currentTimeMillis(); DnsResult dnsResult = HttpDns.getService().getHttpDnsResultForHostSyncBlock(url.getHost()); long cost = System.currentTimeMillis() - beforeResolve; if (dnsResult != null && (!dnsResult.ipv4List.isEmpty() || !dnsResult.ipv6List.isEmpty())) { connection = constructHttpdnsBasedConnection(url, dnsResult); setCookieHeader(connection, cookieMap); } else { ...... }
获取目标域名对应的 IP 地址之后,您可以将 URL 中的域名改写为目标域名对应的 IP 地址。
// 将 URL中的域名改写为 IP 地址。 String requestip = null; String newUrl = rawUrl.toString(); if (!dnsResult.ipv6List.isEmpty()) { requestip = dnsResult.ipv6List.get(0); requestip = "[" + requestip + "]"; } else if (!dnsResult.ipv4List.isEmpty()) { requestip = dnsResult.ipv4List.get(0); } if (!TextUtils.isEmpty(requestip)) { newUrl = newUrl.replaceFirst(rawUrl.getHost(), requestip); Log.d(TAG, "request url is " + newUrl + ", host is " + rawUrl.getHost()); }
同时,您需要调用 setRequestProperty为请求添加 header。这样可以把域名通过 host header 传递给接收请求的服务端。
connection.setRequestProperty("Host", rawUrl.getHost());
如果您的请求带有 SNI 信息,您需要重新设置 SNI,并进行主机名验证。您可以按照以下步骤设置 SNI:
由于本集成方案把 URL 中的域名更改成了 IP 地址,HTTPS 请求中的 SNI(Server Name Indication)信息是不正确的。因此,您需要重新设置 SNI。
继承 SSLSocketFactory 类。
public class SniSocketFactory extends SSLSocketFactory { ... }
重写 createSocket 方法。
设置 SNI。按照不同的 Android API level,选择不同的设置方式:
SNIHostName
对象传入正确的主机名(HostName)。SSLCertificateSocketFactory
类的 setHostname 方法设置 SNI。使用 HostnameVerifier 接口在 TLS 握手过程中验证主机名。如果验证成功,您可以返回 HttpsURLConnection
对象用于后续网络连接。
@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; InetAddress address = plainSocket.getInetAddress(); if (autoClose) { plainSocket.close(); } // create and connect SSL socket, but don't do hostname/certificate verification yet SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(address, port); // enable TLSv1.1/1.2 if available sslSocket.setEnabledProtocols(sslSocket.getSupportedProtocols()); // 通过 SNIServerName 类和 SNIHostName 类为 TLS 握手设置 SNI。 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { SNIHostName sniHostName = new SNIHostName(peerHost); SSLParameters sslParameters = sslSocket.getSSLParameters(); List<SNIServerName> sniHostNameList = new ArrayList<>(1); sniHostNameList.add(sniHostName); sslParameters.setServerNames(sniHostNameList); sslSocket.setSSLParameters(sslParameters); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { ((SSLCertificateSocketFactory) sslSocketFactory).setHostname(sslSocket, peerHost); } else { Log.d(TAG, "No documented SNI support on Android <4.2, trying with reflection"); try { java.lang.reflect.Method setHostnameMethod = sslSocket.getClass().getMethod("setHostname", String.class); setHostnameMethod.invoke(sslSocket, peerHost); } catch (Exception e) { Log.d(TAG, "SNI not useable", e); } } // 验证主机名 SSLSession session = sslSocket.getSession(); if (!hostnameVerifier.verify(peerHost, session)) { throw new SSLPeerUnverifiedException("Cannot verify hostname: " + peerHost); } Log.d(TAG, "Established " + session.getProtocol() + " connection with " + session.getPeerHost() + " using " + session.getCipherSuite()); return sslSocket; }
为 HttpURLConnection
对象配置 SNI 信息。
if ("https".equals(rawUrl.getProtocol())) { HttpsURLConnection connection = (HttpsURLConnection) new URL(newUrl).openConnection(); if (connection != null) { connection.setRequestProperty("Host", rawUrl.getHost()); SniSocketFactory sniSocketFactory = new SniSocketFactory(connection); connection.setSSLSocketFactory(sniSocketFactory); ((HttpsURLConnection) connection).setHostnameVerifier(new HostnameVerifier() { @Override public boolean verify(String s, SSLSession sslSession) { String host = connection.getRequestProperty("Host"); if (host == null) { host = connection.getURL().getHost(); } return HttpsURLConnection.getDefaultHostnameVerifier().verify(host, sslSession); } }); return connection; } } else { return (HttpURLConnection) new URL(newUrl).openConnection(); }
使用 CookieManager 管理 Cookie。同时,您还要处理重定向。如果原先的请求含有 Cookie,则不进行重定向。
// 管理 Cookie // 创建 HttpURlConnection 对象后,设置 CookieMap 和 CookieHeader // requestUrl 是改写之前的请求 URL Map<String, String> cookieMap = SsCookieManager.inst().getCookieMap(requestUrl, null); setCookieHeader(connection, cookieMap);
// 处理重定向 if (needRedirect(code)) { // 原来的请求中含有 cookie,则不拦截 if (containCookie(headers)) { return null; } // 得到重定向的地址 String location = connection.getHeaderField("Location"); if (location == null) { location = connection.getHeaderField("location"); } if (location != null) { if (!(location.startsWith("http://") || location .startsWith("https://"))) { // 某些时候 Location 会省略 host,只返回后面的 path,所以需要补全 url URL originalUrl = new URL(requestUrl); location = originalUrl.getProtocol() + "://" + originalUrl.getHost() + location; } Log.d(TAG, "redirect code is " + code + ", and redirect url is " + location); return recursiveRequest(location, headers, context); } else { return null; } } else { // 重定向结束 Log.d(TAG, "redirect finish, and responsecode is " + code); return connection; }
使用 HttpURLConnection
对象发送请求。
int responseCode = connection.getResponseCode(); InputStream in = null; if (responseCode == 200) { in = connection.getInputStream(); } else { in = connection.getErrorStream(); }
如果您需要了解 HTTPDNS Android SDK 的详细信息,参见 Android SDK 参考。