/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.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://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.harmony.xnet.provider.jsse;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.security.PrivateKey;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPublicKey;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.net.ssl.HandshakeCompletedEvent;
import javax.net.ssl.HandshakeCompletedListener;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSession;

import org.apache.harmony.security.provider.cert.X509CertImpl;
import org.bouncycastle.openssl.PEMWriter;

/**
 * Implementation of the class OpenSSLSocketImpl
 * based on OpenSSL. The JNI native interface for some methods
 * of this this class are defined in the file:
 * org_apache_harmony_xnet_provider_jsse_OpenSSLSocketImpl.cpp
 * 
 * This class only supports SSLv3 and TLSv1. This should be documented elsewhere
 * later, for example in the package.html or a separate reference document. 
 */
public class OpenSSLSocketImpl extends javax.net.ssl.SSLSocket {
    private int ssl_ctx;
    private int ssl;
    private InputStream is;
    private OutputStream os;
    private final Object handshakeLock = new Object();
    private Object readLock = new Object();
    private Object writeLock = new Object();
    private SSLParameters sslParameters;
    private OpenSSLSessionImpl sslSession;
    private Socket socket;
    private boolean autoClose;
    private boolean handshakeStarted = false;
    private ArrayList<HandshakeCompletedListener> listeners;
    private long ssl_op_no = 0x00000000L;
    private int timeout = 0;
    private InetSocketAddress address;

    private static final String[] supportedProtocols = new String[] {
        "SSLv3",
        "TLSv1"
        };

    private static final AtomicInteger instanceCount = new AtomicInteger(0);

    public static int getInstanceCount() {
        return instanceCount.get();
    }

    private static void updateInstanceCount(int amount) {
        instanceCount.addAndGet(amount);
    }

    /**
     * Initialize OpenSSL library.
     */
    private native static void nativeinitstatic();

    static {
        nativeinitstatic();
    }

    private native void nativeinit(String privatekey, String certificate, byte[] seed);

    /**
     * Initialize the SSL socket and set the certificates for the
     * future handshaking.
     */
    private void init() throws IOException {
        String alias = sslParameters.getKeyManager().chooseClientAlias(new String[] { "RSA" }, null, null);
        if (alias != null) {
            PrivateKey privateKey = sslParameters.getKeyManager().getPrivateKey(alias);
            X509Certificate[] certificates = sslParameters.getKeyManager().getCertificateChain(alias);

            ByteArrayOutputStream privateKeyOS = new ByteArrayOutputStream();
            PEMWriter privateKeyPEMWriter = new PEMWriter(new OutputStreamWriter(privateKeyOS));
            privateKeyPEMWriter.writeObject(privateKey);
            privateKeyPEMWriter.close();

            ByteArrayOutputStream certificateOS = new ByteArrayOutputStream();
            PEMWriter certificateWriter = new PEMWriter(new OutputStreamWriter(certificateOS));

            for (int i = 0; i < certificates.length; i++) {
                certificateWriter.writeObject(certificates[i]);
            }
            certificateWriter.close();

            nativeinit(privateKeyOS.toString(), certificateOS.toString(),
                    sslParameters.getSecureRandomMember() != null ?
                    sslParameters.getSecureRandomMember().generateSeed(1024) : null);
        } else {
            nativeinit(null, null,
                    sslParameters.getSecureRandomMember() != null ?
                    sslParameters.getSecureRandomMember().generateSeed(1024) : null);
        }
    }

    /**
     * Class constructor with 2 parameters
     *
     * @param sslParameters Parameters for the SSL
     *            context
     * @param ssl_op_no Parameter to set the enabled
     *            protocols
     * @throws IOException if network fails
     */
    protected OpenSSLSocketImpl(SSLParameters sslParameters, long ssl_op_no) throws IOException {
        super();
        this.sslParameters = sslParameters;
        this.ssl_op_no = ssl_op_no;
        updateInstanceCount(1);
    }

    /**
     * Class constructor with 1 parameter
     *
     * @param sslParameters Parameters for the SSL
     *            context
     * @throws IOException if network fails
     */
    protected OpenSSLSocketImpl(SSLParameters sslParameters) throws IOException {
        super();
        this.sslParameters = sslParameters;
        init();
        updateInstanceCount(1);
    }

