/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package org.apache.harmony.luni.tests.internal.net.www.protocol.https;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Authenticator;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URL;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.util.Arrays;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import junit.framework.TestCase;
import libcore.java.security.TestKeyStore;
import libcore.javax.net.ssl.TestTrustManager;

/**
 * Implementation independent test for HttpsURLConnection.
 * The test needs certstore file placed in system classpath
 * and named as "key_store." + the type of the
 * default KeyStore installed in the system in lower case.
 * <br>
 * For example: if default KeyStore type in the system is BKS
 * (i.e. java.security file sets up the property keystore.type=BKS),
 * thus classpath should point to the directory with "key_store.bks"
 * file.
 * <br>
 * This certstore file should contain self-signed certificate
 * generated by keytool utility in a usual way.
 * <br>
 * The password to the certstore should be "password" (without quotes).
 */
public class HttpsURLConnectionTest extends TestCase {

    // the password to the store
    private static final String KS_PASSWORD = "password";

    // turn on/off logging
    private static final boolean DO_LOG = false;

    // read/connection timeout value
    private static final int TIMEOUT = 5000;

    // OK response code
    private static final int OK_CODE = 200;

    // Not Found response code
    private static final int NOT_FOUND_CODE = 404;

    // Proxy authentication required response code
    private static final int AUTHENTICATION_REQUIRED_CODE = 407;

    private static File store;

    static {
        try {
            store = File.createTempFile("key_store", "bks");
        } catch (Exception e) {
            // ignore
        }
    }

    /**
     * Checks that HttpsURLConnection's default SSLSocketFactory is operable.
     */
    public void testGetDefaultSSLSocketFactory() throws Exception {
        // set up the properties defining the default values needed by SSL stuff
        setUpStoreProperties();

        SSLSocketFactory defaultSSLSF = HttpsURLConnection.getDefaultSSLSocketFactory();
        ServerSocket ss = new ServerSocket(0);
        Socket s = defaultSSLSF.createSocket("localhost", ss.getLocalPort());
        ss.accept();
        s.close();
        ss.close();
    }

    public void testHttpsConnection() throws Throwable {
        // set up the properties defining the default values needed by SSL stuff
        setUpStoreProperties();

        // create the SSL server socket acting as a server
        SSLContext ctx = getContext();
        ServerSocket ss = ctx.getServerSocketFactory().createServerSocket(0);

        // create the HostnameVerifier to check hostname verification
        TestHostnameVerifier hnv = new TestHostnameVerifier();
        HttpsURLConnection.setDefaultHostnameVerifier(hnv);

        // create url connection to be tested
        URL url = new URL("https://localhost:" + ss.getLocalPort());
        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
        connection.setSSLSocketFactory(ctx.getSocketFactory());

        // perform the interaction between the peers
        SSLSocket peerSocket = (SSLSocket) doInteraction(connection, ss);

        // check the connection state
        checkConnectionStateParameters(connection, peerSocket);

        // should silently exit
        connection.connect();
    }

    /**
     * Tests the behaviour of HTTPS connection in case of unavailability
     * of requested resource.
     */
    public void testHttpsConnection_Not_Found_Response() throws Throwable {
        // set up the properties defining the default values needed by SSL stuff
        setUpStoreProperties();

        // create the SSL server socket acting as a server
        SSLContext ctx = getContext();
        ServerSocket ss = ctx.getServerSocketFactory().createServerSocket(0);

        // create the HostnameVerifier to check hostname verification
        TestHostnameVerifier hnv = new TestHostnameVerifier();
        HttpsURLConnection.setDefaultHostnameVerifier(hnv);

        // create url connection to be tested
        URL url = new URL("https://localhost:" + ss.getLocalPort());
        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
        connection.setSSLSocketFactory(ctx.getSocketFactory());

        try {
            doInteraction(connection, ss, NOT_FOUND_CODE);
            fail("Expected exception was not thrown.");
        } catch (FileNotFoundException e) {
            if (DO_LOG) {
                System.out.println("Expected exception was thrown: " + e.getMessage());
                e.printStackTrace();
            }
        }

        // should silently exit
        connection.connect();
    }

