/*
 * Copyright 2005-2006 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */

package sun.net.httpserver;

import java.net.*;
import java.nio.*;
import java.io.*;
import java.nio.channels.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
import javax.net.ssl.*;
import javax.net.ssl.SSLEngineResult.*;
import com.sun.net.httpserver.*;
import com.sun.net.httpserver.spi.*;

/**
 * given a non-blocking SocketChannel, it produces
 * (blocking) streams which encrypt/decrypt the SSL content
 * and handle the SSL handshaking automatically.
 */

class SSLStreams {

    SSLContext sslctx;
    SocketChannel chan;
    TimeSource time;
    ServerImpl server;
    SSLEngine engine;
    EngineWrapper wrapper;
    OutputStream os;
    InputStream is;
    static long readTimeout = ServerConfig.getReadTimeout();
    static long writeTimeout = ServerConfig.getWriteTimeout();

    /* held by thread doing the hand-shake on this connection */
    Lock handshaking = new ReentrantLock();

    SSLStreams (ServerImpl server, SSLContext sslctx, SocketChannel chan) throws IOException {
        this.server = server;
        this.time= (TimeSource)server;
        this.sslctx= sslctx;
        this.chan= chan;
        InetSocketAddress addr =
                (InetSocketAddress)chan.socket().getRemoteSocketAddress();
        engine = sslctx.createSSLEngine (addr.getHostName(), addr.getPort());
        engine.setUseClientMode (false);
        HttpsConfigurator cfg = server.getHttpsConfigurator();
        configureEngine (cfg, addr);
        wrapper = new EngineWrapper (chan, engine);
    }

    private void configureEngine(HttpsConfigurator cfg, InetSocketAddress addr){
        if (cfg != null) {
            Parameters params = new Parameters (cfg, addr);
            cfg.configure (params);
            SSLParameters sslParams = params.getSSLParameters();
            if (sslParams != null) {
                engine.setSSLParameters (sslParams);
            } else {
                /* tiger compatibility */
                if (params.getCipherSuites() != null) {
                    try {
                        engine.setEnabledCipherSuites (
                            params.getCipherSuites()
                        );
                    } catch (IllegalArgumentException e) { /* LOG */}
                }
                engine.setNeedClientAuth (params.getNeedClientAuth());
                engine.setWantClientAuth (params.getWantClientAuth());
                if (params.getProtocols() != null) {
                    try {
                        engine.setEnabledProtocols (
                            params.getProtocols()
                        );
                    } catch (IllegalArgumentException e) { /* LOG */}
                }
            }
        }
    }

    class Parameters extends HttpsParameters {
        InetSocketAddress addr;
        SSLParameters params;
        HttpsConfigurator cfg;

        Parameters (HttpsConfigurator cfg, InetSocketAddress addr) {
            this.addr = addr;
            this.cfg = cfg;
        }
        public InetSocketAddress getClientAddress () {
            return addr;
        }
        public HttpsConfigurator getHttpsConfigurator() {
            return cfg;
        }
        public void setSSLParameters (SSLParameters p) {
            params = p;
        }
        SSLParameters getSSLParameters () {
            return params;
        }
    }

    /**
     * cleanup resources allocated inside this object
     */
    void close () throws IOException {
        wrapper.close();
    }

    /**
     * return the SSL InputStream
     */
    InputStream getInputStream () throws IOException {
        if (is == null) {
            is = new InputStream();
        }
        return is;
    }

    /**
     * return the SSL OutputStream
     */
    OutputStream getOutputStream () throws IOException {
        if (os == null) {
            os = new OutputStream();
        }
        return os;
    }

    SSLEngine getSSLEngine () {
        return engine;
    }

    /**
     * request the engine to repeat the handshake on this session
     * the handshake must be driven by reads/writes on the streams
     * Normally, not necessary to call this.
     */
    void beginHandshake() throws SSLException {
        engine.beginHandshake();
    }

    class WrapperResult {
        SSLEngineResult result;

        /* if passed in buffer was not big enough then the
         * a reallocated buffer is returned here
         */
        ByteBuffer buf;
    }

    int app_buf_size;
    int packet_buf_size;

    enum BufType {
        PACKET, APPLICATION
    };

    private ByteBuffer allocate (BufType type) {
        return allocate (type, -1);
    }

    private ByteBuffer allocate (BufType type, int len) {
        assert engine != null;
        synchronized (this) {
            int size;
            if (type == BufType.PACKET) {
                if (packet_buf_size == 0) {
                    SSLSession sess = engine.getSession();
                    packet_buf_size = sess.getPacketBufferSize();
                }
                if (len > packet_buf_size) {
                    packet_buf_size = len;
                }
                size = packet_buf_size;
            } else {
                if (app_buf_size == 0) {
                    SSLSession sess = engine.getSession();
                    app_buf_size = sess.getApplicationBufferSize();
                }
                if (len > app_buf_size) {
                    app_buf_size = len;
                }
                size = app_buf_size;
            }
            return ByteBuffer.allocate (size);
        }
    }

