如何通过 HTTP 代理连接 SSL 套接字?
How to connect a SSL socket through a HTTP proxy?
我正在尝试使用 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 从来没有 RETURNS。通过代理机器的 wireshark 转储,我可以看到代理和服务器之间发生了一次 tcp 握手。通过 Web 会话的转储,我可以看到客户端通常会在此时启动 SSL 握手,这在我的场景中不会发生。
这不是信任管理器的问题,因为证书从未返回给我并且从未经过验证。
编辑:
经过讨论,这是我正在尝试的代码的更完整版本 运行。上面这个版本以简单的 (new Socket(...)) 作为参数是我后来尝试过的。
我尝试调试的代码的原始版本抛出异常
java.net.ConnectException: failed to connect to /192.168.1.100 (port 443): connect failed: ETIMEDOUT (Connection timed out)
顺序如下(再次简化):
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
,或 https.proxyHost/proxyPort
属性,仅支持通过 HttpURLConnection,
的 HTTP 代理,不支持通过 Socket.
要让你自己的 SSLSocket
工作,你需要做的就是创建一个明文套接字,在其上发出 HTTP CONNECT
命令,检查 200 的响应,然后将其包装在 SSLSocket.
中
EDIT 发送CONNECT命令时,当然不能关闭socket;在阅读它的回复时,你不能使用 BufferedReader,
否则你会丢失数据;要么手动阅读该行,要么使用 DataInputStream.readLine(),
尽管它已被弃用。您还需要完全遵循 RFC 2616。
我现在没有时间 test/write 有针对性的解决方案,但是 Upgrading socket to SSLSocket with STARTTLS: recv failed 似乎涵盖了基本问题。
在你的情况下,你需要连接到代理,发出代理连接,然后升级连接 - 基本上你 CONNECT 取代了引用问题中的 STARTTLS,并且检查“ 670 ”不是需要。
你必须使用 javax.net lib 。您可以使用 javax.net.ssl.*.
存档到您的目标
我认为您可以使用 oracle 文档获得解决方案。这是 link。
结合 MacDaddy 的回答和 Viktor Mukhachev 的评论,使用 SSLSocket
而不是 Socket
而不是 Proxy
。
代码:
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();
}
}
我正在尝试使用 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 从来没有 RETURNS。通过代理机器的 wireshark 转储,我可以看到代理和服务器之间发生了一次 tcp 握手。通过 Web 会话的转储,我可以看到客户端通常会在此时启动 SSL 握手,这在我的场景中不会发生。
这不是信任管理器的问题,因为证书从未返回给我并且从未经过验证。
编辑:
经过讨论,这是我正在尝试的代码的更完整版本 运行。上面这个版本以简单的 (new Socket(...)) 作为参数是我后来尝试过的。
我尝试调试的代码的原始版本抛出异常
java.net.ConnectException: failed to connect to /192.168.1.100 (port 443): connect failed: ETIMEDOUT (Connection timed out)
顺序如下(再次简化):
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
,或 https.proxyHost/proxyPort
属性,仅支持通过 HttpURLConnection,
的 HTTP 代理,不支持通过 Socket.
要让你自己的 SSLSocket
工作,你需要做的就是创建一个明文套接字,在其上发出 HTTP CONNECT
命令,检查 200 的响应,然后将其包装在 SSLSocket.
EDIT 发送CONNECT命令时,当然不能关闭socket;在阅读它的回复时,你不能使用 BufferedReader,
否则你会丢失数据;要么手动阅读该行,要么使用 DataInputStream.readLine(),
尽管它已被弃用。您还需要完全遵循 RFC 2616。
我现在没有时间 test/write 有针对性的解决方案,但是 Upgrading socket to SSLSocket with STARTTLS: recv failed 似乎涵盖了基本问题。
在你的情况下,你需要连接到代理,发出代理连接,然后升级连接 - 基本上你 CONNECT 取代了引用问题中的 STARTTLS,并且检查“ 670 ”不是需要。
你必须使用 javax.net lib 。您可以使用 javax.net.ssl.*.
存档到您的目标我认为您可以使用 oracle 文档获得解决方案。这是 link。
结合 MacDaddy 的回答和 Viktor Mukhachev 的评论,使用 SSLSocket
而不是 Socket
而不是 Proxy
。
代码:
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();
}
}