/*
 * 
 * Copyright 2004 Sun Microsystems, Inc. All Rights Reserved.
 * 
 * The contents of this file are subject to the Sun Community Source
 * License v3.0/Jini Technology Specific Attachment v 1.0 (the
 * "License"). You may not use this file except in compliance with the
 * License. You may obtain a copy of the License at
 * http://wwws.sun.com/software/jini/licensing/SCSL3_JiniTSA1.html.
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
 * the License for the specific language governing rights and limitations
 * under the License.
 * 
 * The Reference Code is Jini Technology Core Platform code, v2.0. The
 * Developer of the Reference Code is Sun Microsystems, Inc.
 * 
 * Contributor(s): Sun Microsystems, Inc.
 * 
 * The contents of this file comply with the Jini Technology Core
 * Platform Compatibility Kit, v2.0A.
 * 
 * Tester(s): Sun Microsystems, Inc.
 * 
 * Test Platform(s):
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Solaris(TM)SPARC/x86
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Linux (Intel x86)
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Windows (Intel Platform)
 * 
 */

package net.jini.jeri.ssl;

import com.sun.jini.action.GetLongAction;
import com.sun.jini.jeri.internal.connection.BasicServerConnManager;
import com.sun.jini.jeri.internal.connection.ServerConnManager;
import com.sun.jini.jeri.internal.runtime.Util;
import com.sun.jini.logging.Levels;
import com.sun.jini.thread.Executor;
import com.sun.jini.thread.GetThreadPoolAction;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.channels.SocketChannel;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.GeneralSecurityException;
import java.security.Permission;
import java.security.Principal;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.security.cert.CertPath;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.security.auth.Subject;
import javax.security.auth.x500.X500Principal;
import javax.security.auth.x500.X500PrivateCredential;
import net.jini.core.constraint.InvocationConstraints;
import net.jini.io.UnsupportedConstraintException;
import net.jini.jeri.Endpoint;
import net.jini.jeri.RequestDispatcher;
import net.jini.jeri.ServerEndpoint.ListenContext;
import net.jini.jeri.ServerEndpoint.ListenCookie;
import net.jini.jeri.ServerEndpoint.ListenEndpoint;
import net.jini.jeri.ServerEndpoint.ListenHandle;
import net.jini.jeri.ServerEndpoint;
import net.jini.jeri.connection.InboundRequestHandle;
import net.jini.jeri.connection.ServerConnection;
import net.jini.security.AuthenticationPermission;
import net.jini.security.Security;
import net.jini.security.SecurityContext;

/**
 * Provides the implementation of SslServerEndpoint so that the implementation
 * can be inherited by HttpsServerEndpoint without revealing the inheritance in
 * the public API.
 *
 * @author Sun Microsystems, Inc.
 */
class SslServerEndpointImpl extends Utilities {

    /* -- Fields -- */

    /** Server logger */
    static final Logger logger = serverLogger;

    /**
     * The maximum time a session should be used before expiring -- non-final
     * to facilitate testing.  Use 24 hours to allow the client, which uses
     * 23.5 hours, to renegotiate a new session before the server timeout.
     */
    static long maxServerSessionDuration =
	((Long) Security.doPrivileged(
	    new GetLongAction("com.sun.jini.jeri.ssl.maxServerSessionDuration",
			      24 * 60 * 60 * 1000))).longValue();

    /**
     * Executes a Runnable in a system thread -- used for listener accept
     * threads.
     */
    static final Executor systemExecutor = (Executor)
	Security.doPrivileged(new GetThreadPoolAction(false));

    /** The default server connection manager. */
    private static final ServerConnManager defaultServerConnectionManager =
	new BasicServerConnManager();

    /** The associated server endpoint. */
    final ServerEndpoint serverEndpoint;

    /** The server subject, or null if the server is anonymous. */
    final Subject serverSubject;

    /**
     * The principals to use for authentication, or null if the server is
     * anonymous.
     */
    final Set serverPrincipals;

    /**
     * The host name that clients should use to connect to this server, or null
     * if enumerateListenEndpoints should compute the default.
     */
    final String serverHost;

    /** The server port */
    final int port;

    /** The socket factory for use in the associated Endpoint. */
    final SocketFactory socketFactory;

    /** The server socket factory. */
    final ServerSocketFactory serverSocketFactory;

    /**
     * The permissions needed to authenticate when listening on this endpoint,
     * or null if the server is anonymous.
     */
    Permission[] listenPermissions;

    /** The listen endpoint. */
    private final ListenEndpoint listenEndpoint;

    /** The factory for creating JSSE sockets -- set by sslInit */
    private SSLSocketFactory sslSocketFactory;

    /**
     * The authentication manager for the SSLContext for this endpoint -- set
     * by sslInit.
     */
    private ServerAuthManager authManager;

    /** The server connection manager. */
    ServerConnManager serverConnectionManager = defaultServerConnectionManager;

    /* -- Methods -- */

