/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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.mina.filter.ssl;

import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSession;

import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.filterchain.IoFilter;
import org.apache.mina.core.filterchain.IoFilterAdapter;
import org.apache.mina.core.filterchain.IoFilterChain;
import org.apache.mina.core.future.DefaultWriteFuture;
import org.apache.mina.core.future.IoFuture;
import org.apache.mina.core.future.IoFutureListener;
import org.apache.mina.core.future.WriteFuture;
import org.apache.mina.core.service.IoAcceptor;
import org.apache.mina.core.service.IoHandler;
import org.apache.mina.core.session.AttributeKey;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.core.write.DefaultWriteRequest;
import org.apache.mina.core.write.WriteRequest;
import org.apache.mina.core.write.WriteToClosedSessionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * An SSL filter that encrypts and decrypts the data exchanged in the session.
 * Adding this filter triggers SSL handshake procedure immediately by sending
 * a SSL 'hello' message, so you don't need to call
 * {@link #startSsl(IoSession)} manually unless you are implementing StartTLS
 * (see below).  If you don't want the handshake procedure to start
 * immediately, please specify {@code false} as {@code autoStart} parameter in
 * the constructor.
 * <p>
 * This filter uses an {@link SSLEngine} which was introduced in Java 5, so
 * Java version 5 or above is mandatory to use this filter. And please note that
 * this filter only works for TCP/IP connections.
 *
 * <h2>Implementing StartTLS</h2>
 * <p>
 * You can use {@link #DISABLE_ENCRYPTION_ONCE} attribute to implement StartTLS:
 * <pre>
 * public void messageReceived(IoSession session, Object message) {
 *    if (message instanceof MyStartTLSRequest) {
 *        // Insert SSLFilter to get ready for handshaking
 *        session.getFilterChain().addFirst(sslFilter);
 *
 *        // Disable encryption temporarily.
 *        // This attribute will be removed by SSLFilter
 *        // inside the Session.write() call below.
 *        session.setAttribute(SSLFilter.DISABLE_ENCRYPTION_ONCE, Boolean.TRUE);
 *
 *        // Write StartTLSResponse which won't be encrypted.
 *        session.write(new MyStartTLSResponse(OK));
 *
 *        // Now DISABLE_ENCRYPTION_ONCE attribute is cleared.
 *        assert session.getAttribute(SSLFilter.DISABLE_ENCRYPTION_ONCE) == null;
 *    }
 * }
 * </pre>
 *
 * @author <a href="http://mina.apache.org">Apache MINA Project</a>
 * @org.apache.xbean.XBean
 */
public class SslFilter extends IoFilterAdapter {
    /** The logger */
    private static final Logger LOGGER = LoggerFactory.getLogger(SslFilter.class);

    /**
     * A session attribute key that stores underlying {@link SSLSession}
     * for each session.
     */
    public static final AttributeKey SSL_SESSION = new AttributeKey(SslFilter.class, "session");

    /**
     * A session attribute key that makes next one write request bypass
     * this filter (not encrypting the data).  This is a marker attribute,
     * which means that you can put whatever as its value. ({@link Boolean#TRUE}
     * is preferred.)  The attribute is automatically removed from the session
     * attribute map as soon as {@link IoSession#write(Object)} is invoked,
     * and therefore should be put again if you want to make more messages
     * bypass this filter.  This is especially useful when you implement
     * StartTLS.
     */
    public static final AttributeKey DISABLE_ENCRYPTION_ONCE = new AttributeKey(SslFilter.class, "disableOnce");

    /**
     * A session attribute key that makes this filter to emit a
     * {@link IoHandler#messageReceived(IoSession, Object)} event with a
     * special message ({@link SslEvent#SECURED} or {@link SslEvent#UNSECURED}).
     * This is a marker attribute, which means that you can put whatever as its
     * value. ({@link Boolean#TRUE} is preferred.)  By default, this filter
     * doesn't emit any events related with SSL session flow control.
     */
    public static final AttributeKey USE_NOTIFICATION = new AttributeKey(SslFilter.class, "useNotification");

    /**
     * A session attribute key that should be set to an {@link InetSocketAddress}.
     * Setting this attribute causes
     * {@link SSLContext#createSSLEngine(String, int)} to be called passing the
     * hostname and port of the {@link InetSocketAddress} to get an
     * {@link SSLEngine} instance. If not set {@link SSLContext#createSSLEngine()}
     * will be called.
     * <br>
     * Using this feature {@link SSLSession} objects may be cached and reused
     * when in client mode.
     *
     * @see SSLContext#createSSLEngine(String, int)
     */
    public static final AttributeKey PEER_ADDRESS = new AttributeKey(SslFilter.class, "peerAddress");

    /** An attribute containing the next filter */
    private static final AttributeKey NEXT_FILTER = new AttributeKey(SslFilter.class, "nextFilter");

    private static final AttributeKey SSL_HANDLER = new AttributeKey(SslFilter.class, "handler");

    /** The SslContext used */
    /* No qualifier */final SSLContext sslContext;

    /** A flag used to tell the filter to start the handshake immediately */
    private final boolean autoStart;

    /** A flag used to determinate if the handshake should start immediately */
    public static final boolean START_HANDSHAKE = true;

    /** A flag used to determinate if the handshake should wait for the client to initiate the handshake */
    public static final boolean CLIENT_HANDSHAKE = false;

    private boolean client;

    private boolean needClientAuth;

    private boolean wantClientAuth;

    private String[] enabledCipherSuites;

    private String[] enabledProtocols;

    /**
     * Creates a new SSL filter using the specified {@link SSLContext}.
     * The handshake will start immediately after the filter has been added
     * to the chain.
     * 
     * @param sslContext The SSLContext to use
     */
    public SslFilter(SSLContext sslContext) {
        this(sslContext, START_HANDSHAKE);
    }

    /**
     * Creates a new SSL filter using the specified {@link SSLContext}.
     * If the <tt>autostart</tt> flag is set to <tt>true</tt>, the
     * handshake will start immediately after the filter has been added
     * to the chain.
     * 
     * @param sslContext The SSLContext to use
     * @param autoStart The flag used to tell the filter to start the handshake immediately
     */
    public SslFilter(SSLContext sslContext, boolean autoStart) {
        if (sslContext == null) {
            throw new IllegalArgumentException("sslContext");
        }

        this.sslContext = sslContext;
        this.autoStart = autoStart;
    }

    /**
     * Returns the underlying {@link SSLSession} for the specified session.
     *
     * @param session The current session 
     * @return <tt>null</tt> if no {@link SSLSession} is initialized yet.
     */
    public SSLSession getSslSession(IoSession session) {
        return (SSLSession) session.getAttribute(SSL_SESSION);
    }

    /**
     * (Re)starts SSL session for the specified <tt>session</tt> if not started yet.
     * Please note that SSL session is automatically started by default, and therefore
     * you don't need to call this method unless you've used TLS closure.
     *
     * @param session The session that will be switched to SSL mode
     * @return <tt>true</tt> if the SSL session has been started, <tt>false</tt> if already started.
     * @throws SSLException if failed to start the SSL session
     */
    public boolean startSsl(IoSession session) throws SSLException {
        SslHandler sslHandler = getSslSessionHandler(session);
        boolean started;

        try {
            synchronized (sslHandler) {
                if (sslHandler.isOutboundDone()) {
                    NextFilter nextFilter = (NextFilter) session.getAttribute(NEXT_FILTER);
                    sslHandler.destroy();
                    sslHandler.init();
                    sslHandler.handshake(nextFilter);
                    started = true;
                } else {
                    started = false;
                }
                sslHandler.flushFilterWrite();
            }
            sslHandler.flushMessageReceived();
        } catch (SSLException se) {
            sslHandler.release();
            throw se;
        }

        return started;
    }

    /**
     * An extended toString() method for sessions. If the SSL handshake
     * is not yet completed, we will print (ssl) in small caps. Once it's
     * completed, we will use SSL capitalized.
     */
    /* no qualifier */String getSessionInfo(IoSession session) {
        StringBuilder sb = new StringBuilder();

        if (session.getService() instanceof IoAcceptor) {
            sb.append("Session Server");

        } else {
            sb.append("Session Client");
        }

        sb.append('[').append(session.getId()).append(']');

        SslHandler sslHandler = (SslHandler) session.getAttribute(SSL_HANDLER);

        if (sslHandler == null) {
            sb.append("(no sslEngine)");
        } else if (isSslStarted(session)) {
            if (sslHandler.isHandshakeComplete()) {
                sb.append("(SSL)");
            } else {
                sb.append("(ssl...)");
            }
        }

        return sb.toString();
    }

    /**
     * @return <tt>true</tt> if and only if the specified <tt>session</tt> is
     * encrypted/decrypted over SSL/TLS currently. This method will start
     * to return <tt>false</tt> after TLS <tt>close_notify</tt> message
     * is sent and any messages written after then is not going to get encrypted.
     * 
     * @param session the session we want to check
     */
    public boolean isSslStarted(IoSession session) {
        SslHandler sslHandler = (SslHandler) session.getAttribute(SSL_HANDLER);

        if (sslHandler == null) {
            return false;
        }

        synchronized (sslHandler) {
            return !sslHandler.isOutboundDone();
        }
    }

    /**
     * @return <tt>true</tt> if and only if the conditions for
     * {@link #isSslStarted(IoSession)} are met, and the handhake has
     * completed.
     *
     * @param session the session we want to check
     */
    public boolean isSecured(IoSession session) {
        SslHandler sslHandler = (SslHandler) session.getAttribute(SSL_HANDLER);

        if (sslHandler == null) {
            return false;
        }

        synchronized (sslHandler) {
            return !sslHandler.isOutboundDone() && sslHandler.isHandshakeComplete();
        }
    }


    /**
     * Stops the SSL session by sending TLS <tt>close_notify</tt> message to
     * initiate TLS closure.
     *
     * @param session the {@link IoSession} to initiate TLS closure
     * @return The Future for the initiated closure
     * @throws SSLException if failed to initiate TLS closure
     */
    public WriteFuture stopSsl(IoSession session) throws SSLException {
        SslHandler sslHandler = getSslSessionHandler(session);
        NextFilter nextFilter = (NextFilter) session.getAttribute(NEXT_FILTER);
        WriteFuture future;

        try {
            synchronized (sslHandler) {
                future = initiateClosure(nextFilter, session);
                sslHandler.flushFilterWrite();
            }
        } catch (SSLException se) {
            sslHandler.release();
            throw se;
        }

        return future;
    }

    /**
     * @return <tt>true</tt> if the engine is set to use client mode
     * when handshaking.
     */
    public boolean isUseClientMode() {
        return client;
    }

    /**
     * Configures the engine to use client (or server) mode when handshaking.
     * 
     * @param clientMode <tt>true</tt> when we are in client mode, <tt>false</tt> when in server mode
     */
    public void setUseClientMode(boolean clientMode) {
        this.client = clientMode;
    }

    /**
     * @return <tt>true</tt> if the engine will <em>require</em> client authentication.
     * This option is only useful to engines in the server mode.
     */
    public boolean isNeedClientAuth() {
        return needClientAuth;
    }

    /**
     * Configures the engine to <em>require</em> client authentication.
     * This option is only useful for engines in the server mode.
     * 
     * @param needClientAuth A flag set when we need to authenticate the client
     */
    public void setNeedClientAuth(boolean needClientAuth) {
        this.needClientAuth = needClientAuth;
    }

    /**
     * @return <tt>true</tt> if the engine will <em>request</em> client authentication.
     * This option is only useful to engines in the server mode.
     */
    public boolean isWantClientAuth() {
        return wantClientAuth;
    }

    /**
     * Configures the engine to <em>request</em> client authentication.
     * This option is only useful for engines in the server mode.
     * 
     * @param wantClientAuth A flag set when we want to check the client authentication
     */
    public void setWantClientAuth(boolean wantClientAuth) {
        this.wantClientAuth = wantClientAuth;
    }

    /**
     * @return the list of cipher suites to be enabled when {@link SSLEngine}
     * is initialized. <tt>null</tt> means 'use {@link SSLEngine}'s default.'
     */
    public String[] getEnabledCipherSuites() {
        return enabledCipherSuites;
    }

    /**
     * Sets the list of cipher suites to be enabled when {@link SSLEngine}
     * is initialized.
     *
     * @param cipherSuites <tt>null</tt> means 'use {@link SSLEngine}'s default.'
     */
    public void setEnabledCipherSuites(String[] cipherSuites) {
        this.enabledCipherSuites = cipherSuites;
    }

    /**
     * @return the list of protocols to be enabled when {@link SSLEngine}
     * is initialized. <tt>null</tt> means 'use {@link SSLEngine}'s default.'
     */
    public String[] getEnabledProtocols() {
        return enabledProtocols;
    }

    /**
     * Sets the list of protocols to be enabled when {@link SSLEngine}
     * is initialized.
     *
     * @param protocols <tt>null</tt> means 'use {@link SSLEngine}'s default.'
     */
    public void setEnabledProtocols(String[] protocols) {
        this.enabledProtocols = protocols;
    }

    /**
     * Executed just before the filter is added into the chain, we do :
     * <ul>
     * <li>check that we don't have a SSL filter already present
     * <li>we update the next filter
     * <li>we create the SSL handler helper class
     * <li>and we store it into the session's Attributes
     * </ul>
     */
    @Override
    public void onPreAdd(IoFilterChain parent, String name, NextFilter nextFilter) throws SSLException {
        // Check that we don't have a SSL filter already present in the chain
        if (parent.contains(SslFilter.class)) {
            String msg = "Only one SSL filter is permitted in a chain.";
            LOGGER.error(msg);
            throw new IllegalStateException(msg);
        }

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Adding the SSL Filter {} to the chain", name);
        }

        IoSession session = parent.getSession();
        session.setAttribute(NEXT_FILTER, nextFilter);

        // Create a SSL handler and start handshake.
        SslHandler sslHandler = new SslHandler(this, session);
        
        // Adding the supported ciphers in the SSLHandler
        if ((enabledCipherSuites == null) || (enabledCipherSuites.length == 0)) {
            enabledCipherSuites = sslContext.getServerSocketFactory().getSupportedCipherSuites();
        }

        sslHandler.init();

        session.setAttribute(SSL_HANDLER, sslHandler);
    }

    @Override
    public void onPostAdd(IoFilterChain parent, String name, NextFilter nextFilter) throws SSLException {
        if (autoStart == START_HANDSHAKE) {
            initiateHandshake(nextFilter, parent.getSession());
        }
    }

    @Override
    public void onPreRemove(IoFilterChain parent, String name, NextFilter nextFilter) throws SSLException {
        IoSession session = parent.getSession();
        stopSsl(session);
        session.removeAttribute(NEXT_FILTER);
        session.removeAttribute(SSL_HANDLER);
    }

    // IoFilter impl.
    @Override
    public void sessionClosed(NextFilter nextFilter, IoSession session) throws SSLException {
        SslHandler sslHandler = getSslSessionHandler(session);

        try {
            synchronized (sslHandler) {
                // release resources
                sslHandler.destroy();
            }
        } finally {
            // notify closed session
            nextFilter.sessionClosed(session);
        }
    }

    @Override
    public void messageReceived(NextFilter nextFilter, IoSession session, Object message) throws SSLException {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("{}: Message received : {}", getSessionInfo(session), message);
        }

        SslHandler sslHandler = getSslSessionHandler(session);
        AtomicBoolean canPushMessage = new AtomicBoolean( false );
        
        // The SslHandler instance is *guaranteed* to nit be null here

        synchronized (sslHandler) {
            if (sslHandler.isOutboundDone() && sslHandler.isInboundDone()) {
                // We aren't handshaking here. Let's push the message to the next filter
                
                // Note: we can push the message to the queue immediately, 
                // but don't do so in the synchronized block. We use a protected
                // flag to do so.
                canPushMessage.set( true );
            } else {
                canPushMessage.set( false );
                IoBuffer buf = (IoBuffer) message;
                
                try {
                    if (sslHandler.isOutboundDone()) {
                        sslHandler.destroy();
                        throw new SSLException("Outbound done");
                    }
                
                    // forward read encrypted data to SSL handler
                    sslHandler.messageReceived(nextFilter, buf.buf());
                    
                    // Handle data to be forwarded to application or written to net
                    handleSslData(nextFilter, sslHandler);
                    
                    if (sslHandler.isInboundDone()) {
                        if (sslHandler.isOutboundDone()) {
                            sslHandler.destroy();
                        } else {
                            initiateClosure(nextFilter, session);
                        }
                    
                        if (buf.hasRemaining()) {
                            // Forward the data received after closure.
                            sslHandler.scheduleMessageReceived(nextFilter, buf);
                        }
                    }
                } catch (SSLException ssle) {
                    if (!sslHandler.isHandshakeComplete()) {
                        SSLException newSsle = new SSLHandshakeException("SSL handshake failed.");
                        newSsle.initCause(ssle);
                        ssle = newSsle;
            
                        // Close the session immediately, the handshake has failed
                        session.closeNow();
                    } else {
                        // Free the SSL Handler buffers
                        sslHandler.release();
                    }
                
                    throw ssle;
                }
            }
        }
    
        if (canPushMessage.get()) {
            nextFilter.messageReceived(session, message);
        } else {
            sslHandler.flushMessageReceived();
        }
    }

    @Override
    public void messageSent(NextFilter nextFilter, IoSession session, WriteRequest writeRequest) {
        if (writeRequest instanceof EncryptedWriteRequest) {
            EncryptedWriteRequest wrappedRequest = (EncryptedWriteRequest) writeRequest;
            nextFilter.messageSent(session, wrappedRequest.getParentRequest());
        } else {
            // ignore extra buffers used for handshaking
        }
    }

    @Override
    public void exceptionCaught(NextFilter nextFilter, IoSession session, Throwable cause) throws Exception {

        if (cause instanceof WriteToClosedSessionException) {
            // Filter out SSL close notify, which is likely to fail to flush
            // due to disconnection.
            WriteToClosedSessionException e = (WriteToClosedSessionException) cause;
            List<WriteRequest> failedRequests = e.getRequests();
            boolean containsCloseNotify = false;

            for (WriteRequest r : failedRequests) {
                if (isCloseNotify(r.getMessage())) {
                    containsCloseNotify = true;
                    break;
                }
            }

            if (containsCloseNotify) {
                if (failedRequests.size() == 1) {
                    // close notify is the only failed request; bail out.
                    return;
                }

                List<WriteRequest> newFailedRequests = new ArrayList<>(failedRequests.size() - 1);

                for (WriteRequest r : failedRequests) {
                    if (!isCloseNotify(r.getMessage())) {
                        newFailedRequests.add(r);
                    }
                }

                if (newFailedRequests.isEmpty()) {
                    // the failedRequests were full with close notify; bail out.
                    return;
                }

                cause = new WriteToClosedSessionException(newFailedRequests, cause.getMessage(), cause.getCause());
            }
        }

        nextFilter.exceptionCaught(session, cause);
    }

    private boolean isCloseNotify(Object message) {
        if (!(message instanceof IoBuffer)) {
            return false;
        }

        IoBuffer buf = (IoBuffer) message;
        int offset = buf.position();

        return (buf.get(offset + 0) == 0x15) /* Alert */
                && (buf.get(offset + 1) == 0x03) /* TLS/SSL */
                && ((buf.get(offset + 2) == 0x00) /* SSL 3.0 */
                        || (buf.get(offset + 2) == 0x01) /* TLS 1.0 */
                        || (buf.get(offset + 2) == 0x02) /* TLS 1.1 */
                        || (buf.get(offset + 2) == 0x03)) /* TLS 1.2 */
                        && (buf.get(offset + 3) == 0x00); /* close_notify */
    }

    @Override
    public void filterWrite(NextFilter nextFilter, IoSession session, WriteRequest writeRequest) throws SSLException {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("{}: Writing Message : {}", getSessionInfo(session), writeRequest);
        }

        boolean needsFlush = true;
        SslHandler sslHandler = getSslSessionHandler(session);

        try {
            synchronized (sslHandler) {
                if (!isSslStarted(session)) {
                    sslHandler.scheduleFilterWrite(nextFilter, writeRequest);
                }
                // Don't encrypt the data if encryption is disabled.
                else if (session.containsAttribute(DISABLE_ENCRYPTION_ONCE)) {
                    // Remove the marker attribute because it is temporary.
                    session.removeAttribute(DISABLE_ENCRYPTION_ONCE);
                    sslHandler.scheduleFilterWrite(nextFilter, writeRequest);
                } else {
                    // Otherwise, encrypt the buffer.
                    IoBuffer buf = (IoBuffer) writeRequest.getMessage();

                    if (sslHandler.isWritingEncryptedData()) {
                        // data already encrypted; simply return buffer
                        sslHandler.scheduleFilterWrite(nextFilter, writeRequest);
                    } else if (sslHandler.isHandshakeComplete()) {
                        // SSL encrypt
                        sslHandler.encrypt(buf.buf());
                        IoBuffer encryptedBuffer = sslHandler.fetchOutNetBuffer();
                        writeRequest.setMessage( encryptedBuffer );
                        sslHandler.scheduleFilterWrite(nextFilter, new EncryptedWriteRequest(writeRequest,
                            encryptedBuffer));
                    } else {
                        if (session.isConnected()) {
                            // Handshake not complete yet.
                            sslHandler.schedulePreHandshakeWriteRequest(nextFilter, writeRequest);
                        }

                        needsFlush = false;
                    }
                }
                if (needsFlush) {
                    sslHandler.flushFilterWrite();
                }
            }
        } catch (SSLException se) {
            sslHandler.release();
            throw se;
        }
    }

    @Override
    public void filterClose(final NextFilter nextFilter, final IoSession session) throws SSLException {
        SslHandler sslHandler = (SslHandler) session.getAttribute(SSL_HANDLER);

        if (sslHandler == null) {
            // The connection might already have closed, or
            // SSL might have not started yet.
            nextFilter.filterClose(session);
            return;
        }

        WriteFuture future = null;

        try {
            synchronized (sslHandler) {
                if (isSslStarted(session)) {
                    future = initiateClosure(nextFilter, session);
                    future.addListener(new IoFutureListener<IoFuture>() {
                        @Override
                        public void operationComplete(IoFuture future) {
                            nextFilter.filterClose(session);
                        }
                    });
                }
                sslHandler.flushFilterWrite();
            }
        } catch (SSLException se) {
            sslHandler.release();
            throw se;
        } finally {
            if (future == null) {
                nextFilter.filterClose(session);
            }
        }
    }

    /**
     * Initiate the SSL handshake. This can be invoked if you have set the 'autoStart' to
     * false when creating the SslFilter instance.
     * 
     * @param session The session for which the SSL handshake should be done
     * @throws SSLException If the handshake failed
     */
    public void initiateHandshake(IoSession session) throws SSLException {
        IoFilterChain filterChain = session.getFilterChain();
        
        if (filterChain == null) {
            throw new SSLException("No filter chain");
        }
        
        IoFilter.NextFilter nextFilter = filterChain.getNextFilter(SslFilter.class);
        
        if (nextFilter == null) {
            throw new SSLException("No SSL next filter in the chain");
        }
        
        initiateHandshake(nextFilter, session);
    }

    private void initiateHandshake(NextFilter nextFilter, IoSession session) throws SSLException {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("{} : Starting the first handshake", getSessionInfo(session));
        }
        
        SslHandler sslHandler = getSslSessionHandler(session);

        try {
            synchronized (sslHandler) {
                sslHandler.handshake(nextFilter);
                sslHandler.flushFilterWrite();
            }
            sslHandler.flushMessageReceived();
        } catch (SSLException se) {
            sslHandler.release();
            throw se;
        }
    }

    private WriteFuture initiateClosure(NextFilter nextFilter, IoSession session) throws SSLException {
        SslHandler sslHandler = getSslSessionHandler(session);
        WriteFuture future = null;

        // if already shut down
        try {
            synchronized(sslHandler) {
                if (!sslHandler.closeOutbound()) {
                    return DefaultWriteFuture.newNotWrittenFuture(session, new IllegalStateException(
                            "SSL session is shut down already."));
                }
    
                // there might be data to write out here?
                future = sslHandler.writeNetBuffer(nextFilter);
    
                if (future == null) {
                    future = DefaultWriteFuture.newWrittenFuture(session);
                }
    
                if (sslHandler.isInboundDone()) {
                    sslHandler.destroy();
                }
            }

            // Inform that the session is not any more secured
            session.getFilterChain().fireEvent(SslEvent.UNSECURED);
        } catch (SSLException se) {
            sslHandler.release();
            throw se;
        }

        return future;
    }

    // Utilities
    private void handleSslData(NextFilter nextFilter, SslHandler sslHandler) throws SSLException {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("{}: Processing the SSL Data ", getSessionInfo(sslHandler.getSession()));
        }

        // Flush any buffered write requests occurred before handshaking.
        if (sslHandler.isHandshakeComplete()) {
            sslHandler.flushPreHandshakeEvents();
        }

        // Write encrypted data to be written (if any)
        sslHandler.writeNetBuffer(nextFilter);

        // handle app. data read (if any)
        handleAppDataRead(nextFilter, sslHandler);
    }

    private void handleAppDataRead(NextFilter nextFilter, SslHandler sslHandler) {
        // forward read app data
        IoBuffer readBuffer = sslHandler.fetchAppBuffer();

        if (readBuffer.hasRemaining()) {
            sslHandler.scheduleMessageReceived(nextFilter, readBuffer);
        }
    }

    private SslHandler getSslSessionHandler(IoSession session) {
        SslHandler sslHandler = (SslHandler) session.getAttribute(SSL_HANDLER);

        if (sslHandler == null) {
            throw new IllegalStateException();
        }

        synchronized(sslHandler) {
            if (sslHandler.getSslFilter() != this) {
                throw new IllegalArgumentException("Not managed by this filter.");
            }
        }

        return sslHandler;
    }

    /**
     * A message that is sent from {@link SslFilter} when the connection became
     * secure or is not secure anymore.
     *
     * @author <a href="http://mina.apache.org">Apache MINA Project</a>
     */
    public static class SslFilterMessage {
        private final String name;

        private SslFilterMessage(String name) {
            this.name = name;
        }

        @Override
        public String toString() {
            return name;
        }
    }
    
    /**
     * A private class used to store encrypted messages. This is necessary
     * to be able to emit the messageSent event with the proper original
     * message, but not for handshake messages, which will be swallowed.
     *
     */
    /* package protected */ static class EncryptedWriteRequest extends DefaultWriteRequest {
        // Thee encrypted messagee
        private final IoBuffer encryptedMessage;
        
        // The original message
        private WriteRequest parentRequest;

        /**
         * Create a new instance of an EncryptedWriteRequest
         * @param writeRequest The parent request
         * @param encryptedMessage The encrypted message
         */
        private EncryptedWriteRequest(WriteRequest writeRequest, IoBuffer encryptedMessage) {
            super(encryptedMessage);
            parentRequest = writeRequest;
            this.encryptedMessage = encryptedMessage;
        }

        /**
         * @return teh encrypted message
         */
        @Override
        public Object getMessage() {
            return encryptedMessage;
        }

        /**
         * @return The parent WriteRequest
         */
        public WriteRequest getParentRequest() {
            return parentRequest;
        }
        
        /**
         * {@inheritDoc}
         */
        @Override
        public WriteFuture getFuture() {
            return parentRequest.getFuture();
        }
    }
}
