8222527: HttpClient doesn't send HOST header when tunelling HTTP/1.1 through http proxy
HttpClient no longer filters out system host header when sending tunelling CONNECT request to proxy Reviewed-by: michaelm
This commit is contained in:
parent
0d35ef38e6
commit
853da81cfe
@ -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.
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||||
*
|
*
|
||||||
* This code is free software; you can redistribute it and/or modify it
|
* This code is free software; you can redistribute it and/or modify it
|
||||||
@ -103,7 +103,8 @@ class Http1Request {
|
|||||||
HttpClient client = http1Exchange.client();
|
HttpClient client = http1Exchange.client();
|
||||||
|
|
||||||
// Filter overridable headers from userHeaders
|
// 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;
|
final HttpHeaders uh = userHeaders;
|
||||||
|
|
||||||
|
@ -285,6 +285,16 @@ abstract class HttpConnection implements Closeable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// Composes a new immutable HttpHeaders that combines the
|
||||||
// user and system header but only keeps those headers that
|
// user and system header but only keeps those headers that
|
||||||
// start with "proxy-"
|
// start with "proxy-"
|
||||||
|
@ -178,7 +178,12 @@ public final class Utils {
|
|||||||
! (k.equalsIgnoreCase("Authorization")
|
! (k.equalsIgnoreCase("Authorization")
|
||||||
&& k.equalsIgnoreCase("Proxy-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) ->
|
private static final Predicate<String> IS_PROXY_HEADER = (k) ->
|
||||||
k != null && k.length() > 6 && "proxy-".equalsIgnoreCase(k.substring(0,6));
|
k != null && k.length() > 6 && "proxy-".equalsIgnoreCase(k.substring(0,6));
|
||||||
private static final Predicate<String> NO_PROXY_HEADER =
|
private static final Predicate<String> NO_PROXY_HEADER =
|
||||||
@ -250,7 +255,8 @@ public final class Utils {
|
|||||||
|
|
||||||
public static final BiPredicate<String, String> PROXY_TUNNEL_FILTER =
|
public static final BiPredicate<String, String> PROXY_TUNNEL_FILTER =
|
||||||
(s,v) -> isAllowedForProxy(s, v, PROXY_AUTH_TUNNEL_DISABLED_SCHEMES,
|
(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 =
|
public static final BiPredicate<String, String> PROXY_FILTER =
|
||||||
(s,v) -> isAllowedForProxy(s, v, PROXY_AUTH_DISABLED_SCHEMES,
|
(s,v) -> isAllowedForProxy(s, v, PROXY_AUTH_DISABLED_SCHEMES,
|
||||||
ALL_HEADERS);
|
ALL_HEADERS);
|
||||||
|
@ -80,6 +80,8 @@ public abstract class DigestEchoServer implements HttpServerAdapters {
|
|||||||
Boolean.parseBoolean(System.getProperty("test.debug", "false"));
|
Boolean.parseBoolean(System.getProperty("test.debug", "false"));
|
||||||
public static final boolean NO_LINGER =
|
public static final boolean NO_LINGER =
|
||||||
Boolean.parseBoolean(System.getProperty("test.nolinger", "false"));
|
Boolean.parseBoolean(System.getProperty("test.nolinger", "false"));
|
||||||
|
public static final boolean TUNNEL_REQUIRES_HOST =
|
||||||
|
Boolean.parseBoolean(System.getProperty("test.requiresHost", "false"));
|
||||||
public enum HttpAuthType {
|
public enum HttpAuthType {
|
||||||
SERVER, PROXY, SERVER307, PROXY305
|
SERVER, PROXY, SERVER307, PROXY305
|
||||||
/* add PROXY_AND_SERVER and SERVER_PROXY_NONE */
|
/* add PROXY_AND_SERVER and SERVER_PROXY_NONE */
|
||||||
@ -1522,6 +1524,36 @@ public abstract class DigestEchoServer implements HttpServerAdapters {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
boolean authorize(StringBuilder response, String requestLine, String headers) {
|
||||||
if (authorization != null) {
|
if (authorization != null) {
|
||||||
return authorization.authorize(response, requestLine, headers);
|
return authorization.authorize(response, requestLine, headers);
|
||||||
@ -1635,6 +1667,7 @@ public abstract class DigestEchoServer implements HttpServerAdapters {
|
|||||||
assert connect.equalsIgnoreCase("connect");
|
assert connect.equalsIgnoreCase("connect");
|
||||||
String hostport = tokenizer.nextToken();
|
String hostport = tokenizer.nextToken();
|
||||||
InetSocketAddress targetAddress;
|
InetSocketAddress targetAddress;
|
||||||
|
List<String> hosts = new ArrayList<>();
|
||||||
try {
|
try {
|
||||||
URI uri = new URI("https", hostport, "/", null, null);
|
URI uri = new URI("https", hostport, "/", null, null);
|
||||||
int port = uri.getPort();
|
int port = uri.getPort();
|
||||||
@ -1659,9 +1692,30 @@ public abstract class DigestEchoServer implements HttpServerAdapters {
|
|||||||
System.out.println(now() + "Tunnel: Reading header: "
|
System.out.println(now() + "Tunnel: Reading header: "
|
||||||
+ (line = readLine(ccis)));
|
+ (line = readLine(ccis)));
|
||||||
headers.append(line).append("\r\n");
|
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());
|
final boolean authorize = authorize(response, requestLine, headers.toString());
|
||||||
if (!authorize) {
|
if (!authorize) {
|
||||||
System.out.println(now() + "Tunnel: Sending "
|
System.out.println(now() + "Tunnel: Sending "
|
||||||
|
@ -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.
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||||
*
|
*
|
||||||
* This code is free software; you can redistribute it and/or modify it
|
* This code is free software; you can redistribute it and/or modify it
|
||||||
@ -47,8 +47,9 @@ import static java.lang.System.out;
|
|||||||
* proxy P is downgraded to HTTP/1.1, then a new h2 request
|
* proxy P is downgraded to HTTP/1.1, then a new h2 request
|
||||||
* going to a different host through the same proxy will not
|
* going to a different host through the same proxy will not
|
||||||
* be preemptively downgraded. That, is the stack should attempt
|
* be preemptively downgraded. That, is the stack should attempt
|
||||||
* a new h2 connection to the new host.
|
* a new h2 connection to the new host. It also verifies that
|
||||||
* @bug 8196967
|
* the stack sends the appropriate "host" header to the proxy.
|
||||||
|
* @bug 8196967 8222527
|
||||||
* @library /test/lib http2/server
|
* @library /test/lib http2/server
|
||||||
* @build jdk.test.lib.net.SimpleSSLContext HttpServerAdapters DigestEchoServer HttpsTunnelTest
|
* @build jdk.test.lib.net.SimpleSSLContext HttpServerAdapters DigestEchoServer HttpsTunnelTest
|
||||||
* @modules java.net.http/jdk.internal.net.http.common
|
* @modules java.net.http/jdk.internal.net.http.common
|
||||||
@ -58,7 +59,14 @@ import static java.lang.System.out;
|
|||||||
* java.base/sun.net.www.http
|
* java.base/sun.net.www.http
|
||||||
* java.base/sun.net.www
|
* java.base/sun.net.www
|
||||||
* java.base/sun.net
|
* 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 {
|
public class HttpsTunnelTest implements HttpServerAdapters {
|
||||||
@ -116,6 +124,18 @@ public class HttpsTunnelTest implements HttpServerAdapters {
|
|||||||
try {
|
try {
|
||||||
URI uri1 = new URI("https://" + http1Server.serverAuthority() + "/foo/https1");
|
URI uri1 = new URI("https://" + http1Server.serverAuthority() + "/foo/https1");
|
||||||
URI uri2 = new URI("https://" + http2Server.serverAuthority() + "/foo/https2");
|
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());
|
ProxySelector ps = ProxySelector.of(proxy.getProxyAddress());
|
||||||
//HttpClient.Builder.NO_PROXY;
|
//HttpClient.Builder.NO_PROXY;
|
||||||
HttpsTunnelTest test = new HttpsTunnelTest();
|
HttpsTunnelTest test = new HttpsTunnelTest();
|
||||||
@ -126,11 +146,12 @@ public class HttpsTunnelTest implements HttpServerAdapters {
|
|||||||
assert lines.size() == data.length;
|
assert lines.size() == data.length;
|
||||||
String body = lines.stream().collect(Collectors.joining("\r\n"));
|
String body = lines.stream().collect(Collectors.joining("\r\n"));
|
||||||
HttpRequest.BodyPublisher reqBody = HttpRequest.BodyPublishers.ofString(body);
|
HttpRequest.BodyPublisher reqBody = HttpRequest.BodyPublishers.ofString(body);
|
||||||
HttpRequest req1 = HttpRequest
|
HttpRequest.Builder req1Builder = HttpRequest
|
||||||
.newBuilder(uri1)
|
.newBuilder(uri1)
|
||||||
.version(Version.HTTP_2)
|
.version(Version.HTTP_2)
|
||||||
.POST(reqBody)
|
.POST(reqBody);
|
||||||
.build();
|
if (provideCustomHost) req1Builder.header("host", customHttp1Host);
|
||||||
|
HttpRequest req1 = req1Builder.build();
|
||||||
out.println("\nPosting to HTTP/1.1 server at: " + req1);
|
out.println("\nPosting to HTTP/1.1 server at: " + req1);
|
||||||
HttpResponse<Stream<String>> response = client.send(req1, BodyHandlers.ofLines());
|
HttpResponse<Stream<String>> response = client.send(req1, BodyHandlers.ofLines());
|
||||||
out.println("Checking response...");
|
out.println("Checking response...");
|
||||||
@ -145,12 +166,14 @@ public class HttpsTunnelTest implements HttpServerAdapters {
|
|||||||
if (!lines.equals(respLines)) {
|
if (!lines.equals(respLines)) {
|
||||||
throw new RuntimeException("Unexpected response 1: " + respLines);
|
throw new RuntimeException("Unexpected response 1: " + respLines);
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpRequest.BodyPublisher reqBody2 = HttpRequest.BodyPublishers.ofString(body);
|
HttpRequest.BodyPublisher reqBody2 = HttpRequest.BodyPublishers.ofString(body);
|
||||||
HttpRequest req2 = HttpRequest
|
HttpRequest.Builder req2Builder = HttpRequest
|
||||||
.newBuilder(uri2)
|
.newBuilder(uri2)
|
||||||
.version(Version.HTTP_2)
|
.version(Version.HTTP_2)
|
||||||
.POST(reqBody2)
|
.POST(reqBody2);
|
||||||
.build();
|
if (provideCustomHost) req2Builder.header("host", customHttp2Host);
|
||||||
|
HttpRequest req2 = req2Builder.build();
|
||||||
out.println("\nPosting to HTTP/2 server at: " + req2);
|
out.println("\nPosting to HTTP/2 server at: " + req2);
|
||||||
response = client.send(req2, BodyHandlers.ofLines());
|
response = client.send(req2, BodyHandlers.ofLines());
|
||||||
out.println("Checking response...");
|
out.println("Checking response...");
|
||||||
@ -176,4 +199,26 @@ public class HttpsTunnelTest implements HttpServerAdapters {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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.
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||||
*
|
*
|
||||||
* This code is free software; you can redistribute it and/or modify it
|
* This code is free software; you can redistribute it and/or modify it
|
||||||
@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @test
|
* @test
|
||||||
* @bug 8087112
|
* @bug 8087112 8222527
|
||||||
* @summary this test verifies that a client may provides authorization
|
* @summary this test verifies that a client may provides authorization
|
||||||
* headers directly when connecting with a server over SSL, and
|
* headers directly when connecting with a server over SSL, and
|
||||||
* it verifies that the client honor the jdk.http.auth.*.disabledSchemes
|
* it verifies that the client honor the jdk.http.auth.*.disabledSchemes
|
||||||
@ -45,10 +45,12 @@
|
|||||||
* @run main/othervm/timeout=300
|
* @run main/othervm/timeout=300
|
||||||
* -Djdk.http.auth.proxying.disabledSchemes=Basic
|
* -Djdk.http.auth.proxying.disabledSchemes=Basic
|
||||||
* -Djdk.http.auth.tunneling.disabledSchemes=Basic
|
* -Djdk.http.auth.tunneling.disabledSchemes=Basic
|
||||||
|
* -Dtest.requiresHost=true
|
||||||
* ProxyAuthDisabledSchemesSSL SSL PROXY
|
* ProxyAuthDisabledSchemesSSL SSL PROXY
|
||||||
* @run main/othervm/timeout=300
|
* @run main/othervm/timeout=300
|
||||||
* -Djdk.http.auth.proxying.disabledSchemes=Digest
|
* -Djdk.http.auth.proxying.disabledSchemes=Digest
|
||||||
* -Djdk.http.auth.tunneling.disabledSchemes=Digest
|
* -Djdk.http.auth.tunneling.disabledSchemes=Digest
|
||||||
|
* -Dtest.requiresHost=true
|
||||||
* ProxyAuthDisabledSchemesSSL SSL PROXY
|
* ProxyAuthDisabledSchemesSSL SSL PROXY
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user