# HG changeset patch # User dfuchs # Date 1518629742 0 # Node ID 249a863b0aca92bbfd7388cee9ba14108614cbb2 # Parent e2a780d8c6f01287674f8b21520b2684209bbadb http-client-branch: 8196967: HTTP1/HTTP2 Proxy Issue diff -r e2a780d8c6f0 -r 249a863b0aca src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java --- 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(); } diff -r e2a780d8c6f0 -r 249a863b0aca src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java --- 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. */ diff -r e2a780d8c6f0 -r 249a863b0aca src/java.net.http/share/classes/jdk/internal/net/http/PlainProxyConnection.java --- 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; } } diff -r e2a780d8c6f0 -r 249a863b0aca src/java.net.http/share/classes/jdk/internal/net/http/Response.java --- 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; } diff -r e2a780d8c6f0 -r 249a863b0aca test/jdk/java/net/httpclient/DigestEchoClient.java --- 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 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(); diff -r e2a780d8c6f0 -r 249a863b0aca test/jdk/java/net/httpclient/DigestEchoServer.java --- 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> 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"; diff -r e2a780d8c6f0 -r 249a863b0aca test/jdk/java/net/httpclient/HttpServerAdapters.java --- 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()); diff -r e2a780d8c6f0 -r 249a863b0aca test/jdk/java/net/httpclient/HttpsTunnelTest.java --- /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 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> 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 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(); + } + } + +}