如何通过HTTP代理连接SSL套接字?

15
我正在尝试使用Java (Android)来连接到一个带有SSL套接字的服务器。请注意,这不是HTTP数据。这是一种混合了文本和二进制数据的专有协议。
我想通过HTTP代理中继SSL连接,但是我遇到了很多问题。现在我使用的场景和我的浏览器似乎使用的场景与Squid代理如下:
[客户端]-> [HTTP连接]-> [代理]-> [SSL连接]-> [服务器]
这对于浏览器来说是有效的,因为代理在建立ssl连接后会立即进行TLS协商。然而,我的代码似乎没有这样做。
final TrustManager[] trustManager = new TrustManager[] { new MyX509TrustManager() };
final SSLContext context = SSLContext.getInstance("TLS");
context.init(null, trustManager, null);
SSLSocketFactory factory = context.getSocketFactory();
Socket s = factory.createSocket(new Socket(proxy_ip, 3128), hostName, port, true);

我遇到的问题是 createSocket 永远不会返回。在代理机器上使用 Wireshark 能够看到代理和服务器之间进行了 TCP 握手。使用 Web 会话转储时,我可以看到客户端通常在此时启动 SSL 握手,但在我的情况下没有发生。
这不是信任管理器的问题,因为证书从未返回给我,也从未得到验证。
编辑:
经过讨论,这是我尝试运行的更完整版本代码。上面的简单 (new Socket(...)) 参数是我后来尝试的内容。
我正在调试的原始代码版本会抛出以下异常: java.net.ConnectException: failed to connect to /192.168.1.100 (port 443): connect failed: ETIMEDOUT (连接超时)
序列如下(稍微简化一下):
final Socket proxySocket = new Socket();
proxySocket.connect(proxyAddress, 2000); // 2 seconds as the connection timeout for connecting to the proxy server 
[Start a thread and write to outputStream=socket.getOutputStream()]
final String proxyRequest = String.format("CONNECT %s:%d HTTP/1.1\r\nProxy-Connection: keep-alive\r\nConnection: keep-alive\r\nHost: %s:%d\r\n\r\n", hostName, port, hostName, port);
outputStream.close(); // Closing or not doesn't change anything
[Stop using that thread and let it exit by reaching the end of its main function]

然后使用以下代码读取响应:
    final InputStreamReader isr = new InputStreamReader(proxySocket.getInputStream());
    final BufferedReader br = new BufferedReader(isr);
    final String statusLine = br.readLine();

    boolean proxyConnectSuccess = false;
    // readLine consumed the CRLF
    final Pattern statusLinePattern = Pattern.compile("^HTTP/\\d+\\.\\d+ (\\d\\d\\d) .*");
    final Matcher statusLineMatcher = statusLinePattern.matcher(statusLine);
    if (statusLineMatcher.matches())
    {
        final String statusCode = statusLineMatcher.group(1);
        if (null != statusCode && 0 < statusCode.length() && '2' == statusCode.charAt(0))
        {
            proxyConnectSuccess = true;
        }
    }

    // Consume rest of proxy response
    String line;
    while ( "".equals((line = br.readLine())) == false )
    {
    }

我可以说这段代码能够工作,因为它在没有SSL的情况下就能正常运行。这里创建的套接字proxySocket会被传递给createSocket函数,而不是像原始示例中一样即兴创建一个新的套接字。


java.net.Proxy在HTTP下无法工作。它似乎需要SOCKS代理。我已经找到了一个适用于Sun OS的补丁,但没有找到适用于Android的补丁。这对于HTTP的工作至关重要,因为Android不能在没有第三方应用程序的情况下提供HTTPS / SOCKS支持(默认安装在2015年1月不提供足够的配置)。 - Eric
如果您通过HTTP代理连接到SSL套接字,我建议尽可能考虑使用WebSockets - 特别是因为错误消息“java.net.ConnectException:无法连接到/ 192.168.1.100(端口443)”显示了尝试连接到标准HTTPS端口。当然,这取决于您的用例... - Phil Lello
4个回答

17

java.net.Proxyhttps.proxyHost/proxyPort 属性只支持通过 HttpURLConnection 进行 HTTP 代理,而不支持通过 Socket 进行代理。

要使自己的 SSLSocket 正常工作,您只需要创建一个明文套接字,在其上发出一个 HTTP CONNECT 命令,检查响应是否为200,然后用 SSLSocket 包装它。

编辑提示 发送CONNECT命令时,当然不能关闭套接字;在读取其回复时,不能使用 BufferedReader,否则会丢失数据;请手动读取行或使用 DataInputStream.readLine(),尽管它已经过时。您还需要完全遵循 RFC 2616 规范。


3
Java 8支持HTTP隧道代理,当您使用接受代理的Socket构造函数时:new Socket(new Proxy(Proxy.Type.HTTP, socketAddress)) - Viktor Mukhachev

8
您需要使用javax.net lib,可以使用javax.net.ssl.*将其存档到您的目标位置。
我认为您可以在oracle文档中找到解决方案。这是链接:

SSLSocketClientWithTunneling


2
那个链接有一个小错误。其中一个请求行以\n结尾,应该是\r\n - user207421

3

结合 MacDaddy 的回答和 Viktor Mukhachev 的评论,使用在代理上的 Socket 上使用 SSLSocket

代码:

import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

public class SSLThroughProxy {
    public static void main(String[] args) throws IOException {
        final String REQUEST = "GET / HTTP/1.1\r\n" +
                "Host: github.com\r\n" +
                "Connection: close\r\n" +
                "\r\n";

        Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("your-proxy-host", 8080));
        Socket socket = new Socket(proxy);

        InetSocketAddress address = new InetSocketAddress("github.com", 443);
        socket.connect(address);

        SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
        SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket, address.getHostName(), address.getPort(), true);
        sslSocket.startHandshake();

        sslSocket.getOutputStream().write(REQUEST.getBytes());

        InputStream inputStream = sslSocket.getInputStream();
        byte[] bytes = inputStream.readAllBytes();
        System.out.println(new String(bytes, StandardCharsets.UTF_8));

        sslSocket.close();
    }
}

1
我现在没有时间测试/编写有针对性的解决方案,但将socket升级为带STARTTLS的SSLSocket:接收失败似乎涵盖了基本问题。
在您的情况下,您需要连接到代理,发出代理连接,然后升级连接 - 实质上,您的CONNECT取代了参考问题中的STARTTLS,并且不需要检查“ 670 ”。

1
但需要检查HTTP 200的状态码。 - user207421

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接