/**
 * Copyright (c) 2000/2001 Thomas Kopp
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */
// $Id: SSLSocketClientFactory.java,v 1.4 2003/02/04 17:54:04 ylafon Exp $

package org.w3c.jigsaw.https.socket;

import java.net.InetAddress;
import java.net.ServerSocket;
import java.io.IOException;

import java.security.Provider;
import java.security.Security;

import javax.net.ssl.SSLKeyException;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ServerSocketFactory;

import org.w3c.jigsaw.http.httpd;
import org.w3c.jigsaw.http.socket.SocketClientFactory;
import org.w3c.util.ObservableProperties;

/**
 * @author Thomas Kopp, Dialogika GmbH
 * @version 1.0.2, 27 December 2000
 * 
 * This class extends a Jigsaw SocketClientFactory designed for the 
 * http protocol
 * in order to supply a SocketClientFactory for the https protocol 
 * in accordance with the JSSE API.
 */
public class SSLSocketClientFactory extends SocketClientFactory {

    /**
     * static flag for enabling debug output if applicable
     */
    private static boolean debug = false;

    /**
     * property key for specifying the TLS protocol handler
     */
    public static final String PROTOCOL_HANDLER_P = 
                                         "org.w3c.jigsaw.ssl.protocol.handler";
    
    /**
     * property key for specifying the TLS security provider
     */
    public static final String SECURITY_PROVIDER_P = 
                                             SSLProperties.SECURITY_PROVIDER_P;

    /**
     * property key for specifying the keystore path
     */
    public static final String KEYSTORE_PATH_P = SSLProperties.KEYSTORE_PATH_P;

    /**
     * property key for specifying the keystore password
     */
    public static final String KEYSTORE_PASSWORD_P = 
                                             SSLProperties.KEYSTORE_PASSWORD_P;

    /**
     * property key for specifying the truststore path 
     * (future use for client mode)
     */
    public static final String TRUSTSTORE_PATH_P =
                                               SSLProperties.TRUSTSTORE_PATH_P;

    /**
     * property key for specifying the truststore password 
     * (future use for client mode)
     */
    public static final String TRUSTSTORE_PASSWORD_P = 
                                           SSLProperties.TRUSTSTORE_PASSWORD_P;

    /**
     * property key for the system protocol package lookup
     */
    public static final String PROTOCOL_HANDLER_S = 
                                                  "java.protocol.handler.pkgs";

    /**
     * property key for the system keystore path
     */
    public static final String KEYSTORE_PATH_S = "javax.net.ssl.keyStore";

    /**
     * property key for the system keystore password
     */
    public static final String KEYSTORE_PASSWORD_S = 
                                              "javax.net.ssl.keyStorePassword";

    /**
     * property key for the system truststore path
     * (future usage for client mode)
     */
    public static final String TRUSTSTORE_PATH_S = "javax.net.ssl.trustStore";

    /**
     * property key for the system keystore password 
     * (future usage for client mode)
     */
    public static final String TRUSTSTORE_PASSWORD_S = 
                                           "javax.net.ssl.trustStorePassword";

    /**
     * default security provider for TLS support
     */
    public static final String SECURITY_PROVIDER_D = 
                                      "com.sun.net.ssl.internal.ssl.Provider";

    /**
     * default TLS protocol handler (should always be set to default)
     */
    public static final String PROTOCOL_HANDLER_D = 
                                      "com.sun.net.ssl.internal.www.protocol";
    
    /**
     * static basic factory for the SocketClientFactory with TLS support
     */
    private static ServerSocketFactory factory = null;
    
    /**
     * the daemon of this factory
     */
    private httpd daemon = null;
    
    /**
     * the daemon bind address for this factory
     */
    private InetAddress bindAddr = null;
    
    /**
     * the daemon bind address for this factory
     */
    private int maxClients = 0;