    /** Creates an instance of this class. */
    SslServerEndpointImpl(ServerEndpoint serverEndpoint,
			  Subject serverSubject,
			  X500Principal[] serverPrincipals,
			  String serverHost,
			  int port,
			  SocketFactory socketFactory,
			  ServerSocketFactory serverSocketFactory)
    {
	this.serverEndpoint = serverEndpoint;
	boolean useCurrentSubject = serverSubject == null;
	if (useCurrentSubject) {
	    final AccessControlContext acc = AccessController.getContext();
	    serverSubject = (Subject) AccessController.doPrivileged(
		new PrivilegedAction() {
		    public Object run() {
			return Subject.getSubject(acc);
		    }
		});
	}
	this.serverPrincipals = (serverPrincipals == null)
	    ? computePrincipals(serverSubject)
	    : checkPrincipals(serverPrincipals);
	/* Set listenPermissions before calling hasListenPermissions */
	if (this.serverPrincipals == null) {
	    listenPermissions = null;
	} else {
	    listenPermissions =
		new AuthenticationPermission[this.serverPrincipals.size()];
	    int i = 0;
	    for (Iterator iter = this.serverPrincipals.iterator();
		 iter.hasNext();
		 i++)
	    {
		Principal p = (Principal) iter.next();
		listenPermissions[i] = new AuthenticationPermission(
		    Collections.singleton(p), null, "listen");
	    }
	}
	if (this.serverPrincipals == null ||
	    /* Don't use current subject without any permission */
	    (useCurrentSubject &&
	     serverPrincipals != null &&
	     !hasListenPermissions()))
	{
	    this.serverSubject = null;
	    this.listenPermissions = null;
	} else {
	    this.serverSubject = serverSubject;
	}
	this.serverHost = serverHost;
	if (port < 0 || port > 0xFFFF) {
	    throw new IllegalArgumentException("Invalid port: " + port);
	}
	this.port = port;
	this.socketFactory = socketFactory;
	this.serverSocketFactory = serverSocketFactory;
	listenEndpoint = createListenEndpoint();
    }

    /** Computes the principals in the subject available for authentication */
    private static Set computePrincipals(Subject subject) {
	if (subject == null) {
	    return null;
	}
	/* Get principals from the subject */
	X500PrivateCredential[] credentials =
	    (X500PrivateCredential[]) AccessController.doPrivileged(
		new SubjectCredentials.GetAllPrivateCredentialsAction(
		    subject));
	Set result = SubjectCredentials.getPrincipals(
	    subject, ANY_KEY_ALGORITHM, credentials);
	/* Remove ones we don't have authentication listen permission for. */
	SecurityManager sm = System.getSecurityManager();
	if (sm != null) {
	    for (Iterator iter = result.iterator(); iter.hasNext(); ) {
		Principal p = (Principal) iter.next();
		try {
		    sm.checkPermission(
			new AuthenticationPermission(
			    Collections.singleton(p), null, "listen"));
		} catch (SecurityException e) {
		    logger.log(Levels.HANDLED,
			       "compute principals for server endpoint " +
			       "caught exception",
			       e);
		    iter.remove();
		}
	    }
	}
	return result.isEmpty() ? null : result;
    }

    /**
     * Returns true if the caller has AuthenticationPermission for listen on
     * this endpoint.
     */
    private boolean hasListenPermissions() {
	try {
	    checkListenPermissions(false);
	    return true;
	} catch (SecurityException e) {
	    logger.log(Levels.HANDLED,
		       "check listen permissions for server endpoint " +
		       "caught exception",
		       e);
	    return false;
	}
    }

    /**
     * Checks that principals is not empty and contains no nulls, and returns
     * it as a set.  Returns null if no principals are specified.
     */
    private static Set checkPrincipals(X500Principal[] principals) {
	if (principals.length == 0) {
	    return null;
	}
	Set result = new HashSet(principals.length);
	for (int i = principals.length; --i >= 0; ) {
	    X500Principal p = principals[i];
	    if (p == null) {
		throw new NullPointerException(
		    "Server principal cannot be null");
	    }
	    result.add(p);
	}
	return result;
    }

    /**
     * Initializes the sslSocketFactory and authManager fields.  Wait to do
     * this until needed, because creating the SSLContext requires initializing
     * the secure random number generator, which can be time consuming.
     */
    private void sslInit() {
	assert Thread.holdsLock(this);
	SSLContextInfo info = getServerSSLContextInfo(
	    serverSubject, serverPrincipals);
	sslSocketFactory = info.sslContext.getSocketFactory();
	authManager = (ServerAuthManager) info.authManager;
    }

    /** Returns the SSLSocketFactory, calling sslInit if needed. */
    final SSLSocketFactory getSSLSocketFactory() {
	synchronized (this) {
	    if (sslSocketFactory == null) {
		sslInit();
	    }
	}
	return sslSocketFactory;
    }

    /** Returns the ServerAuthManager, calling sslInit if needed. */
    final ServerAuthManager getAuthManager() {
	synchronized (this) {
	    if (authManager == null) {
		sslInit();
	    }
	}
	return authManager;
    }

    /** Returns a hash code value for this object. */
    public int hashCode() {
	return getClass().hashCode()
	    ^ System.identityHashCode(serverSubject)
	    ^ (serverPrincipals == null ? 0 : serverPrincipals.hashCode())
	    ^ (serverHost == null ? 0 : serverHost.hashCode())
	    ^ port
	    ^ (socketFactory != null ? socketFactory.hashCode() : 0)
	    ^ (serverSocketFactory != null
	       ? serverSocketFactory.hashCode() : 0);
    }

