深究OKHttp之隧道

上一篇文章我分享了OKHttp的链接过程。今天,咱们来细致的研究一下关于隧道创建链接相关的细节。java

隧道

RealConnection 的 connect 方法中, 会创建 Socket 链接。在创建 Socket 链接的时候,会分状况判断,若是须要创建隧道,那么就创建隧道连接。若是不须要,就直接进行 Socket 链接。安全

if (route.requiresTunnel()) {
	connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
	if (rawSocket == null) {
		// We were unable to connect the tunnel but properly closed down our resources.
		break;
	}
} else {
	connectSocket(connectTimeout, readTimeout, call, eventListener);
}
复制代码

进一步查看 requiresTunnel :bash

public boolean requiresTunnel() {
	return address.sslSocketFactory != null && proxy.type() == Proxy.Type.HTTP;
}
复制代码

这个方法有以下的注释:服务器

/** * Returns true if this route tunnels HTTPS through an HTTP proxy. See <a * href="http://www.ietf.org/rfc/rfc2817.txt">RFC 2817, Section 5.2</a>. */
复制代码

咱们查看 rfc2817 的 5.2 章节:微信

Requesting a Tunnel with CONNECT并发

A CONNECT method requests that a proxy establish a tunnel connection
   on its behalf. The Request-URI portion of the Request-Line is always
   an 'authority' as defined by URI Generic Syntax [2], which is to say
   the host name and port number destination of the requested connection
   separated by a colon:

      CONNECT server.example.com:80 HTTP/1.1
      Host: server.example.com:80




Khare & Lawrence            Standards Track                     [Page 6]

RFC 2817                  HTTP Upgrade to TLS                   May 2000


   Other HTTP mechanisms can be used normally with the CONNECT method --
   except end-to-end protocol Upgrade requests, of course, since the
   tunnel must be established first.

   For example, proxy authentication might be used to establish the
   authority to create a tunnel:

      CONNECT server.example.com:80 HTTP/1.1
      Host: server.example.com:80
      Proxy-Authorization: basic aGVsbG86d29ybGQ=

   Like any other pipelined HTTP/1.1 request, data to be tunneled may be
   sent immediately after the blank line. The usual caveats also apply:
   data may be discarded if the eventual response is negative, and the
   connection may be reset with no response if more than one TCP segment
   is outstanding.
复制代码

这里会发现,当知足以下 2 个条件的时候,会经过 CONNECT 这个method来创建隧道链接app

  • https 协议
  • 使用了 HTTP 代理

那么到底隧道和使用了 CONNECT 分别是怎么回事,又有什么区别呢?socket

隧道的定义

参考 《HTTP权威指南》, 隧道(tunnel)是创建起来后,就会在两条链接之间对原始数据进行盲转发的 HTTP 应用程序。HTTP 隧道一般用来在一条或者多条 HTTP 链接上转发非 HTTP 数据,转发时不会窥探数据。 ** 隧道创建能够直接创建,也能够经过 CONNECT 来创建。ui

  1. 不使用CONNECT 的隧道

不使用 CONNECT 的隧道,实现了数据包的重组和转发。在代理收到客户端的请求后,会从新建立请求,并发送到目标服务器。当目标服务器返回了数据以后,代理会对 response 进行解析,而且从新组装 response, 发送给客户端。因此,这种方式下创建的隧道,代理能够对客户端和目标服务器之间的通讯数据进行窥探和篡改。this

  1. 使用 CONNECT 的隧道

当客户端发起 CONNECT 请求的时候,就是在告诉代理,先在代理服务器和目标服务器之间创建链接,这个链接创建起来以后,目标服务器会给代理一个回复,代理会把这个回复返回给客户端,表示隧道创建的状态。这种状况下,代理只负责转发,没法窥探和篡改数据。

到这里,咱们就能理解为何 HTTPS 在有 HTTP 代理的状况下为何要经过 CONNECT 来创建 SSL 隧道,由于 HTTPS 的数据是加密后的数据,代理在正常状况下没法对加密后的数据进行解密。保证了它的安全性。

OKHttp的隧道创建

下面咱们来看看 OKHttp 是如何进行隧道的创建的。查看 connectTunnel 方法:

for (int i = 0; i < MAX_TUNNEL_ATTEMPTS; i++) {
	connectSocket(connectTimeout, readTimeout, call, eventListener);
	tunnelRequest = createTunnel(readTimeout, writeTimeout, tunnelRequest, url);
    if (tunnelRequest == null) break; // Tunnel successfully created.
}
复制代码

在 21 次重试范围内,进行 socket 和 tunnel 的链接。若是 createTunnel 返回是 null ,说明隧道创建成功。

查看 createTunnel 方法:

private Request createTunnel(int readTimeout, int writeTimeout, Request tunnelRequest, HttpUrl url) throws IOException {
    
    // 1. CONNECT method 发出的内容
    String requestLine = "CONNECT " + Util.hostHeader(url, true) + " HTTP/1.1";
    
    while (true) {
        
        //2. 使用 http 1.1的方式发送 CONNECT 的数据
		Http1Codec tunnelConnection = new Http1Codec(null, null, source, sink);
        tunnelConnection.writeRequest(tunnelRequest.headers(), requestLine);
      	tunnelConnection.finishRequest();
        
        Response response = tunnelConnection.readResponseHeaders(false)
          .request(tunnelRequest)
          .build();
        
        Source body = tunnelConnection.newFixedLengthSource(contentLength);
      	Util.skipAll(body, Integer.MAX_VALUE, TimeUnit.MILLISECONDS);
      	body.close();
        
        // 3. 查看 code,根据code 的返回值作处理
        
        switch (response.code()) {
			case HTTP_OK:
				if (!source.buffer().exhausted() || !sink.buffer().exhausted()) {
            		throw new IOException("TLS tunnel buffered too many bytes!");
          		}
          		return null;
            case HTTP_PROXY_AUTH:
                tunnelRequest = route.address().proxyAuthenticator().authenticate(route, response);
                if (tunnelRequest == null) throw new IOException("Failed to authenticate with proxy");

          		if ("close".equalsIgnoreCase(response.header("Connection"))) {
            		return tunnelRequest;
          		}
          		break;
            default:
                throw new IOException(
              "Unexpected response code for CONNECT: " + response.code());
        }
        
    }
}
复制代码

这里咱们能够结合 rfc 来看到隧道创建的方式:

  1. 发送一个 CONNECT 请求,请求内容是
请求行:
CONNECT [host]:[port] HTTP/1.1

请求头:
Host: host:port
Proxy-Connection:Keep-Alive
User-Agent:okhttp/3.10.0 (已此3.10.0版本为例)
复制代码
  1. 隧道链接的结果根据 CONNECT 请求的 response code来判断:
  • 200

若是意外发送了其余数据,会抛出 IO 异常以外,隧道被认为链接成功

  • 407

若是是 407 ,则说明创建隧道的代理服务器须要身份验证。OKHttp 若是没在 OKHttpClient 设置 ProxyAuthenticator 的具体实现,就会返回 null 抛出 Failed to authenticate with proxy 的异常信息。若是提供了具体实现经过了验证,还回去判断 response header里面的 Connection ,若是值是 close ,那么会返回具体的链接。而后从新请求进行链接,直到隧道创建成功。

经过上面 2 个步骤,就创建起来了 OKHttp 的 http 隧道。

这里,咱们引用一张《HTTP权威指南》的图来讲明这一过程:

image.png

请关注个人微信公众号 【半行代码】

相关文章
相关标签/搜索