http-client-branch: 8196967: HTTP1/HTTP2 Proxy Issue http-client-branch
authordfuchs
Wed, 14 Feb 2018 17:35:42 +0000
branchhttp-client-branch
changeset 56128 249a863b0aca
parent 56127 e2a780d8c6f0
child 56129 fe88abe462c9
http-client-branch: 8196967: HTTP1/HTTP2 Proxy Issue
src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java
src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java
src/java.net.http/share/classes/jdk/internal/net/http/PlainProxyConnection.java
src/java.net.http/share/classes/jdk/internal/net/http/Response.java
test/jdk/java/net/httpclient/DigestEchoClient.java
test/jdk/java/net/httpclient/DigestEchoServer.java
test/jdk/java/net/httpclient/HttpServerAdapters.java
test/jdk/java/net/httpclient/HttpsTunnelTest.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();
         }
--- 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();
+        }
+    }
+
+}