/*
 * 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 legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * 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 legal-notices/CDDLv1_0.txt.
 * 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.
 *      Portions Copyright 2012-2015 ForgeRock AS
 */
package org.opends.server.extensions;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.security.cert.Certificate;

import org.opends.server.api.ClientConnection;

/**
 * This class implements a SASL byte channel that can be used during
 * confidentiality and integrity.
 */
public final class SASLByteChannel implements ConnectionSecurityProvider
{

  /** Private implementation. */
  private final class ByteChannelImpl implements ByteChannel
  {

    /** {@inheritDoc} */
    @Override
    public void close() throws IOException
    {
      synchronized (readLock)
      {
        synchronized (writeLock)
        {
          saslContext.dispose();
          channel.close();
        }
      }
    }



    /** {@inheritDoc} */
    @Override
    public boolean isOpen()
    {
      return saslContext != null;
    }



    /** {@inheritDoc} */
    @Override
    public int read(final ByteBuffer unwrappedData) throws IOException
    {
      synchronized (readLock)
      {
        // Only read and unwrap new data if needed.
        if (!recvUnwrappedBuffer.hasRemaining())
        {
          final int read = doRecvAndUnwrap();
          if (read <= 0)
          {
            // No data read or end of stream.
            return read;
          }
        }

        // Copy available data.
        final int startPos = unwrappedData.position();
        if (recvUnwrappedBuffer.remaining() > unwrappedData.remaining())
        {
          // Unwrapped data does not fit in client buffer so copy one byte at a
          // time: it's annoying that there is no easy way to do this with
          // ByteBuffers.
          while (unwrappedData.hasRemaining())
          {
            unwrappedData.put(recvUnwrappedBuffer.get());
          }
        }
        else
        {
          // Unwrapped data fits client buffer so block copy.
          unwrappedData.put(recvUnwrappedBuffer);
        }
        return unwrappedData.position() - startPos;
      }
    }



    /** {@inheritDoc} */
    @Override
    public int write(final ByteBuffer unwrappedData) throws IOException
    {
      // This method will block until the entire message is sent.
      final int bytesWritten = unwrappedData.remaining();

      // Synchronized in order to prevent interleaving and reordering.
      synchronized (writeLock)
      {
        // Write data in sendBufferSize segments.
        while (unwrappedData.hasRemaining())
        {
          final int remaining = unwrappedData.remaining();
          final int wrapSize = (remaining < sendUnwrappedBufferSize) ? remaining
              : sendUnwrappedBufferSize;

          final byte[] wrappedDataBytes;
          if (unwrappedData.hasArray())
          {
            // Avoid extra copy if ByteBuffer is array based.
            wrappedDataBytes = saslContext.wrap(unwrappedData.array(),
                unwrappedData.arrayOffset() + unwrappedData.position(),
                wrapSize);
          }
          else
          {
            // Non-array based ByteBuffer, so copy.
            unwrappedData.get(sendUnwrappedBytes, 0, wrapSize);
            wrappedDataBytes = saslContext
                .wrap(sendUnwrappedBytes, 0, wrapSize);
          }
          unwrappedData.position(unwrappedData.position() + wrapSize);

          // Encode SASL packet: 4 byte length + wrapped data.
          if (sendWrappedBuffer.capacity() < wrappedDataBytes.length + 4)
          {
            // Resize the send buffer.
            sendWrappedBuffer =
                ByteBuffer.allocate(wrappedDataBytes.length + 4);
          }
          sendWrappedBuffer.clear();
          sendWrappedBuffer.putInt(wrappedDataBytes.length);
          sendWrappedBuffer.put(wrappedDataBytes);
          sendWrappedBuffer.flip();

          // Write the SASL packet: our IO stack will block until all the data
          // is written.
          channel.write(sendWrappedBuffer);
        }
      }

      return bytesWritten;
    }