    /**
     * Class constructor with 3 parameters
     *
     * @throws IOException if network fails
     * @throws java.net.UnknownHostException host not defined
     */
    protected OpenSSLSocketImpl(String host, int port,
            SSLParameters sslParameters)
        throws IOException {
        super(host, port);
        this.sslParameters = sslParameters;
        init();
        updateInstanceCount(1);
    }


    /**
     * Class constructor with 3 parameters: 1st is InetAddress
     *
     * @throws IOException if network fails
     * @throws java.net.UnknownHostException host not defined
     */
    protected OpenSSLSocketImpl(InetAddress address, int port,
            SSLParameters sslParameters)
        throws IOException {
        super(address, port);
        this.sslParameters = sslParameters;
        init();
        updateInstanceCount(1);
    }


    /**
     * Class constructor with 5 parameters: 1st is host
     *
     * @throws IOException if network fails
     * @throws java.net.UnknownHostException host not defined
     */
    protected OpenSSLSocketImpl(String host, int port, InetAddress clientAddress,
            int clientPort, SSLParameters sslParameters)
        throws IOException {
        super(host, port, clientAddress, clientPort);
        this.sslParameters = sslParameters;
        init();
        updateInstanceCount(1);
    }

    /**
     * Class constructor with 5 parameters: 1st is InetAddress
     *
     * @throws IOException if network fails
     * @throws java.net.UnknownHostException host not defined
     */
    protected OpenSSLSocketImpl(InetAddress address, int port,
            InetAddress clientAddress, int clientPort, SSLParameters sslParameters)
        throws IOException {
        super(address, port, clientAddress, clientPort);
        this.sslParameters = sslParameters;
        init();
        updateInstanceCount(1);
    }

    /**
     * Constructor with 5 parameters: 1st is socket. Enhances an existing socket
     * with SSL functionality.
     *
     * @throws IOException if network fails
     */
    protected OpenSSLSocketImpl(Socket socket, String host, int port,
            boolean autoClose, SSLParameters sslParameters) throws IOException {
        super();
        this.socket = socket;
        this.timeout = socket.getSoTimeout();
        this.address = new InetSocketAddress(host, port);
        this.autoClose = autoClose;
        this.sslParameters = sslParameters;
        init();
        updateInstanceCount(1);
    }

    /**
     * Adds OpenSSL functionality to the existing socket and starts the SSL
     * handshaking.
     */
    private native boolean nativeconnect(int ctx, Socket sock, boolean client_mode, int sslsession) throws IOException;
    private native int nativegetsslsession(int ssl);
    private native String nativecipherauthenticationmethod();

    /**
     * Gets the suitable session reference from the session cache container.
     *
     * @return OpenSSLSessionImpl
     */
    private OpenSSLSessionImpl getCachedClientSession() {
        if (super.getInetAddress() == null ||
                super.getInetAddress().getHostAddress() == null ||
                super.getInetAddress().getHostName() == null) {
            return null;
        }
        ClientSessionContext sessionContext
                = sslParameters.getClientSessionContext();
        return (OpenSSLSessionImpl) sessionContext.getSession(
                super.getInetAddress().getHostName(),
                super.getPort());
    }

    /**
     * Ensures that logger is lazily loaded. The outer class seems to load
     * before logging is ready.
     */
    static class LoggerHolder {
        static final Logger logger = Logger.getLogger(
                OpenSSLSocketImpl.class.getName());
    }

