/* * Copyright (c) 2018, 2023, 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.test.lib.net.SimpleSSLContext; import javax.net.ssl.SSLContext; import java.net.InetAddress; 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.net.http.HttpResponse.BodyHandlers; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.httpclient.test.lib.common.HttpServerAdapters; import jdk.httpclient.test.lib.http2.Http2TestServer; import static java.lang.String.format; import static java.lang.System.out; import static java.net.http.HttpClient.Version.HTTP_1_1; import static java.net.http.HttpClient.Version.HTTP_2; /** * @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. It also verifies that * the stack sends the appropriate "host" header to the proxy. * @bug 8196967 8222527 * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.httpclient.test.lib.common.HttpServerAdapters jdk.test.lib.net.SimpleSSLContext * DigestEchoServer 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 { 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 { HttpTestServer http1Server = HttpTestServer.create(HTTP_1_1, context); http1Server.addHandler(new HttpTestEchoHandler(), "/"); http1Server.start(); HttpTestServer http2Server = HttpTestServer.create(HTTP_2, SSLContext.getDefault()); http2Server.addHandler(new HttpTestEchoHandler(), "/"); http2Server.start(); DigestEchoServer.TunnelingProxy proxy = DigestEchoServer.createHttpsProxyTunnel( DigestEchoServer.HttpAuthSchemeType.NONE); 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(); 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.BodyPublishers.ofString(body); HttpRequest.Builder req1Builder = HttpRequest .newBuilder(uri1) .version(Version.HTTP_2) .POST(reqBody); if (provideCustomHost) req1Builder.header("host", customHttp1Host); HttpRequest req1 = req1Builder.build(); out.println("\nPosting to HTTP/1.1 server at: " + req1); HttpResponse> response = client.send(req1, BodyHandlers.ofLines()); out.println("Checking response..."); if (response.statusCode() != 200) { throw new RuntimeException("Unexpected status code: " + response); } if (response.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.BodyPublishers.ofString(body); HttpRequest.Builder req2Builder = HttpRequest .newBuilder(uri2) .version(Version.HTTP_2) .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..."); 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(); } } /** * 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; } }