    /**
     * Two instances of this class are equal if they have the same actual
     * class; have server subjects that compare equal using ==; have server
     * principals that are either both null or are equal when compared as the
     * elements of a Set; have the same server host and port; have socket
     * factories that are either both null, or have the same actual class and
     * are equal; and have server socket factories that are either both null,
     * or have the same actual class and are equal.
     */
    public boolean equals(Object object) {
	if (object == null || object.getClass() != getClass()) {
	    return false;
	}
	SslServerEndpointImpl other = (SslServerEndpointImpl) object;
	return serverSubject == other.serverSubject
	    && safeEquals(serverPrincipals, other.serverPrincipals)
	    && safeEquals(serverHost, other.serverHost)
	    && port == other.port
	    && Util.sameClassAndEquals(socketFactory, other.socketFactory)
	    && Util.sameClassAndEquals(serverSocketFactory,
				       other.serverSocketFactory);
    }

    /** Returns a string representation of this object. */
    public String toString() {
	return getClassName(this) + fieldsToString();
    }

    /** Returns a string representation of the fields of this object. */
    final String fieldsToString() {
	return "[" +
	    (serverPrincipals == null
	     ? ""
	     : serverPrincipals.toString() + ", ") +
	    (serverHost == null ? "" : serverHost + ":") +
	    port + 
	    (serverSocketFactory != null ? ", " + serverSocketFactory : "") +
	    (socketFactory != null ? ", " + socketFactory : "") +
	    "]";
    }

    /* -- Implement ServerCapabilities -- */

    final InvocationConstraints checkConstraints(
	InvocationConstraints constraints)
	throws UnsupportedConstraintException
    {
	try {
	    checkListenPermissions(false);
	} catch (SecurityException e) {
	    if (logger.isLoggable(Levels.FAILED)) {
		logThrow(logger, Levels.FAILED,
			 SslServerEndpoint.class, "checkConstraints",
			 "check constraints for {0}\nwith {1}\nthrows",
			 new Object[] { this, constraints },
			 e);
	    }
	    throw e;
	}
	Set clientPrincipals = getClientPrincipals(constraints);
	if (clientPrincipals == null) {
	    clientPrincipals = Collections.singleton(UNKNOWN_PRINCIPAL);
	}
	Map serverKeyTypes = new HashMap();
	List certPaths =
	    SubjectCredentials.getCertificateChains(serverSubject);
	if (certPaths != null) {
	    for (int i = certPaths.size(); --i >= 0; ) {
		CertPath chain = (CertPath) certPaths.get(i);
		X509Certificate cert = SubjectCredentials.firstX509Cert(chain);
		X500Principal principal = SubjectCredentials.getPrincipal(
		    serverSubject, cert);
		if (principal != null) {
		    Collection keyTypes =
			(Collection) serverKeyTypes.get(principal);
		    if (keyTypes == null) {
			keyTypes = new ArrayList(1);
			serverKeyTypes.put(principal, keyTypes);
		    }
		    keyTypes.add(cert.getPublicKey().getAlgorithm());
		}
	    }
	}
	String[] suites = getSupportedCipherSuites();
	for (int suiteIndex = suites.length; --suiteIndex >= 0; ) {
	    String suite = suites[suiteIndex];
	    String suiteKeyType = getKeyAlgorithm(suite);
	    Iterator sIter =
		(serverPrincipals == null
		 ? Collections.EMPTY_SET : serverPrincipals).iterator();
	    X500Principal server;
	    do {
		if (sIter.hasNext()) {
		    server = (X500Principal) sIter.next();
		    assert server != null;
		    Collection keyTypes =
			(Collection) serverKeyTypes.get(server);
		    if (keyTypes == null || !keyTypes.contains(suiteKeyType)) {
			continue;
		    }
		} else {
		    server = null;
		}
		Iterator cIter = clientPrincipals.iterator();
		Principal client;
		do {
		    if (cIter.hasNext()) {
			client = (Principal) cIter.next();
			assert client != null;
		    } else {
			client = null;
		    }
		    InvocationConstraints unfulfilledConstraints =
			getUnfulfilledConstraints(
			    suite, client, server, constraints);
		    if (unfulfilledConstraints != null) {
			if (logger.isLoggable(Level.FINE)) {
			    logger.log(Level.FINE,
				       "check constraints for {0}\n" +
				       "with {1}\nreturns {2}",
				       new Object[] {
					   serverEndpoint, constraints,
					   unfulfilledConstraints
				       });
			}
			return unfulfilledConstraints;
		    }
		} while (client != null);
	    } while (server != null);
	}
	UnsupportedConstraintException uce =
	    new UnsupportedConstraintException(
	    "Constraints are not supported: " + constraints);
	if (logger.isLoggable(Levels.FAILED)) {
	    logThrow(
		logger, Levels.FAILED,
		SslServerEndpoint.class, "checkConstraints",
		"check constraints for {0}\nwith {1}\nthrows",
		new Object[] { serverEndpoint, constraints },
		uce);
	}
	throw uce;
    }

