//
//  ========================================================================
//  Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.io.nio;

import android.annotation.SuppressLint;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;

import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.AsyncEndPoint;
import org.eclipse.jetty.io.Buffer;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;

/* ------------------------------------------------------------ */
/** SSL Connection.
 * An AysyncConnection that acts as an interceptor between and EndPoint and another
 * Connection, that implements TLS encryption using an {@link SSLEngine}.
 * <p>
 * The connector uses an {@link AsyncEndPoint} (like {@link SelectChannelEndPoint}) as
 * it's source/sink of encrypted data.   It then provides {@link #getSslEndPoint()} to
 * expose a source/sink of unencrypted data to another connection (eg HttpConnection).
 */
public class SslConnection extends AbstractConnection implements AsyncConnection
{
    private final Logger _logger = Log.getLogger("org.eclipse.jetty.io.nio.ssl");

    private static final NIOBuffer __ZERO_BUFFER=new IndirectNIOBuffer(0);

    private static final ThreadLocal<SslBuffers> __buffers = new ThreadLocal<SslBuffers>();
    private final SSLEngine _engine;
    private final SSLSession _session;
    private AsyncConnection _connection;
    private final SslEndPoint _sslEndPoint;
    private int _allocations;
    private SslBuffers _buffers;
    private NIOBuffer _inbound;
    private NIOBuffer _unwrapBuf;
    private NIOBuffer _outbound;
    private AsyncEndPoint _aEndp;
    private boolean _allowRenegotiate=true;
    private boolean _handshook;
    private boolean _ishut;
    private boolean _oshut;
    private final AtomicBoolean _progressed = new AtomicBoolean();

    /* ------------------------------------------------------------ */
    /* this is a half baked buffer pool
     */
    private static class SslBuffers
    {
        final NIOBuffer _in;
        final NIOBuffer _out;
        final NIOBuffer _unwrap;

        SslBuffers(int packetSize, int appSize)
        {
            _in=new IndirectNIOBuffer(packetSize);
            _out=new IndirectNIOBuffer(packetSize);
            _unwrap=new IndirectNIOBuffer(appSize);
        }
    }

    /* ------------------------------------------------------------ */
    public SslConnection(SSLEngine engine,EndPoint endp)
    {
        this(engine,endp,System.currentTimeMillis());
    }

    /* ------------------------------------------------------------ */
    public SslConnection(SSLEngine engine,EndPoint endp, long timeStamp)
    {
        super(endp,timeStamp);
        _engine=engine;
        _session=_engine.getSession();
        _aEndp=(AsyncEndPoint)endp;
        _sslEndPoint = newSslEndPoint();
    }

    /* ------------------------------------------------------------ */
    protected SslEndPoint newSslEndPoint()
    {
        return new SslEndPoint();
    }

    /* ------------------------------------------------------------ */
    private void allocateBuffers()
    {
        synchronized (this)
        {
            if (_allocations++==0)
            {
                if (_buffers==null)
                {
                    _buffers=__buffers.get();
                    if (_buffers==null)
                        _buffers=new SslBuffers(_session.getPacketBufferSize()*2,_session.getApplicationBufferSize()*2);
                    _inbound=_buffers._in;
                    _outbound=_buffers._out;
                    _unwrapBuf=_buffers._unwrap;
                    __buffers.set(null);
                }
            }
        }
    }

    /* ------------------------------------------------------------ */
    private void releaseBuffers()
    {
        synchronized (this)
        {
            if (--_allocations==0)
            {
                if (_buffers!=null &&
                    _inbound.length()==0 &&
                    _outbound.length()==0 &&
                    _unwrapBuf.length()==0)
                {
                    _inbound=null;
                    _outbound=null;
                    _unwrapBuf=null;
                    __buffers.set(_buffers);
                    _buffers=null;
                }
            }
        }
    }