    /* reallocates the buffer by :-
     * 1. creating a new buffer double the size of the old one
     * 2. putting the contents of the old buffer into the new one
     * 3. set xx_buf_size to the new size if it was smaller than new size
     *
     * flip is set to true if the old buffer needs to be flipped
     * before it is copied.
     */
    private ByteBuffer realloc (ByteBuffer b, boolean flip, BufType type) {
        synchronized (this) {
            int nsize = 2 * b.capacity();
            ByteBuffer n = allocate (type, nsize);
            if (flip) {
                b.flip();
            }
            n.put(b);
            b = n;
        }
        return b;
    }
    /**
     * This is a thin wrapper over SSLEngine and the SocketChannel,
     * which guarantees the ordering of wraps/unwraps with respect to the underlying
     * channel read/writes. It handles the UNDER/OVERFLOW status codes
     * It does not handle the handshaking status codes, or the CLOSED status code
     * though once the engine is closed, any attempt to read/write to it
     * will get an exception.  The overall result is returned.
     * It functions synchronously/blocking
     */
    class EngineWrapper {

        SocketChannel chan;
        SSLEngine engine;
        SelectorCache sc;
        Selector write_selector, read_selector;
        SelectionKey wkey, rkey;
        Object wrapLock, unwrapLock;
        ByteBuffer unwrap_src, wrap_dst;
        boolean closed = false;
        int u_remaining; // the number of bytes left in unwrap_src after an unwrap()

        EngineWrapper (SocketChannel chan, SSLEngine engine) throws IOException {
            this.chan = chan;
            this.engine = engine;
            wrapLock = new Object();
            unwrapLock = new Object();
            unwrap_src = allocate(BufType.PACKET);
            wrap_dst = allocate(BufType.PACKET);
            sc = SelectorCache.getSelectorCache();
            write_selector = sc.getSelector();
            wkey = chan.register (write_selector, SelectionKey.OP_WRITE);
            read_selector = sc.getSelector();
            wkey = chan.register (read_selector, SelectionKey.OP_READ);
        }

        void close () throws IOException {
            sc.freeSelector (write_selector);
            sc.freeSelector (read_selector);
        }

        /* try to wrap and send the data in src. Handles OVERFLOW.
         * Might block if there is an outbound blockage or if another
         * thread is calling wrap(). Also, might not send any data
         * if an unwrap is needed.
         */
        WrapperResult wrapAndSend(ByteBuffer src) throws IOException {
            return wrapAndSendX(src, false);
        }

        WrapperResult wrapAndSendX(ByteBuffer src, boolean ignoreClose) throws IOException {
            if (closed && !ignoreClose) {
                throw new IOException ("Engine is closed");
            }
            Status status;
            WrapperResult r = new WrapperResult();
            synchronized (wrapLock) {
                wrap_dst.clear();
                do {
                    r.result = engine.wrap (src, wrap_dst);
                    status = r.result.getStatus();
                    if (status == Status.BUFFER_OVERFLOW) {
                        wrap_dst = realloc (wrap_dst, true, BufType.PACKET);
                    }
                } while (status == Status.BUFFER_OVERFLOW);
                if (status == Status.CLOSED && !ignoreClose) {
                    closed = true;
                    return r;
                }
                if (r.result.bytesProduced() > 0) {
                    wrap_dst.flip();
                    int l = wrap_dst.remaining();
                    assert l == r.result.bytesProduced();
                    long currtime = time.getTime();
                    long maxtime = currtime + writeTimeout;
                    while (l>0) {
                        write_selector.select(writeTimeout); // timeout
                        currtime = time.getTime();
                        if (currtime > maxtime) {
                            throw new SocketTimeoutException ("write timed out");
                        }
                        write_selector.selectedKeys().clear();
                        l -= chan.write (wrap_dst);
                    }
                }
            }
            return r;
        }

