关于Java中http(s)代理的问题探究

前言

最近在Chrome上发现一个免费的VPN插件AVSTAR VPN,本着学习研究的精神,获取到了代理服务器的地址和端口.于是就想能不能把这个代理服务器的地址配置用于IDEAHTTP Proxy

尝试配置

通过调试上述的AVSTAR VPN插件,成功的获取到了代理配置如下,一段PAC脚本:

function FindProxyForURL (url, host) {
var blackList = new Array('192.168.*', '127.0.0.1', '134.209.63.251', 'astarvpn.center', '*.douyu.com');
for (var i = 0; i < blackList.length; i++) {
if (shExpMatch(host, blackList[i])) return 'DIRECT';
}
return 'HTTPS usa1.cn-cloudflare.com:443';
}

Windows 10

把这段代码保存为test.pac,再把它Deploy到本地的一个HTTP Server,然后配置系统代理的脚本地址,配置成功后,能够通过浏览器访问诸如Google等网站.

IDEA

打开IDEA后,找到代理设置 File | Settings | Appearance & Behavior | System Settings | HTTP Proxy

选择 Manual proxy configuration,配置内容如下

Host name: usa1.cn-cloudflare.com

Port number: 443

配置好后,点击check connection,输入https://www.google.com,发现无法访问.

问题探究

分析IDEA中HTTP(S)代理源码

IDEA-community源码下载下来,分析IDEA的代理是如何工作,发现IDEA里发起的所有HTTP(S)请求都是通过com.intellij.util.io.HttpRequests这个类完成的.

那么配置代理如何应用的呢.

创建connection