    /**
     * Starts a TLS/SSL handshake on this connection using some native methods
     * from the OpenSSL library. It can negotiate new encryption keys, change
     * cipher suites, or initiate a new session. The certificate chain is
     * verified if the correspondent property in java.Security is set. All
     * listensers are notified at the end of the TLS/SSL handshake.
     *
     * @throws <code>IOException</code> if network fails
     */
    public synchronized void startHandshake() throws IOException {
        synchronized (handshakeLock) {
            if (!handshakeStarted) {
                handshakeStarted = true;
            } else {
                return;
            }
        }

        OpenSSLSessionImpl session = getCachedClientSession();

        // Check if it's allowed to create a new session (default is true)
        if (session == null && !sslParameters.getEnableSessionCreation()) {
            throw new SSLHandshakeException("SSL Session may not be created");
        } else {
            Socket socket = this.socket != null ? this.socket : this;
            int sessionId = session != null ? session.session : 0;
            boolean reusedSession;
            synchronized (OpenSSLSocketImpl.class) {
                reusedSession = nativeconnect(ssl_ctx, socket,
                        sslParameters.getUseClientMode(), sessionId);
            }
            if (reusedSession) {
                // nativeconnect shouldn't return true if the session is not
                // done
                session.lastAccessedTime = System.currentTimeMillis();
                sslSession = session;

                LoggerHolder.logger.fine("Reused cached session for "
                        + getInetAddress().getHostName() + ".");
            } else {
                if (session != null) {
                    LoggerHolder.logger.fine("Reuse of cached session for "
                            + getInetAddress().getHostName() + " failed.");
                } else {
                    LoggerHolder.logger.fine("Created new session for "
                            + getInetAddress().getHostName() + ".");
                }

                ClientSessionContext sessionContext
                        = sslParameters.getClientSessionContext();
                synchronized (OpenSSLSocketImpl.class) {
                    sessionId = nativegetsslsession(ssl);
                }
                if (address == null) {
                    sslSession = new OpenSSLSessionImpl(
                            sessionId, sslParameters,
                            super.getInetAddress().getHostName(),
                            super.getPort(), sessionContext);
                } else  {
                    sslSession = new OpenSSLSessionImpl(
                            sessionId, sslParameters,
                            address.getHostName(), address.getPort(),
                            sessionContext);
                }

                try {
                    X509Certificate[] peerCertificates = (X509Certificate[])
                            sslSession.getPeerCertificates();

                    if (peerCertificates == null
                            || peerCertificates.length == 0) {
                        throw new SSLException("Server sends no certificate");
                    }

                    String authMethod;
                    synchronized (OpenSSLSocketImpl.class) {
                        authMethod = nativecipherauthenticationmethod();
                    }
                    sslParameters.getTrustManager().checkServerTrusted(
                            peerCertificates,
                            authMethod);
                    sessionContext.putSession(sslSession);
                } catch (CertificateException e) {
                    throw new SSLException("Not trusted server certificate", e);
                }
            }
        }

        if (listeners != null) {
            // notify the listeners
            HandshakeCompletedEvent event =
                new HandshakeCompletedEvent(this, sslSession);
            int size = listeners.size();
            for (int i = 0; i < size; i++) {
                listeners.get(i).handshakeCompleted(event);
            }
        }
    }

    // To be synchronized because of the verify_callback
    native synchronized void nativeaccept(Socket socketObject, int m_ctx, boolean client_mode);

    /**
     * Performs the first part of a SSL/TLS handshaking process with a given
     * 'host' connection and initializes the SSLSession.
     */
    protected void accept(int m_ctx, boolean client_mode) throws IOException {
        // Must be set because no handshaking is necessary
        // in this situation
        handshakeStarted = true;

        nativeaccept(this, m_ctx, client_mode);

        ServerSessionContext sessionContext
                = sslParameters.getServerSessionContext();
        sslSession = new OpenSSLSessionImpl(nativegetsslsession(ssl),
                sslParameters, super.getInetAddress().getHostName(),
                super.getPort(), sessionContext);
        sslSession.lastAccessedTime = System.currentTimeMillis();
        sessionContext.putSession(sslSession);
    }

    /**
     * Callback methode for the OpenSSL native certificate verification process.
     *
     * @param bytes Byte array containing the cert's
     *            information.
     * @return 0 if the certificate verification fails or 1 if OK
     */
    @SuppressWarnings("unused")
    private int verify_callback(byte[][] bytes) {
        try {
            X509Certificate[] peerCertificateChain
                    = new X509Certificate[bytes.length];
            for(int i = 0; i < bytes.length; i++) {
                peerCertificateChain[i] =
                    new X509CertImpl(javax.security.cert.X509Certificate.getInstance(bytes[i]).getEncoded());
            }

            try {
                // TODO "null" String
                sslParameters.getTrustManager().checkClientTrusted(peerCertificateChain, "null");
            } catch (CertificateException e) {
                throw new AlertException(AlertProtocol.BAD_CERTIFICATE,
                        new SSLException("Not trusted server certificate", e));
            }
        } catch (javax.security.cert.CertificateException e) {
            return 0;
        } catch (IOException e) {
            return 0;
        }
        return 1;
    }

