8222527: HttpClient doesn't send HOST header when tunelling HTTP/1.1 through http proxy
authordfuchs
Thu, 18 Apr 2019 17:56:46 +0100
changeset 54579 270557b396eb
parent 54578 895a6a380484
child 54580 79e95d8dd85d
8222527: HttpClient doesn't send HOST header when tunelling HTTP/1.1 through http proxy Summary: HttpClient no longer filters out system host header when sending tunelling CONNECT request to proxy Reviewed-by: michaelm
src/java.net.http/share/classes/jdk/internal/net/http/Http1Request.java
src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java
src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java
test/jdk/java/net/httpclient/DigestEchoServer.java
test/jdk/java/net/httpclient/HttpsTunnelTest.java
test/jdk/java/net/httpclient/ProxyAuthDisabledSchemesSSL.java
--- a/src/java.net.http/share/classes/jdk/internal/net/http/Http1Request.java	Mon Apr 15 15:52:38 2019 -0300
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1Request.java	Thu Apr 18 17:56:46 2019 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2019, 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
@@ -103,7 +103,8 @@
         HttpClient client = http1Exchange.client();
 
         // Filter overridable headers from userHeaders
-        userHeaders = HttpHeaders.of(userHeaders.map(), Utils.CONTEXT_RESTRICTED(client));
+        userHeaders = HttpHeaders.of(userHeaders.map(),
+                      connection.contextRestricted(request, client));
 
         final HttpHeaders uh = userHeaders;
 
--- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java	Mon Apr 15 15:52:38 2019 -0300
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpConnection.java	Thu Apr 18 17:56:46 2019 +0100
@@ -285,6 +285,16 @@
         }
     }
 
+    BiPredicate<String,String> contextRestricted(HttpRequestImpl request, HttpClient client) {
+        if (!isTunnel() && request.isConnect()) {
+            // establishing a proxy tunnel
+            assert request.proxy() == null;
+            return Utils.PROXY_TUNNEL_RESTRICTED(client);
+        } else {
+            return Utils.CONTEXT_RESTRICTED(client);
+        }
+    }
+
     // Composes a new immutable HttpHeaders that combines the
     // user and system header but only keeps those headers that
     // start with "proxy-"
--- a/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java	Mon Apr 15 15:52:38 2019 -0300
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java	Thu Apr 18 17:56:46 2019 +0100
@@ -178,7 +178,12 @@
                 ! (k.equalsIgnoreCase("Authorization")
                         && k.equalsIgnoreCase("Proxy-Authorization"));
     }
+    private static final BiPredicate<String, String> HOST_RESTRICTED = (k,v) -> !"host".equalsIgnoreCase(k);
+    public static final BiPredicate<String, String> PROXY_TUNNEL_RESTRICTED(HttpClient client)  {
+        return CONTEXT_RESTRICTED(client).and(HOST_RESTRICTED);
+    }
 
+    private static final Predicate<String> IS_HOST = "host"::equalsIgnoreCase;
     private static final Predicate<String> IS_PROXY_HEADER = (k) ->
             k != null && k.length() > 6 && "proxy-".equalsIgnoreCase(k.substring(0,6));
     private static final Predicate<String> NO_PROXY_HEADER =
@@ -250,7 +255,8 @@
 
     public static final BiPredicate<String, String> PROXY_TUNNEL_FILTER =
             (s,v) -> isAllowedForProxy(s, v, PROXY_AUTH_TUNNEL_DISABLED_SCHEMES,
-                    IS_PROXY_HEADER);
+                    // Allows Proxy-* and Host headers when establishing the tunnel.
+                    IS_PROXY_HEADER.or(IS_HOST));
     public static final BiPredicate<String, String> PROXY_FILTER =
             (s,v) -> isAllowedForProxy(s, v, PROXY_AUTH_DISABLED_SCHEMES,
                     ALL_HEADERS);
--- a/test/jdk/java/net/httpclient/DigestEchoServer.java	Mon Apr 15 15:52:38 2019 -0300
+++ b/test/jdk/java/net/httpclient/DigestEchoServer.java	Thu Apr 18 17:56:46 2019 +0100
@@ -80,6 +80,8 @@
             Boolean.parseBoolean(System.getProperty("test.debug", "false"));
     public static final boolean NO_LINGER =
             Boolean.parseBoolean(System.getProperty("test.nolinger", "false"));
+    public static final boolean TUNNEL_REQUIRES_HOST =
+            Boolean.parseBoolean(System.getProperty("test.requiresHost", "false"));
     public enum HttpAuthType {
         SERVER, PROXY, SERVER307, PROXY305
         /* add PROXY_AND_SERVER and SERVER_PROXY_NONE */
@@ -1522,6 +1524,36 @@
             }
         }
 
