8181422: ClassCastException in HTTP Client
authordfuchs
Wed, 16 Aug 2017 10:55:05 +0100
changeset 46157 f3c2dcb8d8fe
parent 46156 79e8a865c5b8
child 46158 e86ab8b9a00c
8181422: ClassCastException in HTTP Client Summary: Added missing AsyncSSLTunnelConnection Reviewed-by: michaelm
jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AbstractAsyncSSLConnection.java
jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLConnection.java
jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLTunnelConnection.java
jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ExchangeImpl.java
jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http2Connection.java
jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpConnection.java
jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PlainTunnelingConnection.java
jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SSLConnection.java
jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SSLTunnelConnection.java
jdk/test/java/net/httpclient/ProxyTest.java
jdk/test/java/net/httpclient/http2/ProxyTest2.java
jdk/test/java/net/httpclient/http2/server/Http2TestServer.java
jdk/test/java/net/httpclient/http2/server/Http2TestServerConnection.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AbstractAsyncSSLConnection.java	Wed Aug 16 10:55:05 2017 +0100
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2015, 2017, 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;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.util.concurrent.CompletableFuture;
+import javax.net.ssl.SSLEngine;
+import jdk.incubator.http.internal.common.ExceptionallyCloseable;
+
+
+/**
+ * Asynchronous version of SSLConnection.
+ *
+ * There are two concrete implementations of this class: AsyncSSLConnection
+ * and AsyncSSLTunnelConnection.
+ * This abstraction is useful when downgrading from HTTP/2 to HTTP/1.1 over
+ * an SSL connection. See ExchangeImpl::get in the case where an ALPNException
+ * is thrown.
+ *
+ * Note: An AsyncSSLConnection wraps a PlainHttpConnection, while an
+ *       AsyncSSLTunnelConnection wraps a PlainTunnelingConnection.
+ *       If both these wrapped classes where made to inherit from a
+ *       common abstraction then it might be possible to merge
+ *       AsyncSSLConnection and AsyncSSLTunnelConnection back into
+ *       a single class - and simply use different factory methods to
+ *       create different wrappees, but this is left up for further cleanup.
+ *
+ */
+abstract class AbstractAsyncSSLConnection extends HttpConnection
+               implements AsyncConnection, ExceptionallyCloseable {
+
+
+    AbstractAsyncSSLConnection(InetSocketAddress addr, HttpClientImpl client) {
+        super(addr, client);
+    }
+
+    abstract SSLEngine getEngine();
+    abstract AsyncSSLDelegate sslDelegate();
+    abstract HttpConnection plainConnection();
+    abstract HttpConnection downgrade();
+
+    @Override
+    final boolean isSecure() {
+        return true;
+    }
+
+    // Blocking read functions not used here
+    @Override
+    protected final ByteBuffer readImpl() throws IOException {
+        throw new UnsupportedOperationException("Not supported.");
+    }
+
+    // whenReceivedResponse only used in HTTP/1.1 (Http1Exchange)
+    // AbstractAsyncSSLConnection is only used with HTTP/2
+    @Override
+    final CompletableFuture<Void> whenReceivingResponse() {
+        throw new UnsupportedOperationException("Not supported.");
+    }
+
+}
--- a/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLConnection.java	Tue Aug 15 19:19:50 2017 -0700
+++ b/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLConnection.java	Wed Aug 16 10:55:05 2017 +0100
@@ -35,14 +35,12 @@
 import javax.net.ssl.SSLEngine;
 
 import jdk.incubator.http.internal.common.ByteBufferReference;
-import jdk.incubator.http.internal.common.ExceptionallyCloseable;
 import jdk.incubator.http.internal.common.Utils;
 
 /**
  * Asynchronous version of SSLConnection.
  */