    /**
     * Returns an input stream for this SSL socket using native calls to the
     * OpenSSL library.
     *
     * @return: an input stream for reading bytes from this socket.
     * @throws: <code>IOException</code> if an I/O error occurs when creating
     *          the input stream, the socket is closed, the socket is not
     *          connected, or the socket input has been shutdown.
     */
    public InputStream getInputStream() throws IOException {
        synchronized(this) {
            if (is == null) {
                is = new SSLInputStream();
            }

            return is;
        }
    }

    /**
     * Returns an output stream for this SSL socket using native calls to the
     * OpenSSL library.
     *
     * @return an output stream for writing bytes to this socket.
     * @throws <code>IOException</code> if an I/O error occurs when creating
     *             the output stream, or no connection to the socket exists.
     */
    public OutputStream getOutputStream() throws IOException {
        synchronized(this) {
            if (os == null) {
                os = new SSLOutputStream();
            }

            return os;
        }
    }

    /**
     * This method is not supported for this SSLSocket implementation.
     */
    public void shutdownInput() throws IOException {
        throw new UnsupportedOperationException(
        "Method shutdownInput() is not supported.");
    }

    /**
     * This method is not supported for this SSLSocket implementation.
     */
    public void shutdownOutput() throws IOException {
        throw new UnsupportedOperationException(
        "Method shutdownOutput() is not supported.");
    }

    /**
     * Reads with the native SSL_read function from the encrypted data stream
     * @return -1 if error or the end of the stream is reached.
     */
    private native int nativeread(int timeout) throws IOException;
    private native int nativeread(byte[] b, int off, int len, int timeout) throws IOException;

    /**
     * This inner class provides input data stream functionality
     * for the OpenSSL native implementation. It is used to
     * read data received via SSL protocol.
     */
    private class SSLInputStream extends InputStream {
        SSLInputStream() throws IOException {
            /**
            /* Note: When startHandshake() throws an exception, no
             * SSLInputStream object will be created.
             */
            OpenSSLSocketImpl.this.startHandshake();
        }

        /**
         * Reads one byte. If there is no data in the underlying buffer,
         * this operation can block until the data will be
         * available.
         * @return read value.
         * @throws <code>IOException</code>
         */
        public int read() throws IOException {
            synchronized(readLock) {
                return OpenSSLSocketImpl.this.nativeread(timeout);
            }
        }

        /**
         * Method acts as described in spec for superclass.
         * @see java.io.InputStream#read(byte[],int,int)
         */
        public int read(byte[] b, int off, int len) throws IOException {
            synchronized(readLock) {
                return OpenSSLSocketImpl.this.nativeread(b, off, len, timeout);
            }
        }
    }

    /**
     * Writes with the native SSL_write function to the encrypted data stream.
     */
    private native void nativewrite(int b) throws IOException;
    private native void nativewrite(byte[] b, int off, int len) throws IOException;

    /**
     * This inner class provides output data stream functionality
     * for the OpenSSL native implementation. It is used to
     * write data according to the encryption parameters given in SSL context.
     */
    private class SSLOutputStream extends OutputStream {
        SSLOutputStream() throws IOException {
            /**
            /* Note: When startHandshake() throws an exception, no
             * SSLInputStream object will be created.
             */
            OpenSSLSocketImpl.this.startHandshake();
        }

        /**
         * Method acts as described in spec for superclass.
         * @see java.io.OutputStream#write(int)
         */
        public void write(int b) throws IOException {
            synchronized(writeLock) {
                OpenSSLSocketImpl.this.nativewrite(b);
            }
        }

        /**
         * Method acts as described in spec for superclass.
         * @see java.io.OutputStream#write(byte[],int,int)
         */
        public void write(byte[] b, int start, int len) throws IOException {
            synchronized(writeLock) {
                OpenSSLSocketImpl.this.nativewrite(b, start, len);
            }
        }
    }


    /**
     * The SSL session used by this connection is returned. The SSL session
     * determines which cipher suite should be used by all connections within
     * that session and which identities have the session's client and server.
     * This method starts the SSL handshake.
     * @return the SSLSession.
     * @throws <code>IOException</code> if the handshake fails
     */
    public SSLSession getSession() {
        try {
            startHandshake();
        } catch (IOException e) {
            Logger.getLogger(getClass().getName()).log(Level.WARNING,
                    "Error negotiating SSL connection.", e);

            // return an invalid session with
            // invalid cipher suite of "SSL_NULL_WITH_NULL_NULL"
            return SSLSessionImpl.NULL_SESSION;
        }
        return sslSession;
    }