        /* block until a complete message is available and return it
         * in dst, together with the Result. dst may have been re-allocated
         * so caller should check the returned value in Result
         * If handshaking is in progress then, possibly no data is returned
         */
        WrapperResult recvAndUnwrap(ByteBuffer dst) throws IOException {
            Status status = Status.OK;
            WrapperResult r = new WrapperResult();
            r.buf = dst;
            if (closed) {
                throw new IOException ("Engine is closed");
            }
            boolean needData;
            if (u_remaining > 0) {
                unwrap_src.compact();
                unwrap_src.flip();
                needData = false;
            } else {
                unwrap_src.clear();
                needData = true;
            }
            synchronized (unwrapLock) {
                int x,y;
                do {
                    if (needData) {
                        long currTime = time.getTime();
                        long maxtime = currTime + readTimeout;
                        do {
                            if (currTime > maxtime) {
                                throw new SocketTimeoutException ("read timedout");
                            }
                            y = read_selector.select (readTimeout);
                            currTime = time.getTime();
                        } while (y != 1);
                        read_selector.selectedKeys().clear();
                        x = chan.read (unwrap_src);
                        if (x == -1) {
                            throw new IOException ("connection closed for reading");
                        }
                        unwrap_src.flip();
                    }
                    r.result = engine.unwrap (unwrap_src, r.buf);
                    status = r.result.getStatus();
                    if (status == Status.BUFFER_UNDERFLOW) {
                        if (unwrap_src.limit() == unwrap_src.capacity()) {
                            /* buffer not big enough */
                            unwrap_src = realloc (
                                unwrap_src, false, BufType.PACKET
                            );
                        } else {
                            /* Buffer not full, just need to read more
                             * data off the channel. Reset pointers
                             * for reading off SocketChannel
                             */
                            unwrap_src.position (unwrap_src.limit());
                            unwrap_src.limit (unwrap_src.capacity());
                        }
                        needData = true;
                    } else if (status == Status.BUFFER_OVERFLOW) {
                        r.buf = realloc (r.buf, true, BufType.APPLICATION);
                        needData = false;
                    } else if (status == Status.CLOSED) {
                        closed = true;
                        r.buf.flip();
                        return r;
                    }
                } while (status != Status.OK);
            }
            u_remaining = unwrap_src.remaining();
            return r;
        }
    }

    /**
     * send the data in the given ByteBuffer. If a handshake is needed
     * then this is handled within this method. When this call returns,
     * all of the given user data has been sent and any handshake has been
     * completed. Caller should check if engine has been closed.
     */
    public WrapperResult sendData (ByteBuffer src) throws IOException {
        WrapperResult r=null;
        while (src.remaining() > 0) {
            r = wrapper.wrapAndSend(src);
            Status status = r.result.getStatus();
            if (status == Status.CLOSED) {
                doClosure ();
                return r;
            }
            HandshakeStatus hs_status = r.result.getHandshakeStatus();
            if (hs_status != HandshakeStatus.FINISHED &&
                hs_status != HandshakeStatus.NOT_HANDSHAKING)
            {
                doHandshake(hs_status);
            }
        }
        return r;
    }

    /**
     * read data thru the engine into the given ByteBuffer. If the
     * given buffer was not large enough, a new one is allocated
     * and returned. This call handles handshaking automatically.
     * Caller should check if engine has been closed.
     */
    public WrapperResult recvData (ByteBuffer dst) throws IOException {
        /* we wait until some user data arrives */
        WrapperResult r = null;
        assert dst.position() == 0;
        while (dst.position() == 0) {
            r = wrapper.recvAndUnwrap (dst);
            dst = (r.buf != dst) ? r.buf: dst;
            Status status = r.result.getStatus();
            if (status == Status.CLOSED) {
                doClosure ();
                return r;
            }

            HandshakeStatus hs_status = r.result.getHandshakeStatus();
            if (hs_status != HandshakeStatus.FINISHED &&
                hs_status != HandshakeStatus.NOT_HANDSHAKING)
            {
                doHandshake (hs_status);
            }
        }
        dst.flip();
        return r;
    }

    /* we've received a close notify. Need to call wrap to send
     * the response
     */
    void doClosure () throws IOException {
        try {
            handshaking.lock();
            ByteBuffer tmp = allocate(BufType.APPLICATION);
            WrapperResult r;
            do {
                tmp.clear();
                tmp.flip ();
                r = wrapper.wrapAndSendX (tmp, true);
            } while (r.result.getStatus() != Status.CLOSED);
        } finally {
            handshaking.unlock();
        }
    }

    /* do the (complete) handshake after acquiring the handshake lock.
     * If two threads call this at the same time, then we depend
     * on the wrapper methods being idempotent. eg. if wrapAndSend()
     * is called with no data to send then there must be no problem
     */
    void doHandshake (HandshakeStatus hs_status) throws IOException {
        try {
            handshaking.lock();
            ByteBuffer tmp = allocate(BufType.APPLICATION);
            while (hs_status != HandshakeStatus.FINISHED &&
                   hs_status != HandshakeStatus.NOT_HANDSHAKING)
            {
                WrapperResult r = null;
                switch (hs_status) {
                    case NEED_TASK:
                        Runnable task;
                        while ((task = engine.getDelegatedTask()) != null) {
                            /* run in current thread, because we are already
                             * running an external Executor
                             */
                            task.run();
                        }
                        /* fall thru - call wrap again */
                    case NEED_WRAP:
                        tmp.clear();
                        tmp.flip();
                        r = wrapper.wrapAndSend(tmp);
                        break;

                    case NEED_UNWRAP:
                        tmp.clear();
                        r = wrapper.recvAndUnwrap (tmp);
                        if (r.buf != tmp) {
                            tmp = r.buf;
                        }
                        assert tmp.position() == 0;
                        break;
                }
                hs_status = r.result.getHandshakeStatus();
            }
        } finally {
            handshaking.unlock();
        }
    }

