/**
 * 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: SSLAdapter.java,v 1.1 2008/09/16 14:16:16 sfwang Exp $

package org.w3c.jigsaw.https;

import java.io.ByteArrayInputStream;

import java.lang.reflect.Method;

import java.net.MalformedURLException;
import java.net.URL;

import java.util.StringTokenizer;

import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.security.cert.CertificateFactory;

import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;

import org.w3c.jigsaw.daemon.ServerHandlerInitException;

import org.w3c.jigsaw.auth.AuthFilter;

import org.w3c.jigsaw.http.httpd;
import org.w3c.jigsaw.http.Client;
import org.w3c.jigsaw.http.Request;

import org.w3c.jigsaw.https.socket.SSLProperties;
import org.w3c.jigsaw.https.socket.SSLSocketClient;
import org.w3c.jigsaw.https.socket.SSLSocketClientFactory;

import org.w3c.tools.resources.ProtocolException;
import org.w3c.tools.resources.RequestInterface;

import org.w3c.util.ObservableProperties;

/**
 * @author Thomas Kopp, Dialogika GmbH
 * @version 1.1, 27 December 2000, 6 February 2004
 * 
 * This class supplies a Jigsaw SSL daemon adapter
 * for enabling Jigsaw https support in accordance with the JSSE API
 * wrapping SSL features in order to solve multiple inheritance problems
 */
public class SSLAdapter {
    
    /**
     * The throwable initCause method via introspection 
     * (for JDK backward compatibility)
     */
    private static final Method initCause;
    
    /**
     * The new style certificate support level indicator
     * (for JSSE backward compatibility)
     */
    private static final boolean supportsNewStyleCertificates;
    
    /**
     * The X509 factory for compatible certificate conversions
     */
    private static final CertificateFactory x509Factory;

    static {
        Class c = java.lang.Throwable.class;
        Class cp[] = { java.lang.Throwable.class };
        Method ic = null;
        try {
            ic = c.getMethod("initCause", cp);
        } catch (Exception ex) {
            ic = null;
        }
        finally {
            initCause = ic;
        }
        boolean supported = false;
        CertificateFactory fact = null;
        try {
            supported = (null != 
	      javax.net.ssl.SSLSession.class.getMethod("getPeerCertificates",
						       (Class [])null));
        } catch (Exception ex) {
            supported = false;
            try {
                fact = CertificateFactory.getInstance("X.509");
            } catch (Exception sub) {
                fact = null;
            }
        }
        finally {
            supportsNewStyleCertificates = supported;
            x509Factory  = fact;
        }
    }
	
    /**
     * flag for enabling debug output if applicable
     */
    private static boolean debug = false;
    
    /**
     * The internal no laceholder object, which has a different than the 
     * expected type
     */
    private static final Object NO_ENTRY = "null";
    
    /**
     * The servlet api spec request attribute name of the cipher suite
     */
    private static final String ALGORITHM = 
                                          "javax.servlet.request.cipher_suite";
    
    /**
     * The servlet api spec request attribute name of the key size
     */
    private static final String KEYSIZE = "javax.servlet.request.key_size";
    
    /**
     * The servlet api spec request attribute name of the certificate chain
     */
    private static final String CERTCHAIN = 
                                       "javax.servlet.request.X509Certificate";
    
    /**
     * The servlet api spec attribute value for client authentication
     */
    private static final String CLIENT_CERT_AUTH = "CLIENT_CERT";

    
    /**
     * flag indicating TLS support
     */
    private boolean ssl_enabled = false;
    
    /**
     * reference to the daemon in question
     */
    private httpd daemon = null;
    
    /**
     * uri of the daemon in question
     */
    private URL url = null;
    
    /**
     * Fills in the stack trace of a cause if possible with respect to the
     * api level.
     *
     * @param throwable  the thowable to be extended
     * @param cause  the cause to be filled in
     */  
    public static final void fillInStackTrace(Throwable throwable,
					      Throwable cause) {
        if (null != initCause) {
	    try {
		Object[] param = { cause };
		initCause.invoke(throwable, param);
	    } catch (Exception iex) {
                // ignore
            }
        }
    }
    
