test/jdk/java/net/httpclient/DigestEchoClient.java
changeset 49765 ee6f7a61f3a5
child 52121 934969c63223
child 56451 9585061fdb04
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/DigestEchoClient.java	Tue Apr 17 08:54:17 2018 -0700
@@ -0,0 +1,700 @@
+/*
+ * Copyright (c) 2018, 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.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.BodyHandler;
+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.testlibrary.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 /lib/testlibrary http2/server
+ * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters DigestEchoServer
+ *        ReferenceTracker DigestEchoClient
+ * @modules java.net.http/jdk.internal.net.http.common
+ *          java.net.http/jdk.internal.net.http.frame
+ *          java.net.http/jdk.internal.net.http.hpack
+ *          java.logging
+ *          java.base/sun.net.www.http
+ *          java.base/sun.net.www
+ *          java.base/sun.net
+ * @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 properly
+                if (authType == DigestEchoServer.HttpAuthType.PROXY305) 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();
+            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);
+        HttpResponse<String> r;
+        CompletableFuture<HttpResponse<String>> cf1;
+        String auth = 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);
+                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<Stream<String>> 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<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));
+                    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<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);
+                }
+            }
+        } finally {
+        }
+        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;
+
+        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);
+                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<Stream<String>> 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<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);
+                    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<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);
+                }
+            }
+        } 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);
+        }
+    }
+}