--- a/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java Wed Feb 14 17:02:56 2018 +0000
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java Wed Feb 14 17:35:42 2018 +0000
@@ -420,13 +420,13 @@
}
static String keyFor(HttpConnection connection) {
- boolean isProxy = connection.isProxied();
+ boolean isProxy = connection.isProxied(); // tunnel or plain clear connection through proxy
boolean isSecure = connection.isSecure();
InetSocketAddress addr = connection.address();
return keyString(isSecure, isProxy, addr.getHostString(), addr.getPort());
}
-
+
static String keyFor(URI uri, InetSocketAddress proxy) {
boolean isSecure = uri.getScheme().equalsIgnoreCase("https");
boolean isProxy = proxy != null;
@@ -434,10 +434,16 @@
String host;
int port;
- if (proxy != null) {
+ if (proxy != null && !isSecure) {
+ // clear connection through proxy: use
+ // proxy host / proxy port
host = proxy.getHostString();
port = proxy.getPort();
} else {
+ // either secure tunnel connection through proxy
+ // or direct connection to host, but in either
+ // case only that host can be reached through
+ // the connection: use target host / target port
host = uri.getHost();
port = uri.getPort();
}
--- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java Wed Feb 14 17:02:56 2018 +0000
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java Wed Feb 14 17:35:42 2018 +0000
@@ -119,7 +119,11 @@
/** Tells whether, or not, this connection is secure ( over SSL ) */
abstract boolean isSecure();
- /** Tells whether, or not, this connection is proxied. */
+ /**
+ * Tells whether, or not, this connection is proxied.
+ * Returns true for tunnel connections, or clear connection to
+ * any host through proxy.
+ */
abstract boolean isProxied();
/** Tells whether, or not, this connection is open. */
--- a/src/java.net.http/share/classes/jdk/internal/net/http/PlainProxyConnection.java Wed Feb 14 17:02:56 2018 +0000
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/PlainProxyConnection.java Wed Feb 14 17:35:42 2018 +0000
@@ -37,4 +37,7 @@
ConnectionPool.CacheKey cacheKey() {
return new ConnectionPool.CacheKey(null, address);
}
+
+ @Override
+ public boolean isProxied() { return true; }
}
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Response.java Wed Feb 14 17:02:56 2018 +0000
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Response.java Wed Feb 14 17:35:42 2018 +0000
@@ -102,11 +102,7 @@
HttpHeaders headers() {
return headers;
}
-
-// Exchange<?> exchange() {
-// return exchange;
-// }
-
+
int statusCode() {
return statusCode;
}
--- a/test/jdk/java/net/httpclient/DigestEchoClient.java Wed Feb 14 17:02:56 2018 +0000
+++ b/test/jdk/java/net/httpclient/DigestEchoClient.java Wed Feb 14 17:35:42 2018 +0000
@@ -43,8 +43,6 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.net.ssl.SSLContext;
-import javax.net.ServerSocketFactory;
-import javax.net.ssl.SSLServerSocketFactory;
import java.net.http.HttpClient;
import java.net.http.HttpClient.Version;
import java.net.http.HttpRequest;
@@ -174,7 +172,6 @@
}
static final List<Boolean> BOOLEANS = List.of(true, false);
- final ServerSocketFactory factory;
final boolean useSSL;
final DigestEchoServer.HttpAuthSchemeType authScheme;
final DigestEchoServer.HttpAuthType authType;
@@ -185,8 +182,6 @@
this.useSSL = useSSL;
this.authScheme = authScheme;
this.authType = authType;
- factory = useSSL ? SSLServerSocketFactory.getDefault()
- : ServerSocketFactory.getDefault();
}
static final AtomicLong clientCount = new AtomicLong();
--- a/test/jdk/java/net/httpclient/DigestEchoServer.java Wed Feb 14 17:02:56 2018 +0000
+++ b/test/jdk/java/net/httpclient/DigestEchoServer.java Wed Feb 14 17:35:42 2018 +0000
@@ -24,11 +24,6 @@
*/
import com.sun.net.httpserver.BasicAuthenticator;
-import com.sun.net.httpserver.Filter;
-import com.sun.net.httpserver.Headers;
-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;
@@ -42,13 +37,11 @@
import java.math.BigInteger;
import java.net.Authenticator;
import java.net.HttpURLConnection;
-import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.PasswordAuthentication;
import java.net.ServerSocket;
import java.net.Socket;
-import java.net.SocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
@@ -64,6 +57,7 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Random;
+import java.util.StringTokenizer;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
@@ -80,7 +74,7 @@
* a test implementation implemented only for tests purposes.
* @author danielfuchs
*/
-public class DigestEchoServer implements HttpServerAdapters {
+public abstract class DigestEchoServer implements HttpServerAdapters {
public static final boolean DEBUG =
Boolean.parseBoolean(System.getProperty("test.debug", "false"));
@@ -147,7 +141,7 @@
final HttpTestHandler delegate; // unused
final String key;
- private DigestEchoServer(String key,
+ DigestEchoServer(String key,
HttpTestServer server,
DigestEchoServer target,
HttpTestHandler delegate) {
@@ -505,7 +499,7 @@
ProcessHandle.current().pid(),
impl.getAddress().getPort(),
version, protocol, authType, schemeType);
- final DigestEchoServer server = new DigestEchoServer(key, impl, null, delegate);
+ final DigestEchoServer server = new DigestEchoServerImpl(key, impl, null, delegate);
final HttpTestHandler handler =
server.createHandler(schemeType, auth, authType, false);
HttpTestContext context = impl.addHandler(handler, path);
@@ -536,7 +530,7 @@
version, protocol, authType, schemeType);
final DigestEchoServer server = "https".equalsIgnoreCase(protocol)
? new HttpsProxyTunnel(key, impl, null, delegate)
- : new DigestEchoServer(key, impl, null, delegate);
+ : new DigestEchoServerImpl(key, impl, null, delegate);
final HttpTestHandler hh = server.createHandler(HttpAuthSchemeType.NONE,
null, HttpAuthType.SERVER,
@@ -580,7 +574,7 @@
HttpAuthType.SERVER, code300)
+ "->" + redirectTarget.key;
final DigestEchoServer redirectingServer =
- new DigestEchoServer(key, impl, redirectTarget, null);
+ new DigestEchoServerImpl(key, impl, redirectTarget, null);
InetSocketAddress redirectAddr = redirectTarget.getAddress();
URL locationURL = url(targetProtocol, redirectAddr, "/");
final HttpTestHandler hh = redirectingServer.create300Handler(key, locationURL,
@@ -590,27 +584,44 @@
return redirectingServer;
}
- public InetSocketAddress getAddress() {
- return new InetSocketAddress("127.0.0.1",
- serverImpl.getAddress().getPort());
- }
+ public abstract InetSocketAddress getServerAddress();
+ public abstract InetSocketAddress getProxyAddress();
+ public abstract InetSocketAddress getAddress();
+ public abstract void stop();
+ public abstract Version getServerVersion();
- public InetSocketAddress getServerAddress() {
- return new InetSocketAddress("127.0.0.1",
- serverImpl.getAddress().getPort());
- }
+ private static class DigestEchoServerImpl extends DigestEchoServer {
+ DigestEchoServerImpl(String key,
+ HttpTestServer server,
+ DigestEchoServer target,
+ HttpTestHandler delegate) {
+ super(key, Objects.requireNonNull(server), target, delegate);
+ }
+
+ public InetSocketAddress getAddress() {
+ return new InetSocketAddress("127.0.0.1",
+ serverImpl.getAddress().getPort());
+ }
- public InetSocketAddress getProxyAddress() {
- return new InetSocketAddress("127.0.0.1",
- serverImpl.getAddress().getPort());
- }
+ public InetSocketAddress getServerAddress() {
+ return new InetSocketAddress("127.0.0.1",
+ serverImpl.getAddress().getPort());
+ }
+
+ public InetSocketAddress getProxyAddress() {
+ return new InetSocketAddress("127.0.0.1",
+ serverImpl.getAddress().getPort());
+ }
- public Version getServerVersion() { return serverImpl.getVersion(); }
+ public Version getServerVersion() {
+ return serverImpl.getVersion();
+ }
- public void stop() {
- serverImpl.stop();
- if (redirect != null) {
- redirect.stop();
+ public void stop() {
+ serverImpl.stop();
+ if (redirect != null) {
+ redirect.stop();
+ }
}
}
@@ -1420,12 +1431,17 @@
}
}
+ public interface TunnelingProxy {
+ InetSocketAddress getProxyAddress();
+ void stop();
+ }
+
// This is a bit hacky: HttpsProxyTunnel is an HTTPTestServer hidden
// behind a fake proxy that only understands CONNECT requests.
// The fake proxy is just a server socket that intercept the
// CONNECT and then redirect streams to the real server.
static class HttpsProxyTunnel extends DigestEchoServer
- implements Runnable {
+ implements Runnable, TunnelingProxy {
final ServerSocket ss;
final CopyOnWriteArrayList<CompletableFuture<Void>> connectionCFs
@@ -1455,9 +1471,23 @@
}
@Override
+ public Version getServerVersion() {
+ // serverImpl is not null when this proxy
+ // serves a single server. It will be null
+ // if this proxy can serve multiple servers.
+ if (serverImpl != null) return serverImpl.getVersion();
+ return null;
+ }
+
+ @Override
public void stop() {
stopped = true;
- super.stop();
+ if (serverImpl != null) {
+ serverImpl.stop();
+ }
+ if (redirect != null) {
+ redirect.stop();
+ }
try {
ss.close();
} catch (IOException ex) {
@@ -1516,14 +1546,21 @@
@Override
public InetSocketAddress getAddress() {
- return new InetSocketAddress(ss.getInetAddress(), ss.getLocalPort());
+ return new InetSocketAddress("127.0.0.1",
+ ss.getLocalPort());
}
+ @Override
public InetSocketAddress getProxyAddress() {
return getAddress();
}
+ @Override
public InetSocketAddress getServerAddress() {
- return new InetSocketAddress("127.0.0.1",
- serverImpl.getAddress().getPort());
+ // serverImpl can be null if this proxy can serve
+ // multiple servers.
+ if (serverImpl != null) {
+ return serverImpl.getAddress();
+ }
+ return null;
}
@@ -1574,6 +1611,27 @@
// We should probably check that the next word following
// CONNECT is the host:port of our HTTPS serverImpl.
// Some improvement for a followup!
+ StringTokenizer tokenizer = new StringTokenizer(requestLine);
+ String connect = tokenizer.nextToken();
+ assert connect.equalsIgnoreCase("connect");
+ String hostport = tokenizer.nextToken();
+ InetSocketAddress targetAddress;
+ try {
+ URI uri = new URI("https", hostport, "/", null, null);
+ int port = uri.getPort();
+ port = port == -1 ? 443 : port;
+ targetAddress = new InetSocketAddress(uri.getHost(), port);
+ if (serverImpl != null) {
+ assert targetAddress.getHostString()
+ .equalsIgnoreCase(serverImpl.getAddress().getHostString());
+ assert targetAddress.getPort() == serverImpl.getAddress().getPort();
+ }
+ } catch (Throwable x) {
+ System.err.printf("Bad target address: \"%s\" in \"%s\"%n",
+ hostport, requestLine);
+ toClose.close();
+ continue;
+ }
// Read all headers until we find the empty line that
// signals the end of all headers.
@@ -1595,9 +1653,12 @@
toClose.close();
continue;
}
+ System.out.println(now()
+ + "Tunnel connecting to target server at "
+ + targetAddress.getAddress() + ":" + targetAddress.getPort());
targetConnection = new Socket(
- serverImpl.getAddress().getAddress(),
- serverImpl.getAddress().getPort());
+ targetAddress.getAddress(),
+ targetAddress.getPort());
// Then send the 200 OK response to the client
System.out.println(now() + "Tunnel: Sending "
@@ -1659,6 +1720,26 @@
}
}
+ /**
+ * Creates a TunnelingProxy that can serve multiple servers.
+ * The server address is extracted from the CONNECT request line.
+ * @param authScheme The authentication scheme supported by the proxy.
+ * Typically one of DIGEST, BASIC, NONE.
+ * @return A new TunnelingProxy able to serve multiple servers.
+ * @throws IOException If the proxy could not be created.
+ */
+ public static TunnelingProxy createHttpsProxyTunnel(HttpAuthSchemeType authScheme)
+ throws IOException {
+ HttpsProxyTunnel result = new HttpsProxyTunnel("", null, null, null);
+ if (authScheme != HttpAuthSchemeType.NONE) {
+ result.configureAuthentication(null,
+ authScheme,
+ AUTHENTICATOR,
+ HttpAuthType.PROXY);
+ }
+ return result;
+ }
+
private static String protocol(String protocol) {
if ("http".equalsIgnoreCase(protocol)) return "http";
else if ("https".equalsIgnoreCase(protocol)) return "https";
--- a/test/jdk/java/net/httpclient/HttpServerAdapters.java Wed Feb 14 17:02:56 2018 +0000
+++ b/test/jdk/java/net/httpclient/HttpServerAdapters.java Wed Feb 14 17:35:42 2018 +0000
@@ -298,7 +298,8 @@
try (InputStream is = t.getRequestBody();
OutputStream os = t.getResponseBody()) {
byte[] bytes = is.readAllBytes();
- printBytes(System.out,"Bytes: ", bytes);
+ printBytes(System.out,"Echo server got "
+ + t.getExchangeVersion() + " bytes: ", bytes);
if (t.getRequestHeaders().firstValue("Content-type").isPresent()) {
t.getResponseHeaders().addHeader("Content-type",
t.getRequestHeaders().firstValue("Content-type").get());
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/HttpsTunnelTest.java Wed Feb 14 17:35:42 2018 +0000
@@ -0,0 +1,179 @@
+/*
+ * Copyright (c) 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.
+ *
+ * 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.HttpsConfigurator;
+import com.sun.net.httpserver.HttpsServer;
+import jdk.testlibrary.SimpleSSLContext;
+import javax.net.ssl.SSLContext;
+import java.net.InetSocketAddress;
+import java.net.ProxySelector;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpClient.Version;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static java.lang.String.format;
+import static java.lang.System.out;
+import static java.net.http.HttpResponse.BodyHandler.asLines;
+
+/**
+ * @test
+ * @summary This test verifies that if an h2 connection going through a
+ * proxy P is downgraded to HTTP/1.1, then a new h2 request
+ * going to a different host through the same proxy will not
+ * be preemptively downgraded. That, is the stack should attempt
+ * a new h2 connection to the new host.
+ * @bug 8196967
+ * @library /lib/testlibrary http2/server
+ * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters DigestEchoServer HttpsTunnelTest
+ * @modules java.net.http/jdk.internal.net.http.common
+ * java.net.http/jdk.internal.net.http.frame
+ * java.net.http/jdk.internal.net.http.hpack
+ * java.logging
+ * java.base/sun.net.www.http
+ * java.base/sun.net.www
+ * java.base/sun.net
+ * @run main/othervm -Djdk.internal.httpclient.debug=true HttpsTunnelTest
+ */
+
+public class HttpsTunnelTest implements HttpServerAdapters {
+
+ static final String data[] = {
+ "Lorem ipsum",
+ "dolor sit amet",
+ "consectetur adipiscing elit, sed do eiusmod tempor",
+ "quis nostrud exercitation ullamco",
+ "laboris nisi",
+ "ut",
+ "aliquip ex ea commodo consequat." +
+ "Duis aute irure dolor in reprehenderit in voluptate velit esse" +
+ "cillum dolore eu fugiat nulla pariatur.",
+ "Excepteur sint occaecat cupidatat non proident."
+ };
+
+ static final SSLContext context;
+ static {
+ try {
+ context = new SimpleSSLContext().get();
+ SSLContext.setDefault(context);
+ } catch (Exception x) {
+ throw new ExceptionInInitializerError(x);
+ }
+ }
+
+ HttpsTunnelTest() {
+ }
+
+ public HttpClient newHttpClient(ProxySelector ps) {
+ HttpClient.Builder builder = HttpClient
+ .newBuilder()
+ .sslContext(context)
+ .proxy(ps);
+ return builder.build();
+ }
+
+ public static void main(String[] args) throws Exception {
+ InetSocketAddress sa = new InetSocketAddress(0);
+ HttpsServer server1 = HttpsServer.create(sa, 0);
+ server1.setHttpsConfigurator(new HttpsConfigurator(context));
+ HttpTestServer http1Server =
+ HttpTestServer.of(server1);
+ http1Server.addHandler(new HttpTestEchoHandler(), "/");
+ http1Server.start();
+ HttpTestServer http2Server = HttpTestServer.of(
+ new Http2TestServer("127.0.0.1", true, 0));
+ http2Server.addHandler(new HttpTestEchoHandler(), "/");
+ http2Server.start();
+
+ DigestEchoServer.TunnelingProxy proxy = DigestEchoServer.createHttpsProxyTunnel(
+ DigestEchoServer.HttpAuthSchemeType.NONE);
+
+ try {
+ URI uri1 = new URI("https:/" + http1Server.getAddress() + "/foo/https1");
+ URI uri2 = new URI("https:/" + http2Server.getAddress() + "/foo/https2");
+ ProxySelector ps = ProxySelector.of(proxy.getProxyAddress());
+ //HttpClient.Builder.NO_PROXY;
+ HttpsTunnelTest test = new HttpsTunnelTest();
+ HttpClient client = test.newHttpClient(ps);
+ out.println("Proxy is: " + ps.select(uri2));
+
+ List<String> lines = List.of(Arrays.copyOfRange(data, 0, data.length));
+ assert lines.size() == data.length;
+ String body = lines.stream().collect(Collectors.joining("\r\n"));
+ HttpRequest.BodyPublisher reqBody = HttpRequest.BodyPublisher.fromString(body);
+ HttpRequest req1 = HttpRequest
+ .newBuilder(uri1)
+ .version(Version.HTTP_2)
+ .POST(reqBody)
+ .build();
+ out.println("\nPosting to HTTP/1.1 server at: " + req1);
+ HttpResponse<Stream<String>> response = client.send(req1, asLines());
+ out.println("Checking response...");
+ if (response.statusCode() != 200) {
+ throw new RuntimeException("Unexpected status code: " + response);
+ }
+ if (response.version() != Version.HTTP_1_1) {
+ throw new RuntimeException("Unexpected protocol version: "
+ + response.version());
+ }
+ List<String> respLines = response.body().collect(Collectors.toList());
+ if (!lines.equals(respLines)) {
+ throw new RuntimeException("Unexpected response 1: " + respLines);
+ }
+ HttpRequest.BodyPublisher reqBody2 = HttpRequest.BodyPublisher.fromString(body);
+ HttpRequest req2 = HttpRequest
+ .newBuilder(uri2)
+ .version(Version.HTTP_2)
+ .POST(reqBody2)
+ .build();
+ out.println("\nPosting to HTTP/2 server at: " + req2);
+ response = client.send(req2, asLines());
+ out.println("Checking response...");
+ if (response.statusCode() != 200) {
+ throw new RuntimeException("Unexpected status code: " + response);
+ }
+ if (response.version() != Version.HTTP_2) {
+ throw new RuntimeException("Unexpected protocol version: "
+ + response.version());
+ }
+ respLines = response.body().collect(Collectors.toList());
+ if (!lines.equals(respLines)) {
+ throw new RuntimeException("Unexpected response 2: " + respLines);
+ }
+ } catch(Throwable t) {
+ out.println("Unexpected exception: exiting: " + t);
+ t.printStackTrace();
+ throw t;
+ } finally {
+ proxy.stop();
+ http1Server.stop();
+ http2Server.stop();
+ }
+ }
+
+}