/*
 * 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 Sun Microsystems, Inc.
 */

package org.opends.server.extensions;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslException;
import org.opends.server.api.ClientConnection;
import org.opends.server.api.ConnectionSecurityProvider;
import org.opends.server.config.ConfigEntry;
import org.opends.server.config.ConfigException;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.protocols.ldap.LDAPClientConnection;
import org.opends.server.types.DebugLogLevel;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.DisconnectReason;
import org.opends.server.types.InitializationException;
import static org.opends.server.loggers.debug.DebugLogger.*;

/**
 * This class provides an implementation of a connection security provider that
 * provides SASL confidentiality/integrity between the server and client.
 *
 */
public class SASLSecurityProvider extends ConnectionSecurityProvider {

    // The tracer object for the debug logger.
    private static final DebugTracer TRACER = getTracer();

    //The client connection associated with this provider.
    private ClientConnection connection;

    //The socket channel associated with this provider.
    private SocketChannel sockChannel;

    //The SASL context associated with the provider
    private SASLContext saslContext;

    //The number of bytes in the length buffer.
    private final int lengthSize = 4;

    //A byte buffer used to hold the length of the clear buffer.
    private ByteBuffer lengthBuf =  ByteBuffer.allocate(lengthSize);

    //The SASL mechanism name.
    private String name;