    /**
     * represents an SSL input stream. Multiple https requests can
     * be sent over one stream. closing this stream causes an SSL close
     * input.
     */
    class InputStream extends java.io.InputStream {

        ByteBuffer bbuf;
        boolean closed = false;

        /* this stream eof */
        boolean eof = false;

        boolean needData = true;

        InputStream () {
            bbuf = allocate (BufType.APPLICATION);
        }

        public int read (byte[] buf, int off, int len) throws IOException {
            if (closed) {
                throw new IOException ("SSL stream is closed");
            }
            if (eof) {
                return 0;
            }
            int available=0;
            if (!needData) {
                available = bbuf.remaining();
                needData = (available==0);
            }
            if (needData) {
                bbuf.clear();
                WrapperResult r = recvData (bbuf);
                bbuf = r.buf== bbuf? bbuf: r.buf;
                if ((available=bbuf.remaining()) == 0) {
                    eof = true;
                    return 0;
                } else {
                    needData = false;
                }
            }
            /* copy as much as possible from buf into users buf */
            if (len > available) {
                len = available;
            }
            bbuf.get (buf, off, len);
            return len;
        }

        public int available () throws IOException {
            return bbuf.remaining();
        }

        public boolean markSupported () {
            return false; /* not possible with SSLEngine */
        }

        public void reset () throws IOException {
            throw new IOException ("mark/reset not supported");
        }

        public long skip (long s) throws IOException {
            int n = (int)s;
            if (closed) {
                throw new IOException ("SSL stream is closed");
            }
            if (eof) {
                return 0;
            }
            int ret = n;
            while (n > 0) {
                if (bbuf.remaining() >= n) {
                    bbuf.position (bbuf.position()+n);
                    return ret;
                } else {
                    n -= bbuf.remaining();
                    bbuf.clear();
                    WrapperResult r = recvData (bbuf);
                    bbuf = r.buf==bbuf? bbuf: r.buf;
                }
            }
            return ret; /* not reached */
        }

        /**
         * close the SSL connection. All data must have been consumed
         * before this is called. Otherwise an exception will be thrown.
         * [Note. May need to revisit this. not quite the normal close() symantics
         */
        public void close () throws IOException {
            eof = true;
            engine.closeInbound ();
        }

        public int read (byte[] buf) throws IOException {
            return read (buf, 0, buf.length);
        }

        byte single[] = new byte [1];

        public int read () throws IOException {
            int n = read (single, 0, 1);
            if (n == 0) {
                return -1;
            } else {
                return single[0] & 0xFF;
            }
        }
    }

    /**
     * represents an SSL output stream. plain text data written to this stream
     * is encrypted by the stream. Multiple HTTPS responses can be sent on
     * one stream. closing this stream initiates an SSL closure
     */
    class OutputStream extends java.io.OutputStream {
        ByteBuffer buf;
        boolean closed = false;
        byte single[] = new byte[1];

        OutputStream() {
            buf = allocate(BufType.APPLICATION);
        }

        public void write(int b) throws IOException {
            single[0] = (byte)b;
            write (single, 0, 1);
        }

        public void write(byte b[]) throws IOException {
            write (b, 0, b.length);
        }
        public void write(byte b[], int off, int len) throws IOException {
            if (closed) {
                throw new IOException ("output stream is closed");
            }
            while (len > 0) {
                int l = len > buf.capacity() ? buf.capacity() : len;
                buf.clear();
                buf.put (b, off, l);
                len -= l;
                off += l;
                buf.flip();
                WrapperResult r = sendData (buf);
                if (r.result.getStatus() == Status.CLOSED) {
                    closed = true;
                    if (len > 0) {
                        throw new IOException ("output stream is closed");
                    }
                }
            }
        }

        public void flush() throws IOException {
            /* no-op */
        }

        public void close() throws IOException {
            WrapperResult r=null;
            engine.closeOutbound();
            closed = true;
            HandshakeStatus stat = HandshakeStatus.NEED_WRAP;
            buf.clear();
            while (stat == HandshakeStatus.NEED_WRAP) {
                r = wrapper.wrapAndSend (buf);
                stat = r.result.getHandshakeStatus();
            }
            assert r.result.getStatus() == Status.CLOSED;
        }
    }
}