    /**
     * Registers a listener to be notified that a SSL handshake
     * was successfully completed on this connection.
     * @throws <code>IllegalArgumentException</code> if listener is null.
     */
    public void addHandshakeCompletedListener(
            HandshakeCompletedListener listener) {
        if (listener == null) {
            throw new IllegalArgumentException("Provided listener is null");
        }
        if (listeners == null) {
            listeners = new ArrayList();
        }
        listeners.add(listener);
    }

    /**
     * The method removes a registered listener.
     * @throws IllegalArgumentException if listener is null or not registered
     */
    public void removeHandshakeCompletedListener(
            HandshakeCompletedListener listener) {
        if (listener == null) {
            throw new IllegalArgumentException("Provided listener is null");
        }
        if (listeners == null) {
            throw new IllegalArgumentException(
                    "Provided listener is not registered");
        }
        if (!listeners.remove(listener)) {
            throw new IllegalArgumentException(
                    "Provided listener is not registered");
        }
    }

    /**
     * Returns true if new SSL sessions may be established by this socket.
     *
     * @return true if the session may be created; false if a session already
     *         exists and must be resumed.
     */
    public boolean getEnableSessionCreation() {
        return sslParameters.getEnableSessionCreation();
    }

    /**
     * Set a flag for the socket to inhibit or to allow the creation of a new
     * SSL sessions. If the flag is set to false, and there are no actual
     * sessions to resume, then there will be no successful handshaking.
     *
     * @param flag true if session may be created; false
     *            if a session already exists and must be resumed.
     */
    public void setEnableSessionCreation(boolean flag) {
        sslParameters.setEnableSessionCreation(flag);
    }

    /**
     * Gets all available ciphers from the current OpenSSL library.
     * Needed by OpenSSLSocketFactory too.
     */
    static native String[] nativegetsupportedciphersuites();

    /**
     * The names of the cipher suites which could be used by the SSL connection
     * are returned.
     * @return an array of cipher suite names
     */
    public String[] getSupportedCipherSuites() {
        return nativegetsupportedciphersuites();
    }

    private native String[] nativegetenabledciphersuites();

    /**
     * The names of the cipher suites that are in use in the actual the SSL
     * connection are returned.
     *
     * @return an array of cipher suite names
     */
    public String[] getEnabledCipherSuites() {
        return nativegetenabledciphersuites();
    }

    /**
     * Calls the SSL_CTX_set_cipher_list(...) OpenSSL function with the passed
     * char array.
     */
    private native void nativesetenabledciphersuites(String controlString);

    private boolean findSuite(String suite) {
        String[] supportedCipherSuites = nativegetsupportedciphersuites();
        for(int i = 0; i < supportedCipherSuites.length; i++)
            if (supportedCipherSuites[i].equals(suite)) return true;
        throw new IllegalArgumentException("Protocol " + suite +
        " is not supported.");
    }

    /**
     * This method enables the cipher suites listed by
     * getSupportedCipherSuites().
     *
     * @param suites names of all the cipher suites to
     *            put on use
     * @throws IllegalArgumentException when one or more of the
     *             ciphers in array suites are not supported, or when the array
     *             is null.
     */
    public void setEnabledCipherSuites(String[] suites) {
        if (suites == null) {
            throw new IllegalArgumentException("Provided parameter is null");
        }
        String controlString = "";
        for(int i = 0; i < suites.length; i++) {
            findSuite(suites[i]);
            if (i == 0) controlString = suites[i];
            else controlString += ":" + suites[i];
        }
        nativesetenabledciphersuites(controlString);
    }

    /**
     * The names of the protocols' versions that may be used on this SSL
     * connection.
     * @return an array of protocols names
     */
    public String[] getSupportedProtocols() {
        return supportedProtocols.clone();
    }

    /**
     * SSL mode of operation with or without back compatibility. See the OpenSSL
     * ssl.h header file for more information.
     */
    static private long SSL_OP_NO_SSLv3 = 0x02000000L;
    static private long SSL_OP_NO_TLSv1 = 0x04000000L;