    /**
     * Supplies the ssl session attached to the specified request if any.
     *
     * @param request  the request in question
     * @return  the attached ssl session or null if not applicable
     */
    private static final SSLSession getSession(Request request) {
     	Client cl = request.getClient();
	if (cl instanceof SSLSocketClient) {
	    return ((SSLSocketClient)cl).getSession();
        }
        return null;
    }
    
    /**
     * An ugly way to compute the key size (to be improved)
     * @param algorithm  the algorithm name
     * @return the key size as an integer object
     */
    private static final Integer getKeySize(String algorithm) {
        if (null != algorithm) {
	    StringTokenizer parser = new StringTokenizer(algorithm, "_");
	    while (parser.hasMoreTokens()) {
		try {
		    return Integer.valueOf(parser.nextToken());
		}
		catch (NumberFormatException ex) {
		    // ignore and continue lookup
		}
	    }
        }
        return null;
    }
    
    /**
     * Supplies the calculated or cached key size.
     * @param algorithm  the algorithm name
     * @param session  the ssl underlying session
     * @return the key size as an integer object
     */
    private static final Integer getKeySize(String algorithm,
					    SSLSession session) {
       // FIXME: find a better way to compute the key size
       //        at least this ugly computation gets cached
	   //        at the moment
	Object keysize = session.getValue(KEYSIZE + "." + algorithm);
	if (keysize instanceof Integer) {
	    return (Integer)keysize;
	} else {
	    if (null == keysize) {
		Integer keysize2 = getKeySize(algorithm);
		if (null != keysize2) {
		    session.putValue(KEYSIZE+"."+algorithm, keysize2);
                    return keysize2;
		} else {
		    session.putValue(KEYSIZE+"."+algorithm, NO_ENTRY);
		    return null;
		}
	    } else {
		return null; // keysize could not be computed,
		             //i.e. equals NO_ENTRY
	    }
	}
    }
    
    /**
     * Supplies the peer certificates if available.
     *
     * @param session  the underlying ssl session
     * @return  the certificate chain or null
     * @throws SSLPeerUnverifiedException  iff certificates cannot be ontained
     */
    private static final Certificate[] getPeerCertificates(SSLSession session) 
        throws SSLPeerUnverifiedException {
        try {
	    // using introspection due to possible compatibility issues
	    if (supportsNewStyleCertificates) {
		return session.getPeerCertificates();
	    } else {
		// using deprecated method as a fallback with
		// explicit certificate conversion
		if (null != x509Factory) {
		    javax.security.cert.X509Certificate[] oldStyleCerts;
		    oldStyleCerts = session.getPeerCertificateChain();
		    if (null != oldStyleCerts) {
			int count = oldStyleCerts.length;
			X509Certificate[] newStyleCerts;
			newStyleCerts =  new X509Certificate[count];
			for (int i = 0; i < count; i++) {
			    newStyleCerts[i] = 
			     (X509Certificate)x509Factory.generateCertificate(
				 new ByteArrayInputStream(
				     oldStyleCerts[i].getEncoded()));
			}
			return newStyleCerts;
		    } else { 
			throw new SSLPeerUnverifiedException("No peer "+
						     "certificates available");
		    }
		} else {
		    throw new SSLPeerUnverifiedException("No suitable"+
		     		      " certificate compatibility applicable");
		}
	    }
	} catch (SSLPeerUnverifiedException ex) {
	    throw ex;
        } catch (Exception ex) {
	    SSLPeerUnverifiedException sub;
	    sub = new SSLPeerUnverifiedException(ex.toString());
	    fillInStackTrace(sub, ex);
	    throw sub;
        }
    }
    
    /**
     * constructor for a TLS support adapter
     * @param server reference to the daemon in question
     */
    public SSLAdapter(httpd server) {
        if (null != server) {
	    ssl_enabled = false;
	    daemon = server;
	    url = null;
        } else {
	    throw new NullPointerException("No daemon intance supplied for "+
					   " creating SSL adapter");
	}
    }
    
