/*
 * @(#)Https.java
 *
 * Copyright (c) 2001-2002 JScape
 * 1147 S. 53rd Pl., Mesa, Arizona, 85206, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of
 * JScape. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 * the terms of the license agreement you entered into with JScape.
 */
package com.jscape.inet.https;

import com.jscape.inet.http.Http;
import com.jscape.inet.http.HttpException;
import com.jscape.inet.util.URLParser;
import com.jscape.util.IOTools;

import javax.net.ssl.*;
import java.io.*;
import java.net.Socket;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.Locale;
import java.util.Vector;


/**
 * Implements the basic functionality of a HTTPS client. This class extends the
 * Http class therefore providing all the same functionality but for  SSL
 * connections.  See the com.jscape.inet.http package for more information.
 * Use of this class requires JDK 1.4 or JDK 1.3.x and the JCE  (Java
 * Cryptography Extensions).  The JCE may be downloaded from
 * http://java.sun.com/products/archive/jce/1.2/
 * <p/>
 * <P>
 * When using JDK 1.3.x and JCE be sure to place the JCE jar files (jcert.jar,
 * jnet.jar and jsse.jar) in your JDK_HOME/lib/jre/ext directory.
 * </p>
 * <p/>
 * <P>
 * JDK 1.4 and greater Example Usage:
 * </p>
 * <p/>
 * <P>
 * <pre>
 * Https https = new Https();
 *
 * // build HTTP request
 * HttpRequest request = new HttpRequest("https://www.myserver.com");
 *
 * // get HTTP response
 * try {
 *   HttpResponse response = https.getResponse(request);
 *   System.out.println(response.getBody());
 * }
 * catch(Exception e) {
 *   System.out.println(e);
 * }
 * </pre>
 * </p>
 * <p/>
 * <P>
 * JDK 1.3.x with JCE Example Usage:
 * </p>
 * <p/>
 * <P>
 * <pre>
 * Https https = new Https();
 *
 * // register SSL provider
 * javax.security.Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider());
 *  // build HTTP request
 * HttpRequest request = new HttpRequest("https://www.myserver.com");
 *
 * // get HTTP response
 * try {
 *   HttpResponse response = https.getResponse(request);
 *   System.out.println(response.getBody());
 * }
 * catch(Exception e) {
 *   System.out.println(e);
 * }
 * </pre>
 * </p>
 */
