/*
 * Project: e-Dec Example Code
 * (C) 2017 Bundesamt fuer Informatik und Telekommunikation, CH
 */
package ch.admin.bit.edec.examples.ws;

import ch.admin.bit.edec.examples.PropertyProvider;
import ch.admin.bit.edec.examples.fileHandling.FileHandler;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
import org.bouncycastle.tls.DefaultTlsClient;
import org.bouncycastle.tls.TlsClient;
import org.bouncycastle.tls.TlsClientProtocol;
import org.bouncycastle.tls.crypto.TlsCrypto;
import org.bouncycastle.tls.crypto.impl.bc.BcTlsCrypto;

import javax.net.ssl.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.Socket;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.*;

//import javax.net.ssl.*;

/**
 * This class provides functionality to execute a HTTPS request with certificate authentication.
 */
public class HttpsRequest extends AbstractRequest {
  private static final Log log = LogFactory.getLog(HttpsRequest.class);

  private static final String HEADER_NAME_SSL_HEADER_ADDITION = "SSL_CLIENT_CERT_S_DN_CN";
  private static final String HEADER_NAME_HOST = "Host";
  private static final String HEADER_NAME_CONTENT_TYPE = "Content-Type";
  private static final String HEADER_NAME_SOAP_ACTION = "SOAPAction";
  //private static final String[] CYPHER_SUITE = new String[] { "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" };
  private static final String[] CYPHER_SUITE = new String[] { "TLS_DHE_RSA_WITH_AES_128_CBC_SHA" };

  // Supported ssl versions
  private static final String[] TLS_VERSIONS = new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" };

  private static final String CONTENT_TYPE = "text/xml;charset=UTF-8";

  /**
   * Executes the request. All information needed are provided in the connection parameters.
   *
   * @param connParams The parameters needed for making the request.
   * @return The response as text.
   * @throws IOException Thrown if anything out of the ordinary happened.
   */
  public static String doRequest(SslConnectionParams connParams) throws IOException {
    StringBuilder responseStringBuilder = new StringBuilder();
    BufferedReader reader = null;
    CloseableHttpClient httpClient = null;

    // Add the security provider
    if (PropertyProvider.getProperty(PropertyProvider.PROP_USE_BOUNCYCASTLE).equalsIgnoreCase("true")) {
      setProvider();
    }

    try {
      // prepare input streams for trust- and keystore
      InputStream truststoreInput = Thread.currentThread().getContextClassLoader()
          .getResourceAsStream(connParams.getTruststorePath());
      InputStream keystoreInput = Thread.currentThread().getContextClassLoader()
          .getResourceAsStream(connParams.getKeystorePath());

      // load truststore
      KeyStore trustStore = FileHandler.loadStore(truststoreInput, connParams.getTruststorePassword());
      // load keystore
      KeyStore keyStore = FileHandler.loadStore(keystoreInput, connParams.getKeystorePassword());

      // create SSL context
      SSLContextBuilder contextBuilder = SSLContexts.custom();
      contextBuilder.loadTrustMaterial(trustStore, new TrustSelfSignedStrategy());
      contextBuilder.loadKeyMaterial(keyStore, connParams.getIdentityPassword().toCharArray());
      SSLContext sslcontext = contextBuilder.build();
      log.info("Using Provider: " + sslcontext.getProvider().getName());

      // Accept all certificates. Not a good idea for production!
      HostnameVerifier hnv = new HostnameVerifier() {
        public boolean verify(String string, SSLSession ssls) {
          return true;
        }
      };

      // create connection factory
      /*SSLConnectionSocketFactory sslConnectionFactory = new SSLConnectionSocketFactory(sslcontext, TLS_VERSIONS,
          CYPHER_SUITE, hnv);*/
      SSLConnectionSocketFactory sslConnectionFactory = new SSLConnectionSocketFactory(sslcontext, TLS_VERSIONS,
          null, hnv);

      // create registry
      Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
          .register("https", sslConnectionFactory).build();

      // create HttpClient
      HttpClientBuilder builder = HttpClientBuilder.create();
      builder.setSSLSocketFactory(sslConnectionFactory);
      //builder.setConnectionManager(new BasicHttpClientConnectionManager(socketFactoryRegistry));
      httpClient = builder.build();

      // create post and set additional header values
      HttpPost httpPost = new HttpPost(connParams.getUrl().toURI());
      httpPost.setHeader(HEADER_NAME_HOST, connParams.getHost());
      if (!"".equals(connParams.getSslHeaderAddition())) {
        httpPost.setHeader(HEADER_NAME_SSL_HEADER_ADDITION, connParams.getSslHeaderAddition());
      }
      httpPost.setHeader(HEADER_NAME_CONTENT_TYPE, CONTENT_TYPE);
      //httpPost.setHeader("Content-Length", String.valueOf(connParams.getData().length()));
      httpPost.setHeader(HEADER_NAME_SOAP_ACTION, "\"" + connParams.getSoapAction() + "\"");
      httpPost.setEntity(new StringEntity(connParams.getRequest(),
          ContentType.create("text/xml", StandardCharsets.UTF_8.name())));

      // execute request
      CloseableHttpResponse response = httpClient.execute(httpPost);
      log.info("Server response: " + response.getStatusLine());
      try {

        HttpEntity entity = response.getEntity();

        // read response
        InputStream input = entity.getContent();
        reader = new BufferedReader(new InputStreamReader(input));
        responseStringBuilder = createResponse(reader);
        EntityUtils.consume(entity);
      }
      finally {
        response.close();
      }
    }
    catch (URISyntaxException e) {
      log.error(e.getMessage());
    }
    catch (NoSuchAlgorithmException e) {
      log.error(e.getMessage());
    }
    catch (KeyStoreException e) {
      log.error(e.getMessage());
    }
    catch (KeyManagementException e) {
      log.error(e.getMessage());
    }
    catch (Throwable e) {
      log.error(e.getMessage());
    }
    finally {
      httpClient.close();

      if (PropertyProvider.getProperty(PropertyProvider.PROP_USE_BOUNCYCASTLE).equalsIgnoreCase("true")) {
        // remove provider again. This is important if used together with other providers (e.g. IAIK)
        removeProvider();
      }
    }

    return responseStringBuilder.toString();
  }

