web-dev-qa-db-ja.com

Javaのプロキシ経由でHTTPSリクエストを送信するにはどうすればよいですか?

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" );

デフォルトのSSLSocketFactoryのTrustManagerは次のように設定されます。

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で行った内容がオーバーライドされます。まだ必要ないの?

別の更新:

すべてを信頼するTrustManagerを使用するSSLSocketFactoryを使用するようにSSLTunnelSocketFactoryクラスを変更しました。これにより違いが生じたようには見えません。これは、SSLTunnelSocketFactoryのcreateSocketメソッドです。

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()が呼び出されると、コールスタックHttpsClient.afterConnectに従って、同じ引数を使用して同じcreateSocketメソッドが再度呼び出されます。ただし、Socket sがnullである場合を除き、result.startHandshake()に到達すると繰り返しますが、結果は同じSSLHandshakeExceptionです。

複雑さを増すこのパズルの重要な部分を見逃していますか?

これはスタックトレースです。

 javax.net.ssl.SSLHandshakeException:ハンドシェイク中にリモートホストが接続を閉じました
 at com.Sun.net.ssl.internal.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.Java:808)
 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)
原因:Java.io.EOFExce ption:com.Sun.net.ssl.internal.ssl.InputRecord.read(InputRecord.Java:333)
 at com.Sun.net.ssl.internalでSSLピアが誤ってシャットダウンしました
 .ssl.SSLSocketImpl.readRecord(SSLSocketImpl.Java:789)
 ... 8 more 
27
Jeff Hillman

セキュリティ上の理由からプロキシでHTTP接続を終了できないため、HTTPSプロキシは意味がありません。信頼ポリシーを使用すると、プロキシサーバーにHTTPSポートがある場合に機能する場合があります。エラーは、HTTPSを使用してHTTPプロキシポートに接続することにより発生します。

プロキシCONNECTコマンドを使用して、SSLトンネリングを使用してプロキシ経由で接続できます(多くの人がそのプロキシを呼び出します)。ただし、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」を使用しました。

30
ZZ Coder

独自にロールバックする代わりに、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();
  }
  */
0
user187702