8181422: ClassCastException in HTTP Client
Summary: Added missing AsyncSSLTunnelConnection
Reviewed-by: michaelm
--- /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));