  /*
      ---------------------------------
     / BouncyCastle Provider Handling /
     --------------------------------
  */

  private static final String DEFAULT_PROVIDER_NAME = BouncyCastleProvider.PROVIDER_NAME;
  private static final String DEFAULT_JSSE_PROVIDER_NAME = BouncyCastleJsseProvider.PROVIDER_NAME;

  static private void setProvider() {
    try {
      Security.removeProvider(DEFAULT_PROVIDER_NAME);
      Security.insertProviderAt(new BouncyCastleProvider(), 1);
      log.info("Added BouncyCastleProvider.");

      Security.removeProvider(DEFAULT_JSSE_PROVIDER_NAME);
      Security.insertProviderAt(new BouncyCastleJsseProvider(), 2);
      log.info("Added BouncyCastleJsseProvider.");
    }
    catch (Exception ex) {
      log.error("Couldn't register BC provider!");
    }
    printProviders();
  }

  static private void removeProvider() {
    if (Security.getProvider(DEFAULT_PROVIDER_NAME) != null) {
      Security.removeProvider(DEFAULT_PROVIDER_NAME);
      log.info("Removed BouncyCastleProvider.");
    }

    if (Security.getProvider(DEFAULT_JSSE_PROVIDER_NAME) != null) {
      Security.removeProvider(DEFAULT_JSSE_PROVIDER_NAME);
      log.info("Removed BouncyCastleJsseProvider.");
    }
  }

  static private void printProviders() {
    log.info("-----------------------------------------------");
    log.info("Registered Providers");
    for (Provider p : Security.getProviders()) {
      log.info("  - " + p.getName());
    }
    log.info("-----------------------------------------------");
  }

  static TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
    public java.security.cert.X509Certificate[] getAcceptedIssuers() {
      return null;
    }

    public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {
    }

    public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {
    }
  } };

}
