/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2008-2009 Sun Microsystems, Inc.
 */
package org.opends.server.extensions;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SocketChannel;
import java.security.cert.Certificate;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import org.opends.server.admin.std.server.LDAPConnectionHandlerCfg;
import org.opends.server.api.ClientConnection;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.types.DebugLogLevel;
import org.opends.server.util.StaticUtils;

import static org.opends.server.loggers.debug.DebugLogger.*;

/**
 * A class that provides a TLS byte channel implementation.
 *
 */
public class TLSByteChannel implements
                                ByteChannel, ConnectionSecurityProvider {

    private static final DebugTracer TRACER = getTracer();

    private ClientConnection connection;
    private SocketChannel socketChannel;

    private SSLEngine sslEngine;

    //read copy to buffer
    private ByteBuffer appData;
    //read encrypted
    private ByteBuffer appNetData;

    //Write encrypted
    private ByteBuffer netData, tempData;
    private int sslBufferSize, appBufSize;
    private boolean reading = false;

    //Map of cipher phrases to effective key size (bits). Taken from the
    //following RFCs: 5289, 4346, 3268,4132 and 4162.
    private static final Map<String, Integer> cipherMap;

    static {
        cipherMap = new LinkedHashMap<String, Integer>();
        cipherMap.put("_WITH_AES_256_CBC_", new Integer(256));
        cipherMap.put("_WITH_CAMELLIA_256_CBC_", new Integer(256));
        cipherMap.put("_WITH_AES_256_GCM_", new Integer(256));
        cipherMap.put("_WITH_3DES_EDE_CBC_", new Integer(112));
        cipherMap.put("_WITH_AES_128_GCM_", new Integer(128));
        cipherMap.put("_WITH_SEED_CBC_", new Integer(128));
        cipherMap.put("_WITH_CAMELLIA_128_CBC_", new Integer(128));
        cipherMap.put("_WITH_AES_128_CBC_", new Integer(128));
        cipherMap.put("_WITH_IDEA_CBC_", new Integer(128));
        cipherMap.put("_WITH_DES_CBC_", new Integer(56));
        cipherMap.put("_WITH_RC2_CBC_40_", new Integer(40));
        cipherMap.put("_WITH_RC4_40_", new Integer(40));
        cipherMap.put("_WITH_DES40_CBC_", new Integer(40));
        cipherMap.put("_WITH_NULL_", new Integer(0));
    };

    private TLSByteChannel(LDAPConnectionHandlerCfg config, ClientConnection c,
                         SocketChannel socketChannel, SSLContext sslContext)  {

        this.socketChannel = socketChannel;
        this.connection = c;
        String hostName = socketChannel.socket().getInetAddress().getHostName();
        int port = socketChannel.socket().getPort();
        sslEngine = sslContext.createSSLEngine(hostName, port);
        sslEngine.setUseClientMode(false);
        Set<String> protocols = config.getSSLProtocol();
        if (!protocols.isEmpty())
            sslEngine.setEnabledProtocols(protocols.toArray(new String[0]));
        Set<String> ciphers = config.getSSLCipherSuite();
        if (!ciphers.isEmpty())
            sslEngine.setEnabledCipherSuites(ciphers.toArray(new String[0]));
        switch (config.getSSLClientAuthPolicy()) {
        case DISABLED:
            sslEngine.setNeedClientAuth(false);
            sslEngine.setWantClientAuth(false);
            break;
        case REQUIRED:
            sslEngine.setWantClientAuth(true);
            sslEngine.setNeedClientAuth(true);
            break;
        case OPTIONAL:
        default:
            sslEngine.setNeedClientAuth(false);
            sslEngine.setWantClientAuth(true);
            break;
        }
        SSLSession sslSession = sslEngine.getSession();
        sslBufferSize = sslSession.getPacketBufferSize();
        appBufSize = sslSession.getApplicationBufferSize();

        appNetData = ByteBuffer.allocate(sslBufferSize);
        netData = ByteBuffer.allocate(sslBufferSize);

        appData = ByteBuffer.allocate(sslSession.getApplicationBufferSize());
        tempData = ByteBuffer.allocate(sslSession.getApplicationBufferSize());
    }

    /**
     * {@inheritDoc}
     */
    public int getAppBufSize() {
        return appBufSize;
    }

    /**
     * Create an TLS byte channel instance using the specified LDAP connection
     * configuration, client connection, SSL context and socket channel
     * parameters.
     *
     * @param config The LDAP connection configuration.
     * @param c The client connection.
     * @param sslContext The SSL context.
     * @param socketChannel The socket channel.
     * @return A TLS capable byte channel.
     */
    public static TLSByteChannel
    getTLSByteChannel(LDAPConnectionHandlerCfg config, ClientConnection c,
                        SSLContext sslContext, SocketChannel socketChannel) {
        return new TLSByteChannel(config, c, socketChannel, sslContext);
    }

    private SSLEngineResult.HandshakeStatus doTasks() {
        Runnable task;
        while ((task = sslEngine.getDelegatedTask()) != null)
            task.run();
        return sslEngine.getHandshakeStatus();
    }

    private void doHandshakeRead(SSLEngineResult.HandshakeStatus hsStatus)
    throws IOException {
        do {
            doHandshakeOp(hsStatus);
            hsStatus = sslEngine.getHandshakeStatus();
        } while (hsStatus == SSLEngineResult.HandshakeStatus.NEED_WRAP ||
                hsStatus == SSLEngineResult.HandshakeStatus.NEED_TASK);
    }

    private void  doHandshakeOp(SSLEngineResult.HandshakeStatus hsStatus)
    throws IOException {
        SSLEngineResult res;
        switch (hsStatus) {
        case NEED_TASK:
            hsStatus = doTasks();
            break;
        case NEED_WRAP:
            tempData.clear();
            netData.clear();
            res = sslEngine.wrap(tempData, netData);
            hsStatus = res.getHandshakeStatus();
            netData.flip();
            while(netData.hasRemaining()) {
                socketChannel.write(netData);
            }
            hsStatus = sslEngine.getHandshakeStatus();
            return;
        default:
            return;
        }
    }

    /**
     * {@inheritDoc}
     */
    public int read(ByteBuffer clearBuffer) throws IOException {
        SSLEngineResult.HandshakeStatus hsStatus;
        if(!reading)
          appNetData.clear();
        else
          reading = false;
        if(!socketChannel.isOpen())
            return -1;
        if(sslEngine.isInboundDone())
            return -1;
        do {
            int wrappedBytes = socketChannel.read(appNetData);
            appNetData.flip();
            if(wrappedBytes == -1) {
                return -1;
            }
            hsStatus = sslEngine.getHandshakeStatus();
            if (hsStatus == SSLEngineResult.HandshakeStatus.NEED_TASK ||
                    hsStatus == SSLEngineResult.HandshakeStatus.NEED_WRAP)
                doHandshakeRead(hsStatus);
            if(wrappedBytes == 0)
              return 0;
            while (appNetData.hasRemaining()) {
                appData.clear();
                SSLEngineResult res = sslEngine.unwrap(appNetData, appData);
                appData.flip();
                if(res.getStatus() == SSLEngineResult.Status.BUFFER_UNDERFLOW) {
                  appNetData.compact();
                  reading = true;
                  break;
                } else  if(res.getStatus() != SSLEngineResult.Status.OK)
                    return -1;
                hsStatus = sslEngine.getHandshakeStatus();
                if (hsStatus == SSLEngineResult.HandshakeStatus.NEED_TASK ||
                        hsStatus == SSLEngineResult.HandshakeStatus.NEED_WRAP)
                    doHandshakeOp(hsStatus);
                clearBuffer.put(appData);
            }
            hsStatus = sslEngine.getHandshakeStatus();
        } while (hsStatus == SSLEngineResult.HandshakeStatus.NEED_TASK ||
                 hsStatus == SSLEngineResult.HandshakeStatus.NEED_WRAP );
        return clearBuffer.position();
    }

    /**
     * {@inheritDoc}
     */
    public void close() throws IOException {
        sslEngine.closeInbound();
        sslEngine.closeOutbound();
        SSLEngineResult.HandshakeStatus hsStatus =
                                      sslEngine.getHandshakeStatus();
        if(hsStatus != SSLEngineResult.HandshakeStatus.FINISHED &&
           hsStatus !=  SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING)
            doHandshakeWrite(hsStatus);
    }

    /**
     * {@inheritDoc}
     */
    public boolean isOpen() {
        if(sslEngine.isInboundDone() || sslEngine.isOutboundDone())
          return false;
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public int getSSF() {
        int cipherKeySSF = 0;
        String cipherString = sslEngine.getSession().getCipherSuite();
        for(Map.Entry<String, Integer> mapEntry : cipherMap.entrySet()) {
            if(cipherString.indexOf(mapEntry.getKey()) >= 0) {
                cipherKeySSF = mapEntry.getValue().intValue();
                break;
            }
        }
        return cipherKeySSF;
    }

    /**
     * {@inheritDoc}
     */
    public  Certificate[] getClientCertificateChain() {
        try {
          return sslEngine.getSession().getPeerCertificates();
        }
        catch (SSLPeerUnverifiedException e) {
          if (debugEnabled()) {
            TRACER.debugCaught(DebugLogLevel.ERROR, e);
          }
          return new Certificate[0];
        }
    }

    private void doHandshakeUnwrap() throws IOException {
        netData.clear();
        tempData.clear();
        int bytesRead = socketChannel.read(netData);
        if (bytesRead <= 0)
            throw new ClosedChannelException();
         else
          sslEngine.unwrap(netData, tempData);
    }

    private void
    doHandshakeWrite(SSLEngineResult.HandshakeStatus hsStatus)
    throws IOException {
        do {
            if(hsStatus == SSLEngineResult.HandshakeStatus.NEED_UNWRAP) {
                doHandshakeUnwrap();
            } else
               doHandshakeOp(hsStatus);
            hsStatus = sslEngine.getHandshakeStatus();
        } while (hsStatus == SSLEngineResult.HandshakeStatus.NEED_WRAP ||
                hsStatus == SSLEngineResult.HandshakeStatus.NEED_TASK ||
                hsStatus == SSLEngineResult.HandshakeStatus.NEED_UNWRAP);
    }

    /**
     * {@inheritDoc}
     */
    public int write(ByteBuffer clearData) throws IOException {
        if(!socketChannel.isOpen() || sslEngine.isOutboundDone()) {
            throw new ClosedChannelException();
        }
        int originalPosition = clearData.position();
        int originalLimit = clearData.limit();
        int length = originalLimit - originalPosition;
        if (length > sslBufferSize) {
            int pos = originalPosition;
            int lim = originalPosition + sslBufferSize;
            while (pos < originalLimit) {
                clearData.position(pos);
                clearData.limit(lim);
                writeInternal(clearData);
                pos = lim;
                lim = Math.min(originalLimit, pos + sslBufferSize);
            }
            return length;
        }  else {
            return writeInternal(clearData);
        }
    }

    private int writeInternal(ByteBuffer clearData) throws IOException {
        int totBytesSent = 0;
        SSLEngineResult.HandshakeStatus hsStatus;
        hsStatus = sslEngine.getHandshakeStatus();
        if (hsStatus == SSLEngineResult.HandshakeStatus.NEED_TASK ||
                hsStatus == SSLEngineResult.HandshakeStatus.NEED_WRAP ||
                hsStatus == SSLEngineResult.HandshakeStatus.NEED_UNWRAP)
            doHandshakeWrite(hsStatus);
        while(clearData.hasRemaining()) {
            netData.clear();
            SSLEngineResult res = sslEngine.wrap(clearData, netData);
            netData.flip();
            if(res.getStatus() != SSLEngineResult.Status.OK)
                throw new ClosedChannelException();
            if (hsStatus == SSLEngineResult.HandshakeStatus.NEED_TASK ||
                    hsStatus == SSLEngineResult.HandshakeStatus.NEED_WRAP ||
                    hsStatus == SSLEngineResult.HandshakeStatus.NEED_UNWRAP)
                doHandshakeWrite(hsStatus);
            while (netData.hasRemaining()) {
                int bytesWritten = socketChannel.write(netData);
                if (bytesWritten < 0)
                    throw new ClosedChannelException();
                else if (bytesWritten == 0) {
                    int bytesSent = netData.remaining();
                    if(!StaticUtils.writeWithTimeout(connection, netData))
                        throw new ClosedChannelException();
                    totBytesSent += bytesSent;
                } else
                    totBytesSent += bytesWritten;
            }
        }
        return totBytesSent;
    }

    /**
     * {@inheritDoc}
     */
    public ByteChannel wrapChannel(ByteChannel channel) {
        return this;
    }

    /**
     * {@inheritDoc}
     */
    public String getName() {
        return "TLS";
    }

    /**
     * {@inheritDoc}
     */
    public boolean isSecure() {
        return true;
    }
}