    /* ------------------------------------------------------------ */
    public Connection handle() throws IOException
    {
        try
        {
            allocateBuffers();

            boolean progress=true;

            while (progress)
            {
                progress=false;

                // If we are handshook let the delegate connection
                if (_engine.getHandshakeStatus()!=HandshakeStatus.NOT_HANDSHAKING)
                    progress=process(null,null);

                // handle the delegate connection
                AsyncConnection next = (AsyncConnection)_connection.handle();
                if (next!=_connection && next!=null)
                {
                    _connection=next;
                    progress=true;
                }

                _logger.debug("{} handle {} progress={}", _session, this, progress);
            }
        }
        finally
        {
            releaseBuffers();

            if (!_ishut && _sslEndPoint.isInputShutdown() && _sslEndPoint.isOpen())
            {
                _ishut=true;
                try
                {
                    _connection.onInputShutdown();
                }
                catch(Throwable x)
                {
                    _logger.warn("onInputShutdown failed", x);
                    try{_sslEndPoint.close();}
                    catch(IOException e2){
                        _logger.ignore(e2);}
                }
            }
        }

        return this;
    }

    /* ------------------------------------------------------------ */
    public boolean isSuspended()
    {
        return false;
    }

    /* ------------------------------------------------------------ */
    public void onClose()
    {
        Connection connection = _sslEndPoint.getConnection();
        if (connection != null && connection != this)
            connection.onClose();
    }

    /* ------------------------------------------------------------ */
    @Override
    public void onIdleExpired(long idleForMs)
    {
        try
        {
            _logger.debug("onIdleExpired {}ms on {}",idleForMs,this);
            if (_endp.isOutputShutdown())
                _sslEndPoint.close();
            else
                _sslEndPoint.shutdownOutput();
        }
        catch (IOException e)
        {
            _logger.warn(e);
            super.onIdleExpired(idleForMs);
        }
    }

    /* ------------------------------------------------------------ */
    public void onInputShutdown() throws IOException
    {

    }

