src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/SSLDelegate.java
branchhttp-client-branch
changeset 56079 d23b02f37fce
parent 55973 4d9b002587db
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/SSLDelegate.java	Tue Feb 06 14:10:28 2018 +0000
@@ -0,0 +1,489 @@
+/*
+ * Copyright (c) 2015, 2018, Oracle and/or its affiliates. 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.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.incubator.http.internal;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import javax.net.ssl.SSLEngineResult.HandshakeStatus;
+import javax.net.ssl.SSLEngineResult.Status;
+import javax.net.ssl.*;
+import jdk.incubator.http.internal.common.Log;
+import jdk.incubator.http.internal.common.Utils;
+import static javax.net.ssl.SSLEngineResult.HandshakeStatus.*;
+
+/**
+ * Implements the mechanics of SSL by managing an SSLEngine object.
+ * <p>
+ * This class is only used to implement the {@link
+ * AbstractAsyncSSLConnection.SSLConnectionChannel} which is handed of
+ * to RawChannelImpl when creating a WebSocket.
+ */
+class SSLDelegate {
+
+    final SSLEngine engine;
+    final EngineWrapper wrapper;
+    final Lock handshaking = new ReentrantLock();
+    final SocketChannel chan;
+
+    SSLDelegate(SSLEngine eng, SocketChannel chan)
+    {
+        this.engine = eng;
+        this.chan = chan;
+        this.wrapper = new EngineWrapper(chan, engine);
+    }
+
+    // alpn[] may be null
+//    SSLDelegate(SocketChannel chan, HttpClientImpl client, String[] alpn, String sn)
+//        throws IOException
+//    {
+//        serverName = sn;
+//        SSLContext context = client.sslContext();
+//        engine = context.createSSLEngine();
+//        engine.setUseClientMode(true);
+//        SSLParameters sslp = client.sslParameters();
+//        sslParameters = Utils.copySSLParameters(sslp);
+//        if (sn != null) {
+//            SNIHostName sni = new SNIHostName(sn);
+//            sslParameters.setServerNames(List.of(sni));
+//        }
+//        if (alpn != null) {
+//            sslParameters.setApplicationProtocols(alpn);
+//            Log.logSSL("SSLDelegate: Setting application protocols: {0}" + Arrays.toString(alpn));
+//        } else {
+//            Log.logSSL("SSLDelegate: No application protocols proposed");
+//        }
+//        engine.setSSLParameters(sslParameters);
+//        wrapper = new EngineWrapper(chan, engine);
+//        this.chan = chan;
+//        this.client = client;
+//    }
+
+//    SSLParameters getSSLParameters() {
+//        return sslParameters;
+//    }
+
+    static long countBytes(ByteBuffer[] buffers, int start, int number) {
+        long c = 0;
+        for (int i=0; i<number; i++) {
+            c+= buffers[start+i].remaining();
+        }
+        return c;
+    }
+
+
+    static class WrapperResult {
+        static WrapperResult createOK() {
+            WrapperResult r = new WrapperResult();
+            r.buf = null;
+            r.result = new SSLEngineResult(Status.OK, NOT_HANDSHAKING, 0, 0);
+            return r;
+        }
+        SSLEngineResult result;
+
+        ByteBuffer buf; // buffer containing result data
+    }
+
+    int app_buf_size;
+    int packet_buf_size;
+
+    enum BufType {
+        PACKET,
+        APPLICATION
+    }
+
+    ByteBuffer allocate (BufType type) {
+        return allocate (type, -1);
+    }
+
+    // TODO: Use buffer pool for this
+    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) {
+        // TODO: there should be the linear growth, rather than exponential as
+        // we definitely know the maximum amount of space required to unwrap
+        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;
+        final Object wrapLock;
+        final Object 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) {
+            this.chan = chan;
+            this.engine = engine;
+            wrapLock = new Object();
+            unwrapLock = new Object();
+            unwrap_src = allocate(BufType.PACKET);
+            wrap_dst = allocate(BufType.PACKET);
+        }
+
+//        void close () throws IOException {
+//        }
+
+        WrapperResult wrapAndSend(ByteBuffer src, boolean ignoreClose)
+            throws IOException
+        {
+            ByteBuffer[] buffers = new ByteBuffer[1];
+            buffers[0] = src;
+            return wrapAndSend(buffers, 0, 1, ignoreClose);
+        }
+
+        /* 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,
+                                  int offset,
+                                  int len,
+                                  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, offset, len, 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();
+                    while (l>0) {
+                        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;
+            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;
+                do {
+                    if (needData) {
+                        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;
+        }
+    }
+
+//    WrapperResult sendData (ByteBuffer src) throws IOException {
+//        ByteBuffer[] buffers = new ByteBuffer[1];
+//        buffers[0] = src;
+//        return sendData(buffers, 0, 1);
+//    }
+
+    /**
+     * 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.
+     */
+    WrapperResult sendData (ByteBuffer[] src, int offset, int len) throws IOException {
+        WrapperResult r = WrapperResult.createOK();
+        while (countBytes(src, offset, len) > 0) {
+            r = wrapper.wrapAndSend(src, offset, len, false);
+            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.
+     */
+    WrapperResult recvData (ByteBuffer dst) throws IOException {
+        /* we wait until some user data arrives */
+        int mark = dst.position();
+        WrapperResult r = null;
+        int pos = dst.position();
+        while (dst.position() == pos) {
+            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);
+            }
+        }
+        Utils.flipToMark(dst, mark);
+        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.wrapAndSend(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
+     */
+    @SuppressWarnings("fallthrough")
+    void doHandshake (HandshakeStatus hs_status) throws IOException {
+        boolean wasBlocking;
+        try {
+            wasBlocking = chan.isBlocking();
+            handshaking.lock();
+            chan.configureBlocking(true);
+            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, false);
+                        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();
+            }
+            Log.logSSL(getSessionInfo());
+            if (!wasBlocking) {
+                chan.configureBlocking(false);
+            }
+        } finally {
+            handshaking.unlock();
+        }
+    }
+
+//    static void printParams(SSLParameters p) {
+//        System.out.println("SSLParameters:");
+//        if (p == null) {
+//            System.out.println("Null params");
+//            return;
+//        }
+//        for (String cipher : p.getCipherSuites()) {
+//                System.out.printf("cipher: %s\n", cipher);
+//        }
+//        // JDK 8 EXCL START
+//        for (String approto : p.getApplicationProtocols()) {
+//                System.out.printf("application protocol: %s\n", approto);
+//        }
+//        // JDK 8 EXCL END
+//        for (String protocol : p.getProtocols()) {
+//                System.out.printf("protocol: %s\n", protocol);
+//        }
+//        if (p.getServerNames() != null) {
+//            for (SNIServerName sname : p.getServerNames()) {
+//                System.out.printf("server name: %s\n", sname.toString());
+//            }
+//        }
+//    }
+
+    String getSessionInfo() {
+        StringBuilder sb = new StringBuilder();
+        String application = engine.getApplicationProtocol();
+        SSLSession sess = engine.getSession();
+        String cipher = sess.getCipherSuite();
+        String protocol = sess.getProtocol();
+        sb.append("Handshake complete alpn: ")
+                .append(application)
+                .append(", Cipher: ")
+                .append(cipher)
+                .append(", Protocol: ")
+                .append(protocol);
+        return sb.toString();
+    }
+}