-class AsyncSSLConnection extends HttpConnection
-                         implements AsyncConnection, ExceptionallyCloseable {
+class AsyncSSLConnection extends AbstractAsyncSSLConnection {
 
     final AsyncSSLDelegate sslDelegate;
     final PlainHttpConnection plainConnection;
@@ -61,15 +59,14 @@
         plainConnection.configureMode(mode);
     }
 
-    private CompletableFuture<Void> configureModeAsync(Void ignore) {
-        CompletableFuture<Void> cf = new CompletableFuture<>();
-        try {
-            configureMode(Mode.ASYNC);
-            cf.complete(null);
-        } catch (Throwable t) {
-            cf.completeExceptionally(t);
-        }
-        return cf;
+    @Override
+    PlainHttpConnection plainConnection() {
+        return plainConnection;
+    }
+
+    @Override
+    AsyncSSLDelegate sslDelegate() {
+        return sslDelegate;
     }
 
     @Override
@@ -92,11 +89,6 @@
     }
 
     @Override
-    boolean isSecure() {
-        return true;
-    }
-
-    @Override
     boolean isProxied() {
         return false;
     }
@@ -172,6 +164,7 @@
         plainConnection.channel().shutdownOutput();
     }
 
+    @Override
     SSLEngine getEngine() {
         return sslDelegate.getEngine();
     }
@@ -184,18 +177,6 @@
         plainConnection.setAsyncCallbacks(sslDelegate::asyncReceive, errorReceiver, sslDelegate::getNetBuffer);
     }
 