    /* ------------------------------------------------------------ */
    private synchronized boolean process(Buffer toFill, Buffer toFlush) throws IOException
    {
        boolean some_progress=false;
        try
        {
            // We need buffers to progress
            allocateBuffers();

            // if we don't have a buffer to put received data into
            if (toFill==null)
            {
                // use the unwrapbuffer to hold received data.
                _unwrapBuf.compact();
                toFill=_unwrapBuf;
            }
            // Else if the fill buffer is too small for the SSL session
            else if (toFill.capacity()<_session.getApplicationBufferSize())
            {
                // fill to the temporary unwrapBuffer
                boolean progress=process(null,toFlush);

                // if we received any data,
                if (_unwrapBuf!=null && _unwrapBuf.hasContent())
                {
                    // transfer from temp buffer to fill buffer
                    _unwrapBuf.skip(toFill.put(_unwrapBuf));
                    return true;
                }
                else
                    // return progress from recursive call
                    return progress;
            }
            // Else if there is some temporary data
            else if (_unwrapBuf!=null && _unwrapBuf.hasContent())
            {
                // transfer from temp buffer to fill buffer
                _unwrapBuf.skip(toFill.put(_unwrapBuf));
                return true;
            }

            // If we are here, we have a buffer ready into which we can put some read data.

            // If we have no data to flush, flush the empty buffer
            if (toFlush==null)
                toFlush=__ZERO_BUFFER;

            // While we are making progress processing SSL engine
            boolean progress=true;
            while (progress)
            {
                progress=false;

                // Do any real IO
                int filled=0,flushed=0;
                try
                {
                    // Read any available data
                    if (_inbound.space() > 0 && (filled = _endp.fill(_inbound)) > 0)
                        progress = true;

                    // flush any output data
                    if (_outbound.hasContent() && (flushed=_endp.flush(_outbound)) > 0)
                        progress = true;
                }
                catch (IOException e)
                {
                    _endp.close();
                    throw e;
                }
                finally
                {
                    _logger.debug("{} {} {} filled={}/{} flushed={}/{}",_session,this,_engine.getHandshakeStatus(),filled,_inbound.length(),flushed,_outbound.length());
                }

                // handle the current hand share status
                switch(_engine.getHandshakeStatus())
                {
                    case FINISHED:
                        throw new IllegalStateException();

                    case NOT_HANDSHAKING:
                    {
                        // Try unwrapping some application data
                        if (toFill.space()>0 && _inbound.hasContent() && unwrap(toFill))
                            progress=true;

                        // Try wrapping some application data
                        if (toFlush.hasContent() && _outbound.space()>0 && wrap(toFlush))
                            progress=true;
                    }
                    break;

                    case NEED_TASK:
                    {
                        // A task needs to be run, so run it!
                        Runnable task;
                        while ((task=_engine.getDelegatedTask())!=null)
                        {
                            progress=true;
                            task.run();
                        }

                    }
                    break;

                    case NEED_WRAP:
                    {
                        // The SSL needs to send some handshake data to the other side
                        if (_handshook && !_allowRenegotiate)
                            _endp.close();
                        else if (wrap(toFlush))
                            progress=true;
                    }
                    break;

                    case NEED_UNWRAP:
                    {
                        // The SSL needs to receive some handshake data from the other side
                        if (_handshook && !_allowRenegotiate)
                            _endp.close();
                        else if (!_inbound.hasContent()&&filled==-1)
                        {
                            // No more input coming
                            _endp.shutdownInput();
                        }
                        else if (unwrap(toFill))
                            progress=true;
                    }
                    break;
                }

                // pass on ishut/oshut state
                if (_endp.isOpen() && _endp.isInputShutdown() && !_inbound.hasContent())
                    closeInbound();

                if (_endp.isOpen() && _engine.isOutboundDone() && !_outbound.hasContent())
                    _endp.shutdownOutput();

                // remember if any progress has been made
                some_progress|=progress;
            }

            // If we are reading into the temp buffer and it has some content, then we should be dispatched.
            if (toFill==_unwrapBuf && _unwrapBuf.hasContent() && !_connection.isSuspended())
                _aEndp.dispatch();
        }
        finally
        {
            releaseBuffers();
            if (some_progress)
                _progressed.set(true);
        }
        return some_progress;
    }

    private void closeInbound()
    {
        try
        {
            _engine.closeInbound();
        }
        catch (SSLException x)
        {
            _logger.debug(x);
        }
    }

    @SuppressLint("TrulyRandom")
    private synchronized boolean wrap(final Buffer buffer) throws IOException
    {
        ByteBuffer bbuf=extractByteBuffer(buffer);
        final SSLEngineResult result;
        int encrypted_produced = 0;
        int decrypted_consumed = 0;
        synchronized(bbuf)
        {
            _outbound.compact();
            ByteBuffer out_buffer=_outbound.getByteBuffer();
            synchronized(out_buffer)
            {
                try
                {
                    bbuf.position(buffer.getIndex());
                    bbuf.limit(buffer.putIndex());
                    int decrypted_position = bbuf.position();

                    out_buffer.position(_outbound.putIndex());
                    out_buffer.limit(out_buffer.capacity());
                    int encrypted_position = out_buffer.position();

                    result=_engine.wrap(bbuf,out_buffer);
                    if (_logger.isDebugEnabled())
                        _logger.debug("{} wrap {} {} consumed={} produced={}",
                            _session,
                            result.getStatus(),
                            result.getHandshakeStatus(),
                            result.bytesConsumed(),
                            result.bytesProduced());

                    decrypted_consumed = bbuf.position() - decrypted_position;
                    buffer.skip(decrypted_consumed);

                    encrypted_produced = out_buffer.position() - encrypted_position;
                    _outbound.setPutIndex(_outbound.putIndex() + encrypted_produced);
                }
                catch(SSLException e)
                {
                    _logger.debug(String.valueOf(_endp), e);
                    _endp.close();
                    throw e;
                }
                catch (Exception x)
                {
                    throw new IOException(x);
                }
                finally
                {
                    out_buffer.position(0);
                    out_buffer.limit(out_buffer.capacity());
                    bbuf.position(0);
                    bbuf.limit(bbuf.capacity());
                }
            }
        }

        switch(result.getStatus())
        {
            case BUFFER_UNDERFLOW:
                throw new IllegalStateException();

            case BUFFER_OVERFLOW:
                break;

            case OK:
                if (result.getHandshakeStatus()==HandshakeStatus.FINISHED)
                    _handshook=true;
                break;

            case CLOSED:
                _logger.debug("wrap CLOSE {} {}",this,result);
                if (result.getHandshakeStatus()==HandshakeStatus.FINISHED)
                    _endp.close();
                break;

            default:
                _logger.debug("{} wrap default {}",_session,result);
            throw new IOException(result.toString());
        }

        return decrypted_consumed > 0 || encrypted_produced > 0;
    }