    /**
     * Create a SASL security provider with the specified parameters that is
     * capable of processing a confidentiality/integrity SASL connection.
     *
     * @param connection The client connection to read/write the bytes.
     * @param name The SASL mechanism name.
     * @param saslContext The SASL context to process the data through.
     */
    public SASLSecurityProvider(ClientConnection connection, String name,
                               SASLContext saslContext) {
      super();
      this.connection = connection;
      this.name = name;
      this.saslContext = saslContext;
      this.sockChannel = ((LDAPClientConnection) connection).getSocketChannel();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void disconnect(boolean connectionValid) {
        this.saslContext.dispose();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void finalizeConnectionSecurityProvider() {

    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getClearBufferSize() {
        return 0;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getEncodedBufferSize() {
        return 0;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getSecurityMechanismName() {
        return name;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void initializeConnectionSecurityProvider(ConfigEntry configEntry)
            throws ConfigException, InitializationException {
        this.connection = null;
        this.sockChannel = null;
    }

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

    /**
     * {@inheritDoc}
     */
    @Override
    public ConnectionSecurityProvider newInstance(ClientConnection clientConn,
                                                  SocketChannel socketChannel) {
            return new SASLSecurityProvider(clientConn, null,null);
    }

    /**
     * Return the clear buffer length as determined by processing the first
     * 4 bytes of the specified buffer.
     *
     * @param byteBuf The buffer to examine the first 4 bytes of.
     * @return The size of the clear buffer.
     */
    private int getBufLength(ByteBuffer byteBuf) {
        int answer = 0;
        byte[] buf = byteBuf.array();

        for (int i = 0; i < lengthSize; i++) {
            answer <<= 8;
            answer |= ((int)buf[i] & 0xff);
        }
        return answer;
    }

    /**
     * Read from the socket channel into the specified byte buffer the
     * number of bytes specified in the total parameter.
     *
     * @param byteBuf The byte buffer to put the bytes in.
     * @param total The total number of bytes to read from the socket channel.
     * @return The number of bytes read, 0 or -1.
     * @throws IOException If an error occurred reading the socket channel.
     */
    private int readAll(ByteBuffer byteBuf, int total) throws IOException {
        int count = 0;
        while(sockChannel.isOpen() && total > 0) {
            count = sockChannel.read(byteBuf);
            if(count == -1)
                return -1;
           if(count == 0)
                return 0;
            total -= count;
        }
        if(total > 0)
            return -1;
        else
            return byteBuf.position();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean readData() throws DirectoryException {
        int recvBufSize = saslContext.getBufSize(Sasl.MAX_BUFFER);
        try {
            while(true) {
                lengthBuf.clear();
                int readResult = readAll(lengthBuf, lengthSize);
                if (readResult == -1) {
                    //Client connection has been closed. Disconnect and return.
                    connection.disconnect(DisconnectReason.CLIENT_DISCONNECT,
                            false, null);
                    return false;
                } else if(readResult == 0)
                    return true;
                int bufLength = getBufLength(lengthBuf);
                if(bufLength > recvBufSize) {
                    connection.disconnect(DisconnectReason.CLIENT_DISCONNECT,
                            false, null);
                    return false;
                }
                ByteBuffer readBuf = ByteBuffer.allocate(bufLength);
                readResult = readAll(readBuf, bufLength);
                if (readResult == -1) {
                    //Client connection has been closed. Disconnect and return.
                    connection.disconnect(DisconnectReason.CLIENT_DISCONNECT,
                            false, null);
                    return false;
                } else if (readResult == 0)
                    return true;
                byte[] inBytes = readBuf.array();
                byte[] clearBytes =
                                saslContext.unwrap(inBytes, 0, inBytes.length);
                ByteBuffer clearBuffer = ByteBuffer.wrap(clearBytes);
                if (!connection.processDataRead(clearBuffer))
                    return false;
            }
        } catch (SaslException saslEx) {
            if (debugEnabled()) {
                TRACER.debugCaught(DebugLogLevel.ERROR, saslEx);
              }
            //Error trying to unwrap the data.
            connection.disconnect(DisconnectReason.IO_ERROR, false, null);
            return false;
        } catch (IOException ioe) {
            // An error occurred while trying to communicate with the client.
            // Disconnect and return.
            if (debugEnabled()) {
                TRACER.debugCaught(DebugLogLevel.ERROR, ioe);
            }
            connection.disconnect(DisconnectReason.IO_ERROR, false, null);
            return false;
        }
    }

    /**
     * Creates a buffer suitable to send to the client using the specified
     * clear byte array, offset and length of the bytes to wrap.
     *
     * @param clearBytes The clear byte array to send to the client.
     * @param offSet An offset into the byte array to start the wrap at.
     * @param len The length of the bytes to wrap in the byte array.
     * @throws SaslException If the wrap of the bytes fails.
     */
    private ByteBuffer
    createSendBuffer(byte[] clearBytes, int offSet, int len)
    throws SaslException {
        byte[] wrapBytes = saslContext.wrap(clearBytes, offSet, len);
        byte[] outBuf = new  byte[wrapBytes.length + lengthSize];
        writeBufLen(outBuf, wrapBytes.length);
        System.arraycopy(wrapBytes, 0, outBuf, lengthSize, wrapBytes.length);
        return ByteBuffer.wrap(outBuf);
    }

    /**
     *  Writes the specified len parameter into the buffer in a form that can
     *  be sent over a network to the client.
     *
     * @param buf The buffer to hold the length bytes.
     * @param len The length to encode.
     */
    private void writeBufLen(byte[] buf, int len) {
        for (int i = 3; i >= 0; i--) {
            buf[i] = (byte)(len & 0xff);
            len >>>= 8;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override()
    public boolean writeData(ByteBuffer clearData) {
        int maxSendBufSize = saslContext.getBufSize(Sasl.RAW_SEND_SIZE);
        int clearLength = clearData.limit();
        try {
            if(clearLength < maxSendBufSize) {
                ByteBuffer sendBuffer =
                    createSendBuffer(clearData.array(),0,clearLength);
                return writeChannel(sendBuffer);
            } else {
                byte[] clearBytes = clearData.array();
                for(int i=0; i < clearLength; i += maxSendBufSize) {
                    int totLength = (clearLength - i) < maxSendBufSize ?
                                    (clearLength - i) : maxSendBufSize;
                    ByteBuffer sendBuffer =
                                     createSendBuffer(clearBytes, i, totLength);
                    if(!writeChannel(sendBuffer))
                        return false;
                }
            }
        } catch (SaslException e) {
            if (debugEnabled()) {
                TRACER.debugCaught(DebugLogLevel.ERROR, e);
            }
            connection.disconnect(DisconnectReason.IO_ERROR, false, null);
            return false;
        }
        return true;
    }

   /**
    * Write the specified byte buffer to the socket channel.
    *
    * @param buffer The byte buffer to write to the socket channel.
    * @return {@code true} if the byte buffer was successfully written to the
    *         socket channel, or, {@code false} if not.
    */
    private boolean writeChannel(ByteBuffer buffer) {
        try {
            while (buffer.hasRemaining())  {
                int bytesWritten = sockChannel.write(buffer);
                if (bytesWritten < 0)  {
                    connection.disconnect(DisconnectReason.CLIENT_DISCONNECT,
                                          false, null);
                    return false;
                } else if (bytesWritten == 0)  {
                    return writeWithTimeout(buffer);
                }
            }
        }  catch (IOException ioe)  {
            if (debugEnabled()) {
                TRACER.debugCaught(DebugLogLevel.ERROR, ioe);
            }
            connection.disconnect(DisconnectReason.IO_ERROR, false, null);
            return false;
        }
        return true;
    }

    /**
     * Writes the specified byte buffer parameter to the socket channel waiting
     * for a period of time if the buffer cannot be written immediately.
     *
     * @param buffer The byte buffer to write to the channel.
     * @return {@code true} if the bytes were sent, or, {@code false} otherwise.
     * @throws IOException If an IO error occurs while writing the bytes.
     */
    private boolean writeWithTimeout(ByteBuffer buffer) throws IOException {
        long startTime = System.currentTimeMillis();
        long waitTime  = connection.getMaxBlockedWriteTimeLimit();
        if (waitTime <= 0) {
            // We won't support an infinite time limit, so fall back to using
            // five minutes, which is a very long timeout given that we're
            // blocking a worker thread.
            waitTime = 300000L;
        }
        long stopTime = startTime + waitTime;
        Selector selector = connection.getWriteSelector();
        if (selector == null) {
            // The client connection does not provide a selector, so we'll
            // fall back to a more inefficient way that will work without a
            // selector.
            while (buffer.hasRemaining() &&
                   (System.currentTimeMillis() < stopTime)) {
                if (sockChannel.write(buffer) < 0) {
                    // The client connection has been closed
                    connection.disconnect(DisconnectReason.CLIENT_DISCONNECT,
                                          false,  null);
                    return false;
                }
            }
            if (buffer.hasRemaining()) {
                // If we've gotten here, then the write timed out.
                // Terminate the client connection.
                connection.disconnect(DisconnectReason.IO_TIMEOUT, false, null);
                return false;
            }
            return true;
        }
        // Register with the selector for handling write operations.
        SelectionKey key =
                         sockChannel.register(selector, SelectionKey.OP_WRITE);
        try
        {
            selector.select(waitTime);
            while (buffer.hasRemaining())
            {
                long currentTime = System.currentTimeMillis();
                if (currentTime >= stopTime) {
                    // We've been blocked for too long
                    connection.disconnect(DisconnectReason.IO_TIMEOUT,
                                          false, null);
                    return false;
                }
                else {
                    waitTime = stopTime - currentTime;
                }
                Iterator<SelectionKey> iterator =
                                             selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey k = iterator.next();
                    if (k.isWritable()) {
                        int bytesWritten = sockChannel.write(buffer);
                        if (bytesWritten < 0) {
                            // The client connection has been closed.
                            connection.disconnect(
                                      DisconnectReason.CLIENT_DISCONNECT,
                                      false, null);
                            return false;
                        }
                        iterator.remove();
                    }
                }
                if (buffer.hasRemaining()) {
                    selector.select(waitTime);
                }
            }
            return true;
        } finally {
            if (key.isValid()) {
                key.cancel();
                selector.selectNow();
            }
        }
    }

    /**
     * Return if the underlying SASL context is active or not. The SASL context
     * may still be negotiating a multi-stage SASL bind and is not ready to
     * process confidentiality or integrity data yet.
     *
     * @return {@code true} if the underlying SASL context is active or ready
     *         to process confidentiality/integrity messages, or, {@code false}
     *         if not.
    */

    public boolean isActive() {
        return saslContext.isBindComplete();
    }

    /**
     * Return the cipher Security Strength Function of the cipher used in the
     * SSAL context.
     *
     * @return The cipher SSF of the cipher used in the SASL context.
     */
    public int getSSF() {
        return saslContext.getSSF();
    }
}
