/*
 *  soapUI, copyright (C) 2004-2012 smartbear.com 
 *
 *  soapUI is free software; you can redistribute it and/or modify it under the 
 *  terms of version 2.1 of the GNU Lesser General Public License as published by 
 *  the Free Software Foundation.
 *
 *  soapUI 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 at gnu.org.
 */
package com.eviware.soapui.impl.wsdl.support.http;

import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.X509Certificate;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.apache.commons.ssl.KeyMaterial;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.log4j.Logger;

import com.eviware.soapui.SoapUI;
import com.eviware.soapui.support.StringUtils;

public class SoapUISSLSocketFactory extends SSLSocketFactory
{
	// a cache of factories for custom certificates/Keystores at the project level - never cleared
	private static final Map<String, SSLSocketFactory> factoryMap = new ConcurrentHashMap<String, SSLSocketFactory>();
	private final SSLContext sslContext = SSLContext.getInstance( "TLS" );
	private final static Logger log = Logger.getLogger( SoapUISSLSocketFactory.class );

	@SuppressWarnings( "deprecation" )
	public SoapUISSLSocketFactory( KeyStore keyStore, String keystorePassword ) throws KeyManagementException,
			UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException
	{
		super( keyStore );

		// trust everyone!
		X509TrustManager tm = new X509TrustManager()
		{
			@Override
			public X509Certificate[] getAcceptedIssuers()
			{
				return null;
			}

			@Override
			public void checkClientTrusted( X509Certificate[] certs, String authType )
			{
			}

			@Override
			public void checkServerTrusted( X509Certificate[] certs, String authType )
			{
			}
		};

		if( keyStore != null )
		{
			KeyManagerFactory kmfactory = KeyManagerFactory.getInstance( KeyManagerFactory.getDefaultAlgorithm() );
			kmfactory.init( keyStore, keystorePassword != null ? keystorePassword.toCharArray() : null );
			KeyManager[] keymanagers = kmfactory.getKeyManagers();
			sslContext.init( keymanagers, new TrustManager[] { tm }, null );
		}
		else
		{
			sslContext.init( null, new TrustManager[] { tm }, null );
		}

		setHostnameVerifier( SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER );

	}

	private static SSLSocket enableSocket( SSLSocket socket )
	{
		String invalidateSession = System.getProperty( "soapui.https.session.invalidate" );
		String protocols = System.getProperty( "soapui.https.protocols" );
		String ciphers = System.getProperty( "soapui.https.ciphers" );

		if( StringUtils.hasContent( invalidateSession ) )
		{
			socket.getSession().invalidate();
		}

		if( StringUtils.hasContent( protocols ) )
		{
			socket.setEnabledProtocols( protocols.split( "," ) );
		}
		//		else if( socket.getSupportedProtocols() != null )
		//		{
		//			socket.setEnabledProtocols( socket.getSupportedProtocols() );
		//		}

		if( StringUtils.hasContent( ciphers ) )
		{
			socket.setEnabledCipherSuites( ciphers.split( "," ) );
		}
		//		else if( socket.getSupportedCipherSuites() != null )
		//		{
		//			socket.setEnabledCipherSuites(  socket.getSupportedCipherSuites()  );
		//		}

		return socket;
	}

	@Override
	public Socket createSocket( HttpParams params ) throws IOException
	{
		String sslConfig = ( String )params.getParameter( SoapUIHttpRoute.SOAPUI_SSL_CONFIG );

		if( StringUtils.isNullOrEmpty( sslConfig ) )
		{
			return enableSocket( ( SSLSocket )sslContext.getSocketFactory().createSocket() );
		}

		SSLSocketFactory factory = factoryMap.get( sslConfig );

		if( factory != null )
		{
			if( factory == this )
				return enableSocket( ( SSLSocket )sslContext.getSocketFactory().createSocket() );
			else
				return enableSocket( ( SSLSocket )factory.createSocket( params ) );
		}

		try
		{
			// try to create new factory for specified config
			int ix = sslConfig.lastIndexOf( ' ' );
			String keyStore = sslConfig.substring( 0, ix );
			String pwd = sslConfig.substring( ix + 1 );

			KeyStore ks = KeyStore.getInstance( KeyStore.getDefaultType() );

			if( keyStore.trim().length() > 0 )
			{
				File f = new File( keyStore );

				if( f.exists() )
				{
					log.info( "Initializing Keystore from [" + keyStore + "]" );

					try
					{
						KeyMaterial km = new KeyMaterial( f, pwd.toCharArray() );
						ks = km.getKeyStore();
					}
					catch( Exception e )
					{
						SoapUI.logError( e );
						pwd = null;
					}
				}
			}

			factory = new SoapUISSLSocketFactory( ks, pwd );
			factoryMap.put( sslConfig, factory );

			return enableSocket( ( SSLSocket )factory.createSocket( params ) );
		}
		catch( Exception gse )
		{
			SoapUI.logError( gse );
			return enableSocket( ( SSLSocket )super.createSocket( params ) );
		}
	}

	/**
	 * @since 4.1
	 */
	@Override
	public Socket connectSocket( final Socket socket, final InetSocketAddress remoteAddress,
			final InetSocketAddress localAddress, final HttpParams params ) throws IOException, UnknownHostException,
			ConnectTimeoutException
	{
		if( remoteAddress == null )
		{
			throw new IllegalArgumentException( "Remote address may not be null" );
		}
		if( params == null )
		{
			throw new IllegalArgumentException( "HTTP parameters may not be null" );
		}
		Socket sock = socket != null ? socket : new Socket();
		if( localAddress != null )
		{
			sock.setReuseAddress( HttpConnectionParams.getSoReuseaddr( params ) );
			sock.bind( localAddress );
		}

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

		try
		{
			sock.setSoTimeout( soTimeout );
			sock.connect( remoteAddress, connTimeout );
		}
		catch( SocketTimeoutException ex )
		{
			throw new ConnectTimeoutException( "Connect to " + remoteAddress.getHostName() + "/"
					+ remoteAddress.getAddress() + " timed out" );
		}
		SSLSocket sslsock;
		// Setup SSL layering if necessary
		if( sock instanceof SSLSocket )
		{
			sslsock = ( SSLSocket )sock;
		}
		else
		{
			sslsock = ( SSLSocket )getSocketFactory().createSocket( sock, remoteAddress.getHostName(),
					remoteAddress.getPort(), true );
			sslsock = enableSocket( sslsock );
		}
		// do we need it? trust all hosts
//		if( getHostnameVerifier() != null )
//		{
//			try
//			{
//				getHostnameVerifier().verify( remoteAddress.getHostName(), sslsock );
//				// verifyHostName() didn't blowup - good!
//			}
//			catch( IOException iox )
//			{
//				// close the socket before re-throwing the exception
//				try
//				{
//					sslsock.close();
//				}
//				catch( Exception x )
//				{ /* ignore */
//				}
//				throw iox;
//			}
//		}
		return sslsock;
	}

	/**
	 * @since 4.1
	 */
	@Override
	public Socket createLayeredSocket( final Socket socket, final String host, final int port, final boolean autoClose )
			throws IOException, UnknownHostException
	{
		SSLSocket sslSocket = ( SSLSocket )getSocketFactory().createSocket( socket, host, port, autoClose );
		sslSocket = enableSocket( sslSocket );
//		if( getHostnameVerifier() != null )
//		{
//			getHostnameVerifier().verify( host, sslSocket );
//		}
		// verifyHostName() didn't blowup - good!
		return sslSocket;
	}
}
