/*
 * Copyright 2010 Patrick Woodworth
 *
 * Licensed 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.emergent.android.weave.client;

import org.apache.http.conn.scheme.LayeredSocketFactory;
import org.apache.http.conn.scheme.SocketFactory;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

/**
 * This socket factory will create ssl socket that uses configurable validation of
 * certificates (e.g. allowing self-signed).
 */
class WeaveSSLSocketFactory implements SocketFactory, LayeredSocketFactory {

  private static final boolean DISABLE_SERVER_CERT_CHECK = true; // todo look into this

  private static final boolean ENUMERATE_TRUSTED_CAS = false;

  private SSLContext m_sslcontext = null;

  private synchronized SSLContext getSSLContext() throws IOException {
    if (m_sslcontext == null) {
      m_sslcontext = createEasySSLContext();
    }
    return m_sslcontext;
  }

  /**
   * @see SocketFactory#connectSocket(Socket, String, int, InetAddress, int, HttpParams)
   */
  public Socket connectSocket(Socket sock,
                              String host,
                              int port,
                              InetAddress localAddress,
                              int localPort,
                              HttpParams params) throws IOException {

    int connTimeout = HttpConnectionParams.getConnectionTimeout(params);
    int soTimeout = HttpConnectionParams.getSoTimeout(params);

    InetSocketAddress remoteAddress = new InetSocketAddress(host, port);
    SSLSocket sslsock = (SSLSocket)((sock != null) ? sock : createSocket());

    if ((localAddress != null) || (localPort > 0)) {
      if (localPort < 0) {
        localPort = 0;
      }
      InetSocketAddress isa = new InetSocketAddress(localAddress, localPort);
      sslsock.bind(isa);
    }

    sslsock.connect(remoteAddress, connTimeout);
    sslsock.setSoTimeout(soTimeout);
    return sslsock;

  }

  /**
   * @see SocketFactory#createSocket()
   */
  public Socket createSocket() throws IOException {
    return getSSLContext().getSocketFactory().createSocket();
  }

  /**
   * @see SocketFactory#isSecure(Socket)
   */
  public boolean isSecure(Socket socket) throws IllegalArgumentException {
    return true;
  }

  /**
   * @see LayeredSocketFactory#createSocket(Socket, String, int, boolean)
   */
  public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException {
    return getSSLContext().getSocketFactory().createSocket(socket, host, port, autoClose);
  }

  public boolean equals(Object obj) {
    return ((obj != null) && obj.getClass().equals(WeaveSSLSocketFactory.class));
  }

  public int hashCode() {
    return WeaveSSLSocketFactory.class.hashCode();
  }

  private static SSLContext createEasySSLContext() throws IOException {
    try {
      SSLContext context = SSLContext.getInstance("TLS");
      context.init(null, new TrustManager[]{new WeaveX509TrustManager(null)}, null);
      return context;
    } catch (Exception e) {
      throw new IOException(e.getMessage());
    }
  }

  private static class WeaveX509TrustManager implements X509TrustManager {

    private X509TrustManager m_standardTrustManager = null;

    private static boolean sm_issued = false;

    public WeaveX509TrustManager(KeyStore keystore) throws NoSuchAlgorithmException, KeyStoreException {
      super();
      TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
      factory.init(keystore);
      TrustManager[] trustmanagers = factory.getTrustManagers();
      if (trustmanagers.length == 0) {
        throw new NoSuchAlgorithmException("no trust manager found");
      }
      m_standardTrustManager = (X509TrustManager)trustmanagers[0];
    }

    /**
     * @see X509TrustManager#checkClientTrusted(X509Certificate[],String)
     */
    public void checkClientTrusted(X509Certificate[] certificates, String authType) throws CertificateException {
      m_standardTrustManager.checkClientTrusted(certificates, authType);
    }

    /**
     * @see X509TrustManager#checkServerTrusted(X509Certificate[],String)
     */
    public void checkServerTrusted(X509Certificate[] certificates, String authType) throws CertificateException {
      if (ENUMERATE_TRUSTED_CAS && !sm_issued) {
        Dbg.d("CA certs:");
        X509Certificate[] cas = getAcceptedIssuers();
        for (X509Certificate ca : cas) {
          Dbg.d("  " + ca.getSubjectDN());
        }
        sm_issued = true;
      }

      if (DISABLE_SERVER_CERT_CHECK)
        return;

      if ((certificates != null) && (certificates.length == 1)) {
        // self-signed check
        certificates[0].checkValidity();
      } else {
        // normal check
        m_standardTrustManager.checkServerTrusted(certificates, authType);
      }
    }

    /**
     * @see X509TrustManager#getAcceptedIssuers()
     */
    public X509Certificate[] getAcceptedIssuers() {
      return this.m_standardTrustManager.getAcceptedIssuers();
    }
  }
}