    /**
     * Returns null if the constraints are not supported, else any integrity
     * constraints required or preferred by the arguments.
     */
    static InvocationConstraints getUnfulfilledConstraints(
	String cipherSuite,
	Principal client,
	Principal server,
	InvocationConstraints constraints)
    {
	boolean supported = false;
	for (int i = 2; --i >= 0; ) {
	    boolean integrity = i == 0;
	    ConnectionContext context = ConnectionContext.getInstance(
		cipherSuite, client, server, integrity,
		false /* clientSide */, constraints);
	    if (context != null) {
		if (context.getIntegrityRequired()) {
		    return INTEGRITY_REQUIRED;
		} else if (context.getIntegrityPreferred()) {
		    return INTEGRITY_PREFERRED;
		} else {
		    supported = true;
		}
	    }
	}
	return supported ? InvocationConstraints.EMPTY : null;
    }

    /* -- Implement ServerEndpoint -- */

    final Endpoint enumerateListenEndpoints(ListenContext listenContext)
	throws IOException
    {
	Exception exception = null;
	try {
	    String resolvedHost = this.serverHost;
	    if (resolvedHost == null) {
		InetAddress localAddr;
		try {
		    localAddr = (InetAddress) AccessController.doPrivileged(
			new PrivilegedExceptionAction() {
			    public Object run() throws UnknownHostException {
				return InetAddress.getLocalHost();
			    }
			});
		} catch (PrivilegedActionException e) {
		    exception = (Exception) e.getCause();
		    throw (UnknownHostException) exception;
		}
		SecurityManager sm = System.getSecurityManager();
		if (sm != null) {
		    try {
			sm.checkConnect(localAddr.getHostName(), -1);
		    } catch (SecurityException e) {
			exception = e;
			/* Throw new exception to not reveal host name */
			throw new SecurityException(
			    "Access to resolve local host denied");
		    }
		}
		resolvedHost = localAddr.getHostAddress();
	    }
	    Endpoint result = createEndpoint(
		resolvedHost,
		checkCookie(listenContext.addListenEndpoint(listenEndpoint)));
	    if (logger.isLoggable(Level.FINE)) {
		logger.log(Level.FINE,
			   "enumerate listen endpoints for {0}\nreturns {1}",
			   new Object[] { this, result });
	    }
	    return result;
	} finally {
	    if (exception != null && logger.isLoggable(Levels.FAILED)) {
		logThrow(
		    logger, Levels.FAILED,
		    SslServerEndpointImpl.class, "enumerateListenEndpoints",
		    "enumerate listen endpoints for {0}\nthrows",
		    new Object[] { this },
		    exception);
	    }
	}
    }

    /** Creates a listen endpoint for this server endpoint. */
    ListenEndpoint createListenEndpoint() {
	return new SslListenEndpoint();
    }

    /**
     * Creates an endpoint for this server endpoint corresponding to the
     * specified server host and listen cookie.
     */
    Endpoint createEndpoint(String serverHost, SslListenCookie cookie) {
	return SslEndpoint.getInstance(
	    serverHost, cookie.getPort(), socketFactory);
    }

    /**
     * Checks that the argument is a valid listen cookie for this server
     * endpoint.
     */
    private SslListenCookie checkCookie(ListenCookie cookie) {
	if (!(cookie instanceof SslListenCookie)) {
	    throw new IllegalArgumentException(
		"Cookie must be of type SslListenCookie: " + cookie);
	}
	SslListenCookie sslListenCookie = ((SslListenCookie) cookie);
	ServerEndpoint cookieServerEndpoint =
	    sslListenCookie.getServerEndpoint();
	if (!serverEndpoint.equals(cookieServerEndpoint)) {
	    throw new IllegalArgumentException(
		"Cookie has wrong server endpoint: found " +
		cookieServerEndpoint + ", expected " + serverEndpoint);
	}
	return sslListenCookie;
    }

    /**
     * Check for permission to listen on this endpoint, but only checking
     * socket permissions if checkSocket is true.
     */
    final void checkListenPermissions(boolean checkSocket) {
	SecurityManager sm = System.getSecurityManager();
	if (sm != null) {
	    if (checkSocket) {
		sm.checkListen(port);
	    }
	    if (listenPermissions != null) {
		for (int i = listenPermissions.length; --i >= 0; ) {
		    sm.checkPermission(listenPermissions[i]);
		}
	    }
	}
    }

    /** Implements ListenEndpoint */
    class SslListenEndpoint extends Utilities implements ListenEndpoint {

	/* inherit javadoc */
	public void checkPermissions() {
	    checkListenPermissions(true);
	}

	/* inherit javadoc */
	public ListenHandle listen(RequestDispatcher requestDispatcher)
	    throws IOException
	{
	    if (requestDispatcher == null) {
		throw new NullPointerException(
		    "Request dispatcher cannot be null");
	    }
	    checkCredentials();
	    ServerSocket serverSocket = serverSocketFactory != null
		? serverSocketFactory.createServerSocket(port)
		: new ServerSocket(port);
	    return createListenHandle(requestDispatcher, serverSocket);
	}