    /**
     * Tests possibility to set up the default SSLSocketFactory
     * to be used by HttpsURLConnection.
     */
    public void testSetDefaultSSLSocketFactory() throws Throwable {
        // create the SSLServerSocket which will be used by server side
        SSLContext ctx = getContext();
        SSLServerSocket ss = (SSLServerSocket) ctx.getServerSocketFactory().createServerSocket(0);

        SSLSocketFactory socketFactory = (SSLSocketFactory) ctx.getSocketFactory();
        // set up the factory as default
        HttpsURLConnection.setDefaultSSLSocketFactory(socketFactory);
        // check the result
        assertSame("Default SSLSocketFactory differs from expected",
                   socketFactory, HttpsURLConnection.getDefaultSSLSocketFactory());

        // create the HostnameVerifier to check hostname verification
        TestHostnameVerifier hnv = new TestHostnameVerifier();
        HttpsURLConnection.setDefaultHostnameVerifier(hnv);

        // create HttpsURLConnection to be tested
        URL url = new URL("https://localhost:" + ss.getLocalPort());
        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();

        TestHostnameVerifier hnv_late = new TestHostnameVerifier();
        // late initialization: should not be used for created connection
        HttpsURLConnection.setDefaultHostnameVerifier(hnv_late);

        // perform the interaction between the peers
        SSLSocket peerSocket = (SSLSocket) doInteraction(connection, ss);
        // check the connection state
        checkConnectionStateParameters(connection, peerSocket);
        // check the verification process
        assertTrue("Hostname verification was not done", hnv.verified);
        assertFalse("Hostname verification should not be done by this verifier",
                    hnv_late.verified);
        // check the used SSLSocketFactory
        assertSame("Default SSLSocketFactory should be used",
                   HttpsURLConnection.getDefaultSSLSocketFactory(),
                   connection.getSSLSocketFactory());

        // should silently exit
        connection.connect();
    }

    /**
     * Tests possibility to set up the SSLSocketFactory
     * to be used by HttpsURLConnection.
     */
    public void testSetSSLSocketFactory() throws Throwable {
        // create the SSLServerSocket which will be used by server side
        SSLContext ctx = getContext();
        SSLServerSocket ss = (SSLServerSocket) ctx.getServerSocketFactory().createServerSocket(0);

        // create the HostnameVerifier to check hostname verification
        TestHostnameVerifier hnv = new TestHostnameVerifier();
        HttpsURLConnection.setDefaultHostnameVerifier(hnv);

        // create HttpsURLConnection to be tested
        URL url = new URL("https://localhost:" + ss.getLocalPort());
        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();

        SSLSocketFactory socketFactory = (SSLSocketFactory) ctx.getSocketFactory();
        connection.setSSLSocketFactory(socketFactory);

        TestHostnameVerifier hnv_late = new TestHostnameVerifier();
        // late initialization: should not be used for created connection
        HttpsURLConnection.setDefaultHostnameVerifier(hnv_late);

        // perform the interaction between the peers
        SSLSocket peerSocket = (SSLSocket) doInteraction(connection, ss);
        // check the connection state
        checkConnectionStateParameters(connection, peerSocket);
        // check the verification process
        assertTrue("Hostname verification was not done", hnv.verified);
        assertFalse("Hostname verification should not be done by this verifier",
                    hnv_late.verified);
        // check the used SSLSocketFactory
        assertNotSame("Default SSLSocketFactory should not be used",
                      HttpsURLConnection.getDefaultSSLSocketFactory(),
                      connection.getSSLSocketFactory());
        assertSame("Result differs from expected",
                   socketFactory, connection.getSSLSocketFactory());

        // should silently exit
        connection.connect();
    }

    /**
     * Tests the behaviour of HttpsURLConnection in case of retrieving
     * of the connection state parameters before connection has been made.
     */
    public void testUnconnectedStateParameters() throws Throwable {
        // create HttpsURLConnection to be tested
        URL url = new URL("https://localhost:55555");
        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();

        try {
            connection.getCipherSuite();
            fail("Expected IllegalStateException was not thrown");
        } catch (IllegalStateException e) {}
        try {
            connection.getPeerPrincipal();
            fail("Expected IllegalStateException was not thrown");
        } catch (IllegalStateException e) {}
        try {
            connection.getLocalPrincipal();
            fail("Expected IllegalStateException was not thrown");
        } catch (IllegalStateException e) {}

        try {
            connection.getServerCertificates();
            fail("Expected IllegalStateException was not thrown");
        } catch (IllegalStateException e) {}
        try {
            connection.getLocalCertificates();
            fail("Expected IllegalStateException was not thrown");
        } catch (IllegalStateException e) {}
    }