    /** Attempt to read and unwrap the next SASL packet. */
    private int doRecvAndUnwrap() throws IOException
    {
      // Read SASL packets until some unwrapped data is produced or no more
      // data is available on the underlying channel.
      while (true)
      {
        // Read the wrapped packet length first.
        if (recvWrappedLength < 0)
        {
          // The channel read may only partially fill the buffer due to
          // buffering in the underlying channel layer (e.g. SSL layer), so
          // repeatedly read until the length has been read or we are sure
          // that we are unable to proceed.
          while (recvWrappedLengthBuffer.hasRemaining())
          {
            final int read = channel.read(recvWrappedLengthBuffer);
            if (read <= 0)
            {
              // Not enough data available or end of stream.
              return read;
            }
          }

          // Decode the length and reset the length buffer.
          recvWrappedLengthBuffer.flip();
          recvWrappedLength = recvWrappedLengthBuffer.getInt();
          recvWrappedLengthBuffer.clear();

          // Check that the length is valid.
          if (recvWrappedLength > recvWrappedBufferMaximumSize)
          {
            throw new IOException(
                "Client sent a SASL packet specifying a length "
                    + recvWrappedLength
                    + " which exceeds the negotiated limit of "
                    + recvWrappedBufferMaximumSize);
          }

          if (recvWrappedLength < 0)
          {
            throw new IOException(
                "Client sent a SASL packet specifying a negative length "
                    + recvWrappedLength);
          }

          // Prepare the recv buffer for reading.
          recvWrappedBuffer.clear();
          recvWrappedBuffer.limit(recvWrappedLength);
        }

        // Read the wrapped packet data.

        // The channel read may only partially fill the buffer due to
        // buffering in the underlying channel layer (e.g. SSL layer), so
        // repeatedly read until the data has been read or we are sure
        // that we are unable to proceed.
        while (recvWrappedBuffer.hasRemaining())
        {
          final int read = channel.read(recvWrappedBuffer);
          if (read <= 0)
          {
            // Not enough data available or end of stream.
            return read;
          }
        }

        // The complete packet has been read, so unwrap it.
        recvWrappedBuffer.flip();
        final byte[] unwrappedDataBytes = saslContext.unwrap(
            recvWrappedBuffer.array(), 0, recvWrappedLength);
        recvWrappedLength = -1;

        // Only return the unwrapped data if it was non-empty, otherwise try to
        // read another SASL packet.
        if (unwrappedDataBytes.length > 0)
        {
          recvUnwrappedBuffer = ByteBuffer.wrap(unwrappedDataBytes);
          return recvUnwrappedBuffer.remaining();
        }
      }
    }
  }



  /**
   * Return a SASL byte channel instance created using the specified parameters.
   *
   * @param c
   *          A client connection associated with the instance.
   * @param name
   *          The name of the instance (SASL mechanism name).
   * @param context
   *          A SASL context associated with the instance.
   * @return A SASL byte channel.
   */
  public static SASLByteChannel getSASLByteChannel(final ClientConnection c,
      final String name, final SASLContext context)
  {
    return new SASLByteChannel(c, name, context);
  }



  private final String name;
  private final ByteChannel channel;
  private final ByteChannelImpl pimpl = new ByteChannelImpl();
  private final SASLContext saslContext;

  private ByteBuffer recvUnwrappedBuffer;
  private final ByteBuffer recvWrappedBuffer;
  private final int recvWrappedBufferMaximumSize;
  private int recvWrappedLength = -1;
  private final ByteBuffer recvWrappedLengthBuffer = ByteBuffer.allocate(4);

  private final int sendUnwrappedBufferSize;
  private final byte[] sendUnwrappedBytes;
  private ByteBuffer sendWrappedBuffer;

  private final Object readLock = new Object();
  private final Object writeLock = new Object();



  /**
   * Create a SASL byte channel 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.
   */
  private SASLByteChannel(final ClientConnection connection, final String name,
      final SASLContext saslContext)
  {
    this.name = name;
    this.saslContext = saslContext;

    channel = connection.getChannel();
    recvWrappedBufferMaximumSize = saslContext.getMaxReceiveBufferSize();
    sendUnwrappedBufferSize = saslContext.getMaxRawSendBufferSize();

    recvWrappedBuffer = ByteBuffer.allocate(recvWrappedBufferMaximumSize);
    recvUnwrappedBuffer = ByteBuffer.allocate(0);
    sendUnwrappedBytes = new byte[sendUnwrappedBufferSize];
    sendWrappedBuffer = ByteBuffer.allocate(sendUnwrappedBufferSize + 64);
  }



  /** {@inheritDoc} */
  @Override
  public ByteChannel getChannel()
  {
    return pimpl;
  }



  /** {@inheritDoc} */
  @Override
  public Certificate[] getClientCertificateChain()
  {
    return new Certificate[0];
  }



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



  /** {@inheritDoc} */
  @Override
  public int getSSF()
  {
    return saslContext.getSSF();
  }



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

}
