Подтвердить что ты не робот

Как отправить запрос 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-материалами. Я считаю, что я представил все детали, необходимые для понимания того, что я пытаюсь сделать. Если это не так, дайте мне знать.

Update:

После прочтения статьи, предложенной 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: Socket [addr =/proxyHost, port = proxyPort, localport = 24372]]" .

Это бессмысленно для меня, но это может быть причиной того, что после этого сломаются.

Когда вызывается result.startHandshake(), тот же метод createSocket вызывается снова, согласно стеку вызовов, HttpsClient.afterConnect, с теми же аргументами, за исключением того, что Socket s имеет значение null, и когда это происходит, чтобы получить результат. startHandshake() снова, результатом является то же SSLHandshakeException.

Я все еще не вижу важной части этой сложнейшей загадки?

Это трассировка стека:

javax.net.ssl.SSLHandshakeException: Remote host closed connection during handshake
  at com.sun.net.ssl.internal.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:808)
  at com.sun.net.ssl.internal.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1112)
  at com.sun.net.ssl.internal.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1139)
  at com.sun.net.ssl.internal.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1123)
  at gsauthentication.SSLTunnelSocketFactory.createSocket(SSLTunnelSocketFactory.java:106)
  at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:391)
  at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:166)
  at sun.net.www.protocol.https.HttpsURLConnectionImpl.connect(HttpsURLConnectionImpl.java:133)
  at gsauthentication.GSAuthentication.main(GSAuthentication.java:52)
Caused by: java.io.EOFException: SSL peer shut down incorrectly
  at com.sun.net.ssl.internal.ssl.InputRecord.read(InputRecord.java:333)
  at com.sun.net.ssl.internal.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:789)
  ... 8 more
4b9b3361

Ответ 1

Прокси-сервер HTTPS не имеет смысла, потому что вы не можете прекратить свое HTTP-соединение в прокси-сервере по соображениям безопасности. С помощью вашей политики доверия она может работать, если у прокси-сервера есть HTTPS-порт. Ваша ошибка вызвана подключением к HTTP-прокси-порту с помощью HTTPS.

Вы можете подключиться через прокси-сервер, используя SSL-туннелирование (многие называют этот прокси-сервер) с помощью команды proxy CONNECT. Однако Java не поддерживает более новую версию туннелирования прокси. В этом случае вам необходимо самостоятельно управлять туннелированием. Здесь вы можете найти пример кода,

http://www.javaworld.com/javaworld/javatips/jw-javatip111.html

EDIT: если вы хотите поразить все меры безопасности в 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;
    }
 }  );

EDIT 2: Я просто попробовал свою программу, которую написал несколько лет назад, используя SSLTunnelSocketFactory, и она тоже не работает. По-видимому, Sun представила новую ошибку где-то в Java 5. См. Отчет об ошибке,

http://bugs.sun.com/view_bug.do?bug_id=6614957

Хорошей новостью является то, что ошибка туннелирования SSL исправлена, поэтому вы можете просто использовать по умолчанию factory. Я просто попробовал с прокси-сервером, и все работает так, как ожидалось. Смотрите мой код,

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", чтобы вызвать это.

Ответ 2

Попробуйте библиотеку 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();
  }
  */