Android端打开HttpDns的正确姿势(下篇)

达芬奇密码2018-08-17 13:04

在OkHttp中,TLS的处理主要在RealConnection.connectTls()中:

  private void connectTls(ConnectionSpecSelector connectionSpecSelector) throws IOException {
    Address address = route.address();
    SSLSocketFactory sslSocketFactory = address.sslSocketFactory();
    boolean success = false;
    SSLSocket sslSocket = null;
    try {
      // Create the wrapper over the connected socket.
      sslSocket = (SSLSocket) sslSocketFactory.createSocket(
          rawSocket, address.url().host(), address.url().port(), true /* autoClose */);

      // Configure the socket's ciphers, TLS versions, and extensions.
      ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
      if (connectionSpec.supportsTlsExtensions()) {
        Platform.get().configureTlsExtensions(
            sslSocket, address.url().host(), address.protocols());
      }

      // Force handshake. This can throw!
      sslSocket.startHandshake();
      Handshake unverifiedHandshake = Handshake.get(sslSocket.getSession());

      // Verify that the socket's certificates are acceptable for the target host.
      if (!address.hostnameVerifier().verify(address.url().host(), sslSocket.getSession())) {
        X509Certificate cert = (X509Certificate) unverifiedHandshake.peerCertificates().get(0);
        throw new SSLPeerUnverifiedException("Hostname " + address.url().host() + " not verified:"
            + "\n    certificate: " + CertificatePinner.pin(cert)
            + "\n    DN: " + cert.getSubjectDN().getName()
            + "\n    subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));
      }

      // Check that the certificate pinner is satisfied by the certificates presented.
      address.certificatePinner().check(address.url().host(),
          unverifiedHandshake.peerCertificates());

      // Success! Save the handshake and the ALPN protocol.
      String maybeProtocol = connectionSpec.supportsTlsExtensions()
          ? Platform.get().getSelectedProtocol(sslSocket)
          : null;
      socket = sslSocket;
      source = Okio.buffer(Okio.source(socket));
      sink = Okio.buffer(Okio.sink(socket));
      handshake = unverifiedHandshake;
      protocol = maybeProtocol != null
          ? Protocol.get(maybeProtocol)
          : Protocol.HTTP_1_1;
      success = true;
    } catch (AssertionError e) {
      if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
      throw e;
    } finally {
      if (sslSocket != null) {
        Platform.get().afterHandshake(sslSocket);
      }
      if (!success) {
        closeQuietly(sslSocket);
      }
    }
  }

可以看到,在创建了SSLSocket之后,总是会再通过平台相关的接口设置SNI信息。具体对于Android而言,是AndroidPlatform.configureTlsExtensions():

  @Override public void configureTlsExtensions(
      SSLSocket sslSocket, String hostname, List<Protocol> protocols) {
    // Enable SNI and session tickets.
    if (hostname != null) {
      setUseSessionTickets.invokeOptionalWithoutCheckedException(sslSocket, true);
      setHostname.invokeOptionalWithoutCheckedException(sslSocket, hostname);
    }

    // Enable ALPN.
    if (setAlpnProtocols != null && setAlpnProtocols.isSupported(sslSocket)) {
      Object[] parameters = {concatLengthPrefixed(protocols)};
      setAlpnProtocols.invokeWithoutCheckedException(sslSocket, parameters);
    }
  }

可见,前面的解法并不可行。在SSLSocket创建期间设置的SNI信息,总是会由于SNI的再次设置而被冲掉,而后一次SNI信息来源则是URL,也就是ip地址。

HTTPS (含SNI) 解法二

只定制 SSLSocketFactory 的方法,看起来是比较难以达成目的了,有人就想通过更深层的定制,即同时自定义SSLSocket来实现,如GitHub中的 某项目

但这种方法的问题更严重。支持SSL扩展的许多接口,都不是标准的SSLSocket接口,比如用于支持SNI的setHostname()接口,用于支持ALPN的setAlpnProtocols() 和 getAlpnSelectedProtocol() 接口等。这样的接口还会随着SSL/TLS协议的发展而不断增加。许多网路库,如OkHttp,在调用这些接口时主要通过反射完成。而在自己定义SSLSocket实现的时候,很容易遗漏掉这些接口的实现,进而折损掉某些系统本身支持的SSL扩展。

这种方法难免挂一漏万。

接入HttpDns的更好方法

前面遇到的那些问题,主要都是由于替换URL中的域名为IP地址发起请求时,URL中域名信息丢失,而URL中的域名在网络库的多个地方被用到而引起。接入 HttpDns 的更好方法是,不要替换请求的URL中的域名部分为IP地址,而只在需要 Dns 的时候,才让HttpDns登场。

具体而言,是使用那些可以定制Dns逻辑的网络库,比如OkHttp,或者 我们的 htcandynetwork-android,实现域名解析的接口,并在该接口的实现中通过 HttpDns 模块来执行域名解析。这样就不会对网络库造成那么多未知的冲击。

如:

    private static class MyDns implements Dns {

        @Override
        public List<InetAddress> lookup(String hostname) throws UnknownHostException {
            List<String> strIps = HttpDns.getInstance().getIpByHost(hostname);
            List<InetAddress> ipList;
            if (strIps != null && strIps.size() > 0) {
                ipList = new ArrayList<>();
                for (String ip : strIps) {
                    ipList.add(InetAddress.getByName(ip));
                }
            } else {
                ipList = Dns.SYSTEM.lookup(hostname);
            }
            return ipList;
        }
    }

    private OkHttp3Utils() {
        okhttp3.OkHttpClient.Builder builder = new okhttp3.OkHttpClient.Builder();
        builder.dns(new MyDns());
        mOkHttpClient = builder.build();
    }

这种方法既简单又副作用小。




相关阅读:

Android端打开HttpDns的正确姿势(上篇)

网易云新用户大礼包:https://www.163yun.com/gift

本文来自网易实践者社区,经作者韩鹏飞授权发布。