	/**
	 * Check that the subject has credentials for the principals specified
	 * when the server endpoint was created.
	 */
	private void checkCredentials() throws UnsupportedConstraintException {
	    if (serverSubject == null) {
		return;
	    }
	    checkListenPermissions(false);
	    Set principals = serverSubject.getPrincipals();
	    /* Keep track of progress; remove entry when check is done */
	    Map progress = new HashMap(serverPrincipals.size());
	    for (Iterator i = serverPrincipals.iterator(); i.hasNext(); ) {
		X500Principal p = (X500Principal) i.next();
		if (!principals.contains(p)) {
		    throw new UnsupportedConstraintException(
			"Missing principal: " + p);
		}
		progress.put(p, X500Principal.class);
	    }
	    X500PrivateCredential[] privateCredentials =
		(X500PrivateCredential[]) AccessController.doPrivileged(
		    new SubjectCredentials.GetAllPrivateCredentialsAction(
			serverSubject));
	    List certPaths =
		SubjectCredentials.getCertificateChains(serverSubject);
	    if (certPaths != null) {
		for (int i = certPaths.size(); --i >= 0; ) {
		    CertPath chain = (CertPath) certPaths.get(i);
		    X509Certificate firstCert = firstX509Cert(chain);
		    X500Principal p = firstCert.getSubjectX500Principal();
		    if (progress.containsKey(p)) {
			try {
			    checkValidity(chain, null);
			} catch (CertificateException e) {
			    progress.put(p, e);
			    continue;
			}
			progress.put(p, CertPath.class);
			for (int j = privateCredentials.length; --j >= 0; ) {
			    X509Certificate cert =
				privateCredentials[j].getCertificate();
			    if (firstCert.equals(cert)) {
				progress.remove(p);
				break;
			    }
			}
		    }
		}
	    }
	    if (!progress.isEmpty()) {
		X500Principal p =
		    (X500Principal) progress.keySet().iterator().next();
		Object result = progress.get(p);
		if (result == X500Principal.class) {
		    throw new UnsupportedConstraintException(
			"Missing public credentials: " + p);
		} else if (result == CertPath.class) {
		    throw new UnsupportedConstraintException(
			"Missing private credentials: " + p);
		} else {
		    throw new UnsupportedConstraintException(
			"Problem with certificates: " + p + "\n" + result,
			(CertificateException) result);
		}
	    }
	}

	/**
	 * Creates a listen handle for the specified dispatcher and server
	 * socket.
	 */
	ListenHandle createListenHandle(RequestDispatcher requestDispatcher,
					ServerSocket serverSocket)
	    throws IOException
	{
	    return new SslListenHandle(requestDispatcher, serverSocket);
	}

	/** Returns a hash code value for this object. */
	public int hashCode() {
	    return getClass().hashCode()
		^ System.identityHashCode(serverSubject)
		^ (serverPrincipals == null ? 0 : serverPrincipals.hashCode())
		^ port
		^ (serverSocketFactory != null
		   ? serverSocketFactory.hashCode() : 0);
	}

	/**
	 * Two instances of this class are equal if they have the same actual
	 * class; have server subjects that compare equal using
	 * <code>==</code>; have server principals that are either both
	 * <code>null</code> or compare equal using <code>equals</code>; have
	 * the same port; and have server socket factories that are both null,
	 * or have the same actual class and are equal. Note that the server
	 * host and socket factory are ignored.
	 */
	public boolean equals(Object object) {
	    if (this == object) {
		return true;
	    } else if (object == null || getClass() != object.getClass()) {
		return false;
	    }
	    SslServerEndpointImpl other =
		((SslListenEndpoint) object).getImpl();
	    return serverSubject == other.serverSubject
		&& safeEquals(serverPrincipals, other.serverPrincipals)
		&& port == other.port
		&& Util.sameClassAndEquals(serverSocketFactory,
					   other.serverSocketFactory);
	}

	/**
	 * Returns the SslServerEndpointImpl associated with this listen
	 * endpoint.
	 */
	private SslServerEndpointImpl getImpl() {
	    return SslServerEndpointImpl.this;
	}
    }

    /** Implements ListenHandle */
    class SslListenHandle extends Utilities implements ListenHandle {

	/** The request handler */
	private final RequestDispatcher requestDispatcher;

	/** The server socket used to accept connections */
	final ServerSocket serverSocket;

	/** The security context at the time of the listen. */
	private final SecurityContext securityContext;

	/** Whether the listen handle has been closed. */
	private boolean closed = false;

	/** Set of connections created by this listen handle */
	private final Set connections = new HashSet();

	/** Used to throttle accept failures */
	private long acceptFailureTime = 0;
	private int acceptFailureCount;

	/** Creates a listen handle */
	SslListenHandle(RequestDispatcher requestDispatcher,
			ServerSocket serverSocket)
	    throws IOException
	{
	    this.requestDispatcher = requestDispatcher;
	    this.serverSocket = serverSocket;
	    securityContext = Security.getContext();
	    systemExecutor.execute(
		new Runnable() {
		    public void run() {
			acceptLoop();
		    }
		},
		toString());
	    logger.log(Level.FINE, "created {0}", this);
	}