private static URLConnection openConnection(RequestBuilderImpl builder, RequestImpl request) throws IOException {
if (builder.myForceHttps && StringUtil.startsWith(request.myUrl, "http:")) {
request.myUrl = "https:" + request.myUrl.substring(5);
}

for (int i = 0; i < builder.myRedirectLimit; i++) {
String url = request.myUrl;

final URLConnection connection;
if (!builder.myUseProxy) {
connection = new URL(url).openConnection(Proxy.NO_PROXY);
}
else if (ApplicationManager.getApplication() == null) {
connection = new URL(url).openConnection();
}
else {
//这里就是设置代理
connection = HttpConfigurable.getInstance().openConnection(url);
}

if (connection instanceof HttpsURLConnection) {
configureSslConnection(url, (HttpsURLConnection)connection);
}
//...省略
}

com.intellij.util.net.HttpConfigurable 源码

设置代理

public URLConnection openConnection(@NotNull String location) throws IOException {
final URL url = new URL(location);
URLConnection urlConnection = null;
//这里会读取配置的代理信息
final List<Proxy> proxies = CommonProxy.getInstance().select(url);
if (ContainerUtil.isEmpty(proxies)) {
urlConnection = url.openConnection();
}
else {
IOException exception = null;
for (Proxy proxy : proxies) {
try {
//给URLConnection 设置代理
urlConnection = url.openConnection(proxy);
}
catch (IOException e) {
// continue iteration
exception = e;
}
}
if (urlConnection == null && exception != null) {
throw exception;
}
}

上述代码片段展示了创建连接,并设置代理,最终还是调用的JDK提供的API.

分析JDK中创建URLConnection的源码

JDK中创建连接源码在sun.net.NetworkClient中.

//var1 是host,var2是port
protected Socket doConnect(String var1, int var2) throws IOException, UnknownHostException {
Socket var3;
if (this.proxy != null) {
if (this.proxy.type() == Type.SOCKS) {
var3 = (Socket)AccessController.doPrivileged(new PrivilegedAction<Socket>() {
public Socket run() {
return new Socket(NetworkClient.this.proxy);
}
});
} else if (this.proxy.type() == Type.DIRECT) {
var3 = this.createSocket();
} else {
//这里创建的普通的套接字
var3 = new Socket(Proxy.NO_PROXY);
}
} else {
//创建套接字
var3 = this.createSocket();
}

if (this.connectTimeout >= 0) {
var3.connect(new InetSocketAddress(var1, var2), this.connectTimeout);
} else if (defaultConnectTimeout > 0) {
var3.connect(new InetSocketAddress(var1, var2), defaultConnectTimeout);
} else {
var3.connect(new InetSocketAddress(var1, var2));
}

if (this.readTimeout >= 0) {
var3.setSoTimeout(this.readTimeout);
} else if (defaultSoTimeout > 0) {
var3.setSoTimeout(defaultSoTimeout);
}

return var3;
}

问题就在于这个Socket,假如代理服务器的协议是HTTP,那么一切都将正常工作,但是如果代理服务器的协议是HTTPS,普通套接字是能够连接,但是整个连接建立后是无法正确通讯的,因为HTTPS协议会进行如下工作:

  1. 客户端向服务器发起SSL通信,报文中包含客户端支持的SSL的指定版本,加密组件列表(所使用的加密算法及密钥长度)

  2. 服务器的响应报文中,包含SSL版本以及加密组件,服务器的加密组件内容是从客户端发来的加密组件列表中筛选出来的,服务器还会发一个公开密钥并且带有公钥证书

  3. 客户端拿到服务器的公开密钥,并验证其公钥证书(使用浏览器中已经植入的CA公开密钥)

  4. 如果验证成功,客户端生成一个Pre-master secret随机密码串,这个随机密码串其实就是之后通信要用的对称密钥,并用服务器的公开密钥进行加密,发送给服务器,以此通知服务器,之后的报文都会通过这个对称密钥来加密

  5. 同时,客户端用约定好的hash算法计算握手消息,然后用生成的密钥进行加密,一起发送给服务器

  6. 服务器收到客户端发来的的公开密钥加密的对称密钥,用自己的私钥对其解密拿到对称密钥,再用对称密钥解析握手消息,验证hash值是否与客户端发来的一致。如果一致,则通知客户端SSL握手成功

  7. 之后的数据交互都是HTTP通信(当然通信会获得SSL保护),且数据都是通过对称密钥来加密(这个密钥不会每次都发,在握手的过程中,服务器已经知道了这个对称密钥,再有数据来时,服务器知道这些数据就是通过对称密钥加密的,于是就直接解密了)

版权声明:本文为CSDN博主「TLpigff」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/lvyibin890/article/details/82462041

要完成上述的步骤,显然是需要SSLSocket,但是JDK中创建连接时直接就没有考虑代理服务器是HTTPS的情况

总结

我在百度,Google,StackOverflow上面找了很多文章,关于设置java https 代理,很多文章都没提到代理服务器是https协议的情况,于是就有了这篇分析.

然后动手简单的实现了下假如代理服务器的协议是HTTPS,该如何实现:


public class Test {
public static void main(String[] args) throws IOException, NoSuchAlgorithmException, KeyManagementException {
SSLSocketFactory sslsf ;
X509TrustManager manager = new TrustM();
TrustManager mytm[] = { manager };
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, mytm, null);
sslsf = sslContext.getSocketFactory();
SSLSocket socket = (SSLSocket) sslsf.createSocket();
socket.connect(new InetSocketAddress("jp1.cn-cloudflare.com", 443),16*1000);
socket.startHandshake();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
writer.write("GET https://www.google.com HTTP/1.0\r\n");
writer.write("host: www.google.com\r\n");
writer.write("Connection: close\r\n");
writer.write("\r\n");
writer.flush();
String line;
StringBuilder stringBuffer = new StringBuilder();
while((line = reader.readLine() )!= null){
stringBuffer.append(line).append("\n");
}
writer.close();
reader.close();
System.out.println(stringBuffer.toString());

System.out.println("|");
}
static class TrustM implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
System.out.println("checkClientTrusted:"+s);
}

@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
System.out.println("checkServerTrusted:"+s);
}

@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
}

上述代码是能够通过HTTPS代理服务器正确的请求到Google的内容

所以目前结论如下:

  1. JDK提供的代理仅支持SOCK,HTTP

  2. 假如代理服务器是HTTPS协议,那么设置代理后将无法完成请求

参考

  1. JAVA设置代理的两种方式(HTTP和HTTPS)

  2. HTTPS协议的工作原理

  3. Java Networking and Proxies

  4. Networking Properties