public class Https
        extends Http {

    private SSLSocketFactory factory = null;
    private SSLSession session;

    protected static final String DEFAULT_PROTOCOL = "TLS";
    protected static final String DEFAULT_ALGORITHM = "SunX509";
    protected static final String DEFAULT_TYPE = "JKS";

    protected String protocol = DEFAULT_PROTOCOL;
    protected String algorithm = DEFAULT_ALGORITHM;
    private String serverFilename = null;
    private String serverPassword = null;
    private String serverStoretype = DEFAULT_TYPE;
    private String clientFilename = null;
    private String clientPassword = null;
    private String clientStoretype = DEFAULT_TYPE;
    protected KeyManager[] keyManagers = null;
    protected TrustManager[] trustManagers = getTrustAllManagers();
    private Vector listeners = new Vector();


    /**
     * Connects to Http server.
     *
     * @throws IOException if an I/O related error occurs
     */
    protected void connect()
            throws IOException {
        synchronized (this) {
            URLParser url = new URLParser(request.getURL());

            String serverHost = url.getHost();
            int serverPort = url.getPort();

            if (serverPort == -1) {
                serverPort = 443;
            }

            if (request.getProxyHost() != null) {
                System.setProperty("https.proxyHost", request.getProxyHost());
                System.setProperty("https.proxyPort", "" + request.getProxyPort());

                Socket tunnel =
                        new Socket(request.getProxyHost(), request.getProxyPort());
                OutputStream out = tunnel.getOutputStream();
                StringBuffer msg = new StringBuffer();
                msg.append("CONNECT " + serverHost + ":" + serverPort + " HTTP/1.0\r\n");

                String userAgent = request.getUserAgent() != null ? request.getUserAgent() : System.getProperty("http.agent");
                msg.append("User-Agent: " + (userAgent != null && !userAgent.isEmpty() ? userAgent : "Java") + "\r\n\r\n");

                byte[] b = null;
                try {
                    b = msg.toString().getBytes(Charset.forName("ASCII7"));
                } catch (Exception use) {
                    b = msg.toString().getBytes(StandardCharsets.US_ASCII);
                }
                out.write(b);
                out.flush();

                byte[] reply = new byte[200];
                int replyLen = 0;
                int newlinesSeen = 0;
                boolean headerDone = false; /* Done on first newline */
                InputStream in = tunnel.getInputStream();

                while (newlinesSeen < 2) {
                    int i = in.read();
                    if (i < 0) {
                        throw new IOException("Unexpected EOF from proxy");
                    }
                    if (i == '\n') {
                        headerDone = true;
                        ++newlinesSeen;
                    } else if (i != '\r') {
                        newlinesSeen = 0;
                        if (!headerDone && replyLen < reply.length) {
                            reply[replyLen++] = (byte) i;
                        }
                    }
                }

                String replyStr = null;
                try {
                    replyStr = new String(reply, 0, replyLen, "ASCII7");
                } catch (UnsupportedEncodingException use) {
                    replyStr = new String(reply, 0, replyLen);
                }

                if (replyStr.toLowerCase(Locale.ENGLISH).indexOf("200") == -1) {
                    throw new IOException("Unable to tunnel SSL thru proxy " + request.getProxyHost() + ":" + request.getProxyPort());
                }

                // check if factory is null
                if (factory == null) {
                    try {
                        factory = getContext().getSocketFactory();
                    } catch (Exception ez) {
                        throw new IOException("Error loading SSL context. " + ez.getMessage());
                    }

                }

                // create SSL socket
                socket =
                        (SSLSocket) factory.createSocket(tunnel, serverHost, serverPort, true);
            } else {

                // check if factory is null
                if (factory == null) {
                    try {
                        factory = getContext().getSocketFactory();
                    } catch (Exception e) {
                        throw new IOException("Error loading SSL context. " + e.getMessage());
                    }

                }

                // create SSL socket
                socket =
                        (SSLSocket) factory.createSocket(serverHost, serverPort);
            }

            for (int i = 0; i < listeners.size(); ++i) {
                HandshakeCompletedListener l =
                        (HandshakeCompletedListener) listeners.get(i);
                ((SSLSocket) socket).addHandshakeCompletedListener(l);
            }

            ((SSLSocket) socket).startHandshake();
            outputStream = socket.getOutputStream();
            inputStream = socket.getInputStream();
            session = ((SSLSocket) socket).getSession();
        }
    }

    /**
     * Gets information about SSL session
     *
     * @return a SSLSession
     *
     * @see javax.net.ssl.SSLSession
     */
    public SSLSession getSession() {
        return session;
    }

    /**
     * Adds an event listener for events implemented by
     * HandShakeCompletedListener interface.
     *
     * @param listener a <CODE>HandShakeCompletedListener</CODE>
     */
    @SuppressWarnings("unchecked")
    public void addHandshakeCompletedListener(
            HandshakeCompletedListener listener) {
        synchronized (this) {
            listeners.addElement(listener);
        }
    }

    /**
     * Removes instance of HandshakeCompletedListener
     *
     * @param listener the HandshakeCompletedListener
     *
     * @see javax.net.ssl.HandshakeCompletedListener
     */
    public void removeHandshakeCompletedListener(
            HandshakeCompletedListener listener) {
        listeners.removeElement(listener);
    }

    /**
     * Sets SSLSocketFactory to be used in establishing a SSL connection
     *
     * @param factory a <CODE>SSLSocketFactory</CODE>
     *
     * @see javax.net.ssl.SSLSocketFactory
     */
    public void setSSLSocketFactory(SSLSocketFactory factory) {
        this.factory = factory;
    }

    /**
     * Gets SSLSocketFactory used in establishing an SSL (Secure Sockets Layer)
     * connection
     *
     * @return a <CODE>SSLSocketFactory</CODE>
     *
     * @see javax.net.ssl.SSLSocketFactory
     */
    public SSLSocketFactory getSSLSocketFactory() {
        return factory;
    }

    /**
     * Sets optional server certificate to be used during authentication.
     * Default expected keystore format is JKS format. Existing certificate may be imported into keystore using
     * the <code>keytool</code> utility provided in the Sun JDK.
     * <p/>
     * Example:
     * <p/>
     * <pre>
     * keytool.exe -import -file x509.cer -keystore your_store_name
     * </pre>
     * <p/>
     * The example above imports the certificate x509.cer into a keystore named your_store_name
     * <p/>
     *
     * @param filename the keystore file containing the server certificate
     * @param password the keystore password
     *
     * @throws HttpException if certificates cannot be loaded
     */
    public void setServerCertificates(String filename, String password)
            throws HttpException {
        setServerCertificates(filename, password, DEFAULT_TYPE);
    }

    /**
     * Sets optional server certificate to be used during authentication.
     * Default expected keystore format is JKS format. Existing certificate may be imported into keystore using
     * the <code>keytool</code> utility provided in the Sun JDK.
     * <p/>
     * Example:
     * <p/>
     * <pre>
     * keytool.exe -import -file x509.cer -keystore your_store_name
     * </pre>
     * <p/>
     * The example above imports the certificate x509.cer into a keystore named your_store_name
     * <p/>
     *
     * @param filename  the keystore file containing the server certificate
     * @param password  the keystore password
     * @param storetype the keystore type valid values include jks and pkcs12
     *
     * @throws HttpException if certificates cannot be loaded
     */
    public void setServerCertificates(String filename, String password, String storetype)
            throws HttpException {
        this.serverFilename = filename;
        this.serverPassword = password;
        this.serverStoretype = storetype != null ? storetype : DEFAULT_TYPE;
        InputStream in = null;

        try {
            KeyStore trustStore = KeyStore.getInstance(this.serverStoretype);
            in = new FileInputStream(this.serverFilename);
            trustStore.load(in, this.serverPassword.toCharArray());
            TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            factory.init(trustStore);
            this.trustManagers = factory.getTrustManagers();
        } catch (Exception e) {
            throw new HttpException("Error loading server certificate from keystore.", e);
        } finally {
            IOTools.close(in);
        }
    }

    /**
     * Sets optional client certificate to be used during authentication.
     * Default expected keystore format is JKS format. Existing certificate may be imported into keystore using
     * the <code>keytool</code> utility provided in the Sun JDK.
     * <p/>
     * Example:
     * <p/>
     * <pre>
     * keytool.exe -import -file x509.cer -keystore your_store_name
     * </pre>
     * <p/>
     * The example above imports the certificate x509.cer into a keystore named your_store_name
     * <p/>
     *
     * @param filename the keystore file containing the client certificate
     * @param password the keystore password
     *
     * @throws HttpException if certificates cannot be loaded
     */
    public void setClientCertificates(String filename, String password)
            throws HttpException {
        setClientCertificates(filename, password, DEFAULT_TYPE);
    }

    /**
     * Sets optional client certificate to be used during authentication.
     * Default expected keystore format is JKS format. Existing certificate may be imported into keystore using
     * the <code>keytool</code> utility provided in the Sun JDK.
     * <p/>
     * Example:
     * <p/>
     * <pre>
     * keytool.exe -import -file x509.cer -keystore your_store_name
     * </pre>
     * <p/>
     * The example above imports the certificate x509.cer into a keystore named your_store_name
     * <p/>
     *
     * @param filename  the keystore file containing the client certificate
     * @param password  the keystore password
     * @param storetype the keystore type valid values include jks and pkcs12
     *
     * @throws HttpException if certificate cannot be loaded
     */
    public void setClientCertificates(String filename, String password, String storetype)
            throws HttpException {
        this.clientFilename = filename;
        this.clientPassword = password;
        this.clientStoretype = storetype != null ? storetype : DEFAULT_TYPE;
        InputStream in = null;

        try {
            KeyStore keyStore = KeyStore.getInstance(this.clientStoretype);
            in = new FileInputStream(this.clientFilename);
            keyStore.load(in, this.clientPassword.toCharArray());
            KeyManagerFactory factory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
            factory.init(keyStore, this.clientPassword.toCharArray());
            this.keyManagers = factory.getKeyManagers();
        } catch (Exception e) {
            throw new HttpException("Error loading client certificate from keystore.", e);
        } finally {
            IOTools.close(in);
        }
    }

    /**
     * Gets trust all managers
     *
     * @return a TrustManager[]
     */
    protected static TrustManager[] getTrustAllManagers() {
        return new TrustManager[]{
                new X509TrustManager() {

                    /** @see X509TrustManager#getAcceptedIssuers() */
                    public X509Certificate[] getAcceptedIssuers() {
                        return new X509Certificate[0];
                    }

                    /** @see X509TrustManager#isClientTrusted(java.security.cert.X509Certificate[]) */
                    public boolean isClientTrusted(X509Certificate[] certs) {
                        return true;
                    }

                    /** @see X509TrustManager#isServerTrusted(java.security.cert.X509Certificate[]) */
                    public boolean isServerTrusted(X509Certificate[] certs) {
                        return true;
                    }

                    public void checkClientTrusted(
                            X509Certificate[] certs,
                            String val) {

                    }

                    public void checkServerTrusted(
                            X509Certificate[] certs,
                            String val) {

                    }
                }
        };
    }

    /**
     * Gets an SSLContext
     *
     * @return an SSLContext
     *
     * @throws Exception if an error occurs
     */
    private SSLContext getContext()
            throws Exception {
        SSLContext context = SSLContext.getInstance(this.protocol);
        context.init(this.keyManagers, this.trustManagers, new SecureRandom());
        return context;
    }

}