    /**
     * Tests if setHostnameVerifier() method replaces default verifier.
     */
    public void testSetHostnameVerifier() throws Throwable {
        // setting up the properties pointing to the key/trust stores
        setUpStoreProperties();

        // create the SSLServerSocket which will be used by server side
        SSLServerSocket ss = (SSLServerSocket)
                getContext().getServerSocketFactory().createServerSocket(0);

        // create the HostnameVerifier to check that Hostname verification
        // is done
        TestHostnameVerifier hnv = new TestHostnameVerifier();
        HttpsURLConnection.setDefaultHostnameVerifier(hnv);

        // create HttpsURLConnection to be tested
        URL url = new URL("https://localhost:" + ss.getLocalPort());
        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
        connection.setSSLSocketFactory(getContext().getSocketFactory());

        TestHostnameVerifier hnv_late = new TestHostnameVerifier();
        // replace default verifier
        connection.setHostnameVerifier(hnv_late);

        // perform the interaction between the peers and check the results
        SSLSocket peerSocket = (SSLSocket) doInteraction(connection, ss);
        assertTrue("Hostname verification was not done", hnv_late.verified);
        assertFalse("Hostname verification should not be done by this verifier",
                    hnv.verified);
        checkConnectionStateParameters(connection, peerSocket);

        // should silently exit
        connection.connect();
    }

    /**
     * Tests the behaviour in case of sending the data to the server.
     */
    public void test_doOutput() throws Throwable {
        // setting up the properties pointing to the key/trust stores
        setUpStoreProperties();

        // create the SSLServerSocket which will be used by server side
        SSLServerSocket ss = (SSLServerSocket)
                getContext().getServerSocketFactory().createServerSocket(0);

        // create the HostnameVerifier to check that Hostname verification
        // is done
        TestHostnameVerifier hnv = new TestHostnameVerifier();
        HttpsURLConnection.setDefaultHostnameVerifier(hnv);

        // create HttpsURLConnection to be tested
        URL url = new URL("https://localhost:" + ss.getLocalPort());
        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
        connection.setSSLSocketFactory(getContext().getSocketFactory());
        connection.setDoOutput(true);

        // perform the interaction between the peers and check the results
        SSLSocket peerSocket = (SSLSocket) doInteraction(connection, ss);
        checkConnectionStateParameters(connection, peerSocket);

        // should silently exit
        connection.connect();
    }

    /**
     * Tests HTTPS connection process made through the proxy server.
     */
    public void testProxyConnection() throws Throwable {
        // setting up the properties pointing to the key/trust stores
        setUpStoreProperties();

        // create the SSLServerSocket which will be used by server side
        ServerSocket ss = new ServerSocket(0);

        // create the HostnameVerifier to check that Hostname verification
        // is done
        TestHostnameVerifier hnv = new TestHostnameVerifier();
        HttpsURLConnection.setDefaultHostnameVerifier(hnv);

        // create HttpsURLConnection to be tested
        URL url = new URL("https://requested.host:55556/requested.data");
        HttpsURLConnection connection = (HttpsURLConnection)
                url.openConnection(new Proxy(Proxy.Type.HTTP,
                                             new InetSocketAddress("localhost",
                                                                   ss.getLocalPort())));
        connection.setSSLSocketFactory(getContext().getSocketFactory());

        // perform the interaction between the peers and check the results
        SSLSocket peerSocket = (SSLSocket) doInteraction(connection, ss);
        checkConnectionStateParameters(connection, peerSocket);

        // should silently exit
        connection.connect();
    }

    /**
     * Tests HTTPS connection process made through the proxy server.
     * Proxy server needs authentication.
     */
    public void testProxyAuthConnection() throws Throwable {
        // setting up the properties pointing to the key/trust stores
        setUpStoreProperties();

        // create the SSLServerSocket which will be used by server side
        ServerSocket ss = new ServerSocket(0);

        // create the HostnameVerifier to check that Hostname verification
        // is done
        TestHostnameVerifier hnv = new TestHostnameVerifier();
        HttpsURLConnection.setDefaultHostnameVerifier(hnv);

        Authenticator.setDefault(new Authenticator() {

            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication("user", "password"
                        .toCharArray());
            }
        });

        // create HttpsURLConnection to be tested
        URL url = new URL("https://requested.host:55555/requested.data");
        HttpsURLConnection connection = (HttpsURLConnection)
                url.openConnection(new Proxy(Proxy.Type.HTTP,
                                             new InetSocketAddress("localhost",
                                                                   ss.getLocalPort())));
        connection.setSSLSocketFactory(getContext().getSocketFactory());

