| /* | |
| * Copyright (c) 2021, 2025, 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 java.io.IOException; | |
| 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.nio.charset.StandardCharsets; | |
| import java.util.Arrays; | |
| import java.util.Base64; | |
| import java.util.List; | |
| import java.util.stream.Collectors; | |
| import java.util.stream.Stream; | |
| import javax.net.ssl.SSLContext; | |
| import jdk.httpclient.test.lib.common.HttpServerAdapters; | |
| import jdk.test.lib.net.SimpleSSLContext; | |
| import static java.lang.System.out; | |
| /* | |
| * @test | |
| * @bug 8262027 | |
| * @summary Verify that it's possible to handle proxy authentication manually | |
| * even when using an HTTPS tunnel. This test uses an authenticating | |
| * proxy (basic auth) serving an authenticated server (basic auth). | |
| * The test also helps verifying the fix for 8262027. | |
| * @library /test/lib /test/jdk/java/net/httpclient/lib | |
| * @build jdk.httpclient.test.lib.common.HttpServerAdapters jdk.test.lib.net.SimpleSSLContext | |
| * ProxyServer HttpsTunnelAuthTest | |
| * @run main/othervm -Djdk.httpclient.HttpClient.log=requests,headers,errors | |
| * -Djdk.http.auth.tunneling.disabledSchemes | |
| * -Djdk.httpclient.allowRestrictedHeaders=connection | |
| * -Djdk.internal.httpclient.debug=true | |
| * HttpsTunnelAuthTest | |
| * | |
| */ | |
| //-Djdk.internal.httpclient.debug=true -Dtest.debug=true | |
| public class HttpsTunnelAuthTest implements HttpServerAdapters, AutoCloseable { | |
| 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); | |
| } | |
| } | |
| final String realm = "earth"; | |
| final String sUserName = "arthur"; | |
| final String pUserName = "porpoise"; | |
| final String sPassword = "dent"; | |
| final String pPassword = "fish"; | |
| final String proxyAuth = "Basic " + Base64.getEncoder().withoutPadding() | |
| .encodeToString((pUserName+":"+pPassword).getBytes(StandardCharsets.US_ASCII)); | |
| final String serverAuth = "Basic " + Base64.getEncoder().withoutPadding() | |
| .encodeToString((sUserName+":"+sPassword).getBytes(StandardCharsets.UTF_8)); | |
| final DigestEchoServer.HttpTestAuthenticator testAuth = | |
| new DigestEchoServer.HttpTestAuthenticator(realm, sUserName); | |
| DigestEchoServer http1Server; | |
| DigestEchoServer https1Server; | |
| DigestEchoServer https2Server; | |
| ProxyServer proxy; | |
| ProxySelector proxySelector; | |
| HttpClient client; | |
| HttpsTunnelAuthTest() { | |
| } | |
| void setUp() throws IOException { | |
| HttpServerAdapters.enableServerLogging(); | |
| // Creates an HTTP/1.1 Server that will authenticate for | |
| // arthur with password dent | |
| http1Server = DigestEchoServer.createServer(Version.HTTP_1_1, | |
| "http", | |
| DigestEchoServer.HttpAuthType.SERVER, | |
| testAuth, | |
| DigestEchoServer.HttpAuthSchemeType.BASICSERVER, | |
| new HttpTestEchoHandler(), | |
| "/"); | |
| // Creates a TLS HTTP/1.1 Server that will authenticate for | |
| // arthur with password dent | |
| https1Server = DigestEchoServer.createServer(Version.HTTP_1_1, | |
| "https", | |
| DigestEchoServer.HttpAuthType.SERVER, | |
| testAuth, | |
| DigestEchoServer.HttpAuthSchemeType.BASICSERVER, | |
| new HttpTestEchoHandler(), | |
| "/"); | |
| // Creates a TLS HTTP/2 Server that will authenticate for | |
| // arthur with password dent | |
| https2Server = DigestEchoServer.createServer(Version.HTTP_2, | |
| "https", | |
| DigestEchoServer.HttpAuthType.SERVER, | |
| testAuth, | |
| DigestEchoServer.HttpAuthSchemeType.BASICSERVER, | |
| new HttpTestEchoHandler(), "/"); | |
| // Creates a proxy server that will authenticate for | |
| // porpoise with password fish. | |
| proxy = new ProxyServer(0, true, pUserName, pPassword); | |
| // Creates a proxy selector that unconditionally select the | |
| // above proxy. | |
| proxySelector = ProxySelector.of(proxy.getProxyAddress()); | |
| } | |
| @Override | |
| public void close() throws Exception { | |
| if (proxy != null) close(proxy); | |
| if (http1Server != null) close(http1Server::stop); | |
| if (https1Server != null) close(https1Server::stop); | |
| if (https2Server != null) close(https2Server::stop); | |
| } | |
| private void close(AutoCloseable closeable) { | |
| try { | |
| closeable.close(); | |
| } catch (Exception x) { | |
| // OK to ignore and just log | |
| System.err.println("ignoring failure during close() of " + closeable + " due to: " + x); | |
| } | |
| } | |
| 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 { | |
| try (HttpsTunnelAuthTest test = new HttpsTunnelAuthTest()) { | |
| test.setUp(); | |
| try (HttpClient client = test.newHttpClient(test.proxySelector)) { | |
| // tests proxy and server authentication through: | |
| // - plain proxy connection to plain HTTP/1.1 server, | |
| test.test(client, Version.HTTP_1_1, "http", "/foo/http1"); | |
| } | |
| // can't really test plain proxy connection to plain HTTP/2 server: | |
| // this is not supported: we downgrade to HTTP/1.1 in that case | |
| // so that is actually somewhat equivalent to the first case: | |
| // therefore we will use a new client to force re-authentication | |
| // of the proxy connection. | |
| try (HttpClient client = test.newHttpClient(test.proxySelector)) { | |
| test.test(client, Version.HTTP_2, "http", "/foo/http2"); | |
| // - proxy tunnel SSL connection to HTTP/1.1 server | |
| test.test(client, Version.HTTP_1_1, "https", "/foo/https1"); | |
| // - proxy tunnel SSl connection to HTTP/2 server | |
| test.test(client, Version.HTTP_2, "https", "/foo/https2"); | |
| } | |
| } | |
| } | |
| DigestEchoServer server(String scheme, Version version) { | |
| return switch (scheme) { | |
| case "https" -> secure(version); | |
| case "http" -> unsecure(version); | |
| default -> throw new IllegalArgumentException(scheme); | |
| }; | |
| } | |
| DigestEchoServer unsecure(Version version) { | |
| return switch (version) { | |
| // when accessing HTTP/2 through a proxy we downgrade to HTTP/1.1 | |
| case HTTP_1_1, HTTP_2 -> http1Server; | |
| default -> throw new IllegalArgumentException(String.valueOf(version)); | |
| }; | |
| } | |
| DigestEchoServer secure(Version version) { | |
| return switch (version) { | |
| case HTTP_1_1 -> https1Server; | |
| case HTTP_2 -> https2Server; | |
| default -> throw new IllegalArgumentException(String.valueOf(version)); | |
| }; | |
| } | |
| Version expectedVersion(String scheme, Version version) { | |
| // when trying to send a plain HTTP/2 request through a proxy | |
| // it should be downgraded to HTTP/1 | |
| return "http".equals(scheme) ? Version.HTTP_1_1 : version; | |
| } | |
| public void test(HttpClient client, Version version, String scheme, String path) throws Exception { | |
| System.out.printf("%nTesting %s, %s, %s%n", version, scheme, path); | |
| DigestEchoServer server = server(scheme, version); | |
| try { | |
| URI uri = jdk.test.lib.net.URIBuilder.newBuilder() | |
| .scheme(scheme) | |
| .loopback() | |
| .port(server.getServerAddress().getPort()) | |
| .path(path).build(); | |
| out.println("Proxy is: " + proxySelector.select(uri)); | |
| 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.BodyPublishers.ofString(body); | |
| // Build first request, with no authorization header | |
| HttpRequest.Builder req1Builder = HttpRequest | |
| .newBuilder(uri) | |
| .version(Version.HTTP_2) | |
| .POST(reqBody); | |
| HttpRequest req1 = req1Builder.build(); | |
| out.printf("%nPosting to %s server at: %s%n", expectedVersion(scheme, version), req1); | |
| // send first request, with no authorization: we expect 407 | |
| HttpResponse<Stream<String>> response = client.send(req1, BodyHandlers.ofLines()); | |
| out.println("Checking response: " + response); | |
| if (response.body() != null) response.body().sequential().forEach(out::println); | |
| // check that we got 407, and check that we got the expected | |
| // Proxy-Authenticate header | |
| if (response.statusCode() != 407) { | |
| throw new RuntimeException("Unexpected status code: " + response); | |
| } | |
| var pAuthenticate = response.headers().firstValue("proxy-authenticate").get(); | |
| if (!pAuthenticate.equals("Basic realm=\"proxy realm\"")) { | |
| throw new RuntimeException("Unexpected proxy-authenticate: " + pAuthenticate); | |
| } | |
| // Second request will have Proxy-Authorization, no Authorization. | |
| // We should get 401 from the server this time. | |
| out.printf("%nPosting with Proxy-Authorization to %s server at: %s%n", expectedVersion(scheme, version), req1); | |
| HttpRequest authReq1 = HttpRequest.newBuilder(req1, (k, v)-> true) | |
| .header("proxy-authorization", proxyAuth).build(); | |
| response = client.send(authReq1, BodyHandlers.ofLines()); | |
| out.println("Checking response: " + response); | |
| if (response.body() != null) response.body().sequential().forEach(out::println); | |
| // Check that we have 401, and that we got the expected | |
| // WWW-Authenticate header | |
| if (response.statusCode() != 401) { | |
| throw new RuntimeException("Unexpected status code: " + response); | |
| } | |
| var sAuthenticate = response.headers().firstValue("www-authenticate").get(); | |
| if (!sAuthenticate.startsWith("Basic realm=\"earth\"")) { | |
| throw new RuntimeException("Unexpected authenticate: " + sAuthenticate); | |
| } | |
| // Third request has both Proxy-Authorization and Authorization, | |
| // so we now expect 200 | |
| out.printf("%nPosting with Authorization to %s server at: %s%n", expectedVersion(scheme, version), req1); | |
| HttpRequest authReq2 = HttpRequest.newBuilder(authReq1, (k, v)-> true) | |
| .header("authorization", serverAuth).build(); | |
| response = client.send(authReq2, BodyHandlers.ofLines()); | |
| out.println("Checking response: " + response); | |
| // Check that we have 200 and the expected body echoed back. | |
| // Check that the response version is as expected too. | |
| if (response.statusCode() != 200) { | |
| throw new RuntimeException("Unexpected status code: " + response); | |
| } | |
| if (response.version() != expectedVersion(scheme, version)) { | |
| 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); | |
| } | |
| } catch(Throwable t) { | |
| out.println("Unexpected exception: exiting: " + t); | |
| t.printStackTrace(out); | |
| throw t; | |
| } | |
| } | |
| } |