/* * 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 java.io.IOException; import java.io.UncheckedIOException; import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; import java.math.BigInteger; 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.HttpRequest.BodyPublisher; import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Base64; import java.util.EnumSet; import java.util.List; import java.util.Optional; import java.util.Random; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; 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 sun.net.NetProperties; import sun.net.www.HeaderParser; import static java.lang.System.out; import static java.lang.String.format; /** * @test * @summary this test verifies that a client may provides authorization * headers directly when connecting with a server. * @bug 8087112 * @library /test/lib /test/jdk/java/net/httpclient/lib * @build jdk.httpclient.test.lib.common.HttpServerAdapters jdk.test.lib.net.SimpleSSLContext * DigestEchoServer ReferenceTracker DigestEchoClient * @run main/othervm DigestEchoClient * @run main/othervm -Djdk.http.auth.proxying.disabledSchemes= * -Djdk.http.auth.tunneling.disabledSchemes= * DigestEchoClient */ public class DigestEchoClient { 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 AtomicLong serverCount = new AtomicLong(); static final class EchoServers { final DigestEchoServer.HttpAuthType authType; final DigestEchoServer.HttpAuthSchemeType authScheme; final String protocolScheme; final String key; final DigestEchoServer server; final Version serverVersion; private EchoServers(DigestEchoServer server, Version version, String protocolScheme, DigestEchoServer.HttpAuthType authType, DigestEchoServer.HttpAuthSchemeType authScheme) { this.authType = authType; this.authScheme = authScheme; this.protocolScheme = protocolScheme; this.key = key(version, protocolScheme, authType, authScheme); this.server = server; this.serverVersion = version; } static String key(Version version, String protocolScheme, DigestEchoServer.HttpAuthType authType, DigestEchoServer.HttpAuthSchemeType authScheme) { return String.format("%s:%s:%s:%s", version, protocolScheme, authType, authScheme); } private static EchoServers create(Version version, String protocolScheme, DigestEchoServer.HttpAuthType authType, DigestEchoServer.HttpAuthSchemeType authScheme) { try { serverCount.incrementAndGet(); DigestEchoServer server = DigestEchoServer.create(version, protocolScheme, authType, authScheme); return new EchoServers(server, version, protocolScheme, authType, authScheme); } catch (IOException x) { throw new UncheckedIOException(x); } } public static DigestEchoServer of(Version version, String protocolScheme, DigestEchoServer.HttpAuthType authType, DigestEchoServer.HttpAuthSchemeType authScheme) { String key = key(version, protocolScheme, authType, authScheme); return servers.computeIfAbsent(key, (k) -> create(version, protocolScheme, authType, authScheme)).server; } public static void stop() { for (EchoServers s : servers.values()) { s.server.stop(); } } private static final ConcurrentMap servers = new ConcurrentHashMap<>(); } final static String PROXY_DISABLED = NetProperties.get("jdk.http.auth.proxying.disabledSchemes"); final static String TUNNEL_DISABLED = NetProperties.get("jdk.http.auth.tunneling.disabledSchemes"); static { System.out.println("jdk.http.auth.proxying.disabledSchemes=" + PROXY_DISABLED); System.out.println("jdk.http.auth.tunneling.disabledSchemes=" + TUNNEL_DISABLED); } static final AtomicInteger NC = new AtomicInteger(); static final Random random = new Random(); static final SSLContext context; static { try { context = new SimpleSSLContext().get(); SSLContext.setDefault(context); } catch (Exception x) { throw new ExceptionInInitializerError(x); } } static final List BOOLEANS = List.of(true, false); final boolean useSSL; final DigestEchoServer.HttpAuthSchemeType authScheme; final DigestEchoServer.HttpAuthType authType; DigestEchoClient(boolean useSSL, DigestEchoServer.HttpAuthSchemeType authScheme, DigestEchoServer.HttpAuthType authType) throws IOException { this.useSSL = useSSL; this.authScheme = authScheme; this.authType = authType; } static final AtomicLong clientCount = new AtomicLong(); static final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; public HttpClient newHttpClient(DigestEchoServer server) { clientCount.incrementAndGet(); HttpClient.Builder builder = HttpClient.newBuilder(); builder = builder.proxy(ProxySelector.of(null)); if (useSSL) { builder.sslContext(context); } switch (authScheme) { case BASIC: builder = builder.authenticator(DigestEchoServer.AUTHENTICATOR); break; case BASICSERVER: // don't set the authenticator: we will handle the header ourselves. // builder = builder.authenticator(DigestEchoServer.AUTHENTICATOR); break; default: break; } switch (authType) { case PROXY: builder = builder.proxy(ProxySelector.of(server.getProxyAddress())); break; case PROXY305: builder = builder.proxy(ProxySelector.of(server.getProxyAddress())); builder = builder.followRedirects(HttpClient.Redirect.NORMAL); break; case SERVER307: builder = builder.followRedirects(HttpClient.Redirect.NORMAL); break; default: break; } return TRACKER.track(builder.build()); } public static List serverVersions(Version clientVersion) { if (clientVersion == Version.HTTP_1_1) { return List.of(clientVersion); } else { return List.of(Version.values()); } } public static List clientVersions() { return List.of(Version.values()); } public static List expectContinue(Version serverVersion) { if (serverVersion == Version.HTTP_1_1) { return BOOLEANS; } else { // our test HTTP/2 server does not support Expect: 100-Continue return List.of(Boolean.FALSE); } } public static void main(String[] args) throws Exception { HttpServerAdapters.enableServerLogging(); boolean useSSL = false; EnumSet types = EnumSet.complementOf(EnumSet.of(DigestEchoServer.HttpAuthType.PROXY305)); Throwable failed = null; if (args != null && args.length >= 1) { useSSL = "SSL".equals(args[0]); if (args.length > 1) { List httpAuthTypes = Stream.of(Arrays.copyOfRange(args, 1, args.length)) .map(DigestEchoServer.HttpAuthType::valueOf) .collect(Collectors.toList()); types = EnumSet.copyOf(httpAuthTypes); } } try { for (DigestEchoServer.HttpAuthType authType : types) { // The test server does not support PROXY305 properly if (authType == DigestEchoServer.HttpAuthType.PROXY305) continue; EnumSet basics = EnumSet.of(DigestEchoServer.HttpAuthSchemeType.BASICSERVER, DigestEchoServer.HttpAuthSchemeType.BASIC); for (DigestEchoServer.HttpAuthSchemeType authScheme : basics) { DigestEchoClient dec = new DigestEchoClient(useSSL, authScheme, authType); for (Version clientVersion : clientVersions()) { for (Version serverVersion : serverVersions(clientVersion)) { for (boolean expectContinue : expectContinue(serverVersion)) { for (boolean async : BOOLEANS) { for (boolean preemptive : BOOLEANS) { dec.testBasic(clientVersion, serverVersion, async, expectContinue, preemptive); } } } } } } EnumSet digests = EnumSet.of(DigestEchoServer.HttpAuthSchemeType.DIGEST); for (DigestEchoServer.HttpAuthSchemeType authScheme : digests) { DigestEchoClient dec = new DigestEchoClient(useSSL, authScheme, authType); for (Version clientVersion : clientVersions()) { for (Version serverVersion : serverVersions(clientVersion)) { for (boolean expectContinue : expectContinue(serverVersion)) { for (boolean async : BOOLEANS) { dec.testDigest(clientVersion, serverVersion, async, expectContinue); } } } } } } } catch(Throwable t) { out.println(DigestEchoServer.now() + ": Unexpected exception: " + t); t.printStackTrace(); failed = t; throw t; } finally { Thread.sleep(100); AssertionError trackFailed = TRACKER.check(500); EchoServers.stop(); System.out.println(" ---------------------------------------------------------- "); System.out.println(String.format("DigestEchoClient %s %s", useSSL ? "SSL" : "CLEAR", types)); System.out.println(String.format("Created %d clients and %d servers", clientCount.get(), serverCount.get())); System.out.println(String.format("basics: %d requests sent, %d ns / req", basicCount.get(), basics.get())); System.out.println(String.format("digests: %d requests sent, %d ns / req", digestCount.get(), digests.get())); System.out.println(" ---------------------------------------------------------- "); if (trackFailed != null) { if (failed != null) { failed.addSuppressed(trackFailed); if (failed instanceof Error) throw (Error) failed; if (failed instanceof Exception) throw (Exception) failed; } throw trackFailed; } } } boolean isSchemeDisabled() { String disabledSchemes; if (isProxy(authType)) { disabledSchemes = useSSL ? TUNNEL_DISABLED : PROXY_DISABLED; } else return false; if (disabledSchemes == null || disabledSchemes.isEmpty()) { return false; } String scheme; switch (authScheme) { case DIGEST: scheme = "Digest"; break; case BASIC: scheme = "Basic"; break; case BASICSERVER: scheme = "Basic"; break; case NONE: return false; default: throw new InternalError("Unknown auth scheme: " + authScheme); } return Stream.of(disabledSchemes.split(",")) .map(String::trim) .filter(scheme::equalsIgnoreCase) .findAny() .isPresent(); } final static AtomicLong basics = new AtomicLong(); final static AtomicLong basicCount = new AtomicLong(); // @Test void testBasic(Version clientVersion, Version serverVersion, boolean async, boolean expectContinue, boolean preemptive) throws Exception { final boolean addHeaders = authScheme == DigestEchoServer.HttpAuthSchemeType.BASICSERVER; // !preemptive has no meaning if we don't handle the authorization // headers ourselves if (!preemptive && !addHeaders) return; out.println(format("*** testBasic: client: %s, server: %s, async: %s, useSSL: %s, " + "authScheme: %s, authType: %s, expectContinue: %s preemptive: %s***", clientVersion, serverVersion, async, useSSL, authScheme, authType, expectContinue, preemptive)); DigestEchoServer server = EchoServers.of(serverVersion, useSSL ? "https" : "http", authType, authScheme); URI uri = DigestEchoServer.uri(useSSL ? "https" : "http", server.getServerAddress(), "/foo/"); HttpClient client = newHttpClient(server); ReferenceQueue queue = new ReferenceQueue<>(); WeakReference ref = new WeakReference<>(client, queue); HttpResponse r; CompletableFuture> cf1; String auth = null; try { for (int i=0; i lines = List.of(Arrays.copyOfRange(data, 0, i+1)); assert lines.size() == i + 1; String body = lines.stream().collect(Collectors.joining("\r\n")); BodyPublisher reqBody = BodyPublishers.ofString(body); HttpRequest.Builder builder = HttpRequest.newBuilder(uri).version(clientVersion) .POST(reqBody).expectContinue(expectContinue); boolean isTunnel = isProxy(authType) && useSSL; if (addHeaders) { // handle authentication ourselves assert !client.authenticator().isPresent(); if (auth == null) auth = "Basic " + getBasicAuth("arthur"); try { if ((i > 0 || preemptive) && (!isTunnel || i == 0 || isSchemeDisabled())) { // In case of a SSL tunnel through proxy then only the // first request should require proxy authorization // Though this might be invalidated if the server decides // to close the connection... out.println(String.format("%s adding %s: %s", DigestEchoServer.now(), authorizationKey(authType), auth)); builder = builder.header(authorizationKey(authType), auth); } } catch (IllegalArgumentException x) { throw x; } } else { // let the stack do the authentication assert client.authenticator().isPresent(); } long start = System.nanoTime(); HttpRequest request = builder.build(); HttpResponse> resp; try { if (async) { resp = client.sendAsync(request, BodyHandlers.ofLines()).join(); } else { resp = client.send(request, BodyHandlers.ofLines()); } } catch (Throwable t) { long stop = System.nanoTime(); synchronized (basicCount) { long n = basicCount.getAndIncrement(); basics.set((basics.get() * n + (stop - start)) / (n + 1)); } // unwrap CompletionException if (t instanceof CompletionException) { assert t.getCause() != null; t = t.getCause(); } out.println(DigestEchoServer.now() + ": Unexpected exception: " + t); throw new RuntimeException("Unexpected exception: " + t, t); } if (addHeaders && !preemptive && (i==0 || isSchemeDisabled())) { assert resp.statusCode() == 401 || resp.statusCode() == 407; Stream respBody = resp.body(); if (respBody != null) { System.out.printf("Response body (%s):\n", resp.statusCode()); respBody.forEach(System.out::println); } System.out.println(String.format("%s received: adding header %s: %s", resp.statusCode(), authorizationKey(authType), auth)); request = HttpRequest.newBuilder(uri).version(clientVersion) .POST(reqBody).header(authorizationKey(authType), auth).build(); if (async) { resp = client.sendAsync(request, BodyHandlers.ofLines()).join(); } else { resp = client.send(request, BodyHandlers.ofLines()); } } final List respLines; try { if (isSchemeDisabled()) { if (resp.statusCode() != 407) { throw new RuntimeException("expected 407 not received"); } System.out.println("Scheme disabled for [" + authType + ", " + authScheme + ", " + (useSSL ? "HTTP" : "HTTPS") + "]: Received expected " + resp.statusCode()); continue; } else { System.out.println("Scheme enabled for [" + authType + ", " + authScheme + ", " + (useSSL ? "HTTPS" : "HTTP") + "]: Expecting 200, response is: " + resp); assert resp.statusCode() == 200 : "200 expected, received " + resp; respLines = resp.body().collect(Collectors.toList()); } } finally { long stop = System.nanoTime(); synchronized (basicCount) { long n = basicCount.getAndIncrement(); basics.set((basics.get() * n + (stop - start)) / (n + 1)); } } if (!lines.equals(respLines)) { throw new RuntimeException("Unexpected response: " + respLines); } } } finally { client = null; System.gc(); while (!ref.refersTo(null)) { System.gc(); if (queue.remove(100) == ref) break; } var error = TRACKER.checkShutdown(900); if (error != null) throw error; } System.out.println("OK"); } String getBasicAuth(String username) { StringBuilder builder = new StringBuilder(username); builder.append(':'); for (char c : DigestEchoServer.AUTHENTICATOR.getPassword(username)) { builder.append(c); } return Base64.getEncoder().encodeToString(builder.toString().getBytes(StandardCharsets.UTF_8)); } final static AtomicLong digests = new AtomicLong(); final static AtomicLong digestCount = new AtomicLong(); // @Test void testDigest(Version clientVersion, Version serverVersion, boolean async, boolean expectContinue) throws Exception { String test = format("testDigest: client: %s, server: %s, async: %s, useSSL: %s, " + "authScheme: %s, authType: %s, expectContinue: %s", clientVersion, serverVersion, async, useSSL, authScheme, authType, expectContinue); out.println("*** " + test + " ***"); DigestEchoServer server = EchoServers.of(serverVersion, useSSL ? "https" : "http", authType, authScheme); URI uri = DigestEchoServer.uri(useSSL ? "https" : "http", server.getServerAddress(), "/foo/"); HttpClient client = newHttpClient(server); HttpResponse r; CompletableFuture> cf1; byte[] cnonce = new byte[16]; String cnonceStr = null; DigestEchoServer.DigestResponse challenge = null; try { for (int i=0; i lines = List.of(Arrays.copyOfRange(data, 0, i+1)); assert lines.size() == i + 1; String body = lines.stream().collect(Collectors.joining("\r\n")); HttpRequest.BodyPublisher reqBody = HttpRequest.BodyPublishers.ofString(body); HttpRequest.Builder reqBuilder = HttpRequest .newBuilder(uri).version(clientVersion).POST(reqBody) .expectContinue(expectContinue); boolean isTunnel = isProxy(authType) && useSSL; String digestMethod = isTunnel ? "CONNECT" : "POST"; // In case of a tunnel connection only the first request // which establishes the tunnel needs to authenticate with // the proxy. if (challenge != null && (!isTunnel || isSchemeDisabled())) { assert cnonceStr != null; String auth = digestResponse(uri, digestMethod, challenge, cnonceStr); try { reqBuilder = reqBuilder.header(authorizationKey(authType), auth); } catch (IllegalArgumentException x) { throw x; } } long start = System.nanoTime(); HttpRequest request = reqBuilder.build(); HttpResponse> resp; if (async) { resp = client.sendAsync(request, BodyHandlers.ofLines()).join(); } else { resp = client.send(request, BodyHandlers.ofLines()); } System.out.println(resp); assert challenge != null || resp.statusCode() == 401 || resp.statusCode() == 407 : "challenge=" + challenge + ", resp=" + resp + ", test=[" + test + "]"; if (resp.statusCode() == 401 || resp.statusCode() == 407) { // This assert may need to be relaxed if our server happened to // decide to close the tunnel connection, in which case we would // receive 407 again... assert challenge == null || !isTunnel || isSchemeDisabled() : "No proxy auth should be required after establishing an SSL tunnel"; System.out.println("Received " + resp.statusCode() + " answering challenge..."); random.nextBytes(cnonce); cnonceStr = new BigInteger(1, cnonce).toString(16); System.out.println("Response headers: " + resp.headers()); Optional authenticateOpt = resp.headers().firstValue(authenticateKey(authType)); String authenticate = authenticateOpt.orElseThrow( () -> new RuntimeException(authenticateKey(authType) + ": not found")); assert authenticate.startsWith("Digest "); HeaderParser hp = new HeaderParser(authenticate.substring("Digest ".length())); String qop = hp.findValue("qop"); String nonce = hp.findValue("nonce"); if (qop == null && nonce == null) { throw new RuntimeException("QOP and NONCE not found"); } challenge = DigestEchoServer.DigestResponse .create(authenticate.substring("Digest ".length())); String auth = digestResponse(uri, digestMethod, challenge, cnonceStr); try { request = HttpRequest.newBuilder(uri).version(clientVersion) .POST(reqBody).header(authorizationKey(authType), auth).build(); } catch (IllegalArgumentException x) { throw x; } if (async) { resp = client.sendAsync(request, BodyHandlers.ofLines()).join(); } else { resp = client.send(request, BodyHandlers.ofLines()); } System.out.println(resp); } final List respLines; try { if (isSchemeDisabled()) { if (resp.statusCode() != 407) { throw new RuntimeException("expected 407 not received"); } System.out.println("Scheme disabled for [" + authType + ", " + authScheme + ", " + (useSSL ? "HTTP" : "HTTPS") + "]: Received expected " + resp.statusCode()); continue; } else { assert resp.statusCode() == 200; respLines = resp.body().collect(Collectors.toList()); } } finally { long stop = System.nanoTime(); synchronized (digestCount) { long n = digestCount.getAndIncrement(); digests.set((digests.get() * n + (stop - start)) / (n + 1)); } } if (!lines.equals(respLines)) { throw new RuntimeException("Unexpected response: " + respLines); } } } finally { } System.out.println("OK"); } // WARNING: This is not a full fledged implementation of DIGEST. // It does contain bugs and inaccuracy. static String digestResponse(URI uri, String method, DigestEchoServer.DigestResponse challenge, String cnonce) throws NoSuchAlgorithmException { int nc = NC.incrementAndGet(); DigestEchoServer.DigestResponse response1 = new DigestEchoServer.DigestResponse("earth", "arthur", challenge.nonce, cnonce, String.valueOf(nc), uri.toASCIIString(), challenge.algorithm, challenge.qop, challenge.opaque, null); String response = DigestEchoServer.DigestResponse.computeDigest(true, method, DigestEchoServer.AUTHENTICATOR.getPassword("arthur"), response1); String auth = "Digest username=\"arthur\", realm=\"earth\"" + ", response=\"" + response + "\", uri=\""+uri.toASCIIString()+"\"" + ", qop=\"" + response1.qop + "\", cnonce=\"" + response1.cnonce + "\", nc=\"" + nc + "\", nonce=\"" + response1.nonce + "\""; if (response1.opaque != null) { auth = auth + ", opaque=\"" + response1.opaque + "\""; } return auth; } static String authenticateKey(DigestEchoServer.HttpAuthType authType) { switch (authType) { case SERVER: return "www-authenticate"; case SERVER307: return "www-authenticate"; case PROXY: return "proxy-authenticate"; case PROXY305: return "proxy-authenticate"; default: throw new InternalError("authType: " + authType); } } static String authorizationKey(DigestEchoServer.HttpAuthType authType) { switch (authType) { case SERVER: return "authorization"; case SERVER307: return "Authorization"; case PROXY: return "Proxy-Authorization"; case PROXY305: return "proxy-Authorization"; default: throw new InternalError("authType: " + authType); } } static boolean isProxy(DigestEchoServer.HttpAuthType authType) { switch (authType) { case SERVER: return false; case SERVER307: return false; case PROXY: return true; case PROXY305: return true; default: throw new InternalError("authType: " + authType); } } }