-    // Blocking read functions not used here
-
-    @Override
-    protected ByteBuffer readImpl() throws IOException {
-        throw new UnsupportedOperationException("Not supported.");
-    }
-
-    @Override
-    CompletableFuture<Void> whenReceivingResponse() {
-        throw new UnsupportedOperationException("Not supported.");
-    }
-
     @Override
     public void startReading() {
         plainConnection.startReading();
@@ -206,4 +187,9 @@
     public void stopAsyncReading() {
         plainConnection.stopAsyncReading();
     }
+
+    @Override
+    SSLConnection downgrade() {
+        return new SSLConnection(this);
+    }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/AsyncSSLTunnelConnection.java	Wed Aug 16 10:55:05 2017 +0100
@@ -0,0 +1,206 @@
+/*
+ * Copyright (c) 2015, 2017, 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;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLParameters;
+import jdk.incubator.http.internal.common.ByteBufferReference;
+import jdk.incubator.http.internal.common.Utils;
+
+/**
+ * An SSL tunnel built on a Plain (CONNECT) TCP tunnel.
+ */
+class AsyncSSLTunnelConnection extends AbstractAsyncSSLConnection {
+
+    final PlainTunnelingConnection plainConnection;
+    final AsyncSSLDelegate sslDelegate;
+    final String serverName;
+
+    @Override
+    public void connect() throws IOException, InterruptedException {
+        plainConnection.connect();
+        configureMode(Mode.ASYNC);
+        startReading();
+        sslDelegate.connect();
+    }
+
+    @Override
+    boolean connected() {
+        return plainConnection.connected() && sslDelegate.connected();
+    }
+
+    @Override
+    public CompletableFuture<Void> connectAsync() {
+        throw new InternalError();
+    }
+
+    AsyncSSLTunnelConnection(InetSocketAddress addr,
+                        HttpClientImpl client,
+                        String[] alpn,
+                        InetSocketAddress proxy)
+    {
+        super(addr, client);
+        this.serverName = Utils.getServerName(addr);
+        this.plainConnection = new PlainTunnelingConnection(addr, proxy, client);
+        this.sslDelegate = new AsyncSSLDelegate(plainConnection, client, alpn, serverName);
+    }
+
+    @Override
+    synchronized void configureMode(Mode mode) throws IOException {
+        super.configureMode(mode);
+        plainConnection.configureMode(mode);
+    }
+
+    @Override
+    SSLParameters sslParameters() {
+        return sslDelegate.getSSLParameters();
+    }
+
+    @Override
+    public String toString() {
+        return "AsyncSSLTunnelConnection: " + super.toString();
+    }
+
+    @Override
+    PlainTunnelingConnection plainConnection() {
+        return plainConnection;
+    }
+
+    @Override
+    AsyncSSLDelegate sslDelegate() {
+        return sslDelegate;
+    }
+
+    @Override
+    ConnectionPool.CacheKey cacheKey() {
+        return ConnectionPool.cacheKey(address, plainConnection.proxyAddr);
+    }
+
+    @Override
+    long write(ByteBuffer[] buffers, int start, int number) throws IOException {
+        //debugPrint("Send", buffers, start, number);
+        ByteBuffer[] bufs = Utils.reduce(buffers, start, number);
+        long n = Utils.remaining(bufs);
+        sslDelegate.writeAsync(ByteBufferReference.toReferences(bufs));
+        sslDelegate.flushAsync();
+        return n;
+    }
+
+    @Override
+    long write(ByteBuffer buffer) throws IOException {
+        //debugPrint("Send", buffer);
+        long n = buffer.remaining();
+        sslDelegate.writeAsync(ByteBufferReference.toReferences(buffer));
+        sslDelegate.flushAsync();
+        return n;
+    }
+
+    @Override
+    public void writeAsync(ByteBufferReference[] buffers) throws IOException {
+        sslDelegate.writeAsync(buffers);
+    }
+
+    @Override
+    public void writeAsyncUnordered(ByteBufferReference[] buffers) throws IOException {
+        sslDelegate.writeAsyncUnordered(buffers);
+    }
+
+    @Override
+    public void flushAsync() throws IOException {
+        sslDelegate.flushAsync();
+    }
+
+    @Override
+    public void close() {
+        Utils.close(sslDelegate, plainConnection.channel());
+    }
+
+    @Override
+    void shutdownInput() throws IOException {
+        plainConnection.channel().shutdownInput();
+    }
+
+    @Override
+    void shutdownOutput() throws IOException {
+        plainConnection.channel().shutdownOutput();
+    }
+
+    @Override
+    SocketChannel channel() {
+        return plainConnection.channel();
+    }
+
+    @Override
+    boolean isProxied() {
+        return true;
+    }
+
+    @Override
+    public void setAsyncCallbacks(Consumer<ByteBufferReference> asyncReceiver,
+                                  Consumer<Throwable> errorReceiver,
+                                  Supplier<ByteBufferReference> readBufferSupplier) {
+        sslDelegate.setAsyncCallbacks(asyncReceiver, errorReceiver, readBufferSupplier);
+        plainConnection.setAsyncCallbacks(sslDelegate::asyncReceive, errorReceiver, sslDelegate::getNetBuffer);
+    }
+
+    @Override
+    public void startReading() {
+        plainConnection.startReading();
+        sslDelegate.startReading();
+    }
+
+    @Override
+    public void stopAsyncReading() {
+        plainConnection.stopAsyncReading();
+    }
+
+    @Override
+    public void enableCallback() {
+        sslDelegate.enableCallback();
+    }
+
+    @Override
+    public void closeExceptionally(Throwable cause) throws IOException {
+        Utils.close(cause, sslDelegate, plainConnection.channel());
+    }
+
+    @Override
+    SSLEngine getEngine() {
+        return sslDelegate.getEngine();
+    }
+
+    @Override
+    SSLTunnelConnection downgrade() {
+        return new SSLTunnelConnection(this);
+    }
+}
--- a/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ExchangeImpl.java	Tue Aug 15 19:19:50 2017 -0700
+++ b/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ExchangeImpl.java	Wed Aug 16 10:55:05 2017 +0100
@@ -82,9 +82,9 @@
                 c = c2.getConnectionFor(request);
             } catch (Http2Connection.ALPNException e) {
                 // failed to negotiate "h2"
-                AsyncSSLConnection as = e.getConnection();
+                AbstractAsyncSSLConnection as = e.getConnection();
                 as.stopAsyncReading();
-                SSLConnection sslc = new SSLConnection(as);
+                HttpConnection sslc = as.downgrade();
                 ExchangeImpl<U> ex = new Http1Exchange<>(exchange, sslc);
                 return ex;
             }
--- a/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http2Connection.java	Tue Aug 15 19:19:50 2017 -0700
+++ b/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http2Connection.java	Wed Aug 16 10:55:05 2017 +0100
@@ -211,12 +211,13 @@
         this.hpackIn = new Decoder(clientSettings.getParameter(HEADER_TABLE_SIZE));
         this.windowUpdater = new ConnectionWindowUpdateSender(this, client.getReceiveBufferSize());
     }