    /**
     * The names of the protocols' versions that are in use on this SSL
     * connection.
     * 
     * @return an array of protocols names
     */
    @Override
    public String[] getEnabledProtocols() {
        ArrayList<String> array = new ArrayList<String>();

        if ((ssl_op_no & SSL_OP_NO_SSLv3) == 0x00000000L) {
            array.add(supportedProtocols[1]);
        }
        if ((ssl_op_no & SSL_OP_NO_TLSv1) == 0x00000000L) {
            array.add(supportedProtocols[2]);
        }
        return array.toArray(new String[array.size()]);
    }

    private native void nativesetenabledprotocols(long l);

    /**
     * This method enables the protocols' versions listed by
     * getSupportedProtocols().
     * 
     * @param protocols The names of all the protocols to put on use
     * 
     * @throws IllegalArgumentException when one or more of the names in the
     *             array are not supported, or when the array is null.
     */
    @Override
    public synchronized void setEnabledProtocols(String[] protocols) {

        if (protocols == null) {
            throw new IllegalArgumentException("Provided parameter is null");
        }

        ssl_op_no  = SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1;

        for(int i = 0; i < protocols.length; i++) {
            if (protocols[i].equals("SSLv3"))
                ssl_op_no ^= SSL_OP_NO_SSLv3;
            else if (protocols[i].equals("TLSv1"))
                ssl_op_no ^= SSL_OP_NO_TLSv1;
            else throw new IllegalArgumentException("Protocol " + protocols[i] +
            " is not supported.");
        }

        nativesetenabledprotocols(ssl_op_no);
    }

    /**
     * This method gives true back if the SSL socket is set to client mode.
     *
     * @return true if the socket should do the handshaking as client.
     */
    public boolean getUseClientMode() {
        return sslParameters.getUseClientMode();
    }

    /**
     * This method set the actual SSL socket to client mode.
     *
     * @param mode true if the socket starts in client
     *            mode
     * @throws IllegalArgumentException if mode changes during
     *             handshake.
     */
    public synchronized void setUseClientMode(boolean mode) {
        if (handshakeStarted) {
            throw new IllegalArgumentException(
            "Could not change the mode after the initial handshake has begun.");
        }
        sslParameters.setUseClientMode(mode);
    }

    /**
     * Returns true if the SSL socket requests client's authentication. Relevant
     * only for server sockets!
     *
     * @return true if client authentication is desired, false if not.
     */
    public boolean getWantClientAuth() {
        return sslParameters.getWantClientAuth();
    }

    /**
     * Returns true if the SSL socket needs client's authentication. Relevant
     * only for server sockets!
     *
     * @return true if client authentication is desired, false if not.
     */
    public boolean getNeedClientAuth() {
        return sslParameters.getNeedClientAuth();
    }

    /**
     * Sets the SSL socket to use client's authentication. Relevant only for
     * server sockets!
     *
     * @param need true if client authentication is
     *            desired, false if not.
     */
    public void setNeedClientAuth(boolean need) {
        sslParameters.setNeedClientAuth(need);
    }

    /**
     * Sets the SSL socket to use client's authentication. Relevant only for
     * server sockets! Notice that in contrast to setNeedClientAuth(..) this
     * method will continue the negotiation if the client decide not to send
     * authentication credentials.
     *
     * @param want true if client authentication is
     *            desired, false if not.
     */
    public void setWantClientAuth(boolean want) {
        sslParameters.setWantClientAuth(want);
    }

    /**
     * This method is not supported for SSLSocket implementation.
     */
    public void sendUrgentData(int data) throws IOException {
        throw new SocketException(
                "Method sendUrgentData() is not supported.");
    }

    /**
     * This method is not supported for SSLSocket implementation.
     */
    public void setOOBInline(boolean on) throws SocketException {
        throw new SocketException(
                "Methods sendUrgentData, setOOBInline are not supported.");
    }

    /**
     * Set the read timeout on this socket. The SO_TIMEOUT option, is specified
     * in milliseconds. The read operation will block indefinitely for a zero
     * value.
     *
     * @param timeout the read timeout value
     * @throws SocketException if an error occurs setting the option
     */
    public synchronized void setSoTimeout(int timeout) throws SocketException {
        super.setSoTimeout(timeout);
        this.timeout = timeout;
    }

    private native void nativeinterrupt() throws IOException;
    private native void nativeclose() throws IOException;