	/** Handles new socket connections. */
	final void acceptLoop() {
	    while (true) {
		Socket socket = null;
		Throwable exception;
		SslServerConnection connection = null;
		try {
		    socket = serverSocket.accept();

		    /* Send data without delay */
		    try {
			socket.setTcpNoDelay(true);
		    } catch (SocketException e) {
		    }
		    /*
		     * Send periodic pings so we can tell if the connection
		     * dies.
		     */
		    try {
			socket.setKeepAlive(true);
		    } catch (SocketException e) {
		    }

		    connection = serverConnection(socket);
		    synchronized (this) {
			if (closed) {
			    try {
				connection.closeInternal(
				    false /* removeFromListener */);
			    } catch (IOException e) {
			    }
			    break;
			}
			connections.add(connection);
		    }
		    final SslServerConnection finalConnection = connection;
		    AccessController.doPrivileged(
			securityContext.wrap(
			    new PrivilegedAction() {
				public Object run() {
				    handleConnection(
					finalConnection, requestDispatcher);
				    return null;
				}
			    }),
			securityContext.getAccessControlContext());
		    continue;
		} catch (Exception e) {
		    exception = e;
		} catch (Error e) {
		    exception = e;
		}
		/* Problem occurred */
		boolean closedSync;
		synchronized (this) {
		    closedSync = closed;
		}
		if (!closedSync && logger.isLoggable(Level.INFO)) {
		    String msg = "handling connection {0} throws";
		    Object arg = connection;
		    if (connection == null) {
			msg = "accepting connection on {0} throws";
			arg = this;
		    }
		    logThrow(logger, Level.INFO,
			     SslListenHandle.class, "acceptLoop",
			     msg, new Object[] { arg }, exception);
		}
		if (socket != null) {
		    try {
			socket.close();
		    } catch (IOException e) {
		    }
		}
		if (closedSync) {
		    break;
		}
		boolean continueAccept =
		    (exception instanceof Exception ||
		     exception instanceof OutOfMemoryError ||
		     exception instanceof NoClassDefFoundError) &&
		    continueAfterAcceptFailure(exception);
		if (!continueAccept) {
		    if (exception instanceof Error) {
			throw (Error) exception;
		    } else {
			return;
		    }
		}
	    }
	}

	/**
	 * Throttles the accept loop after ServerSocket.accept throws
	 * an exception, and decides whether to continue at all.  The
	 * current code is borrowed from the JRMP implementation; it
	 * always continues, but it delays the loop after bursts of
	 * failed accepts.
	 */
	private boolean continueAfterAcceptFailure(Throwable t) {
	    /*
	     * If we get a burst of NFAIL failures in NMSEC milliseconds,
	     * then wait for ten seconds.  This is to ensure that individual
	     * failures don't cause hiccups, but sustained failures don't
	     * hog the CPU in futile accept-fail-retry looping.
	     */
	    final int NFAIL = 10;
	    final int NMSEC = 5000;
	    long now = System.currentTimeMillis();
	    if (acceptFailureTime == 0L ||
		(now - acceptFailureTime) > NMSEC)
	    {
		// failure time is very old, or this is first failure
		acceptFailureTime = now;
		acceptFailureCount = 0;
	    } else {
		// failure window was started recently
		acceptFailureCount++;
		if (acceptFailureCount >= NFAIL) {
		    try {
			Thread.sleep(10000);
		    } catch (InterruptedException ignore) {
		    }
		    // no need to reset counter/timer
		}
	    }
	    return true;
	}

	/** Returns a string representation of this object. */
	public String toString() {
	    return getClassName(this) + "[" +
		serverHost + ":" + getPort() + "]";
	}

	/** Returns a connection for the specified socket. */
	SslServerConnection serverConnection(Socket socket)
	    throws IOException
	{
	    return new SslServerConnection(this, socket);
	}

	/** Handles a newly accepted server connection. */
	void handleConnection(SslServerConnection connection,
			      RequestDispatcher requestDispatcher)
	{
	    serverConnectionManager.handleConnection(
		connection, requestDispatcher);
	}

	/** Returns the port on which this handle is listening. */
	private int getPort() {
	    return serverSocket.getLocalPort();
	}

	/* inherit javadoc */
	public synchronized void close() {
	    if (!closed) {
		logger.log(Level.FINE, "closing {0}", this);
		closed = true;
		try {
		    serverSocket.close();
		} catch (IOException e) {
		}
		for (Iterator i = connections.iterator(); i.hasNext(); ) {
		    SslServerConnection connection =
			(SslServerConnection) i.next();
		    try {
			/*
			 * Call closeInternal because close would call back to
			 * remove the connection and invalidate the iterator.
			 */
			connection.closeInternal(
			    false /* removeFromListener */);
		    } catch (IOException e) {
		    }
		    i.remove();
		}
	    }
	}

	/**
	 * Called when a connection is closed without a call to close on this
	 * listener.
	 */
	synchronized void noteConnectionClosed(
	    SslServerConnection connection)
	{
	    connections.remove(connection);
	}

	/* inherit javadoc */
	public ListenCookie getCookie() {
	    return new SslListenCookie(getPort());
	}
    }