    private synchronized boolean unwrap(final Buffer buffer) throws IOException
    {
        if (!_inbound.hasContent())
            return false;

        ByteBuffer bbuf=extractByteBuffer(buffer);
        final SSLEngineResult result;
        int encrypted_consumed = 0;
        int decrypted_produced = 0;
        synchronized(bbuf)
        {
            ByteBuffer in_buffer=_inbound.getByteBuffer();
            synchronized(in_buffer)
            {
                try
                {
                    bbuf.position(buffer.putIndex());
                    bbuf.limit(buffer.capacity());
                    int decrypted_position = bbuf.position();

                    in_buffer.position(_inbound.getIndex());
                    in_buffer.limit(_inbound.putIndex());
                    int encrypted_position = in_buffer.position();

                    result=_engine.unwrap(in_buffer,bbuf);
                    if (_logger.isDebugEnabled())
                        _logger.debug("{} unwrap {} {} consumed={} produced={}",
                            _session,
                            result.getStatus(),
                            result.getHandshakeStatus(),
                            result.bytesConsumed(),
                            result.bytesProduced());

                    encrypted_consumed = in_buffer.position() - encrypted_position;
                    _inbound.skip(encrypted_consumed);
                    _inbound.compact();

                    decrypted_produced = bbuf.position() - decrypted_position;
                    buffer.setPutIndex(buffer.putIndex() + decrypted_produced);
                }
                catch(SSLException e)
                {
                    _logger.debug(String.valueOf(_endp), e);
                    _endp.close();
                    throw e;
                }
                catch (Exception x)
                {
                    throw new IOException(x);
                }
                finally
                {
                    in_buffer.position(0);
                    in_buffer.limit(in_buffer.capacity());
                    bbuf.position(0);
                    bbuf.limit(bbuf.capacity());
                }
            }
        }

        switch(result.getStatus())
        {
            case BUFFER_UNDERFLOW:
                if (_endp.isInputShutdown())
                    _inbound.clear();
                break;

            case BUFFER_OVERFLOW:
                if (_logger.isDebugEnabled()) _logger.debug("{} unwrap {} {}->{}",_session,result.getStatus(),_inbound.toDetailString(),buffer.toDetailString());
                break;

            case OK:
                if (result.getHandshakeStatus()==HandshakeStatus.FINISHED)
                    _handshook=true;
                break;

            case CLOSED:
                _logger.debug("unwrap CLOSE {} {}",this,result);
                if (result.getHandshakeStatus()==HandshakeStatus.FINISHED)
                    _endp.close();
                break;

            default:
                _logger.debug("{} wrap default {}",_session,result);
            throw new IOException(result.toString());
        }

        return encrypted_consumed > 0 || decrypted_produced > 0;
    }

    /* ------------------------------------------------------------ */
    private ByteBuffer extractByteBuffer(Buffer buffer)
    {
        if (buffer.buffer() instanceof NIOBuffer)
            return ((NIOBuffer)buffer.buffer()).getByteBuffer();
        return ByteBuffer.wrap(buffer.array());
    }

    /* ------------------------------------------------------------ */
    public AsyncEndPoint getSslEndPoint()
    {
        return _sslEndPoint;
    }