    /**
     * Closes the SSL socket. Once closed, a socket is not available for further
     * use anymore under any circumstance. A new socket must be created.
     *
     * @throws <code>IOException</code> if an I/O error happens during the
     *             socket's closure.
     */
    public void close() throws IOException {
        // TODO: Close SSL sockets using a background thread so they close
        // gracefully.

        synchronized (handshakeLock) {
            if (!handshakeStarted) {
                handshakeStarted = true;
                
                synchronized (this) {
                    nativefree();

                    if (socket != null) {
                        if (autoClose && !socket.isClosed()) socket.close();
                    } else {
                        if (!super.isClosed()) super.close();
                    }
                }
                
                return;
            }
        }

        nativeinterrupt();

        synchronized (this) {
            synchronized (writeLock) {
                synchronized (readLock) {

                    IOException pendingException = null;

                    // Shut down the SSL connection, per se.
                    try {
                        if (handshakeStarted) {
                            nativeclose();
                        }
                    } catch (IOException ex) {
                        /*
                         * Note the exception at this point, but try to continue
                         * to clean the rest of this all up before rethrowing.
                         */
                        pendingException = ex;
                    }

                    /*
                     * Even if the above call failed, it is still safe to free
                     * the native structs, and we need to do so lest we leak
                     * memory.
                     */
                    nativefree();

                    if (socket != null) {
                        if (autoClose && !socket.isClosed())
                            socket.close();
                    } else {
                        if (!super.isClosed())
                            super.close();
                    }

                    if (pendingException != null) {
                        throw pendingException;
                    }
                }
            }
        }
    }

    private native void nativefree();

    protected void finalize() throws IOException {
        updateInstanceCount(-1);

        if (ssl == 0) {
            /*
             * It's already been closed, so there's no need to do anything
             * more at this point.
             */
            return;
        }

        // Note the underlying socket up-front, for possible later use.
        Socket underlyingSocket = socket;

        // Fire up a thread to (hopefully) do all the real work.
        Finalizer f = new Finalizer();
        f.setDaemon(true);
        f.start();

        /*
         * Give the finalizer thread one second to run. If it fails to
         * terminate in that time, interrupt it (which may help if it
         * is blocked on an interruptible I/O operation), make a note
         * in the log, and go ahead and close the underlying socket if
         * possible.
         */
        try {
            f.join(1000);
        } catch (InterruptedException ex) {
            // Reassert interrupted status.
            Thread.currentThread().interrupt();
        }

        if (f.isAlive()) {
            f.interrupt();
            Logger.global.log(Level.SEVERE,
                    "Slow finalization of SSL socket (" + this + ", for " +
                    underlyingSocket + ")");
            if ((underlyingSocket != null) && !underlyingSocket.isClosed()) {
                underlyingSocket.close();
            }
        }
    }

    /**
     * Helper class for a thread that knows how to call
     * {@link OpenSSLSocketImpl#close} on behalf of instances being finalized,
     * since that call can take arbitrarily long (e.g., due to a slow network),
     * and an overly long-running finalizer will cause the process to be
     * totally aborted.
     */
    private class Finalizer extends Thread {
        public void run() {
            Socket underlyingSocket = socket; // for error reporting
            try {
                close();
            } catch (IOException ex) {
                /*
                 * Clear interrupted status, so that the Logger call
                 * immediately below won't get spuriously interrupted.
                 */
                Thread.interrupted();

                Logger.global.log(Level.SEVERE,
                        "Trouble finalizing SSL socket (" +
                        OpenSSLSocketImpl.this + ", for " + underlyingSocket +
                        ")",
                        ex);
            }
        }
    }

    /**
     * Verifies an RSA signature. Conceptually, this method doesn't really
     * belong here, but due to its native code being closely tied to OpenSSL
     * (just like the rest of this class), we put it here for the time being.
     * This also solves potential problems with native library initialization.
     *
     * @param message The message to verify
     * @param signature The signature to verify
     * @param algorithm The hash/sign algorithm to use, i.e. "RSA-SHA1"
     * @param key The RSA public key to use
     * @return true if the verification succeeds, false otherwise
     */
    public static boolean verifySignature(byte[] message, byte[] signature, String algorithm, RSAPublicKey key) {
        byte[] modulus = key.getModulus().toByteArray();
        byte[] exponent = key.getPublicExponent().toByteArray();

        return nativeverifysignature(message, signature, algorithm, modulus, exponent) == 1;
    }

    private static native int nativeverifysignature(byte[] message, byte[] signature,
            String algorithm, byte[] modulus, byte[] exponent);
}