        // perform the interaction between the peers and check the results
        SSLSocket peerSocket = (SSLSocket) doInteraction(connection, ss);
        checkConnectionStateParameters(connection, peerSocket);

        // should silently exit
        connection.connect();
    }

    /**
     * Tests HTTPS connection process made through the proxy server.
     * 2 HTTPS connections are opened for one URL. For the first time
     * the connection is opened through one proxy,
     * for the second time through another.
     */
    public void testConsequentProxyConnection() throws Throwable {
        // setting up the properties pointing to the key/trust stores
        setUpStoreProperties();

        // create the SSLServerSocket which will be used by server side
        ServerSocket ss = new ServerSocket(0);

        // create the HostnameVerifier to check that Hostname verification
        // is done
        TestHostnameVerifier hnv = new TestHostnameVerifier();
        HttpsURLConnection.setDefaultHostnameVerifier(hnv);

        // create HttpsURLConnection to be tested
        URL url = new URL("https://requested.host:55555/requested.data");
        HttpsURLConnection connection = (HttpsURLConnection)
                url.openConnection(new Proxy(Proxy.Type.HTTP,
                                             new InetSocketAddress("localhost",
                                                                   ss.getLocalPort())));
        connection.setSSLSocketFactory(getContext().getSocketFactory());

        // perform the interaction between the peers and check the results
        SSLSocket peerSocket = (SSLSocket) doInteraction(connection, ss);
        checkConnectionStateParameters(connection, peerSocket);

        // create another SSLServerSocket which will be used by server side
        ss = new ServerSocket(0);

        connection = (HttpsURLConnection) url.openConnection(new Proxy(
                Proxy.Type.HTTP, new InetSocketAddress("localhost", ss.getLocalPort())));
        connection.setSSLSocketFactory(getContext().getSocketFactory());

        // perform the interaction between the peers and check the results
        peerSocket = (SSLSocket) doInteraction(connection, ss);
        checkConnectionStateParameters(connection, peerSocket);
    }

    /**
     * Tests HTTPS connection process made through the proxy server.
     * Proxy server needs authentication.
     * Client sends data to the server.
     */
    public void testProxyAuthConnection_doOutput() throws Throwable {
        // setting up the properties pointing to the key/trust stores
        setUpStoreProperties();

        // create the SSLServerSocket which will be used by server side
        ServerSocket ss = new ServerSocket(0);

        // create the HostnameVerifier to check that Hostname verification
        // is done
        TestHostnameVerifier hnv = new TestHostnameVerifier();
        HttpsURLConnection.setDefaultHostnameVerifier(hnv);

        Authenticator.setDefault(new Authenticator() {

            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication("user", "password"
                        .toCharArray());
            }
        });

        // create HttpsURLConnection to be tested
        URL url = new URL("https://requested.host:55554/requested.data");
        HttpsURLConnection connection = (HttpsURLConnection)
                url.openConnection(new Proxy(Proxy.Type.HTTP,
                                             new InetSocketAddress("localhost",
                                                                   ss.getLocalPort())));
        connection.setSSLSocketFactory(getContext().getSocketFactory());
        connection.setDoOutput(true);

        // perform the interaction between the peers and check the results
        SSLSocket peerSocket = (SSLSocket) doInteraction(connection, ss, OK_CODE, true);
        checkConnectionStateParameters(connection, peerSocket);
    }

    /**
     * Tests HTTPS connection process made through the proxy server.
     * Proxy server needs authentication but client fails to authenticate
     * (Authenticator was not set up in the system).
     */
    public void testProxyAuthConnectionFailed() throws Throwable {
        // setting up the properties pointing to the key/trust stores
        setUpStoreProperties();

        // create the SSLServerSocket which will be used by server side
        ServerSocket ss = new ServerSocket(0);

        // create the HostnameVerifier to check that Hostname verification
        // is done
        TestHostnameVerifier hnv = new TestHostnameVerifier();
        HttpsURLConnection.setDefaultHostnameVerifier(hnv);

        // create HttpsURLConnection to be tested
        URL url = new URL("https://requested.host:55555/requested.data");
        HttpsURLConnection connection = (HttpsURLConnection)
                url.openConnection(new Proxy(Proxy.Type.HTTP,
                                             new InetSocketAddress("localhost",
                                                                   ss.getLocalPort())));
        connection.setSSLSocketFactory(getContext().getSocketFactory());

        // perform the interaction between the peers and check the results
        try {
            doInteraction(connection, ss, AUTHENTICATION_REQUIRED_CODE, true);
        } catch (IOException e) {
            // SSL Tunnelling failed
            if (DO_LOG) {
                System.out.println("Got expected IOException: "
                                   + e.getMessage());
            }
        }
    }

    /**
     * Tests the behaviour of HTTPS connection in case of unavailability
     * of requested resource.
     */
    public void testProxyConnection_Not_Found_Response() throws Throwable {
        // setting up the properties pointing to the key/trust stores
        setUpStoreProperties();

        // create the SSLServerSocket which will be used by server side
        ServerSocket ss = new ServerSocket(0);

        // create the HostnameVerifier to check that Hostname verification
        // is done
        TestHostnameVerifier hnv = new TestHostnameVerifier();
        HttpsURLConnection.setDefaultHostnameVerifier(hnv);

        // create HttpsURLConnection to be tested
        URL url = new URL("https://localhost:" + ss.getLocalPort());
        HttpsURLConnection connection = (HttpsURLConnection)
                url.openConnection(new Proxy(Proxy.Type.HTTP,
                                             new InetSocketAddress("localhost",
                                                                   ss.getLocalPort())));
        connection.setSSLSocketFactory(getContext().getSocketFactory());

        try {
            doInteraction(connection, ss, NOT_FOUND_CODE); // NOT FOUND
            fail("Expected exception was not thrown.");
        } catch (FileNotFoundException e) {
            if (DO_LOG) {
                System.out.println("Expected exception was thrown: "
                                   + e.getMessage());
            }
        }
    }

    /**
     * Log the name of the test case to be executed.
     */
    public void setUp() throws Exception {
        super.setUp();

        if (DO_LOG) {
            System.out.println();
            System.out.println("------------------------");
            System.out.println("------ " + getName());
            System.out.println("------------------------");
        }

        if (store != null) {
            String ksFileName = ("org/apache/harmony/luni/tests/key_store."
                                 + KeyStore.getDefaultType().toLowerCase());
            InputStream in = getClass().getClassLoader().getResourceAsStream(ksFileName);
            FileOutputStream out = new FileOutputStream(store);
            BufferedInputStream bufIn = new BufferedInputStream(in, 8192);
            while (bufIn.available() > 0) {
                byte[] buf = new byte[128];
                int read = bufIn.read(buf);
                out.write(buf, 0, read);
            }
            bufIn.close();
            out.close();
        } else {
            fail("couldn't set up key store");
        }
    }

    public void tearDown() {
        if (store != null) {
            store.delete();
        }
    }

    /**
     * Checks the HttpsURLConnection getter's values and compares
     * them with actual corresponding values of remote peer.
     */
    public static void checkConnectionStateParameters(
            HttpsURLConnection clientConnection, SSLSocket serverPeer)
            throws Exception {
        SSLSession session = serverPeer.getSession();

        assertEquals(session.getCipherSuite(), clientConnection.getCipherSuite());
        assertEquals(session.getLocalPrincipal(), clientConnection.getPeerPrincipal());
        assertEquals(session.getPeerPrincipal(), clientConnection.getLocalPrincipal());

        Certificate[] serverCertificates = clientConnection.getServerCertificates();
        Certificate[] localCertificates = session.getLocalCertificates();
        assertTrue("Server certificates differ from expected",
                   Arrays.equals(serverCertificates, localCertificates));

        localCertificates = clientConnection.getLocalCertificates();
        serverCertificates = session.getPeerCertificates();
        assertTrue("Local certificates differ from expected",
                   Arrays.equals(serverCertificates, localCertificates));
    }

    /**
     * Returns the file name of the key/trust store. The key store file
     * (named as "key_store." + extension equals to the default KeyStore
     * type installed in the system in lower case) is searched in classpath.
     * @throws junit.framework.AssertionFailedError if property was not set
     * or file does not exist.
     */
    private static String getKeyStoreFileName() {
        return store.getAbsolutePath();
    }

    /**
     * Builds and returns the context used for secure socket creation.
     */
    private static SSLContext getContext() throws Exception {
        String type = KeyStore.getDefaultType();
        String keyStore = getKeyStoreFileName();
        File keyStoreFile = new File(keyStore);
        FileInputStream fis = new FileInputStream(keyStoreFile);

        KeyStore ks = KeyStore.getInstance(type);
        ks.load(fis, KS_PASSWORD.toCharArray());
        fis.close();
        if (DO_LOG && false) {
            TestKeyStore.dump("HttpsURLConnection.getContext", ks, KS_PASSWORD.toCharArray());
        }

        String kmfAlgorithm = KeyManagerFactory.getDefaultAlgorithm();
        KeyManagerFactory kmf = KeyManagerFactory.getInstance(kmfAlgorithm);
        kmf.init(ks, KS_PASSWORD.toCharArray());
        KeyManager[] keyManagers = kmf.getKeyManagers();

        String tmfAlgorthm = TrustManagerFactory.getDefaultAlgorithm();
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorthm);
        tmf.init(ks);
        TrustManager[] trustManagers = tmf.getTrustManagers();
        if (DO_LOG) {
            trustManagers = TestTrustManager.wrap(trustManagers);
        }

        SSLContext ctx = SSLContext.getInstance("TLSv1");
        ctx.init(keyManagers, trustManagers, null);
        return ctx;
    }

    /**
     * Sets up the properties pointing to the key store and trust store
     * and used as default values by JSSE staff. This is needed to test
     * HTTPS behaviour in the case of default SSL Socket Factories.
     */
    private static void setUpStoreProperties() throws Exception {
        String type = KeyStore.getDefaultType();

        System.setProperty("javax.net.ssl.keyStoreType", type);
        System.setProperty("javax.net.ssl.keyStore", getKeyStoreFileName());
        System.setProperty("javax.net.ssl.keyStorePassword", KS_PASSWORD);

        System.setProperty("javax.net.ssl.trustStoreType", type);
        System.setProperty("javax.net.ssl.trustStore", getKeyStoreFileName());
        System.setProperty("javax.net.ssl.trustStorePassword", KS_PASSWORD);
    }

    /**
     * Performs interaction between client's HttpsURLConnection and
     * servers side (ServerSocket).
     */
    public static Socket doInteraction(final HttpsURLConnection clientConnection,
                                       final ServerSocket serverSocket)
            throws Throwable {
        return doInteraction(clientConnection, serverSocket, OK_CODE, false);
    }

    /**
     * Performs interaction between client's HttpsURLConnection and
     * servers side (ServerSocket). Server will response with specified
     * response code.
     */
    public static Socket doInteraction(final HttpsURLConnection clientConnection,
                                       final ServerSocket serverSocket,
                                       final int responseCode)
            throws Throwable {
        return doInteraction(clientConnection, serverSocket, responseCode, false);
    }

    /**
     * Performs interaction between client's HttpsURLConnection and
     * servers side (ServerSocket). Server will response with specified
     * response code.
     * @param doAuthentication specifies
     * if the server needs client authentication.
     */
    public static Socket doInteraction(final HttpsURLConnection clientConnection,
                                       final ServerSocket serverSocket,
                                       final int responseCode,
                                       final boolean doAuthentication)
            throws Throwable {
        // set up the connection
        clientConnection.setDoInput(true);
        clientConnection.setConnectTimeout(TIMEOUT);
        clientConnection.setReadTimeout(TIMEOUT);

        ServerWork server = new ServerWork(serverSocket, responseCode, doAuthentication);

        ClientConnectionWork client = new ClientConnectionWork(clientConnection);

        ExecutorService executorService = Executors.newFixedThreadPool(2);
        try {
            Future<Void> serverFuture = executorService.submit(server);
            Future<Void> clientFuture = executorService.submit(client);

            Throwable t = null;
            try {
                serverFuture.get(30, TimeUnit.SECONDS);
            } catch (ExecutionException e) {
                t = e.getCause();
            }
            try {
                clientFuture.get(30, TimeUnit.SECONDS);
            } catch (ExecutionException e) {
                // two problems? log the first before overwriting
                if (t != null) {
                    t.printStackTrace();
                }
                t = e.getCause();
            }
            if (t != null) {
                throw t;
            }
        } catch (ExecutionException e) {
            throw e.getCause();
        } finally {
            executorService.shutdown();
        }

        return server.peerSocket;
    }

    /**
     * The host name verifier used in test.
     */
    static class TestHostnameVerifier implements HostnameVerifier {

        boolean verified = false;

        public boolean verify(String hostname, SSLSession session) {
            if (DO_LOG) {
                System.out.println("***> verification " + hostname + " "
                                   + session.getPeerHost());
            }
            verified = true;
            return true;
        }
    }

    /**
     * The base class for mock Client and Server.
     */
    static class Work {

        /**
         * The header of OK HTTP response.
         */
        static final String responseHead = "HTTP/1.1 200 OK\r\n";

        /**
         * The response message to be sent to the proxy CONNECT request.
         */
        static final String proxyResponse = responseHead + "\r\n";

        /**
         * The content of the response to be sent during HTTPS session.
         */
        static final String httpsResponseContent
                = "<HTML>\n"
                + "<HEAD><TITLE>HTTPS Response Content</TITLE></HEAD>\n"
                + "</HTML>";

        /**
         * The tail of the response to be sent during HTTPS session.
         */
        static final String httpsResponseTail
                = "Content-type: text/html\r\n"
                + "Content-length: " + httpsResponseContent.length() + "\r\n"
                + "\r\n"
                + httpsResponseContent;

        /**
         * The response requiring client's proxy authentication.
         */
        static final String respAuthenticationRequired
                = "HTTP/1.0 407 Proxy authentication required\r\n"
                + "Proxy-authenticate: Basic realm=\"localhost\"\r\n"
                + "\r\n";

        /**
         * The data to be posted by client to the server.
         */
        static final String clientsData = "_.-^ Client's Data ^-._";

        /**
         * The print stream used for debug log.
         * If it is null debug info will not be printed.
         */
        private PrintStream out = System.out;

        /**
         * Prints log message.
         */
        public synchronized void log(String message) {
            if (DO_LOG && (out != null)) {
                out.println("[" + this + "]: " + message);
            }
        }
    }

    /**
     * The class used for server side works.
     */
    static class ServerWork extends Work implements Callable<Void> {

        // the server socket used for connection
        private final ServerSocket serverSocket;

        // indicates if the server acts as proxy server
        private final boolean actAsProxy;

        // indicates if the server needs proxy authentication
        private final boolean needProxyAuthentication;

        // response code to be send to the client peer
        private final int responseCode;

        // the socket connected with client peer
        private Socket peerSocket;

        /**
         * Creates the thread acting as a server side.
         * @param serverSocket the server socket to be used during connection
         * @param responseCode the response code to be sent to the client
         * @param needProxyAuthentication
         * indicates if the server needs proxy authentication
         */
        public ServerWork(ServerSocket serverSocket,
                          int responseCode,
                          boolean needProxyAuthentication) {
            this.serverSocket = serverSocket;
            this.responseCode = responseCode;
            this.needProxyAuthentication = needProxyAuthentication;
            // will act as a proxy server if the specified server socket
            // is not a secure server socket
            this.actAsProxy = !(serverSocket instanceof SSLServerSocket);
            if (!actAsProxy) {
                // demand client to send its certificate
                ((SSLServerSocket) serverSocket).setNeedClientAuth(true);
            }
        }

        /**
         * Closes the connection.
         */
        public void closeSocket(Socket socket) {
            if (socket == null) {
                return;
            }
            try {
                socket.getInputStream().close();
            } catch (IOException e) {}
            try {
                socket.getOutputStream().close();
            } catch (IOException e) {}
            try {
                socket.close();
            } catch (IOException e) {}
        }

        /**
         * Performs the actual server work.
         * If some exception occurs during the work it will be
         * stored in the <code>thrown</code> field.
         */
        public Void call() throws Exception {
            // the buffer used for reading the messages
            byte[] buff = new byte[2048];
            // the number of bytes read into the buffer
            try {
                // configure the server socket to avoid blocking
                serverSocket.setSoTimeout(TIMEOUT);
                // accept client connection
                peerSocket = serverSocket.accept();
                // configure the client connection to avoid blocking
                peerSocket.setSoTimeout(TIMEOUT);
                log("Client connection ACCEPTED");

                InputStream is = peerSocket.getInputStream();
                OutputStream os = peerSocket.getOutputStream();

                int num = is.read(buff);
                if (num == -1) {
                    log("Unexpected EOF");
                    return null;
                }

                String message = new String(buff, 0, num);
                log("Got request:\n" + message);
                log("------------------");

                if (!actAsProxy) {
                    // Act as Server (not Proxy) side
                    if (message.startsWith("POST")) {
                        // client connection sent some data
                        log("try to read client data");
                        String data = message.substring(message.indexOf("\r\n\r\n")+4);
                        log("client's data: '" + data + "'");
                        // check the received data
                        assertEquals(clientsData, data);
                    }
                } else {
                    if (needProxyAuthentication) {
                        // Do proxy work
                        log("Authentication required...");
                        // send Authentication Request
                        os.write(respAuthenticationRequired.getBytes());
                        // read request
                        num = is.read(buff);
                        if (num == -1) {
                            // this connection was closed,
                            // do clean up and create new one:
                            closeSocket(peerSocket);
                            peerSocket = serverSocket.accept();
                            peerSocket.setSoTimeout(TIMEOUT);
                            log("New client connection ACCEPTED");
                            is = peerSocket.getInputStream();
                            os = peerSocket.getOutputStream();
                            num = is.read(buff);
                        }
                        message = new String(buff, 0, num);
                        log("Got authenticated request:\n" + message);
                        log("------------------");
                        // check provided authorization credentials
                        assertTrue("no proxy-authorization credentials: " + message,
                                   message.toLowerCase().indexOf("proxy-authorization:") != -1);
                    }

                    assertTrue(message.startsWith("CONNECT"));
                    // request for SSL tunnel
                    log("Send proxy response");
                    os.write(proxyResponse.getBytes());

                    log("Perform SSL Handshake...");
                    // create sslSocket acting as a remote server peer
                    SSLSocket sslSocket = (SSLSocket)
                            getContext().getSocketFactory().createSocket(peerSocket,
                                                                         "localhost",
                                                                         peerSocket.getPort(),
                                                                         true); // do autoclose
                    sslSocket.setUseClientMode(false);
                    // demand client authentication
                    sslSocket.setNeedClientAuth(true);
                    sslSocket.startHandshake();
                    peerSocket = sslSocket;
                    is = peerSocket.getInputStream();
                    os = peerSocket.getOutputStream();

                    // read the HTTP request sent by secure connection
                    // (HTTPS request)
                    num = is.read(buff);
                    message = new String(buff, 0, num);
                    log("[Remote Server] Request from SSL tunnel:\n" + message);
                    log("------------------");

                    if (message.startsWith("POST")) {
                        // client connection sent some data
                        log("[Remote Server] try to read client data");
                        String data = message.substring(message.indexOf("\r\n\r\n")+4);
                        log("[Remote Server] client's data: '" + message + "'");
                        // check the received data
                        assertEquals(clientsData, data);
                    }

                    log("[Remote Server] Sending the response by SSL tunnel...");
                }

                // send the response with specified response code
                os.write(("HTTP/1.1 " + responseCode
                          + " Message\r\n" + httpsResponseTail).getBytes());
                os.flush();
                os.close();
                log("Work is DONE actAsProxy=" + actAsProxy);
                return null;
            } finally {
                closeSocket(peerSocket);
                try {
                    serverSocket.close();
                } catch (IOException e) {}
            }
        }

        @Override public String toString() {
            return actAsProxy ? "Proxy Server" : "Server";
        }
    }

    /**
     * The class used for client side work.
     */
    static class ClientConnectionWork extends Work implements Callable<Void> {

        // connection to be used to contact the server side
        private HttpsURLConnection connection;

        /**
         * Creates the thread acting as a client side.
         * @param connection connection to be used to contact the server side
         */
        public ClientConnectionWork(HttpsURLConnection connection) {
            this.connection = connection;
            log("Created over connection: " + connection.getClass());
        }

        /**
         * Performs the actual client work.
         * If some exception occurs during the work it will be
         * stored in the <code>thrown<code> field.
         */
        public Void call() throws Exception {
            log("Opening the connection to " + connection.getURL());
            connection.connect();
            log("Connection has been ESTABLISHED, using proxy: " + connection.usingProxy());
            if (connection.getDoOutput()) {
                log("Posting data");
                // connection configured to post data, do so
                connection.getOutputStream().write(clientsData.getBytes());
            }
            // read the content of HTTP(s) response
            InputStream is = connection.getInputStream();
            log("Input Stream obtained");
            byte[] buff = new byte[2048];
            int num = 0;
            int byt = 0;
            while ((num < buff.length) && ((byt = is.read()) != -1)) {
                buff[num++] = (byte) byt;
            }
            String message = new String(buff, 0, num);
            log("Got content:\n" + message);
            log("------------------");
            log("Response code: " + connection.getResponseCode());
            assertEquals(httpsResponseContent, message);
            return null;
        }

        @Override public String toString() {
            return "Client Connection";
        }
    }
}
