Как отправить запрос HTTPS через прокси в Java?
Я пытаюсь отправить запрос на сервер, используя класс HttpsUrlConnection. На сервере есть проблемы с сертификатами, поэтому я настроил TrustManager, который доверяет всему, а также верификатор имени хоста, который одинаково снисходителен. Этот менеджер отлично работает, когда я делаю запрос напрямую, но, похоже, он вообще не используется, когда я отправляю запрос через прокси.
Я установил настройки прокси-сервера следующим образом:
Properties systemProperties = System.getProperties();
systemProperties.setProperty( "http.proxyHost", "proxyserver" );
systemProperties.setProperty( "http.proxyPort", "8080" );
systemProperties.setProperty( "https.proxyHost", "proxyserver" );
systemProperties.setProperty( "https.proxyPort", "8080" );
TrustManager для SSLSocketFactory по умолчанию настроен так:
SSLContext sslContext = SSLContext.getInstance( "SSL" );
// set up a TrustManager that trusts everything
sslContext.init( null, new TrustManager[]
{
new X509TrustManager()
{
public X509Certificate[] getAcceptedIssuers()
{
return null;
}
public void checkClientTrusted( X509Certificate[] certs, String authType )
{
// everything is trusted
}
public void checkServerTrusted( X509Certificate[] certs, String authType )
{
// everything is trusted
}
}
}, new SecureRandom() );
// this doesn't seem to apply to connections through a proxy
HttpsURLConnection.setDefaultSSLSocketFactory( sslContext.getSocketFactory() );
// setup a hostname verifier that verifies everything
HttpsURLConnection.setDefaultHostnameVerifier( new HostnameVerifier()
{
public boolean verify( String arg0, SSLSession arg1 )
{
return true;
}
} );
Если я запускаю следующий код, я получаю исключение SSLHandshakException ("Закрытое соединение удаленного узла во время рукопожатия"):
URL url = new URL( "https://someurl" );
HttpsURLConnection connection = (HttpsURLConnection)url.openConnection();
connection.setDoOutput( true );
connection.setRequestMethod( "POST" );
connection.setRequestProperty( "Content-Type", "application/x-www-form-urlencoded" );
connection.setRequestProperty( "Content-Length", "0" );
connection.connect();
Я предполагаю, что мне не хватает какой-то настройки, связанной с использованием прокси при работе с SSL. Если я не использую прокси, вызывается мой метод checkServerTrusted; это то, что мне нужно, когда я прохожу прокси.
Я обычно не имею дело с Java, и у меня нет большого опыта работы с HTTP/web. Я считаю, что предоставил все детали, необходимые для понимания того, что я пытаюсь сделать. Если это не так, дайте мне знать.
Обновить:
Прочитав статью, предложенную ZZ Coder, я внес следующие изменения в код подключения:
HttpsURLConnection connection = (HttpsURLConnection)url.openConnection();
connection.setSSLSocketFactory( new SSLTunnelSocketFactory( proxyHost, proxyPort ) );
connection.setDoOutput( true );
connection.setRequestMethod( "POST" );
connection.setRequestProperty( "Content-Type", "application/x-www-form-urlencoded" );
connection.setRequestProperty( "Content-Length", "0" );
connection.connect();
Результат (SSLHandshakeException) тот же. Когда я устанавливаю SLLSocketFactory здесь как SSLTunnelSocketFactory (класс, описанный в статье), то, что я делал с TrustManager и SSLContext, переопределяется. Разве мне это еще не нужно?
Еще одно обновление:
Я изменил класс SSLTunnelSocketFactory, чтобы использовать SSLSocketFactory, который использует мой TrustManager, который доверяет всему. Похоже, что это не имеет никакого значения. Это метод createSocket SSLTunnelSocketFactory:
public Socket createSocket( Socket s, String host, int port, boolean autoClose )
throws IOException, UnknownHostException
{
Socket tunnel = new Socket( tunnelHost, tunnelPort );
doTunnelHandshake( tunnel, host, port );
SSLSocket result = (SSLSocket)dfactory.createSocket(
tunnel, host, port, autoClose );
result.addHandshakeCompletedListener(
new HandshakeCompletedListener()
{
public void handshakeCompleted( HandshakeCompletedEvent event )
{
System.out.println( "Handshake finished!" );
System.out.println(
"\t CipherSuite:" + event.getCipherSuite() );
System.out.println(
"\t SessionId " + event.getSession() );
System.out.println(
"\t PeerHost " + event.getSession().getPeerHost() );
}
} );
result.startHandshake();
return result;
}
Когда мой код вызывает connection.connect, вызывается этот метод, и вызов doTunnelHandshake успешен. Следующая строка кода использует мой SSLSocketFactory для создания SSLSocket; значение toString результата после этого вызова:
"1d49247 [SSL_NULL_WITH_NULL_NULL: Сокет [addr=/proxyHost, порт = proxyPort, localport = 24372]]".
Это бессмысленно для меня, но может быть причиной того, что после этого все рушится.
Когда вызывается result.startHandshake(), тот же самый метод createSocket вызывается снова, в соответствии со стеком вызовов, HttpsClient.afterConnect, с теми же аргументами, за исключением того, что Socket s имеет значение null, и когда он возвращается к result.startHandshake() опять же, результат тот же SSLHandshakeException.
Я все еще скучаю по важной части этой все более сложной головоломки?
Это трассировка стека:
javax.net.ssl.SSLHandshakeException: удаленный хост закрыл соединение во время рукопожатия в com.sun.net.ssl.internal.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:808) в com.sun.net.ssl.internal.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1112) на com.sun.net.ssl.internal.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1139) на com.sun.net.ssl.internal.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1123) в gsauthentication.SSLTunnelSocketFactory.createSocket(SSLTunnelSocketFactory.java:106) на sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:391) на sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:166) на sun.net.www.protocol.https.HttpsURLConnectionImpl.connect(HttpsURLConnectionImpl.java:133) в gsauthentication.GSAuthentication.main(GSAuthentication.java:52) Вызвано: java.io.EOFException: SSL равноправно закрывается на com.sun.net.ssl.internal.ssl.InputRecord.read(InputRecord.java:333) в com.sun.net.ssl.internal.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:789) ... еще 8
2 ответа
HTTPS-прокси не имеет смысла, потому что вы не можете разорвать ваше HTTP-соединение на прокси-сервере из соображений безопасности. С вашей политикой доверия это может работать, если у прокси-сервера есть порт HTTPS. Ваша ошибка вызвана подключением к прокси-порту HTTP с HTTPS.
Вы можете подключиться через прокси-сервер с использованием туннелирования SSL (многие люди называют его прокси-сервером) с помощью прокси-команды CONNECT. Однако Java не поддерживает более новую версию туннелирования прокси. В этом случае вам нужно самостоятельно справиться с туннелированием. Вы можете найти образец кода здесь,
http://www.javaworld.com/javaworld/javatips/jw-javatip111.html
РЕДАКТИРОВАТЬ: Если вы хотите победить все меры безопасности в JSSE, вам все еще нужен ваш собственный TrustManager. Что-то вроде этого,
public SSLTunnelSocketFactory(String proxyhost, String proxyport){
tunnelHost = proxyhost;
tunnelPort = Integer.parseInt(proxyport);
dfactory = (SSLSocketFactory)sslContext.getSocketFactory();
}
...
connection.setSSLSocketFactory( new SSLTunnelSocketFactory( proxyHost, proxyPort ) );
connection.setDefaultHostnameVerifier( new HostnameVerifier()
{
public boolean verify( String arg0, SSLSession arg1 )
{
return true;
}
} );
РЕДАКТИРОВАТЬ 2: Я только что попробовал свою программу, которую я написал несколько лет назад, используя SSLTunnelSocketFactory, и она тоже не работает. По-видимому, когда-то Sun представила новую ошибку в Java 5. Смотрите этот отчет об ошибках,
http://bugs.sun.com/view_bug.do?bug_id=6614957
Хорошей новостью является то, что ошибка туннелирования SSL исправлена, поэтому вы можете просто использовать фабрику по умолчанию. Я только что попробовал с прокси и все работает как положено. Смотри мой код,
public class SSLContextTest {
public static void main(String[] args) {
System.setProperty("https.proxyHost", "proxy.xxx.com");
System.setProperty("https.proxyPort", "8888");
try {
SSLContext sslContext = SSLContext.getInstance("SSL");
// set up a TrustManager that trusts everything
sslContext.init(null, new TrustManager[] { new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
System.out.println("getAcceptedIssuers =============");
return null;
}
public void checkClientTrusted(X509Certificate[] certs,
String authType) {
System.out.println("checkClientTrusted =============");
}
public void checkServerTrusted(X509Certificate[] certs,
String authType) {
System.out.println("checkServerTrusted =============");
}
} }, new SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(
sslContext.getSocketFactory());
HttpsURLConnection
.setDefaultHostnameVerifier(new HostnameVerifier() {
public boolean verify(String arg0, SSLSession arg1) {
System.out.println("hostnameVerifier =============");
return true;
}
});
URL url = new URL("https://www.verisign.net");
URLConnection conn = url.openConnection();
BufferedReader reader =
new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
Это то, что я получаю, когда запускаю программу,
checkServerTrusted =============
hostnameVerifier =============
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
......
Как видите, вызываются как SSLContext, так и hostnameVerifier. HostnameVerifier участвует только в том случае, если имя хоста не соответствует сертификату. Я использовал "www.verisign.net", чтобы вызвать это.
Попробуйте использовать библиотеку Apache Commons HttpClient вместо того, чтобы пытаться свернуть свою собственную: http://hc.apache.org/httpclient-3.x/index.html
Из их примера кода:
HttpClient httpclient = new HttpClient();
httpclient.getHostConfiguration().setProxy("myproxyhost", 8080);
/* Optional if authentication is required.
httpclient.getState().setProxyCredentials("my-proxy-realm", " myproxyhost",
new UsernamePasswordCredentials("my-proxy-username", "my-proxy-password"));
*/
PostMethod post = new PostMethod("https://someurl");
NameValuePair[] data = {
new NameValuePair("user", "joe"),
new NameValuePair("password", "bloggs")
};
post.setRequestBody(data);
// execute method and handle any error responses.
// ...
InputStream in = post.getResponseBodyAsStream();
// handle response.
/* Example for a GET reqeust
GetMethod httpget = new GetMethod("https://someurl");
try {
httpclient.executeMethod(httpget);
System.out.println(httpget.getStatusLine());
} finally {
httpget.releaseConnection();
}
*/