41. OkHttp之-代理

在使用OkHttp時,如果用戶在創建 OkHttpClient 時,配置了 proxy 或者 proxySelector ,則會使用配置的代理,並且 proxy 優先級高於 proxySelector 。而如果未配置,則會獲取機器配置的代理並使用。

//JDK : ProxySelector
try {
    URI uri = new URI("http://restapi.amap.com");
    List<Proxy> proxyList = ProxySelector.getDefault().select(uri);
    System.out.println(proxyList.get(0).address());
    System.out.println(proxyList.get(0).type());
} catch (URISyntaxException e) {
    e.printStackTrace();
}

因此,如果我們不需要自己的App中的請求走代理,則可以配置一個 proxy(Proxy.NO_PROXY) ,這樣也可以避免被 抓包。 NO_PROXY 的定義如下:

代理在Java中對應的抽象類有三種類型:

public static enum Type {
   DIRECT
   HTTP,
   SOCKS;
   private Type() {
   } 
}

DIRECT :無代理, HTTP :http代理, SOCKS :socks代理。第一種自然不用多說,而Http代理與Socks代理有什 麼區別?

對於Socks代理,在HTTP的場景下,代理服務器完成TCP數據包的轉發工作; 而Http代理服務器,在轉發數據之 外,還會解析HTTP的請求及響應,並根據請求及響應的內容做一些處理。

RealConnection 的 connectSocket 方法:

//代理則 new Socket(proxy); 否則無代理或http代理就
//address.socketFactory().createSocket(),相當於直接:new Socket()
rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
                ? address.socketFactory().createSocket()
                : new Socket(proxy);
//connect方法 socket.connect(address);

設置了SOCKS代理的情況下,創建Socket時,爲其傳入proxy,寫代碼時連接時還是以HTTP服務器爲目標地址(實 際上Socket肯定是與SOCKS代理服務器連);但是如果設置的是Http代理,創建的Socket是與Http代理服務器建 立連接。

在 connect 方法時傳遞的 address 來自於下面的集合 inetSocketAddresses RouteSelector 的 resetNextInetSocketAddress 方法:

private void resetNextInetSocketAddress(Proxy proxy) throws IOException {
    // ......
if (proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.SOCKS) { //無代理和socks代理,使用http服務器域名與端口
      socketHost = address.url().host();
      socketPort = address.url().port();
    } else {
      SocketAddress proxyAddress = proxy.address();
      if (!(proxyAddress instanceof InetSocketAddress)) {
        throw new IllegalArgumentException(
            "Proxy.address() is not an " + "InetSocketAddress: " + proxyAddress.getClass());
      }
      InetSocketAddress proxySocketAddress = (InetSocketAddress) proxyAddress;
      socketHost = getHostString(proxySocketAddress);
      socketPort = proxySocketAddress.getPort();
}
// ......
if (proxy.type() == Proxy.Type.SOCKS) {
//socks代理 connect http服務器 (DNS沒用,由代理服務器解析域名)
      inetSocketAddresses.add(InetSocketAddress.createUnresolved(socketHost, socketPort));
    } else {
//無代理,dns解析http服務器
//http代理,dns解析http代理服務器
List<InetAddress> addresses = address.dns().lookup(socketHost); //......
for (int i = 0, size = addresses.size(); i < size; i++) {
        InetAddress inetAddress = addresses.get(i);
        inetSocketAddresses.add(new InetSocketAddress(inetAddress, socketPort));
}
}
}

設置代理時,Http服務器的域名解析會被交給代理服務器執行。但是如果是設置了Http代理,會對Http代理服務器 的域名使用 OkhttpClient 配置的dns解析代理服務器,Http服務器的域名解析被交給代理服務器解析。

上述代碼就是代理與DNS在OkHttp中的使用,但是還有一點需要注意,Http代理也分成兩種類型:普通代理與隧 道代理。
其中普通代理不需要額外的操作,扮演「中間人」的角色,在兩端之間來回傳遞報文。這個“中間人”在收到客戶端 發送的請求報文時,需要正確的處理請求和連接狀態,同時向服務器發送新的請求,在收到響應後,將響應結果包 裝成一個響應體返回給客戶端。在普通代理的流程中,代理兩端都是有可能察覺不到"中間人“的存在。
但是隧道代理不再作爲中間人,無法改寫客戶端的請求,而僅僅是在建立連接後,將客戶端的請求,通過建立好的 隧道,無腦的轉發給終端服務器。隧道代理需要發起Http CONNECT請求,這種請求方式沒有請求體,僅供代理服 務器使用,並不會傳遞給終端服務器。請求頭 部分一旦結束,後面的所有數據,都被視爲應該轉發給終端服務器的 數據,代理需要把他們無腦的直接轉發,直到從客戶端的 TCP 讀通道關閉。CONNECT 的響應報文,在代理服務 器和終端服務器建立連接後,可以向客戶端返回一個 200 Connect established 的狀態碼,以此表示和終端服務 器的連接,建立成功。

RealConnection的connect方法

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 方法的判定爲:當前請求爲https並且存在http代理,這時候 connectTunnel 中會發起:

CONNECT xxxx HTTP/1.1
Host: xxxx
Proxy-Connection: Keep-Alive
User-Agent: okhttp/${version}

的請求,連接成功代理服務器會返回200;如果返回407表示代理服務器需要鑑權(如:付費代理),這時需要在請求 頭中加入 Proxy-Authorization :

  Authenticator authenticator = new Authenticator() {
       @Nullable
       @Override
       public Request authenticate(Route route, Response response) throws IOException {
                  if(response.code == 407){
                  //代理鑑權
                  String credential = Credentials.basic("代理服務用戶名", "代理服務密碼"); return response.request().newBuilder()
                        .header("Proxy-Authorization", credential)
                        .build();
                  }
                  return null;
             }
       };
  new OkHttpClient.Builder().proxyAuthenticator(authenticator);

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章