-        /**
-         * Case 1) Create from upgraded HTTP/1.1 connection.
-         * Is ready to use. Will not be SSL. exchange is the Exchange
-         * that initiated the connection, whose response will be delivered
-         * on a Stream.
-         */
+
+    /**
+     * Case 1) Create from upgraded HTTP/1.1 connection.
+     * Is ready to use. Will not be SSL. exchange is the Exchange
+     * that initiated the connection, whose response will be delivered
+     * on a Stream.
+     */
     Http2Connection(HttpConnection connection,
                     Http2ClientImpl client2,
                     Exchange<?> exchange,
@@ -280,7 +281,7 @@
      * Throws an IOException if h2 was not negotiated
      */
     private void checkSSLConfig() throws IOException {
-        AsyncSSLConnection aconn = (AsyncSSLConnection)connection;
+        AbstractAsyncSSLConnection aconn = (AbstractAsyncSSLConnection)connection;
         SSLEngine engine = aconn.getEngine();
         String alpn = engine.getApplicationProtocol();
         if (alpn == null || !alpn.equals("h2")) {
@@ -906,14 +907,14 @@
      */
     static final class ALPNException extends IOException {
         private static final long serialVersionUID = 23138275393635783L;
-        final AsyncSSLConnection connection;
+        final AbstractAsyncSSLConnection connection;
 
-        ALPNException(String msg, AsyncSSLConnection connection) {
+        ALPNException(String msg, AbstractAsyncSSLConnection connection) {
             super(msg);
             this.connection = connection;
         }
 
-        AsyncSSLConnection getConnection() {
+        AbstractAsyncSSLConnection getConnection() {
             return connection;
         }
     }
--- a/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpConnection.java	Tue Aug 15 19:19:50 2017 -0700
+++ b/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/HttpConnection.java	Wed Aug 16 10:55:05 2017 +0100
@@ -34,7 +34,6 @@
 import java.util.concurrent.CompletableFuture;
 
 import jdk.incubator.http.internal.common.ByteBufferReference;
-import jdk.incubator.http.internal.common.Utils;
 
 /**
  * Wraps socket channel layer and takes care of SSL also.
@@ -136,7 +135,11 @@
             String[] alpn, boolean isHttp2, HttpClientImpl client)
     {
         if (proxy != null) {
-            return new SSLTunnelConnection(addr, client, proxy);
+            if (!isHttp2) {
+                return new SSLTunnelConnection(addr, client, proxy);
+            } else {
+                return new AsyncSSLTunnelConnection(addr, client, alpn, proxy);
+            }
         } else if (!isHttp2) {
             return new SSLConnection(addr, client, alpn);
         } else {
--- a/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PlainTunnelingConnection.java	Tue Aug 15 19:19:50 2017 -0700
+++ b/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/PlainTunnelingConnection.java	Wed Aug 16 10:55:05 2017 +0100
@@ -34,12 +34,15 @@
 import java.nio.ByteBuffer;
 import java.nio.channels.SocketChannel;
 import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
 
 /**
  * A plain text socket tunnel through a proxy. Uses "CONNECT" but does not
- * encrypt. Used by WebSocket. Subclassed in SSLTunnelConnection for encryption.
+ * encrypt. Used by WebSocket, as well as HTTP over SSL + Proxy.
+ * Wrapped in SSLTunnelConnection or AsyncSSLTunnelConnection for encryption.
  */
-class PlainTunnelingConnection extends HttpConnection {
+class PlainTunnelingConnection extends HttpConnection implements AsyncConnection {
 
     final PlainHttpConnection delegate;
     protected final InetSocketAddress proxyAddr;
@@ -116,17 +119,17 @@
     }
 
     @Override
-    void writeAsync(ByteBufferReference[] buffers) throws IOException {
+    public void writeAsync(ByteBufferReference[] buffers) throws IOException {
         delegate.writeAsync(buffers);
     }
 
     @Override
-    void writeAsyncUnordered(ByteBufferReference[] buffers) throws IOException {
+    public void writeAsyncUnordered(ByteBufferReference[] buffers) throws IOException {
         delegate.writeAsyncUnordered(buffers);
     }
 
     @Override
-    void flushAsync() throws IOException {
+    public void flushAsync() throws IOException {
         delegate.flushAsync();
     }
 
@@ -165,4 +168,32 @@
     boolean isProxied() {
         return true;
     }
+
+    @Override
+    public void setAsyncCallbacks(Consumer<ByteBufferReference> asyncReceiver,
+            Consumer<Throwable> errorReceiver,
+            Supplier<ByteBufferReference> readBufferSupplier) {
+        delegate.setAsyncCallbacks(asyncReceiver, errorReceiver, readBufferSupplier);
+    }
+
+    @Override
+    public void startReading() {
+        delegate.startReading();
+    }
+
+    @Override
+    public void stopAsyncReading() {
+        delegate.stopAsyncReading();
+    }
+
+    @Override
+    public void enableCallback() {
+        delegate.enableCallback();
+    }
+
+    @Override
+    synchronized void configureMode(Mode mode) throws IOException {
+        super.configureMode(mode);
+        delegate.configureMode(mode);
+    }
 }
--- a/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SSLConnection.java	Tue Aug 15 19:19:50 2017 -0700
+++ b/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SSLConnection.java	Wed Aug 16 10:55:05 2017 +0100
@@ -77,8 +77,8 @@
      */
     SSLConnection(AsyncSSLConnection c) {
         super(c.address, c.client);
-        this.delegate = c.plainConnection;
-        AsyncSSLDelegate adel = c.sslDelegate;
+        this.delegate = c.plainConnection();
+        AsyncSSLDelegate adel = c.sslDelegate();
         this.sslDelegate = new SSLDelegate(adel.engine, delegate.channel(), client, adel.serverName);
         this.alpn = adel.alpn;
         this.serverName = adel.serverName;
--- a/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SSLTunnelConnection.java	Tue Aug 15 19:19:50 2017 -0700
+++ b/jdk/src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/SSLTunnelConnection.java	Wed Aug 16 10:55:05 2017 +0100
@@ -85,6 +85,19 @@
         delegate = new PlainTunnelingConnection(addr, proxy, client);
     }
 
+    /**
+     * Create an SSLTunnelConnection from an existing connected AsyncSSLTunnelConnection.
+     * Used when downgrading from HTTP/2 to HTTP/1.1
+     */
+    SSLTunnelConnection(AsyncSSLTunnelConnection c) {
+        super(c.address, c.client);
+        this.delegate = c.plainConnection();
+        AsyncSSLDelegate adel = c.sslDelegate();
+        this.sslDelegate = new SSLDelegate(adel.engine, delegate.channel(), client, adel.serverName);
+        this.serverName = adel.serverName;
+        connected = c.connected();
+    }
+
     @Override
     SSLParameters sslParameters() {
         return sslDelegate.getSSLParameters();
--- a/jdk/test/java/net/httpclient/ProxyTest.java	Tue Aug 15 19:19:50 2017 -0700
+++ b/jdk/test/java/net/httpclient/ProxyTest.java	Wed Aug 16 10:55:05 2017 +0100
@@ -56,9 +56,12 @@
 
 /**
  * @test
- * @bug 8185852
- * @summary verifies that passing a proxy with an unresolved address does
- *          not cause  java.nio.channels.UnresolvedAddressException
+ * @bug 8185852 8181422
+ * @summary Verifies that passing a proxy with an unresolved address does
+ *          not cause java.nio.channels.UnresolvedAddressException.
+ *          Verifies that downgrading from HTTP/2 to HTTP/1.1 works through
+ *          an SSL Tunnel connection when the client is HTTP/2 and the server
+ *          and proxy are HTTP/1.1
  * @modules jdk.incubator.httpclient
  * @library /lib/testlibrary/
  * @build jdk.testlibrary.SimpleSSLContext ProxyTest
@@ -111,7 +114,7 @@
         server.start();
         try {
             test(server, HttpClient.Version.HTTP_1_1);
-            // test(server, HttpClient.Version.HTTP_2);
+            test(server, HttpClient.Version.HTTP_2);
         } finally {
             server.stop(0);
             System.out.println("Server stopped");
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/java/net/httpclient/http2/ProxyTest2.java	Wed Aug 16 10:55:05 2017 +0100
@@ -0,0 +1,323 @@
+/*
+ * Copyright (c) 2017, 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.
+ *
+ * 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.
+ */
+
+import com.sun.net.httpserver.HttpContext;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+import com.sun.net.httpserver.HttpsConfigurator;
+import com.sun.net.httpserver.HttpsParameters;
+import com.sun.net.httpserver.HttpsServer;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.net.HttpURLConnection;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.security.NoSuchAlgorithmException;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.HttpResponse;
+import jdk.testlibrary.SimpleSSLContext;
+import java.util.concurrent.*;
+
+/**
+ * @test
+ * @bug 8181422
+ * @summary  Verifies that you can access an HTTP/2 server over HTTPS by
+ *           tunnelling through an HTTP/1.1 proxy.
+ * @modules jdk.incubator.httpclient
+ * @library /lib/testlibrary server
+ * @modules jdk.incubator.httpclient/jdk.incubator.http.internal.common
+ *          jdk.incubator.httpclient/jdk.incubator.http.internal.frame
+ *          jdk.incubator.httpclient/jdk.incubator.http.internal.hpack
+ * @build jdk.testlibrary.SimpleSSLContext ProxyTest2
+ * @run main/othervm ProxyTest2
+ * @author danielfuchs
+ */
+public class ProxyTest2 {
+
+    static {
+        try {
+            HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
+                    public boolean verify(String hostname, SSLSession session) {
+                        return true;
+                    }
+                });
+            SSLContext.setDefault(new SimpleSSLContext().get());
+        } catch (IOException ex) {
+            throw new ExceptionInInitializerError(ex);
+        }
+    }
+
+    static final String RESPONSE = "<html><body><p>Hello World!</body></html>";
+    static final String PATH = "/foo/";
+
+    static Http2TestServer createHttpsServer(ExecutorService exec) throws Exception {
+        Http2TestServer server = new Http2TestServer(true, 0, exec, SSLContext.getDefault());
+        server.addHandler(new Http2Handler() {
+            @Override
+            public void handle(Http2TestExchange he) throws IOException {
+                he.getResponseHeaders().addHeader("encoding", "UTF-8");
+                he.sendResponseHeaders(200, RESPONSE.length());
+                he.getResponseBody().write(RESPONSE.getBytes(StandardCharsets.UTF_8));
+                he.close();
+            }
+        }, PATH);
+
+        return server;
+    }
+
+    public static void main(String[] args)
+            throws Exception
+    {
+        ExecutorService exec = Executors.newCachedThreadPool();
+        Http2TestServer server = createHttpsServer(exec);
+        server.start();
+        try {
+            // Http2TestServer over HTTPS does not support HTTP/1.1
+            // => only test with a HTTP/2 client
+            test(server, HttpClient.Version.HTTP_2);
+        } finally {
+            server.stop();
+            exec.shutdown();
+            System.out.println("Server stopped");
+        }
+    }
+
+    public static void test(Http2TestServer server, HttpClient.Version version)
+            throws Exception
+    {
+        System.out.println("Server is: " + server.getAddress().toString());
+        URI uri = new URI("https://localhost:" + server.getAddress().getPort() + PATH + "x");
+        TunnelingProxy proxy = new TunnelingProxy(server);
+        proxy.start();
+        try {
+            System.out.println("Proxy started");
+            Proxy p = new Proxy(Proxy.Type.HTTP,
+                    InetSocketAddress.createUnresolved("localhost", proxy.getAddress().getPort()));
+            System.out.println("Setting up request with HttpClient for version: "
+                    + version.name() + "URI=" + uri);
+            ProxySelector ps = ProxySelector.of(
+                    InetSocketAddress.createUnresolved("localhost", proxy.getAddress().getPort()));
+            HttpClient client = HttpClient.newBuilder()
+                .version(version)
+                .proxy(ps)
+                .build();
+            HttpRequest request = HttpRequest.newBuilder()
+                .uri(uri)
+                .GET()
+                .build();
+
+            System.out.println("Sending request with HttpClient");
+            HttpResponse<String> response
+                = client.send(request, HttpResponse.BodyHandler.asString());
+            System.out.println("Got response");
+            String resp = response.body();
+            System.out.println("Received: " + resp);
+            if (!RESPONSE.equals(resp)) {
+                throw new AssertionError("Unexpected response");
+            }
+        } finally {
+            System.out.println("Stopping proxy");
+            proxy.stop();
+            System.out.println("Proxy stopped");
+        }
+    }
+
+    static class TunnelingProxy {
+        final Thread accept;
+        final ServerSocket ss;
+        final boolean DEBUG = false;
+        final Http2TestServer serverImpl;
+        TunnelingProxy(Http2TestServer serverImpl) throws IOException {
+            this.serverImpl = serverImpl;
+            ss = new ServerSocket();
+            accept = new Thread(this::accept);
+        }
+
+        void start() throws IOException {
+            ss.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0));
+            accept.start();
+        }
+
+        // Pipe the input stream to the output stream.
+        private synchronized Thread pipe(InputStream is, OutputStream os, char tag) {
+            return new Thread("TunnelPipe("+tag+")") {
+                @Override
+                public void run() {
+                    try {
+                        try {
+                            int c;
+                            while ((c = is.read()) != -1) {
+                                os.write(c);
+                                os.flush();
+                                // if DEBUG prints a + or a - for each transferred
+                                // character.
+                                if (DEBUG) System.out.print(tag);
+                            }
+                            is.close();
+                        } finally {
+                            os.close();
+                        }
+                    } catch (IOException ex) {
+                        if (DEBUG) ex.printStackTrace(System.out);
+                    }
+                }
+            };
+        }
+
+        public InetSocketAddress getAddress() {
+            return new InetSocketAddress(ss.getInetAddress(), ss.getLocalPort());
+        }
+
+        // This is a bit shaky. It doesn't handle continuation
+        // lines, but our client shouldn't send any.
+        // Read a line from the input stream, swallowing the final
+        // \r\n sequence. Stops at the first \n, doesn't complain
+        // if it wasn't preceded by '\r'.
+        //
+        String readLine(InputStream r) throws IOException {
+            StringBuilder b = new StringBuilder();
+            int c;
+            while ((c = r.read()) != -1) {
+                if (c == '\n') break;
+                b.appendCodePoint(c);
+            }
+            if (b.codePointAt(b.length() -1) == '\r') {
+                b.delete(b.length() -1, b.length());
+            }
+            return b.toString();
+        }
+
+        public void accept() {
+            Socket clientConnection = null;
+            try {
+                while (true) {
+                    System.out.println("Tunnel: Waiting for client");
+                    Socket previous = clientConnection;
+                    try {
+                        clientConnection = ss.accept();
+                    } catch (IOException io) {
+                        if (DEBUG) io.printStackTrace(System.out);
+                        break;
+                    } finally {
+                        // we have only 1 client at a time, so it is safe
+                        // to close the previous connection here
+                        if (previous != null) previous.close();
+                    }
+                    System.out.println("Tunnel: Client accepted");
+                    Socket targetConnection = null;
+                    InputStream  ccis = clientConnection.getInputStream();
+                    OutputStream ccos = clientConnection.getOutputStream();
+                    Writer w = new OutputStreamWriter(ccos, "UTF-8");
+                    PrintWriter pw = new PrintWriter(w);
+                    System.out.println("Tunnel: Reading request line");
+                    String requestLine = readLine(ccis);
+                    System.out.println("Tunnel: Request status line: " + requestLine);
+                    if (requestLine.startsWith("CONNECT ")) {
+                        // We should probably check that the next word following
+                        // CONNECT is the host:port of our HTTPS serverImpl.
+                        // Some improvement for a followup!
+
+                        // Read all headers until we find the empty line that
+                        // signals the end of all headers.
+                        while(!requestLine.equals("")) {
+                            System.out.println("Tunnel: Reading header: "
+                                               + (requestLine = readLine(ccis)));
+                        }
+
+                        // Open target connection
+                        targetConnection = new Socket(
+                                serverImpl.getAddress().getAddress(),
+                                serverImpl.getAddress().getPort());
+
+                        // Then send the 200 OK response to the client
+                        System.out.println("Tunnel: Sending "
+                                           + "HTTP/1.1 200 OK\r\n\r\n");
+                        pw.print("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
+                        pw.flush();
+                    } else {
+                        // This should not happen.
+                        throw new IOException("Tunnel: Unexpected status line: "
+                                           + requestLine);
+                    }
+
+                    // Pipe the input stream of the client connection to the
+                    // output stream of the target connection and conversely.
+                    // Now the client and target will just talk to each other.
+                    System.out.println("Tunnel: Starting tunnel pipes");
+                    Thread t1 = pipe(ccis, targetConnection.getOutputStream(), '+');
+                    Thread t2 = pipe(targetConnection.getInputStream(), ccos, '-');
+                    t1.start();
+                    t2.start();
+
+                    // We have only 1 client... wait until it has finished before
+                    // accepting a new connection request.
+                    // System.out.println("Tunnel: Waiting for pipes to close");
+                    t1.join();
+                    t2.join();
+                    System.out.println("Tunnel: Done - waiting for next client");
+                }
+            } catch (Throwable ex) {
+                try {
+                    ss.close();
+                } catch (IOException ex1) {
+                    ex.addSuppressed(ex1);
+                }
+                ex.printStackTrace(System.err);
+            }
+        }
+
+        void stop() throws IOException {
+            ss.close();
+        }
+
+    }
+
+    static class Configurator extends HttpsConfigurator {
+        public Configurator(SSLContext ctx) {
+            super(ctx);
+        }
+
+        @Override
+        public void configure (HttpsParameters params) {
+            params.setSSLParameters (getSSLContext().getSupportedSSLParameters());
+        }
+    }
+
+}
--- a/jdk/test/java/net/httpclient/http2/server/Http2TestServer.java	Tue Aug 15 19:19:50 2017 -0700
+++ b/jdk/test/java/net/httpclient/http2/server/Http2TestServer.java	Wed Aug 16 10:55:05 2017 +0100
@@ -201,7 +201,17 @@
                     InetSocketAddress addr = (InetSocketAddress) socket.getRemoteSocketAddress();
                     Http2TestServerConnection c = new Http2TestServerConnection(this, socket);
                     connections.put(addr, c);
-                    c.run();
+                    try {
+                        c.run();
+                    } catch(Throwable e) {
+                        // we should not reach here, but if we do
+                        // the connection might not have been closed
+                        // and if so then the client might wait
+                        // forever.
+                        connections.remove(addr, c);
+                        c.close();
+                        throw e;
+                    }
                 }
             } catch (Throwable e) {
                 if (!stopping) {
--- a/jdk/test/java/net/httpclient/http2/server/Http2TestServerConnection.java	Tue Aug 15 19:19:50 2017 -0700
+++ b/jdk/test/java/net/httpclient/http2/server/Http2TestServerConnection.java	Wed Aug 16 10:55:05 2017 +0100
@@ -133,10 +133,10 @@
     }
 
     void close() {
+        stopping = true;
         streams.forEach((i, q) -> {
             q.close();
         });
-        stopping = true;
         try {
             socket.close();
             // TODO: put a reset on each stream
@@ -557,7 +557,14 @@
     void writeLoop() {
         try {
             while (!stopping) {
-                Http2Frame frame = outputQ.take();
+                Http2Frame frame;
+                try {
+                    frame = outputQ.take();
+                } catch(IOException x) {
+                    if (stopping && x.getCause() instanceof InterruptedException) {
+                        break;
+                    } else throw x;
+                }
                 if (frame instanceof ResponseHeaders) {
                     ResponseHeaders rh = (ResponseHeaders)frame;
                     HeadersFrame hf = new HeadersFrame(rh.streamid(), rh.getFlags(), encodeHeaders(rh.headers));