+        boolean badRequest(StringBuilder response, String hostport, List<String> hosts) {
+            String message = null;
+            if (hosts.isEmpty()) {
+                message = "No host header provided\r\n";
+            } else if (hosts.size() > 1) {
+                message = "Multiple host headers provided\r\n";
+                for (String h : hosts) {
+                    message = message + "host: " + h + "\r\n";
+                }
+            } else {
+                String h = hosts.get(0);
+                if (!hostport.equalsIgnoreCase(h)
+                        && !hostport.equalsIgnoreCase(h + ":80")
+                        && !hostport.equalsIgnoreCase(h + ":443")) {
+                    message = "Bad host provided: [" + h
+                            + "] doesnot match [" + hostport + "]\r\n";
+                }
+            }
+            if (message != null) {
+                int length = message.getBytes(StandardCharsets.UTF_8).length;
+                response.append("HTTP/1.1 400 BadRequest\r\n")
+                        .append("Content-Length: " + length)
+                        .append("\r\n\r\n")
+                        .append(message);
+                return true;
+            }
+
+            return false;
+        }
+
         boolean authorize(StringBuilder response, String requestLine, String headers) {
             if (authorization != null) {
                 return authorization.authorize(response, requestLine, headers);
@@ -1635,6 +1667,7 @@
                         assert connect.equalsIgnoreCase("connect");
                         String hostport = tokenizer.nextToken();
                         InetSocketAddress targetAddress;
+                        List<String> hosts = new ArrayList<>();
                         try {
                             URI uri = new URI("https", hostport, "/", null, null);
                             int port = uri.getPort();
@@ -1659,9 +1692,30 @@
                             System.out.println(now() + "Tunnel: Reading header: "
                                                + (line = readLine(ccis)));
                             headers.append(line).append("\r\n");
+                            int index = line.indexOf(':');
+                            if (index >= 0) {
+                                String key = line.substring(0, index).trim();
+                                if (key.equalsIgnoreCase("host")) {
+                                    hosts.add(line.substring(index+1).trim());
+                                }
+                            }
+                        }
+                        StringBuilder response = new StringBuilder();
+                        if (TUNNEL_REQUIRES_HOST) {
+                            if (badRequest(response, hostport, hosts)) {
+                                System.out.println(now() + "Tunnel: Sending " + response);
+                                // send the 400 response
+                                pw.print(response.toString());
+                                pw.flush();
+                                toClose.close();
+                                continue;
+                            } else {
+                                assert hosts.size() == 1;
+                                System.out.println(now()
+                                        + "Tunnel: Host header verified " + hosts);
+                            }
                         }
 
-                        StringBuilder response = new StringBuilder();
                         final boolean authorize = authorize(response, requestLine, headers.toString());
                         if (!authorize) {
                             System.out.println(now() + "Tunnel: Sending "
--- a/test/jdk/java/net/httpclient/HttpsTunnelTest.java	Mon Apr 15 15:52:38 2019 -0300
+++ b/test/jdk/java/net/httpclient/HttpsTunnelTest.java	Thu Apr 18 17:56:46 2019 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2018, 2019, 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
@@ -47,8 +47,9 @@
  *          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
+ *          a new h2 connection to the new host. It also verifies that
+ *          the stack sends the appropriate "host" header to the proxy.
+ * @bug 8196967 8222527
  * @library /test/lib http2/server
  * @build jdk.test.lib.net.SimpleSSLContext HttpServerAdapters DigestEchoServer HttpsTunnelTest
  * @modules java.net.http/jdk.internal.net.http.common
@@ -58,7 +59,14 @@
  *          java.base/sun.net.www.http
  *          java.base/sun.net.www
  *          java.base/sun.net
- * @run main/othervm -Djdk.internal.httpclient.debug=true HttpsTunnelTest
+ * @run main/othervm -Dtest.requiresHost=true
+ *                   -Djdk.httpclient.HttpClient.log=headers
+ *                   -Djdk.internal.httpclient.debug=true HttpsTunnelTest
+ * @run main/othervm -Dtest.requiresHost=true
+ *                   -Djdk.httpclient.allowRestrictedHeaders=host
+ *                   -Djdk.httpclient.HttpClient.log=headers
+ *                   -Djdk.internal.httpclient.debug=true HttpsTunnelTest
+ *
  */
 
 public class HttpsTunnelTest implements HttpServerAdapters {
@@ -116,6 +124,18 @@
         try {
             URI uri1 = new URI("https://" + http1Server.serverAuthority() + "/foo/https1");
             URI uri2 = new URI("https://" + http2Server.serverAuthority() + "/foo/https2");
+
+            boolean provideCustomHost = "host".equalsIgnoreCase(
+                    System.getProperty("jdk.httpclient.allowRestrictedHeaders",""));
+
+            String customHttp1Host = null, customHttp2Host = null;
+            if (provideCustomHost) {
+                customHttp1Host = makeCustomHostString(http1Server, uri1);
+                out.println("HTTP/1.1: <" + uri1 + "> [custom host: " + customHttp1Host + "]");
+                customHttp2Host = makeCustomHostString(http2Server, uri2);
+                out.println("HTTP/2:   <" + uri2 + "> [custom host: " + customHttp2Host + "]");
+            }
+
             ProxySelector ps = ProxySelector.of(proxy.getProxyAddress());
                     //HttpClient.Builder.NO_PROXY;
             HttpsTunnelTest test = new HttpsTunnelTest();
@@ -126,11 +146,12 @@
             assert lines.size() == data.length;
             String body = lines.stream().collect(Collectors.joining("\r\n"));
             HttpRequest.BodyPublisher reqBody = HttpRequest.BodyPublishers.ofString(body);
-            HttpRequest req1 = HttpRequest
+            HttpRequest.Builder req1Builder = HttpRequest
                     .newBuilder(uri1)
                     .version(Version.HTTP_2)
-                    .POST(reqBody)
-                    .build();
+                    .POST(reqBody);
+            if (provideCustomHost) req1Builder.header("host", customHttp1Host);
+            HttpRequest req1 = req1Builder.build();
             out.println("\nPosting to HTTP/1.1 server at: " + req1);
             HttpResponse<Stream<String>> response = client.send(req1, BodyHandlers.ofLines());
             out.println("Checking response...");
@@ -145,12 +166,14 @@
             if (!lines.equals(respLines)) {
                 throw new RuntimeException("Unexpected response 1: " + respLines);
             }
+
             HttpRequest.BodyPublisher reqBody2 = HttpRequest.BodyPublishers.ofString(body);
-            HttpRequest req2 = HttpRequest
+            HttpRequest.Builder req2Builder = HttpRequest
                     .newBuilder(uri2)
                     .version(Version.HTTP_2)
-                    .POST(reqBody2)
-                    .build();
+                    .POST(reqBody2);
+            if (provideCustomHost) req2Builder.header("host", customHttp2Host);
+            HttpRequest req2 = req2Builder.build();
             out.println("\nPosting to HTTP/2 server at: " + req2);
             response = client.send(req2, BodyHandlers.ofLines());
             out.println("Checking response...");
@@ -176,4 +199,26 @@
         }
     }
 
+    /**
+     * Builds a custom host string that is different to what is in the URI
+     * authority, that is textually different than what the stack would
+     * send. For CONNECT we should ignore any custom host settings.
+     * The tunnelling proxy will fail with badRequest 400 if it receives
+     * the custom host instead of the expected URI authority string.
+     * @param  server The target server.
+     * @param  uri    The URI to the target server
+     * @return a host value for the custom host header.
+     */
+    static final String makeCustomHostString(HttpTestServer server, URI uri) {
+        String customHttpHost;
+        if (server.serverAuthority().contains("localhost")) {
+            customHttpHost = InetAddress.getLoopbackAddress().getHostAddress();
+        } else {
+            customHttpHost = InetAddress.getLoopbackAddress().getHostName();
+        }
+        if (customHttpHost.contains(":")) customHttpHost = "[" + customHttpHost + "]";
+        if (uri.getPort() != -1) customHttpHost = customHttpHost + ":" + uri.getPort();
+        return customHttpHost;
+    }
+
 }
--- a/test/jdk/java/net/httpclient/ProxyAuthDisabledSchemesSSL.java	Mon Apr 15 15:52:38 2019 -0300
+++ b/test/jdk/java/net/httpclient/ProxyAuthDisabledSchemesSSL.java	Thu Apr 18 17:56:46 2019 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2018, 2019, 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
@@ -23,7 +23,7 @@
 
 /**
  * @test
- * @bug 8087112
+ * @bug 8087112 8222527
  * @summary this test verifies that a client may provides authorization
  *          headers directly when connecting with a server over SSL, and
  *          it verifies that the client honor the jdk.http.auth.*.disabledSchemes
@@ -45,10 +45,12 @@
  * @run main/othervm/timeout=300
  *          -Djdk.http.auth.proxying.disabledSchemes=Basic
  *          -Djdk.http.auth.tunneling.disabledSchemes=Basic
+ *          -Dtest.requiresHost=true
  *          ProxyAuthDisabledSchemesSSL SSL PROXY
  * @run main/othervm/timeout=300
  *          -Djdk.http.auth.proxying.disabledSchemes=Digest
  *          -Djdk.http.auth.tunneling.disabledSchemes=Digest
+ *          -Dtest.requiresHost=true
  *          ProxyAuthDisabledSchemesSSL SSL PROXY
  */