//______________________________________________________________________________
//   |       |
//   | knipp |           Knipp Medien und Kommunikation GmbH
//    -------
//
//
//     Knipp RRI Toolkit
//
//
//     Copyright (C) 2005 by Knipp Medien und Kommunikation GmbH
//
//     This library is free software; you can redistribute it and/or
//     modify it under the terms of the GNU Lesser General Public License 
//     as published by the Free Software Foundation; either version 2.1 
//     of the License, or any later version.
// 
//     This library is distributed in the hope that it will be useful, 
//     but WITHOUT ANY WARRANTY; without even the implied warranty of
//     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 
//     GNU Lesser General Public License for more details.
//
//     You should have received a copy of the GNU Lesser General
//     Public License along with this library; if not, write to the 
//     Free Software Foundation, Inc., 59 Temple Place, Suite 330, 
//     Boston, MA 02111-1307 USA 
//
//
//     initial author:    Klaus Malorny
//
//     version:           1.3 (15112), 2011/05/27 16:00:14
//                        $Revision: 14936 $ 
//                        $Date: 2010-11-05 15:57:40 +0100 (Fri, 05 Nov 2010) $
//                        $Author: Klaus.Malorny $
//

package de.knipp.rritoolkit.tcp;

import java.io.IOException;

import java.util.ArrayList;
import java.util.List;

import java.security.KeyStore;
import java.security.GeneralSecurityException;

import javax.naming.InvalidNameException;

import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.SSLSession;

import javax.security.auth.x500.X500Principal;

import de.knipp.rritoolkit.ChannelFactory;
import de.knipp.rritoolkit.RegistryChannel;


/**
 *  class that implements the {@code ChannelFactory} interface for
 *  SSL/TLS secured TCP connections
 *  
 *  @version  $Revision: 14936 $
 *  @author   Klaus Malorny
 */

public class TcpChannelFactory implements ChannelFactory
{
  /** the parameters */
  
  private TcpParams params;
  
  
  /** the socket factory */
  
  private SSLSocketFactory socketFactory;
  
  /**
   *  constructor
   *
   *  @param theParams            the parameters
   */
  
  public TcpChannelFactory (TcpParams theParams)
  {
    params = theParams;
  }
  
  
  
  /**
   *  create a new channel. If a implementation or configuration specific
   *  limit of number of channels exists and this limit has been reached,
   *  {@code null} is returned. If a channel could not be created for
   *  a different reason, an {@code IOException} is thrown. Channels that
   *  are no longer used by the caller should be closed
   *
   *  @return                    the channel or {@code null} if no more
   *                             channels are available
   *  @throws IOException        if the channel could not be created
   */

  @Override
  public RegistryChannel createChannel ()
    throws IOException
  { 
    try
    {
      TcpParams p;
      SSLSocketFactory sf;

      synchronized (this)
      {
        p = new TcpParams (params);

        if (socketFactory == null)
        {
          KeyManagerFactory kmFactory = null;
          KeyStore certKeyStore = p.getCertKeyStore ();

          if (certKeyStore != null)
          {
            kmFactory = KeyManagerFactory.getInstance (p.getCertAlgorithm ());
            kmFactory.init (certKeyStore, p.getCertPassword ().toCharArray ());
          }

          TrustManagerFactory tmFactory = null;
          KeyStore trustKeyStore = p.getTrustKeyStore ();

          if (trustKeyStore != null)
          {
            tmFactory = TrustManagerFactory.getInstance (
              p.getTrustAlgorithm ());

            tmFactory.init (trustKeyStore);
          }

          SSLContext context = SSLContext.getInstance (p.getProtocol ());

          context.init (
            kmFactory != null ? kmFactory.getKeyManagers () : null,
            tmFactory != null ? tmFactory.getTrustManagers () : null,
            null);

          // create socket

          socketFactory = context.getSocketFactory ();
        }

        sf = socketFactory;
      }

      SSLSocket socket = (SSLSocket) sf.createSocket (
        p.getServerAddress (), p.getServerPort ());

      socket.setSoTimeout (p.getReceiveTimeout ());
      socket.setTcpNoDelay (true);
      socket.setKeepAlive (true);

      if (p.verifyServerName ()) verifyServerName (socket);

      socket.getSession ();   
        // make sure that the SSL has been set up before we signal success
        // by returning a new channel

      return new TcpChannel (p, socket);
      
    } catch (GeneralSecurityException e)
    {
      throw new IOException ("failed to create channel due to " +
        "security problems. See causing exception for details", e);
    }
  }
  
  
  
  /**
   *  verify that the common name of the server certificate's subject
   *  is identical with the reverse mapping of the servers address
   *
   *  @param socket                     the socket
   *  @throws IOException               on SSL related I/O exceptions
   *  @throws GeneralSecurityException  if the addresses do not match
   */
  
  private void verifyServerName (SSLSocket socket)
    throws IOException, GeneralSecurityException
  {
    SSLSession session = socket.getSession ();
    X500Principal p = (X500Principal) session.getPeerPrincipal ();
    List<String> commonNames = new ArrayList<String> ();
    
    try
    {
      LdapName name = new LdapName (p.getName (X500Principal.RFC2253));
      
      for (Rdn rdn: name.getRdns ())
      {
        if (rdn.getType ().equals ("CN"))
        {
          Object value = rdn.getValue ();
          if (value instanceof String)
            commonNames.add ((String) value);
        }
      }
      
    } catch (InvalidNameException e)
    {
      // ignored, will cause no names to be found
    }
    
    boolean failed = true;
    String hostName = null;
    
    if (!commonNames.isEmpty ())
    {
      hostName = socket.getInetAddress ().getCanonicalHostName ();

      for (String cn: commonNames)
      {
        if (hostName.equalsIgnoreCase (cn))
        {
          failed = false;
          break;
        }
      }
    }

    if (failed)
    {
      if (commonNames.isEmpty ())
        throw new GeneralSecurityException ("Server certificate does not " +
          "contain a common name, which is required for host name " +
          "verification");    

      StringBuilder sb = new StringBuilder ();
      sb.append ("common name of server (");
      boolean first = true;
      
      for (String name: commonNames)
      {
        if (first) first = false; else sb.append (", ");
        sb.append (name);
      }
      
      sb.append (") does not match host name (");
      sb.append (hostName);
      sb.append (")");
    
      throw new GeneralSecurityException (sb.toString ());    
    }
  }

  
  
}