    /**
     * method for initializing the properties of a daemon
     * @exception ServerHandlerInitException thrown if initialization fails
     */
    public void initializeProperties() 
        throws ServerHandlerInitException 
    {
        ObservableProperties props = daemon.getProperties();
        // default to well-known factory if applicable        
        if (props.getBoolean(SSLProperties.SSL_ENABLED_P, true)) {
            String factory_class = props.getString(httpd.CLIENT_FACTORY_P,
						   null);
            if (null != factory_class) {
                try {
		    Class factory = Class.forName(factory_class);
		    ssl_enabled = 
		      (SSLSocketClientFactory.class.isAssignableFrom(factory));
                } catch (Exception ex) {
                    String error = "Initialization failed";
                    daemon.fatal(ex, error);
                    if (debug) {
                        System.out.println(error);
                        ex.printStackTrace();
                    }
                    ServerHandlerInitException sub;
                    sub = new ServerHandlerInitException(ex.getMessage());
                    fillInStackTrace(sub, ex);
                    throw sub;
                }
            } else {
		throw new ServerHandlerInitException("No socket client"
						     +" factory specified");
	    }
        } else {
	    ssl_enabled = false;
	}
        url = null;
    }

    /**
     * method for preparing a reply interface for a request
     * @param req the current request to be handled
     * @exception ProtocolException thrown if the request url is malformed
     */
    public void perform(RequestInterface req) 
	throws ProtocolException  {
	Request request = (Request)req;
	if (ssl_enabled) {
	    // set request protocol to https
	    URL url = request.getURL();
	    try {
		request.setURL(new URL("https", url.getHost(), 
				       url.getPort(), url.getFile()));
		
		// tk, 1 February 2004, added SSL client attributes
		// according to Servlet v2.4 spec
		SSLSession session = getSession(request);
		if (null != session) {
		    String algorithm = session.getCipherSuite();
		    request.setState(ALGORITHM, algorithm);
		    
		    Integer keysize = getKeySize(algorithm, session);
		    if (null != keysize) {
			request.setState(KEYSIZE, keysize);
		    }
		    
		    try {
                        Certificate[] chain = getPeerCertificates(session);
                        if (chain instanceof X509Certificate[]) {
                            X509Certificate[] x509chain;
			    x509chain = (X509Certificate[])chain;
                            request.setState(CERTCHAIN, x509chain);
                            request.setState(AuthFilter.STATE_AUTHTYPE,
					     CLIENT_CERT_AUTH);
                            if (x509chain.length > 0) {
				request.setState(AuthFilter.STATE_AUTHUSER,
				        x509chain[0].getSubjectDN().getName());
                            }
                        }
		    } catch (SSLPeerUnverifiedException ex) {
			if (debug) {
			    ex.printStackTrace();
			}
			// no certificates available, ignore
		    }
		}
	    } catch (MalformedURLException ex) {
		String error = "Bad url during switching to https";
		daemon.fatal(ex, error);
		if (debug) {
		    System.out.println(error);
		    ex.printStackTrace();
		}
		ProtocolException sub = new ProtocolException(ex.getMessage());
		fillInStackTrace(sub, ex);
		throw sub;
	    }
	}
    }
    
    /**
     * method for supplying a daemon uri
     * @return uri of the daemon in question
     */
    public URL getURL() {
        if (url == null) {
	    if (ssl_enabled) {
		try {
		    if (daemon.getPort() != 443) {
			url = new URL("https", daemon.getHost(), 
				      daemon.getPort(), "/");
		    } else {
			url = new URL("https", daemon.getHost(), "/");
		    }
		} catch (MalformedURLException ex) {
		    if (debug) {
			ex.printStackTrace();
		    }
		    throw new RuntimeException("Unable to construct "+
					       "server uri. (" + 
					       ex.getMessage() + ")");
		}
	    } else {
		try {
		    if (daemon.getPort() != 80 ) {
			url = new URL("http", daemon.getHost(), 
				      daemon.getPort(), "/");
		    } else {
			url = new URL("http", daemon.getHost(), "/");
		    }
		} catch (MalformedURLException ex) {
		    throw new RuntimeException("Unable to construct"+
					       " server uri. (" +
					       ex.getMessage() + ")");
		}
	    }
	}		
        return url;
    }
    
    /**
     * method for indicating TLS support
     * @return flag for indicating TLS support enabled
     */
    public boolean sslEnabled() {
	return ssl_enabled;
    }
}