    /* ------------------------------------------------------------ */
    public String toString()
    {
        return String.format("%s %s", super.toString(), _sslEndPoint);
    }

    /* ------------------------------------------------------------ */
    /* ------------------------------------------------------------ */
    public class SslEndPoint implements AsyncEndPoint
    {
        public void shutdownOutput() throws IOException
        {
            synchronized (SslConnection.this)
            {
                try
                {
                    _logger.debug("{} ssl endp.oshut {}",_session,this);
                    _oshut=true;
                    _engine.closeOutbound();
                }
                catch (Exception x)
                {
                    throw new IOException(x);
                }
            }
            flush();
        }

        public boolean isOutputShutdown()
        {
            synchronized (SslConnection.this)
            {
                return _oshut||!isOpen()||_engine.isOutboundDone();
            }
        }

        public void shutdownInput() throws IOException
        {
            _logger.debug("{} ssl endp.ishut!",_session);
            // We do not do a closeInput here, as SSL does not support half close.
            // isInputShutdown works it out itself from buffer state and underlying endpoint state.
        }

        public boolean isInputShutdown()
        {
            synchronized (SslConnection.this)
            {
                return _endp.isInputShutdown() &&
                !(_unwrapBuf!=null&&_unwrapBuf.hasContent()) &&
                !(_inbound!=null&&_inbound.hasContent());
            }
        }

        public void close() throws IOException
        {
            _logger.debug("{} ssl endp.close",_session);
            _endp.close();
        }

        public int fill(Buffer buffer) throws IOException
        {
            int size=buffer.length();
            process(buffer, null);

            int filled=buffer.length()-size;

            if (filled==0 && isInputShutdown())
                return -1;
            return filled;
        }

        public int flush(Buffer buffer) throws IOException
        {
            int size = buffer.length();
            process(null, buffer);
            return size-buffer.length();
        }

        public boolean blockWritable(long millisecs) throws IOException
        {
            return _endp.blockWritable(millisecs);
        }

        public boolean isOpen()
        {
            return _endp.isOpen();
        }

        public void flush() throws IOException
        {
            process(null, null);
        }

        public void dispatch()
        {
            _aEndp.dispatch();
        }

        public void scheduleWrite()
        {
            _aEndp.scheduleWrite();
        }

        public boolean hasProgressed()
        {
            return _progressed.getAndSet(false);
        }

        public String getLocalAddr()
        {
            return _aEndp.getLocalAddr();
        }

        public int getLocalPort()
        {
            return _aEndp.getLocalPort();
        }

        public String getRemoteAddr()
        {
            return _aEndp.getRemoteAddr();
        }

        public int getRemotePort()
        {
            return _aEndp.getRemotePort();
        }

        public boolean isBlocking()
        {
            return false;
        }

        public int getMaxIdleTime()
        {
            return _aEndp.getMaxIdleTime();
        }

        public void setMaxIdleTime(int timeMs) throws IOException
        {
            _aEndp.setMaxIdleTime(timeMs);
        }

        public Connection getConnection()
        {
            return _connection;
        }

        public void setConnection(Connection connection)
        {
            _connection=(AsyncConnection)connection;
        }

        public String toString()
        {
            // Do NOT use synchronized (SslConnection.this)
            // because it's very easy to deadlock when debugging is enabled.
            // We do a best effort to print the right toString() and that's it.
            Buffer inbound = _inbound;
            Buffer outbound = _outbound;
            Buffer unwrap = _unwrapBuf;
            int i = inbound == null? -1 : inbound.length();
            int o = outbound == null ? -1 : outbound.length();
            int u = unwrap == null ? -1 : unwrap.length();
            return String.format(Locale.getDefault(), "SSL %s i/o/u=%d/%d/%d ishut=%b oshut=%b {%s}",
                    _engine.getHandshakeStatus(),
                    i, o, u,
                    _ishut, _oshut,
                    _connection);
        }
    }
}
