| /* | |
| * Copyright (c) 2018, 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.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.System.err; | |
| 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 8336655 8338569 | |
| * @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<String, EchoServers> 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<Boolean> 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<Version> serverVersions(Version clientVersion) { | |
| if (clientVersion == Version.HTTP_1_1) { | |
| return List.of(clientVersion); | |
| } else { | |
| return List.of(Version.values()); | |
| } | |
| } | |
| public static List<Version> clientVersions() { | |
| return List.of(Version.values()); | |
| } | |
| public static List<Boolean> 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<DigestEchoServer.HttpAuthType> 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<DigestEchoServer.HttpAuthType> 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 or SERVER307 properly | |
| if (authType == DigestEchoServer.HttpAuthType.PROXY305 || | |
| authType == DigestEchoServer.HttpAuthType.SERVER307) continue; | |
| EnumSet<DigestEchoServer.HttpAuthSchemeType> 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<DigestEchoServer.HttpAuthSchemeType> 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(System.out); | |
| err.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<HttpClient> queue = new ReferenceQueue<>(); | |
| WeakReference<HttpClient> ref = new WeakReference<>(client, queue); | |
| HttpResponse<String> r; | |
| CompletableFuture<HttpResponse<String>> cf1; | |
| String auth = null; | |
| Throwable failed = null; | |
| URI reqURI = null; | |
| try { | |
| for (int i = 0; i < data.length; i++) { | |
| out.println(DigestEchoServer.now() + " ----- iteration " + i + " -----"); | |
| List<String> 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); | |
| URI baseReq = URI.create(uri + "?iteration=" + i + ",async=" + async | |
| + ",addHeaders=" + addHeaders + ",preemptive=" + preemptive | |
| + ",expectContinue=" + expectContinue + ",version=" + clientVersion); | |
| reqURI = URI.create(baseReq + ",basicCount=" + basicCount.get()); | |
| HttpRequest.Builder builder = HttpRequest.newBuilder(reqURI).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<Stream<String>> resp; | |
| try { | |
| if (async) { | |
| out.printf("%s client.sendAsync(%s)%n", DigestEchoServer.now(), request); | |
| resp = client.sendAsync(request, BodyHandlers.ofLines()).join(); | |
| } else { | |
| out.printf("%s client.send(%s)%n", DigestEchoServer.now(), request); | |
| 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)); | |
| } | |
| throw t; | |
| } | |
| if (addHeaders && !preemptive && (i == 0 || isSchemeDisabled())) { | |
| assert resp.statusCode() == 401 || resp.statusCode() == 407; | |
| Stream<String> 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)); | |
| reqURI = URI.create(baseReq + ",withAuthorization=" | |
| + authType + ",basicCount=" + basicCount.get()); | |
| request = HttpRequest.newBuilder(reqURI).version(clientVersion) | |
| .POST(reqBody).header(authorizationKey(authType), auth).build(); | |
| if (async) { | |
| out.printf("%s client.sendAsync(%s)%n", DigestEchoServer.now(), request); | |
| resp = client.sendAsync(request, BodyHandlers.ofLines()).join(); | |
| } else { | |
| out.printf("%s client.send(%s)%n", DigestEchoServer.now(), request); | |
| resp = client.send(request, BodyHandlers.ofLines()); | |
| } | |
| } | |
| final List<String> 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); | |
| } | |
| } | |
| } catch (Throwable t) { | |
| if (reqURI == null) { | |
| failed = t; | |
| throw t; | |
| } | |
| String decoration = "%s Unexpected exception %s for %s".formatted(DigestEchoServer.now(), t, reqURI); | |
| RuntimeException decorated = new RuntimeException(decoration, t); | |
| failed = decorated; | |
| throw decorated; | |
| } 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) { | |
| if (failed != null) error.addSuppressed(failed); | |
| 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<String> r; | |
| CompletableFuture<HttpResponse<String>> cf1; | |
| byte[] cnonce = new byte[16]; | |
| String cnonceStr = null; | |
| DigestEchoServer.DigestResponse challenge = null; | |
| ReferenceQueue<HttpClient> queue = new ReferenceQueue<>(); | |
| WeakReference<HttpClient> ref = new WeakReference<>(client, queue); | |
| URI reqURI = null; | |
| Throwable failed = null; | |
| try { | |
| for (int i = 0; i < data.length; i++) { | |
| out.println(DigestEchoServer.now() + "----- iteration " + i + " -----"); | |
| List<String> 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); | |
| URI baseReq = URI.create(uri + "?iteration=" + i + ",async=" + async | |
| + ",expectContinue=" + expectContinue + ",version=" + clientVersion); | |
| reqURI = URI.create(baseReq + ",digestCount=" + digestCount.get()); | |
| HttpRequest.Builder reqBuilder = HttpRequest | |
| .newBuilder(reqURI).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<Stream<String>> resp; | |
| if (async) { | |
| out.printf("%s client.sendAsync(%s)%n", DigestEchoServer.now(), request); | |
| resp = client.sendAsync(request, BodyHandlers.ofLines()).join(); | |
| } else { | |
| out.printf("%s client.send(%s)%n", DigestEchoServer.now(), request); | |
| 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<String> 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); | |
| reqURI = URI.create(baseReq + ",withAuth=" + authType + ",digestCount=" + digestCount.get()); | |
| try { | |
| request = HttpRequest.newBuilder(reqURI).version(clientVersion) | |
| .POST(reqBody).header(authorizationKey(authType), auth).build(); | |
| } catch (IllegalArgumentException x) { | |
| throw x; | |
| } | |
| if (async) { | |
| out.printf("%s client.sendAsync(%s)%n", DigestEchoServer.now(), request); | |
| resp = client.sendAsync(request, BodyHandlers.ofLines()).join(); | |
| } else { | |
| out.printf("%s client.send(%s)%n", DigestEchoServer.now(), request); | |
| resp = client.send(request, BodyHandlers.ofLines()); | |
| } | |
| System.out.println(resp); | |
| } | |
| final List<String> 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); | |
| } | |
| } | |
| } catch (Throwable t) { | |
| if (reqURI == null) { | |
| failed = t; | |
| throw t; | |
| } | |
| String decoration = "%s Unexpected exception %s for %s".formatted(DigestEchoServer.now(), t, reqURI); | |
| RuntimeException decorated = new RuntimeException(decoration, t); | |
| failed = decorated; | |
| throw decorated; | |
| } 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) { | |
| if (failed != null) { | |
| error.addSuppressed(failed); | |
| } | |
| throw error; | |
| } | |
| } | |
| 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); | |
| } | |
| } | |
| } |