通过 HTTP connect 建立的代理及 Java 中如何使用这种代理

代理的分类

我们通常根据代理协议所在的 OSI 模型中的层级, 分为 HTTP 代理和 SOCKS (v4 or v5) 代理;
根据代理是在客户端还是在服务端, 分为 正向代理(Forward proxy 如: Squid) 和 反向代理(reverse proxy 如: NGINX);
根据客户端是否意识到在使用代理分为一般代理(Common proxy) 和 透明代理(Transparent Proxy);

使用 HTTP 代理访问

如果使用 http 代理访问 http 协议, 那么客户端(通常是浏览器, 也可能是 curl 或者其它编程代码) 建立一个到代理的 TCP 连接, 然后代理假装客户端负责发出请求, 假装服务端返回结果给真正的客户端.
如果使用 http 代理访问 https 协议, 那么通常客户端(浏览器, curl) 使用 HTTP CONNECT 先建立一个隧道, 然后通过隧道建立到目的地址的 https 请求.

HTTP CONNECT

对于开发人员常见的 HTTP 协议来说, 除了有 GET, POST, PUT, DELETE, HEAD, PATCH, OPTIONS 等方法(method)之外, 其实还有一个 CONNECT 方法. HTTP CONNECT
HTTP CONNECT 方法主要用来建立一个双向通信的隧道(tunnel). 比如我们想通过代理 proxy.tianxiaohui.com:8080 访问 https://www.google.com. 那么我们看到的过程是这样的:

  1. 客户端先跟 proxy.tianxiaohui.com 通过三次握手建立 tcp 连接;
  2. 客户端发送一个 HTTP CONNECT 请求给 proxy.tianxiaohui.com:8080, HTTP header 里面通过 host header 包含想要真正想要访问网站的主机和端口 www.google.com:443
  3. 代理收到这个 HTTP CONNECT 请求之后, 通过三次握手跟 www.google.com 建立 tcp 连接, 然后返回给客户端 200 response. 从此开始代理服务器对于这个双向通信隧道只转发 tcp 包, 不管任何内容.
  4. 客户端收到 200 response, 这时一个从客户端到 www.google.com 主机的隧道建立. 相当于没有代理的情况下, tcp 连接建立.
  5. 之后客户端就类似刚建立 tcp 连接一样, 开始 https 协商, 然后 ssl 协商和证书验证之后, 开始发送真正的 HTTP 请求.

客户端抓包验证

在客户端, 可以通过 curl 来实验这个过程, 然后通过 tcpdump 抓包验证.

curl -x 'http://proxy.tianxiaohui.com:8080' https://www.google.com

通过 wireshark 看到的 HTTP 请求和回复是:

CONNECT www.google.com:443 HTTP/1.1
Host: www.google.com:443
User-Agent: curl/7.64.1
Proxy-Connection: Keep-Alive

HTTP/1.1 200 Connection established

详细如图:
proxyCapture.png

通过上面的截图可以看到, 一旦隧道建立好之后, 客户端就尝试和真正的服务器简历 https 连接, 代理只是无脑转发. 即使代理抓包截获数据, 也无法看到真正的请求包里的内容.

https 代理和 https 请求

我们可以通过代理走 http 的请求 和 https 的请求. 同样代理即可以是是 http 协议的代理, 也可以是 https 的代理. 如下图:
proxyAndServer.png

那么一个复杂的例子就是, 客户端走 https 代理, 然后发送 https 请求. 这时候,我们可以看到 HTTP CONNECT 请求是在客户端和代理服务器之间先建立 https 连接后, 然后通过 https 协议发出去的. 之后代理建立和真正的远程服务器建立 tcp 连接, 发送的 http response 也是在 https 协议收到. 到此, 一个隧道建立. 然后客户端和真正的服务器在这个隧道里面再建立真正新的到真正服务器的内层 https 连接, 连接建立好之后, 发送真正的请求.

curl -x 'https://userName:password@proxy.tianxiaohui.com:8443' https://www.gooogle.com

如下图, 由于我们看不到内层的 https 请求, 但是从外层看这个证书验证, 我们可以看到它是代理服务器的证书.
httpsproxycert.png

代理的认证

有些代理需要认证, 这时候需要在 HTTP CONNECT 的时候, 发送 Proxy-Authorization header. 从非加密的但是需要认证的代理服务器发送的请求, 我们看一看到:

curl -x 'http://userName:password@proxy.tianxiaohui.com:8080' https://www.gooogle.com

wireshark 看到的包内容:

CONNECT www.gooogle.com:443 HTTP/1.1
Host: www.gooogle.com:443
Proxy-Authorization: Basic eGse3hbjo4NTEyMTZ2dnZ2Y2NmaGloamhldGmZw==
User-Agent: curl/7.64.1
Proxy-Connection: Keep-Alive

HTTP/1.1 200 Connection established

如果不给认证 header, 就会收到 http status 407 的 response.

Java 程序中使用代理

有些时候, 我们看到Java 程序使用环境变量设置代理, 比如下面这种方式:

System.setProperty("proxyHost", PROXY_HOST);
System.setProperty("proxyPort",  PROXY_PORT);

又或者通过启动参数设置环境变量:

java -Dhttp.proxyHost=proxyhostURL  -Dhttp.proxyPort=proxyPortNumber -jar my.jar

这种方式其实是让所有的连接都走系统代理, 这不是我们想讨论的内容.

下面这种只针对某个主机的连接, 而不是全局所有连接的代理, 才是我们想讨论的范围:

Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxy.tianxiaohui.com", 8080));
URL url = new URL("https://www.google.com");
HttpURLConnection uc = (HttpURLConnection)url.openConnection(proxy);
uc.connect();

Java 代理类型不支持 https 代理

现有的 Java 实现 java.net.Proxy 不支持 HTTPS 代理, 如果想实现这种代理方式, 要自己写 Java 代码连接 https 代理, 验证证书,创建 https 连接, 然后发送 HTTP CONNECT 请求.

Java http 代理的用户名密码验证问题

如果我们的代理服务器不需要用户名密码验证, 一切都没问题, 直接上面的方式去连接就好了.
如果我们的代理服务器需要用户名和密码验证, 就要区分我们最终要访问的网站是 http 还是 https 了.

最终网站是 http

直接在我们要访问的网站的请求里面设置header: Proxy-Authorization 就可以了. 比如下面的代码:

URL url = new URL("http://www.tianxiaohui.com");  
InetSocketAddress addr = new InetSocketAddress("proxy.tianxiaohui.com",8080);  
Proxy proxy = new Proxy(Proxy.Type.HTTP, addr);
URLConnection conn = url.openConnection(proxy);  
   
String headerkey = "Proxy-Authorization";  
String headerValue = "Basic "+Base64.encodeToString("userName:password".getBytes(), false);
conn.setRequestProperty(headerkey, headerValue);  
  
InputStream in = conn.getInputStream();  
String s = IOUtils.toString(in, "utf-8");  
System.out.println(s);  

最终网站是 https

如果要访问的网站是 https, 设置代理用户名密码的认证的方式就不一样了. 需要下面的代码:

Authenticator.setDefault(new Authenticator() {
  protected PasswordAuthentication getPasswordAuthentication() {
    return new PasswordAuthentication("userName", "password".toCharArray());
  }
});

如果是 JDK 8 update 111 之后, 还需要在启动的时候或者任何连接建立之前, 执行下面这句

# 作为启动参数: 
-Djdk.http.auth.tunneling.disabledSchemes=""
# 或者再 main 函数里开始的地方设置:
System.setProperty("jdk.http.auth.tunneling.disabledSchemes", "");

标签: none

添加新评论