深究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

請關注個人微信公衆號 【半行代碼】

相關文章
相關標籤/搜索