    /**
     * method for creating a secure server socket
     * @return a new server socket instance
     * @throws java.io.IOException due to socket creation problems
     */
    public ServerSocket createServerSocket() 
	throws IOException 
    {
        int port = daemon.getPort();
        int clients = Math.max(128, maxClients);
        ServerSocket serversocket = null;
        if (bindAddr == null) {
	    serversocket = getFactory().createServerSocket(port, clients);
	} else {
	    serversocket = getFactory().createServerSocket(port, clients, 
							   bindAddr);
	}
        // Client authentication does not yet work with recent browsers.
        // The following lines would have to be uncommented for
        // implementing client authentication
        // in addition to add client certificate chain verification.
        // tk, 8 April 2001
        // if (serversocket instanceof SSLServerSocket) {
        //    ((SSLServerSocket)serversocket).setNeedClientAuth(true);
        // }
        return serversocket;
    }

    /**
     * method for intializing this factory
     * @param server the daemon of this factory
     */
    public void initialize(httpd server) {
        super.initialize(server);
	daemon = server;
	daemon.registerPropertySet(new SSLProperties(daemon)); 
        ObservableProperties props = daemon.getProperties();

        try {
            
	    // enabling the TLS security provider
	    String supplier = props.getString(SECURITY_PROVIDER_P, 
					      SECURITY_PROVIDER_D); 
	    Class support = Class.forName(supplier);
	    Provider provider = (Provider)support.newInstance();
	    Security.addProvider(provider);
	    if (debug) {
		System.out.println("Provider " + 
				   provider.toString() + " added.");
	    }
		
	    // enabling ths TLS stream handler
	    String protocol = props.getString(PROTOCOL_HANDLER_P,
					      PROTOCOL_HANDLER_D); 
	    if (null != protocol) {
		System.setProperty(PROTOCOL_HANDLER_S, protocol);
		if (debug) {
		    System.out.println("Protocol " + protocol + " added.");
		}
	    }

	    // enabling some TLS custom attributes
	    String keystore = props.getString(KEYSTORE_PATH_P, null); 
	    if (null != keystore) {
		System.setProperty(KEYSTORE_PATH_S, keystore);
                    
		String keypass= props.getString(KEYSTORE_PASSWORD_P, null);
		if (null != keypass) {
		    System.setProperty(KEYSTORE_PASSWORD_S, keypass);
		}
	    }

	    String truststore = props.getString(TRUSTSTORE_PATH_P, null); 
	    if (null != truststore) { 
		System.setProperty(TRUSTSTORE_PATH_S, truststore);

		String trustpass = props.getString(TRUSTSTORE_PASSWORD_P, 
						   null); 
		if (null != trustpass) {
		    System.setProperty(TRUSTSTORE_PASSWORD_S, trustpass);
		}
	    }
        
	    String bindAddrName = props.getString(BINDADDR_P, null);
	    if (bindAddrName != null) {
		try {
		    bindAddr = InetAddress.getByName(bindAddrName);
		} catch (Exception ex) {
		    bindAddr = null;
		}
	    } else {
		bindAddr = null;
	    }
        
	    maxClients = props.getInteger(MAXCLIENTS_P, MAXCLIENTS);
        
	} catch (Exception ex) {
// jdk1.4 only
//	    RuntimeException sub = new RuntimeException(
//		"Unable to initialize secure socket provider", ex);
	    String sub;
	    sub = "Unable to initialize secure socket provider"
		+ ex.toString();
	    daemon.errlog(sub);
	    if (debug) {
		System.err.println("Unable to initialize secure"
				   +" socket provider");
		ex.printStackTrace();
	    }
	    throw new RuntimeException(sub);
	}
    }

