/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2006-2008 Sun Microsystems, Inc.
 */
package org.opends.server.protocols.jmx;

import static org.opends.server.loggers.debug.DebugLogger.*;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.types.DebugLogLevel;

import java.io.IOException;

import java.io.Serializable;
import java.net.Socket;
import java.rmi.server.RMIClientSocketFactory;

import java.util.Map;

// JSSE
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;

import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;

/**
 * A <code>DirectoryRMIClientSocketFactory</code> instance is used by the
 * RMI runtime in order to obtain client sockets for RMI calls via SSL.
 * <p>
 * This class implements <code>RMIClientSocketFactory</code> over the
 * Secure Sockets Layer (SSL) or Transport Layer Security (TLS) protocols.
 * </p>
 */
public class DirectoryRMIClientSocketFactory implements
    RMIClientSocketFactory, Serializable
{
  /**
   * The tracer object for the debug logger.
   */
  private static final DebugTracer TRACER = getTracer();



  /**
   * The serial version identifier required to satisfy the compiler because
   * this class implements the <CODE>java.io.Serializable</CODE> interface.
   * This value was generated using the <CODE>serialver</CODE> command-line
   * utility included with the Java SDK.
   */
  private static final long serialVersionUID = -6701450600497520362L;

  /**
   * the static thread-local connection environment used by the RMI
   * client to configure this factory.
   */
  private static InheritableThreadLocal<Map> tlMapConnectionEnv =
    new InheritableThreadLocal<Map>();

  /**
   * The static thread-local target server hostname used by the RMI
   * client to configure this factory.
   */
  private static InheritableThreadLocal<String> tlStrServerHostname =
    new InheritableThreadLocal<String>();

  /**
   * the connection mode. <code>true</code> indicates that the client
   * will be authenticated by its certificate (SSL protocol).
   * <code>false</code> indicates that we have to perform an lDAP
   * authentication
   */
  private final boolean needClientCertificate;

  /**
   * the ssl socket factory (do not serialize).
   */
  private transient SSLSocketFactory sslSocketFactory = null;

  /**
   * the host to connect to (do not serialize).
   */
  private transient String serverHostname = null;

  /**
   * Constructs a new <code>DirectoryRMIClientSocketFactory</code>.
   *
   * @param wellknown
   *            <code>true</code> for wellknown, <code>false</code>
   *            otherwise
   */
  public DirectoryRMIClientSocketFactory(boolean wellknown)
  {
    this.needClientCertificate = wellknown;

    // We don't force the initialization of the SSLSocketFactory
    // at construction time - because the RMI client socket factory is
    // created on the server side, where that initialization is a
    // priori
    // meaningless, unless both server and client run in the same JVM.
    // So contrarily to what we do for the server side, the
    // initialization
    // of the SSLSocketFactory will be delayed until the first time
    // createSocket() is called.
  }

  /**
   * Sets the thread-local connection environment.
   *
   * @param connectionEnv the new connection env
   */
  public static void setConnectionEnv(Map connectionEnv)
  {
    tlMapConnectionEnv.set(connectionEnv);
  }

  /**
   * Returns the thread-local connection environment.
   *
   * @return Map the connection environment used by new connections
   */
  public static Map getConnectionEnv()
  {
    return tlMapConnectionEnv.get();
  }

  /**
   * Sets the thread-local target server hostname.
   *
   * @param serverHostname
   *            the target server hostname
   */
  public static void setServerHostname(String serverHostname)
  {
    tlStrServerHostname.set(serverHostname);
  }

  /**
   * Returns the thread-local target server hostname.
   *
   * @return String the target server hostname
   */
  public static String getServerHostname()
  {
    return tlStrServerHostname.get();
  }

  /**
   * Returns the connection mode as configured at construction time.
   *
   * @return boolean <code>true</code> for wellknown,
   *         <code>false</code> otherwise
   */
  public boolean getNeedClientCertificate()
  {
    return needClientCertificate;
  }

  /**
   * Creates an SSL socket configured with the right trust stores and the
   * right target host.
   * <p>
   * The keystore and truststore used come from client configuration and
   * depend on the connection mode specified at construction time.
   * <p>
   * The target host is the host specified except if a different host was
   * specified in the connection environment.
   *
   * @param host
   *            the target host as known by the RMI stack
   * @param port
   *            the target port number
   * @return an SSL socket
   *
   * @throws IOException
   *             if an error occurs while configuring the socket
   */
  public Socket createSocket(String host, int port) throws IOException
  {
    //
    // gets ssl socket factory
    SSLSocketFactory sslSocketFactory = getSSLSocketFactory();
    String realhost = getRealServerHostname(host);

    final SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(
        realhost,
        port);

    return sslSocket;
  }

  /**
   * Indicates whether some other object is "equal to" this one.
   * <p>
   * Because each RMI client might have its own configuration, we do not
   * try to optimize anything. Each RMI connector uses its own RMI client
   * socket factory. In other words, Directory RMI clients never share
   * the same client socket factory.
   * </p>
   *
   * @param obj
   *            the reference object with which to compare
   * @return <code>true</code> if this object is the same as the obj
   *         argument <code>false</code> otherwise
   */
  public boolean equals(Object obj)
  {
    return super.equals(obj);
  }

  /**
   * Returns a hash code value for this
   * <code>DirectoryRMIClientSocketFactory</code>.
   *
   * @return a hash code value for this
   *         <code>DirectoryRMIClientSocketFactory</code>
   */
  public int hashCode()
  {
    return super.hashCode();
  }

  /**
   * Returns the real server hostname to connect to.
   *
   * @param rmiHostname
   *            the target hostname as known by RMI stack
   * @return String the real hostname to connect to
   * @throws IOException
   *             if an error occurs
   */
  private synchronized String getRealServerHostname(String rmiHostname)
      throws IOException
  {
    if (serverHostname == null)
    {
      //
      // does the client specify another host by
      // using thread-local static parameter ?
      serverHostname = getServerHostname();

      //
      // if not found here, don't look for real host in static
      // anymore
      if (serverHostname == null)
      {
        serverHostname = "";
      }
    }

    if (serverHostname.length() > 0)
    {
      return serverHostname;
    }
    else
    {
      return rmiHostname;
    }
  }

  /**
   * Returns the ssl socket factory used by this RMI client socket
   * factory.
   *
   * @return SSLSocketFactory the ssl socket factory
   * @throws IOException
   *             if an error occurs
   */
  private synchronized SSLSocketFactory getSSLSocketFactory()
      throws IOException
  {
    if (sslSocketFactory == null)
    {
      if (debugEnabled())
      {
        TRACER.debugVerbose("sslSocketFactory is null, get a new one");
      }

      // socket factory not yet initialized
      // initialize the trust
      Map connectionEnv = getConnectionEnv();
      TrustManager[] tms = null;

      //
      // Check if a trust manager array was provided in the
      // connection
      // Env. If yes, use it for this SSL Connection
      if ((connectionEnv != null)
          && (connectionEnv
          .containsKey(JmxConnectionHandler.TRUST_MANAGER_ARRAY_KEY)))
      {
        try
        {
          tms = (TrustManager[]) connectionEnv
              .get(JmxConnectionHandler.TRUST_MANAGER_ARRAY_KEY);
        }
        catch (Exception e)
        {
          if (debugEnabled())
          {
            TRACER.debugCaught(DebugLogLevel.ERROR, e);
          }
          tms = null;
        }

        if (tms == null)
        {
          //
          // Why? The value is not invalid ?
          // Too bad: we raised an exception
          throw new IOException("invalid type or null value for key ["
              + JmxConnectionHandler.TRUST_MANAGER_ARRAY_KEY
              + "] in connection environment : "
              + connectionEnv
              .get(JmxConnectionHandler.TRUST_MANAGER_ARRAY_KEY));
        }
      }

      // now we have the array of trust Manager
      // we can initialize the ssl ctx
      SSLContext ctx = null;
      try
      {
        ctx = SSLContext.getInstance("TLSv1");
        ctx.init(null, tms, null);
      }
      catch (Exception e)
      {
        if (debugEnabled())
        {
          TRACER.debugCaught(DebugLogLevel.ERROR, e);
        }
        throw new IOException("Unable to initialize SSL context : "
            + e.getMessage());
      }

      // create the SSLSocket
      sslSocketFactory = ctx.getSocketFactory();
    }
    return sslSocketFactory;
  }
}