    /** Implements ListenCookie */
    final class SslListenCookie implements ListenCookie {
	private final int port;

	SslListenCookie(int port) {
	    this.port = port;
	}

	/** Returns the port on which the associated handle is listening. */
	final int getPort() {
	    return port;
	}

	/**
	 * Returns the server endpoint associated with this listen cookie.
	 */
	final ServerEndpoint getServerEndpoint() {
	    return serverEndpoint;
	}
    }

    /** Implements ServerConnection */
    class SslServerConnection extends Utilities implements ServerConnection {

	/** The listen handle that accepted this connection */
	private final SslListenHandle listenHandle;

	/** The JSSE socket used for communication */
	final SSLSocket sslSocket;

	/** The inbound request handle for this connection. */
	private final InboundRequestHandle requestHandle =
	    new InboundRequestHandle() { };

	/**
	 * The session for this connection's socket, or null if not retrieved
	 * yet.  Check that the current session matches to prevent new
	 * handshakes.
	 */
	private SSLSession session;

	/**
	 * The client subject -- depends on session being set.  This instance
	 * is read-only. 
	 */
	private Subject clientSubject;

	/** The client principal -- depends on session being set. */
	private X500Principal clientPrincipal;

	/** The server principal -- depends on session being set. */
	private X500Principal serverPrincipal;

	/**
	 * The authentication permission required for this connection, or null
	 * if the server is anonymous -- depends on session being set.
	 */
	private AuthenticationPermission authPermission;

	/** The cipher suite -- depends on session being set. */
	private String cipherSuite;

	/** True if the connection has been closed. */
	boolean closed;

	/** Creates a server connection */
	SslServerConnection(SslListenHandle listenHandle, Socket socket)
	    throws IOException
	{
	    this.listenHandle = listenHandle;

	    sslSocket = (SSLSocket) getSSLSocketFactory().createSocket(
		socket, socket.getInetAddress().getHostName(),
		socket.getPort(), true /* autoClose */);
	    sslSocket.setEnabledCipherSuites(getSupportedCipherSuites());

	    /* Need to put in server mode before requesting client auth. */
	    sslSocket.setUseClientMode(false);
	    sslSocket.setWantClientAuth(true);

	    logger.log(Level.FINE, "created {0}", this);
	}

	/* inherit javadoc */
	public String toString() {
	    String sessionString;
	    synchronized (this) {
		sessionString = session == null ? "" : session + ", ";
	    }
	    return getClassName(this) + "[" +
		sessionString + 
		serverHost + ":" + sslSocket.getLocalPort() + "<=" +
		sslSocket.getInetAddress().getHostName() + ":" +
		sslSocket.getPort() +
		"]";
	}

	/* -- Implement ServerConnection -- */

	/* inherit javadoc */
	public InputStream getInputStream() throws IOException {
	    return sslSocket.getInputStream();
	}

	/* inherit javadoc */
	public OutputStream getOutputStream() throws IOException {
	    return sslSocket.getOutputStream();
	}

	/* inherit javadoc */
	public SocketChannel getChannel() {
	    return null;
	}

	/* inherit javadoc */
	public InboundRequestHandle processRequestData(InputStream in,
						       OutputStream out)
	{
	    if (in == null || out == null) {
		throw new NullPointerException("Arguments cannot be null");
	    }
	    SecurityException exception;
	    try {
		long now = System.currentTimeMillis();
		decacheSession();
		long create = session.getCreationTime();
		long expiration = create + maxServerSessionDuration;
		/* Check for rollover */
		if (expiration < create) {
		    expiration = Long.MAX_VALUE;
		}
		if (expiration < now) {
		    /*
		     * The session has expired.  Invalidate the session so that
		     * it is not reused, close the connection, and throw an
		     * exception.
		     */
		    session.invalidate();
		    throw new SecurityException("Session has expired");
		}
		if (serverPrincipal != null) {
		    getAuthManager().checkCredentials(session, clientSubject);
		}
		return requestHandle;
	    } catch (SecurityException e) {
		exception = e;
	    } catch (GeneralSecurityException e) {
		exception = new SecurityException(e.getMessage());
		exception.initCause(e);
	    }
	    try {
		close();
	    } catch (IOException e2) {
	    }
	    if (logger.isLoggable(Levels.FAILED)) {
		logThrow(logger, Levels.FAILED,
			 SslServerConnection.class, "processRequestData",
			 "process request data for session {0}\nclient {1}\n" +
			 "throws",
			 new Object[] { session, subjectString(clientSubject) },
			 exception);
	    }
	    throw exception;
	}