    /**
     * method for handling a dynamic property modification
     * @param name the name of the property modified
     * @return true if and only if the modification has been handled 
     * successfully
     */
    public boolean propertyChanged(String name) {
        if (super.propertyChanged(name)) {
	    ObservableProperties props = daemon.getProperties();
        
	    try {

		if (name.equals(SECURITY_PROVIDER_P)) {
		    String supplier = props.getString(SECURITY_PROVIDER_P,
						      SECURITY_PROVIDER_D); 
		    Class support = Class.forName(supplier);
		    Provider provider = (Provider)support.newInstance();
		    Security.addProvider(provider);
		    if (debug) {
			System.out.println("Provider " + provider.toString() +
					   " added.");
		    }
		} else if (name.equals(PROTOCOL_HANDLER_P)) {
		    String protocol = props.getString(PROTOCOL_HANDLER_P,
						      PROTOCOL_HANDLER_D); 
		    if (null != protocol) {
			System.setProperty(PROTOCOL_HANDLER_S, protocol);
			if (debug) {
			    System.out.println("Protocol " + protocol + 
					       " added.");
			}
		    }
		} else if (name.equals(KEYSTORE_PATH_P)) {
		    String keystore = props.getString(KEYSTORE_PATH_P, null); 
		    if (null != keystore) {
			System.setProperty(KEYSTORE_PATH_S, keystore);
		    }
		} else if (name.equals(KEYSTORE_PASSWORD_P)) {
		    String keypass = props.getString(KEYSTORE_PASSWORD_P,
						     null); 
		    if (null != keypass) {
			System.setProperty(KEYSTORE_PASSWORD_S, keypass);
		    }
		} else if (name.equals(TRUSTSTORE_PATH_P)) {
		    String truststore = props.getString(TRUSTSTORE_PATH_P,
							null); 
		    if (null != truststore) {
			System.setProperty(TRUSTSTORE_PATH_S, truststore);
		    }
		} else if (name.equals(TRUSTSTORE_PASSWORD_P)) {
		    String trustpass = props.getString(TRUSTSTORE_PASSWORD_P,
						       null); 
		    if (null != trustpass) {
			System.setProperty(TRUSTSTORE_PASSWORD_S, trustpass);
		    }
		} else if (name.equals(MAXCLIENTS_P)) {
		    int newmax = props.getInteger(MAXCLIENTS_P, -1);
		    if (newmax > maxClients) {
			for (int i = maxClients-newmax; --i >= 0; ) {
			    addClient(true);
			}
		    } else if (newmax > 0) {
			maxClients = newmax;
		    }
		} else if (name.equals(BINDADDR_P)) {
		    try {
			bindAddr = InetAddress.getByName(
			    props.getString(BINDADDR_P, null));
		    } 
		    catch (Exception ex) {
			bindAddr = null;
		    }
		}
		return true;
          
	    } catch (Exception ex) {
// jdk1.4
//		RuntimeException sub = new RuntimeException(
//		    "Unable to modify secure socket provider", ex);
		String sub;
		sub = "Unable to modify secure socket provider" 
		    + ex.toString();
		daemon.errlog(sub);
		if (debug) {
		    ex.printStackTrace();
		}
		// throw sub;
	    }
        }
        return false;
    }

    /**
     * server sockt factory singleton creation
     * @return the singleton secure server socket factory
     * @throws java.io.IOException due to factory creation problems
     */
    private synchronized ServerSocketFactory getFactory() 
    throws SSLKeyException {
        if (null == factory) {
	    factory = SSLServerSocketFactory.getDefault();
           
	    String[] supported = 
		((SSLServerSocketFactory)factory).getSupportedCipherSuites();
	    if (debug) {
		System.out.println("Supported suites:");
		for (int i = 0; i < supported.length; i++) {
		    System.out.println("          " + supported[i]);
		}
		String[] enabled = 
		    ((SSLServerSocketFactory)factory).getDefaultCipherSuites();
		System.out.println("Enabled suites:");
		for (int i = 0; i < enabled.length; i++) {
		    System.out.println("         " + enabled[i]);
		}
	    }
           
	    if (supported.length < 1) {
    		SSLKeyException ex = new SSLKeyException(
            "No cipher suites supported by this"
		    + " SSL socket factory.\n"
		    + "Please check your factory, key store, "
		    + "store password and cerificates");
		daemon.errlog(ex.toString());
		if (debug) {
		    ex.printStackTrace();
		}
		throw ex;
	    }
        }
        return factory;
    }
}