	/**
	 * Make sure the cached session is up to date, and set session-related
	 * fields if needed.
	 */
	private void decacheSession() {
	    synchronized (this) {
		SSLSession socketSession = sslSocket.getSession();
		if (session == socketSession) {
		    return;
		} else if (session != null) {
		    /*
		     * We disable session creation as soon as we notice the
		     * first session, but it is possible that a second
		     * handshake could have started by then, so check that we
		     * have the same session.  -tjb[31.Jan.2003]
		     */
		    throw new SecurityException(
			"New handshake occurred on socket");
		}
		session = socketSession;
		sslSocket.setEnableSessionCreation(false);
		cipherSuite = session.getCipherSuite();
		if ("NULL".equals(getKeyExchangeAlgorithm(cipherSuite))) {
		    throw new SecurityException("Handshake failed");
		}
		clientSubject = getClientSubject(sslSocket);
		clientPrincipal = clientSubject != null
		    ? ((X500Principal)
		       clientSubject.getPrincipals().iterator().next())
		    : null;
		X509Certificate serverCert =
		    getAuthManager().getServerCertificate(session);
		serverPrincipal = serverCert != null
		    ? serverCert.getSubjectX500Principal() : null;
		if (serverPrincipal != null) {
		    authPermission = new AuthenticationPermission(
			Collections.singleton(serverPrincipal),
			(clientPrincipal != null
			 ? Collections.singleton(clientPrincipal) : null),
			"accept");
		}
	    }
	}

	/**
	 * Returns the read-only <code>Subject</code> associated with the
	 * client host connected to the other end of the connection on the
	 * specified <code>SSLSocket</code>.  Returns null if the client is
	 * anonymous.
	 */
	private Subject getClientSubject(SSLSocket socket) {
	    SSLSession session = socket.getSession();
	    try {
		Certificate[] certificateChain = session.getPeerCertificates();
		if (certificateChain != null
		    && certificateChain.length > 0
		    && certificateChain[0] instanceof X509Certificate)
		{
		    X509Certificate cert =
			(X509Certificate) certificateChain[0];
		    return new Subject(
			true,
			Collections.singleton(cert.getSubjectX500Principal()),
			Collections.singleton(
			    getCertFactory().generateCertPath(
				Arrays.asList(certificateChain))),
			Collections.EMPTY_SET);
		}
	    } catch (SSLPeerUnverifiedException e) {
	    } catch (CertificateException e) {
		logger.log(Levels.HANDLED,
			   "get client subject caught exception", e);
	    }
	    return null;
	}

	/* inherit javadoc */
	public void checkPermissions(InboundRequestHandle requestHandle) {
	    check(requestHandle);
	    SecurityManager sm = System.getSecurityManager();
	    if (sm != null) {
		try {
		    sm.checkAccept(sslSocket.getInetAddress().getHostName(),
				   sslSocket.getPort());
		    if (authPermission != null) {
			sm.checkPermission(authPermission);
		    }
		} catch (SecurityException e) {
		    if (logger.isLoggable(Levels.FAILED)) {
			logThrow(logger, Levels.FAILED,
				 SslServerConnection.class, "checkPermissions",
				 "check permissions for {0} throws",
				 new Object[] { this },
				 e);
		    }
		    throw e;
		}
	    }
	}

	/**
	 * Checks that the argument is the request handle for this connection.
	 */
	private void check(InboundRequestHandle requestHandle) {
	    if (requestHandle == null) {
		throw new NullPointerException("Request handle cannot be null");
	    } else if (requestHandle != this.requestHandle) {
		throw new IllegalArgumentException(
		    "Wrong request handle: found " + requestHandle +
		    ", expected " + this.requestHandle);
	    }
	}

	/* inherit javadoc */
	public InvocationConstraints checkConstraints(
	    InboundRequestHandle requestHandle,
	    InvocationConstraints constraints)
	    throws UnsupportedConstraintException
	{
	    check(requestHandle);
	    if (constraints == null) {
		throw new NullPointerException("Constraints cannot be null");
	    }
	    InvocationConstraints result = getUnfulfilledConstraints(
		cipherSuite, clientPrincipal, serverPrincipal, constraints);
	    if (result == null) {
		UnsupportedConstraintException uce = 
		    new UnsupportedConstraintException(
			"Constraints are not supported: " + constraints);
		if (logger.isLoggable(Levels.FAILED)) {
		    logThrow(logger, Levels.FAILED,
			     SslServerConnection.class, "checkConstraints",
			     "check constraints for {0}\nwith {1}\n" +
			     "throws",
			     new Object[] {
				 SslServerConnection.this, constraints
			     },
			     uce);
		}
		throw uce;
	    }
	    if (logger.isLoggable(Level.FINE)) {
		logger.log(Level.FINE,
			   "check constraints for {0}\nwith {1}\nreturns {2}",
			   new Object[] {
			       SslServerConnection.this, constraints,
			       result
			   });
	    }
	    return result;
	}

	/* inherit javadoc */
	public void populateContext(InboundRequestHandle requestHandle,
				    Collection context)
	{
	    check(requestHandle);
	    Util.populateContext(context, sslSocket.getInetAddress());
	    Util.populateContext(context, clientSubject);
	}

	/* inherit javadoc */
	public void close() throws IOException {
	    closeInternal(true);
	}

	/**
	 * Like close, but does not call noteConnectionClosed unless
	 * removeFromListener is true.
	 */
	void closeInternal(boolean removeFromListener)
	    throws IOException
	{
	    synchronized (this) {
		if (closed) {
		    return;
		}
		logger.log(Level.FINE, "closing {0}", this);
		closed = true;
		sslSocket.close();
	    }
	    if (removeFromListener) {
		listenHandle.noteConnectionClosed(this);
	    }
	}
    }
}
