test/jdk/java/net/httpclient/DigestEchoClient.java
changeset 49765 ee6f7a61f3a5
child 52121 934969c63223
child 56451 9585061fdb04
equal deleted inserted replaced
49707:f7fd051519ac 49765:ee6f7a61f3a5
       
     1 /*
       
     2  * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
       
     3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
       
     4  *
       
     5  * This code is free software; you can redistribute it and/or modify it
       
     6  * under the terms of the GNU General Public License version 2 only, as
       
     7  * published by the Free Software Foundation.
       
     8  *
       
     9  * This code is distributed in the hope that it will be useful, but WITHOUT
       
    10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
       
    11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
       
    12  * version 2 for more details (a copy is included in the LICENSE file that
       
    13  * accompanied this code).
       
    14  *
       
    15  * You should have received a copy of the GNU General Public License version
       
    16  * 2 along with this work; if not, write to the Free Software Foundation,
       
    17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
       
    18  *
       
    19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
       
    20  * or visit www.oracle.com if you need additional information or have any
       
    21  * questions.
       
    22  */
       
    23 
       
    24 import java.io.IOException;
       
    25 import java.io.UncheckedIOException;
       
    26 import java.math.BigInteger;
       
    27 import java.net.ProxySelector;
       
    28 import java.net.URI;
       
    29 import java.net.http.HttpClient;
       
    30 import java.net.http.HttpClient.Version;
       
    31 import java.net.http.HttpRequest;
       
    32 import java.net.http.HttpRequest.BodyPublisher;
       
    33 import java.net.http.HttpRequest.BodyPublishers;
       
    34 import java.net.http.HttpResponse;
       
    35 import java.net.http.HttpResponse.BodyHandler;
       
    36 import java.net.http.HttpResponse.BodyHandlers;
       
    37 import java.nio.charset.StandardCharsets;
       
    38 import java.security.NoSuchAlgorithmException;
       
    39 import java.util.Arrays;
       
    40 import java.util.Base64;
       
    41 import java.util.EnumSet;
       
    42 import java.util.List;
       
    43 import java.util.Optional;
       
    44 import java.util.Random;
       
    45 import java.util.concurrent.CompletableFuture;
       
    46 import java.util.concurrent.CompletionException;
       
    47 import java.util.concurrent.ConcurrentHashMap;
       
    48 import java.util.concurrent.ConcurrentMap;
       
    49 import java.util.concurrent.atomic.AtomicInteger;
       
    50 import java.util.concurrent.atomic.AtomicLong;
       
    51 import java.util.stream.Collectors;
       
    52 import java.util.stream.Stream;
       
    53 import javax.net.ssl.SSLContext;
       
    54 import jdk.testlibrary.SimpleSSLContext;
       
    55 import sun.net.NetProperties;
       
    56 import sun.net.www.HeaderParser;
       
    57 import static java.lang.System.out;
       
    58 import static java.lang.String.format;
       
    59 
       
    60 /**
       
    61  * @test
       
    62  * @summary this test verifies that a client may provides authorization
       
    63  *          headers directly when connecting with a server.
       
    64  * @bug 8087112
       
    65  * @library /lib/testlibrary http2/server
       
    66  * @build jdk.testlibrary.SimpleSSLContext HttpServerAdapters DigestEchoServer
       
    67  *        ReferenceTracker DigestEchoClient
       
    68  * @modules java.net.http/jdk.internal.net.http.common
       
    69  *          java.net.http/jdk.internal.net.http.frame
       
    70  *          java.net.http/jdk.internal.net.http.hpack
       
    71  *          java.logging
       
    72  *          java.base/sun.net.www.http
       
    73  *          java.base/sun.net.www
       
    74  *          java.base/sun.net
       
    75  * @run main/othervm DigestEchoClient
       
    76  * @run main/othervm -Djdk.http.auth.proxying.disabledSchemes=
       
    77  *                   -Djdk.http.auth.tunneling.disabledSchemes=
       
    78  *                   DigestEchoClient
       
    79  */
       
    80 
       
    81 public class DigestEchoClient {
       
    82 
       
    83     static final String data[] = {
       
    84         "Lorem ipsum",
       
    85         "dolor sit amet",
       
    86         "consectetur adipiscing elit, sed do eiusmod tempor",
       
    87         "quis nostrud exercitation ullamco",
       
    88         "laboris nisi",
       
    89         "ut",
       
    90         "aliquip ex ea commodo consequat." +
       
    91         "Duis aute irure dolor in reprehenderit in voluptate velit esse" +
       
    92         "cillum dolore eu fugiat nulla pariatur.",
       
    93         "Excepteur sint occaecat cupidatat non proident."
       
    94     };
       
    95 
       
    96     static final AtomicLong serverCount = new AtomicLong();
       
    97     static final class EchoServers {
       
    98         final DigestEchoServer.HttpAuthType authType;
       
    99         final DigestEchoServer.HttpAuthSchemeType authScheme;
       
   100         final String protocolScheme;
       
   101         final String key;
       
   102         final DigestEchoServer server;
       
   103         final Version serverVersion;
       
   104 
       
   105         private EchoServers(DigestEchoServer server,
       
   106                     Version version,
       
   107                     String protocolScheme,
       
   108                     DigestEchoServer.HttpAuthType authType,
       
   109                     DigestEchoServer.HttpAuthSchemeType authScheme) {
       
   110             this.authType = authType;
       
   111             this.authScheme = authScheme;
       
   112             this.protocolScheme = protocolScheme;
       
   113             this.key = key(version, protocolScheme, authType, authScheme);
       
   114             this.server = server;
       
   115             this.serverVersion = version;
       
   116         }
       
   117 
       
   118         static String key(Version version,
       
   119                           String protocolScheme,
       
   120                           DigestEchoServer.HttpAuthType authType,
       
   121                           DigestEchoServer.HttpAuthSchemeType authScheme) {
       
   122             return String.format("%s:%s:%s:%s", version, protocolScheme, authType, authScheme);
       
   123         }
       
   124 
       
   125         private static EchoServers create(Version version,
       
   126                                    String protocolScheme,
       
   127                                    DigestEchoServer.HttpAuthType authType,
       
   128                                    DigestEchoServer.HttpAuthSchemeType authScheme) {
       
   129             try {
       
   130                 serverCount.incrementAndGet();
       
   131                 DigestEchoServer server =
       
   132                     DigestEchoServer.create(version, protocolScheme, authType, authScheme);
       
   133                 return new EchoServers(server, version, protocolScheme, authType, authScheme);
       
   134             } catch (IOException x) {
       
   135                 throw new UncheckedIOException(x);
       
   136             }
       
   137         }
       
   138 
       
   139         public static DigestEchoServer of(Version version,
       
   140                                     String protocolScheme,
       
   141                                     DigestEchoServer.HttpAuthType authType,
       
   142                                     DigestEchoServer.HttpAuthSchemeType authScheme) {
       
   143             String key = key(version, protocolScheme, authType, authScheme);
       
   144             return servers.computeIfAbsent(key, (k) ->
       
   145                     create(version, protocolScheme, authType, authScheme)).server;
       
   146         }
       
   147 
       
   148         public static void stop() {
       
   149             for (EchoServers s : servers.values()) {
       
   150                 s.server.stop();
       
   151             }
       
   152         }
       
   153 
       
   154         private static final ConcurrentMap<String, EchoServers> servers = new ConcurrentHashMap<>();
       
   155     }
       
   156 
       
   157     final static String PROXY_DISABLED = NetProperties.get("jdk.http.auth.proxying.disabledSchemes");
       
   158     final static String TUNNEL_DISABLED = NetProperties.get("jdk.http.auth.tunneling.disabledSchemes");
       
   159     static {
       
   160         System.out.println("jdk.http.auth.proxying.disabledSchemes=" + PROXY_DISABLED);
       
   161         System.out.println("jdk.http.auth.tunneling.disabledSchemes=" + TUNNEL_DISABLED);
       
   162     }
       
   163 
       
   164 
       
   165 
       
   166     static final AtomicInteger NC = new AtomicInteger();
       
   167     static final Random random = new Random();
       
   168     static final SSLContext context;
       
   169     static {
       
   170         try {
       
   171             context = new SimpleSSLContext().get();
       
   172             SSLContext.setDefault(context);
       
   173         } catch (Exception x) {
       
   174             throw new ExceptionInInitializerError(x);
       
   175         }
       
   176     }
       
   177     static final List<Boolean> BOOLEANS = List.of(true, false);
       
   178 
       
   179     final boolean useSSL;
       
   180     final DigestEchoServer.HttpAuthSchemeType authScheme;
       
   181     final DigestEchoServer.HttpAuthType authType;
       
   182     DigestEchoClient(boolean useSSL,
       
   183                      DigestEchoServer.HttpAuthSchemeType authScheme,
       
   184                      DigestEchoServer.HttpAuthType authType)
       
   185             throws IOException {
       
   186         this.useSSL = useSSL;
       
   187         this.authScheme = authScheme;
       
   188         this.authType = authType;
       
   189     }
       
   190 
       
   191     static final AtomicLong clientCount = new AtomicLong();
       
   192     static final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE;
       
   193     public HttpClient newHttpClient(DigestEchoServer server) {
       
   194         clientCount.incrementAndGet();
       
   195         HttpClient.Builder builder = HttpClient.newBuilder();
       
   196         builder = builder.proxy(ProxySelector.of(null));
       
   197         if (useSSL) {
       
   198             builder.sslContext(context);
       
   199         }
       
   200         switch (authScheme) {
       
   201             case BASIC:
       
   202                 builder = builder.authenticator(DigestEchoServer.AUTHENTICATOR);
       
   203                 break;
       
   204             case BASICSERVER:
       
   205                 // don't set the authenticator: we will handle the header ourselves.
       
   206                 // builder = builder.authenticator(DigestEchoServer.AUTHENTICATOR);
       
   207                 break;
       
   208             default:
       
   209                 break;
       
   210         }
       
   211         switch (authType) {
       
   212             case PROXY:
       
   213                 builder = builder.proxy(ProxySelector.of(server.getProxyAddress()));
       
   214                 break;
       
   215             case PROXY305:
       
   216                 builder = builder.proxy(ProxySelector.of(server.getProxyAddress()));
       
   217                 builder = builder.followRedirects(HttpClient.Redirect.NORMAL);
       
   218                 break;
       
   219             case SERVER307:
       
   220                 builder = builder.followRedirects(HttpClient.Redirect.NORMAL);
       
   221                 break;
       
   222             default:
       
   223                 break;
       
   224         }
       
   225         return TRACKER.track(builder.build());
       
   226     }
       
   227 
       
   228     public static List<Version> serverVersions(Version clientVersion) {
       
   229         if (clientVersion == Version.HTTP_1_1) {
       
   230             return List.of(clientVersion);
       
   231         } else {
       
   232             return List.of(Version.values());
       
   233         }
       
   234     }
       
   235 
       
   236     public static List<Version> clientVersions() {
       
   237         return List.of(Version.values());
       
   238     }
       
   239 
       
   240     public static List<Boolean> expectContinue(Version serverVersion) {
       
   241         if (serverVersion == Version.HTTP_1_1) {
       
   242             return BOOLEANS;
       
   243         } else {
       
   244             // our test HTTP/2 server does not support Expect: 100-Continue
       
   245             return List.of(Boolean.FALSE);
       
   246         }
       
   247     }
       
   248 
       
   249     public static void main(String[] args) throws Exception {
       
   250         HttpServerAdapters.enableServerLogging();
       
   251         boolean useSSL = false;
       
   252         EnumSet<DigestEchoServer.HttpAuthType> types =
       
   253                 EnumSet.complementOf(EnumSet.of(DigestEchoServer.HttpAuthType.PROXY305));
       
   254         Throwable failed = null;
       
   255         if (args != null && args.length >= 1) {
       
   256             useSSL = "SSL".equals(args[0]);
       
   257             if (args.length > 1) {
       
   258                 List<DigestEchoServer.HttpAuthType> httpAuthTypes =
       
   259                         Stream.of(Arrays.copyOfRange(args, 1, args.length))
       
   260                                 .map(DigestEchoServer.HttpAuthType::valueOf)
       
   261                                 .collect(Collectors.toList());
       
   262                 types = EnumSet.copyOf(httpAuthTypes);
       
   263             }
       
   264         }
       
   265         try {
       
   266             for (DigestEchoServer.HttpAuthType authType : types) {
       
   267                 // The test server does not support PROXY305 properly
       
   268                 if (authType == DigestEchoServer.HttpAuthType.PROXY305) continue;
       
   269                 EnumSet<DigestEchoServer.HttpAuthSchemeType> basics =
       
   270                         EnumSet.of(DigestEchoServer.HttpAuthSchemeType.BASICSERVER,
       
   271                                 DigestEchoServer.HttpAuthSchemeType.BASIC);
       
   272                 for (DigestEchoServer.HttpAuthSchemeType authScheme : basics) {
       
   273                     DigestEchoClient dec = new DigestEchoClient(useSSL,
       
   274                             authScheme,
       
   275                             authType);
       
   276                     for (Version clientVersion : clientVersions()) {
       
   277                         for (Version serverVersion : serverVersions(clientVersion)) {
       
   278                             for (boolean expectContinue : expectContinue(serverVersion)) {
       
   279                                 for (boolean async : BOOLEANS) {
       
   280                                     for (boolean preemptive : BOOLEANS) {
       
   281                                         dec.testBasic(clientVersion,
       
   282                                                 serverVersion, async,
       
   283                                                 expectContinue, preemptive);
       
   284                                     }
       
   285                                 }
       
   286                             }
       
   287                         }
       
   288                     }
       
   289                 }
       
   290                 EnumSet<DigestEchoServer.HttpAuthSchemeType> digests =
       
   291                         EnumSet.of(DigestEchoServer.HttpAuthSchemeType.DIGEST);
       
   292                 for (DigestEchoServer.HttpAuthSchemeType authScheme : digests) {
       
   293                     DigestEchoClient dec = new DigestEchoClient(useSSL,
       
   294                             authScheme,
       
   295                             authType);
       
   296                     for (Version clientVersion : clientVersions()) {
       
   297                         for (Version serverVersion : serverVersions(clientVersion)) {
       
   298                             for (boolean expectContinue : expectContinue(serverVersion)) {
       
   299                                 for (boolean async : BOOLEANS) {
       
   300                                     dec.testDigest(clientVersion, serverVersion,
       
   301                                             async, expectContinue);
       
   302                                 }
       
   303                             }
       
   304                         }
       
   305                     }
       
   306                 }
       
   307             }
       
   308         } catch(Throwable t) {
       
   309             out.println(DigestEchoServer.now()
       
   310                     + ": Unexpected exception: " + t);
       
   311             t.printStackTrace();
       
   312             failed = t;
       
   313             throw t;
       
   314         } finally {
       
   315             Thread.sleep(100);
       
   316             AssertionError trackFailed = TRACKER.check(500);
       
   317             EchoServers.stop();
       
   318             System.out.println(" ---------------------------------------------------------- ");
       
   319             System.out.println(String.format("DigestEchoClient %s %s", useSSL ? "SSL" : "CLEAR", types));
       
   320             System.out.println(String.format("Created %d clients and %d servers",
       
   321                     clientCount.get(), serverCount.get()));
       
   322             System.out.println(String.format("basics:  %d requests sent, %d ns / req",
       
   323                     basicCount.get(), basics.get()));
       
   324             System.out.println(String.format("digests: %d requests sent, %d ns / req",
       
   325                     digestCount.get(), digests.get()));
       
   326             System.out.println(" ---------------------------------------------------------- ");
       
   327             if (trackFailed != null) {
       
   328                 if (failed != null) {
       
   329                     failed.addSuppressed(trackFailed);
       
   330                     if (failed instanceof Error) throw (Error) failed;
       
   331                     if (failed instanceof Exception) throw (Exception) failed;
       
   332                 }
       
   333                 throw trackFailed;
       
   334             }
       
   335         }
       
   336     }
       
   337 
       
   338     boolean isSchemeDisabled() {
       
   339         String disabledSchemes;
       
   340         if (isProxy(authType)) {
       
   341             disabledSchemes = useSSL
       
   342                     ? TUNNEL_DISABLED
       
   343                     : PROXY_DISABLED;
       
   344         } else return false;
       
   345         if (disabledSchemes == null
       
   346                 || disabledSchemes.isEmpty()) {
       
   347             return false;
       
   348         }
       
   349         String scheme;
       
   350         switch (authScheme) {
       
   351             case DIGEST:
       
   352                 scheme = "Digest";
       
   353                 break;
       
   354             case BASIC:
       
   355                 scheme = "Basic";
       
   356                 break;
       
   357             case BASICSERVER:
       
   358                 scheme = "Basic";
       
   359                 break;
       
   360             case NONE:
       
   361                 return false;
       
   362             default:
       
   363                 throw new InternalError("Unknown auth scheme: " + authScheme);
       
   364         }
       
   365         return Stream.of(disabledSchemes.split(","))
       
   366                 .map(String::trim)
       
   367                 .filter(scheme::equalsIgnoreCase)
       
   368                 .findAny()
       
   369                 .isPresent();
       
   370     }
       
   371 
       
   372     final static AtomicLong basics = new AtomicLong();
       
   373     final static AtomicLong basicCount = new AtomicLong();
       
   374     // @Test
       
   375     void testBasic(Version clientVersion, Version serverVersion, boolean async,
       
   376                    boolean expectContinue, boolean preemptive)
       
   377         throws Exception
       
   378     {
       
   379         final boolean addHeaders = authScheme == DigestEchoServer.HttpAuthSchemeType.BASICSERVER;
       
   380         // !preemptive has no meaning if we don't handle the authorization
       
   381         // headers ourselves
       
   382         if (!preemptive && !addHeaders) return;
       
   383 
       
   384         out.println(format("*** testBasic: client: %s, server: %s, async: %s, useSSL: %s, " +
       
   385                         "authScheme: %s, authType: %s, expectContinue: %s preemptive: %s***",
       
   386                 clientVersion, serverVersion, async, useSSL, authScheme, authType,
       
   387                 expectContinue, preemptive));
       
   388 
       
   389         DigestEchoServer server = EchoServers.of(serverVersion,
       
   390                 useSSL ? "https" : "http", authType, authScheme);
       
   391         URI uri = DigestEchoServer.uri(useSSL ? "https" : "http",
       
   392                 server.getServerAddress(), "/foo/");
       
   393 
       
   394         HttpClient client = newHttpClient(server);
       
   395         HttpResponse<String> r;
       
   396         CompletableFuture<HttpResponse<String>> cf1;
       
   397         String auth = null;
       
   398 
       
   399         try {
       
   400             for (int i=0; i<data.length; i++) {
       
   401                 out.println(DigestEchoServer.now() + " ----- iteration " + i + " -----");
       
   402                 List<String> lines = List.of(Arrays.copyOfRange(data, 0, i+1));
       
   403                 assert lines.size() == i + 1;
       
   404                 String body = lines.stream().collect(Collectors.joining("\r\n"));
       
   405                 BodyPublisher reqBody = BodyPublishers.ofString(body);
       
   406                 HttpRequest.Builder builder = HttpRequest.newBuilder(uri).version(clientVersion)
       
   407                         .POST(reqBody).expectContinue(expectContinue);
       
   408                 boolean isTunnel = isProxy(authType) && useSSL;
       
   409                 if (addHeaders) {
       
   410                     // handle authentication ourselves
       
   411                     assert !client.authenticator().isPresent();
       
   412                     if (auth == null) auth = "Basic " + getBasicAuth("arthur");
       
   413                     try {
       
   414                         if ((i > 0 || preemptive)
       
   415                                 && (!isTunnel || i == 0 || isSchemeDisabled())) {
       
   416                             // In case of a SSL tunnel through proxy then only the
       
   417                             // first request should require proxy authorization
       
   418                             // Though this might be invalidated if the server decides
       
   419                             // to close the connection...
       
   420                             out.println(String.format("%s adding %s: %s",
       
   421                                     DigestEchoServer.now(),
       
   422                                     authorizationKey(authType),
       
   423                                     auth));
       
   424                             builder = builder.header(authorizationKey(authType), auth);
       
   425                         }
       
   426                     } catch (IllegalArgumentException x) {
       
   427                         throw x;
       
   428                     }
       
   429                 } else {
       
   430                     // let the stack do the authentication
       
   431                     assert client.authenticator().isPresent();
       
   432                 }
       
   433                 long start = System.nanoTime();
       
   434                 HttpRequest request = builder.build();
       
   435                 HttpResponse<Stream<String>> resp;
       
   436                 try {
       
   437                     if (async) {
       
   438                         resp = client.sendAsync(request, BodyHandlers.ofLines()).join();
       
   439                     } else {
       
   440                         resp = client.send(request, BodyHandlers.ofLines());
       
   441                     }
       
   442                 } catch (Throwable t) {
       
   443                     long stop = System.nanoTime();
       
   444                     synchronized (basicCount) {
       
   445                         long n = basicCount.getAndIncrement();
       
   446                         basics.set((basics.get() * n + (stop - start)) / (n + 1));
       
   447                     }
       
   448                     // unwrap CompletionException
       
   449                     if (t instanceof CompletionException) {
       
   450                         assert t.getCause() != null;
       
   451                         t = t.getCause();
       
   452                     }
       
   453                     out.println(DigestEchoServer.now()
       
   454                             + ": Unexpected exception: " + t);
       
   455                     throw new RuntimeException("Unexpected exception: " + t, t);
       
   456                 }
       
   457 
       
   458                 if (addHeaders && !preemptive && (i==0 || isSchemeDisabled())) {
       
   459                     assert resp.statusCode() == 401 || resp.statusCode() == 407;
       
   460                     Stream<String> respBody = resp.body();
       
   461                     if (respBody != null) {
       
   462                         System.out.printf("Response body (%s):\n", resp.statusCode());
       
   463                         respBody.forEach(System.out::println);
       
   464                     }
       
   465                     System.out.println(String.format("%s received: adding header %s: %s",
       
   466                             resp.statusCode(), authorizationKey(authType), auth));
       
   467                     request = HttpRequest.newBuilder(uri).version(clientVersion)
       
   468                             .POST(reqBody).header(authorizationKey(authType), auth).build();
       
   469                     if (async) {
       
   470                         resp = client.sendAsync(request, BodyHandlers.ofLines()).join();
       
   471                     } else {
       
   472                         resp = client.send(request, BodyHandlers.ofLines());
       
   473                     }
       
   474                 }
       
   475                 final List<String> respLines;
       
   476                 try {
       
   477                     if (isSchemeDisabled()) {
       
   478                         if (resp.statusCode() != 407) {
       
   479                             throw new RuntimeException("expected 407 not received");
       
   480                         }
       
   481                         System.out.println("Scheme disabled for [" + authType
       
   482                                 + ", " + authScheme
       
   483                                 + ", " + (useSSL ? "HTTP" : "HTTPS")
       
   484                                 + "]: Received expected " + resp.statusCode());
       
   485                         continue;
       
   486                     } else {
       
   487                         System.out.println("Scheme enabled for [" + authType
       
   488                                 + ", " + authScheme
       
   489                                 + ", " + (useSSL ? "HTTPS" : "HTTP")
       
   490                                 + "]: Expecting 200, response is: " + resp);
       
   491                         assert resp.statusCode() == 200 : "200 expected, received " + resp;
       
   492                         respLines = resp.body().collect(Collectors.toList());
       
   493                     }
       
   494                 } finally {
       
   495                     long stop = System.nanoTime();
       
   496                     synchronized (basicCount) {
       
   497                         long n = basicCount.getAndIncrement();
       
   498                         basics.set((basics.get() * n + (stop - start)) / (n + 1));
       
   499                     }
       
   500                 }
       
   501                 if (!lines.equals(respLines)) {
       
   502                     throw new RuntimeException("Unexpected response: " + respLines);
       
   503                 }
       
   504             }
       
   505         } finally {
       
   506         }
       
   507         System.out.println("OK");
       
   508     }
       
   509 
       
   510     String getBasicAuth(String username) {
       
   511         StringBuilder builder = new StringBuilder(username);
       
   512         builder.append(':');
       
   513         for (char c : DigestEchoServer.AUTHENTICATOR.getPassword(username)) {
       
   514             builder.append(c);
       
   515         }
       
   516         return Base64.getEncoder().encodeToString(builder.toString().getBytes(StandardCharsets.UTF_8));
       
   517     }
       
   518 
       
   519     final static AtomicLong digests = new AtomicLong();
       
   520     final static AtomicLong digestCount = new AtomicLong();
       
   521     // @Test
       
   522     void testDigest(Version clientVersion, Version serverVersion,
       
   523                     boolean async, boolean expectContinue)
       
   524             throws Exception
       
   525     {
       
   526         String test = format("testDigest: client: %s, server: %s, async: %s, useSSL: %s, " +
       
   527                              "authScheme: %s, authType: %s, expectContinue: %s",
       
   528                               clientVersion, serverVersion, async, useSSL,
       
   529                               authScheme, authType, expectContinue);
       
   530         out.println("*** " + test + " ***");
       
   531         DigestEchoServer server = EchoServers.of(serverVersion,
       
   532                 useSSL ? "https" : "http", authType, authScheme);
       
   533 
       
   534         URI uri = DigestEchoServer.uri(useSSL ? "https" : "http",
       
   535                 server.getServerAddress(), "/foo/");
       
   536 
       
   537         HttpClient client = newHttpClient(server);
       
   538         HttpResponse<String> r;
       
   539         CompletableFuture<HttpResponse<String>> cf1;
       
   540         byte[] cnonce = new byte[16];
       
   541         String cnonceStr = null;
       
   542         DigestEchoServer.DigestResponse challenge = null;
       
   543 
       
   544         try {
       
   545             for (int i=0; i<data.length; i++) {
       
   546                 out.println(DigestEchoServer.now() + "----- iteration " + i + " -----");
       
   547                 List<String> lines = List.of(Arrays.copyOfRange(data, 0, i+1));
       
   548                 assert lines.size() == i + 1;
       
   549                 String body = lines.stream().collect(Collectors.joining("\r\n"));
       
   550                 HttpRequest.BodyPublisher reqBody = HttpRequest.BodyPublishers.ofString(body);
       
   551                 HttpRequest.Builder reqBuilder = HttpRequest
       
   552                         .newBuilder(uri).version(clientVersion).POST(reqBody)
       
   553                         .expectContinue(expectContinue);
       
   554 
       
   555                 boolean isTunnel = isProxy(authType) && useSSL;
       
   556                 String digestMethod = isTunnel ? "CONNECT" : "POST";
       
   557 
       
   558                 // In case of a tunnel connection only the first request
       
   559                 // which establishes the tunnel needs to authenticate with
       
   560                 // the proxy.
       
   561                 if (challenge != null && (!isTunnel || isSchemeDisabled())) {
       
   562                     assert cnonceStr != null;
       
   563                     String auth = digestResponse(uri, digestMethod, challenge, cnonceStr);
       
   564                     try {
       
   565                         reqBuilder = reqBuilder.header(authorizationKey(authType), auth);
       
   566                     } catch (IllegalArgumentException x) {
       
   567                         throw x;
       
   568                     }
       
   569                 }
       
   570 
       
   571                 long start = System.nanoTime();
       
   572                 HttpRequest request = reqBuilder.build();
       
   573                 HttpResponse<Stream<String>> resp;
       
   574                 if (async) {
       
   575                     resp = client.sendAsync(request, BodyHandlers.ofLines()).join();
       
   576                 } else {
       
   577                     resp = client.send(request, BodyHandlers.ofLines());
       
   578                 }
       
   579                 System.out.println(resp);
       
   580                 assert challenge != null || resp.statusCode() == 401 || resp.statusCode() == 407
       
   581                         : "challenge=" + challenge + ", resp=" + resp + ", test=[" + test + "]";
       
   582                 if (resp.statusCode() == 401 || resp.statusCode() == 407) {
       
   583                     // This assert may need to be relaxed if our server happened to
       
   584                     // decide to close the tunnel connection, in which case we would
       
   585                     // receive 407 again...
       
   586                     assert challenge == null || !isTunnel || isSchemeDisabled()
       
   587                             : "No proxy auth should be required after establishing an SSL tunnel";
       
   588 
       
   589                     System.out.println("Received " + resp.statusCode() + " answering challenge...");
       
   590                     random.nextBytes(cnonce);
       
   591                     cnonceStr = new BigInteger(1, cnonce).toString(16);
       
   592                     System.out.println("Response headers: " + resp.headers());
       
   593                     Optional<String> authenticateOpt = resp.headers().firstValue(authenticateKey(authType));
       
   594                     String authenticate = authenticateOpt.orElseThrow(
       
   595                             () -> new RuntimeException(authenticateKey(authType) + ": not found"));
       
   596                     assert authenticate.startsWith("Digest ");
       
   597                     HeaderParser hp = new HeaderParser(authenticate.substring("Digest ".length()));
       
   598                     String qop = hp.findValue("qop");
       
   599                     String nonce = hp.findValue("nonce");
       
   600                     if (qop == null && nonce == null) {
       
   601                         throw new RuntimeException("QOP and NONCE not found");
       
   602                     }
       
   603                     challenge = DigestEchoServer.DigestResponse
       
   604                             .create(authenticate.substring("Digest ".length()));
       
   605                     String auth = digestResponse(uri, digestMethod, challenge, cnonceStr);
       
   606                     try {
       
   607                         request = HttpRequest.newBuilder(uri).version(clientVersion)
       
   608                             .POST(reqBody).header(authorizationKey(authType), auth).build();
       
   609                     } catch (IllegalArgumentException x) {
       
   610                         throw x;
       
   611                     }
       
   612 
       
   613                     if (async) {
       
   614                         resp = client.sendAsync(request, BodyHandlers.ofLines()).join();
       
   615                     } else {
       
   616                         resp = client.send(request, BodyHandlers.ofLines());
       
   617                     }
       
   618                     System.out.println(resp);
       
   619                 }
       
   620                 final List<String> respLines;
       
   621                 try {
       
   622                     if (isSchemeDisabled()) {
       
   623                         if (resp.statusCode() != 407) {
       
   624                             throw new RuntimeException("expected 407 not received");
       
   625                         }
       
   626                         System.out.println("Scheme disabled for [" + authType
       
   627                                 + ", " + authScheme +
       
   628                                 ", " + (useSSL ? "HTTP" : "HTTPS")
       
   629                                 + "]: Received expected " + resp.statusCode());
       
   630                         continue;
       
   631                     } else {
       
   632                         assert resp.statusCode() == 200;
       
   633                         respLines = resp.body().collect(Collectors.toList());
       
   634                     }
       
   635                 } finally {
       
   636                     long stop = System.nanoTime();
       
   637                     synchronized (digestCount) {
       
   638                         long n = digestCount.getAndIncrement();
       
   639                         digests.set((digests.get() * n + (stop - start)) / (n + 1));
       
   640                     }
       
   641                 }
       
   642                 if (!lines.equals(respLines)) {
       
   643                     throw new RuntimeException("Unexpected response: " + respLines);
       
   644                 }
       
   645             }
       
   646         } finally {
       
   647         }
       
   648         System.out.println("OK");
       
   649     }
       
   650 
       
   651     // WARNING: This is not a full fledged implementation of DIGEST.
       
   652     // It does contain bugs and inaccuracy.
       
   653     static String digestResponse(URI uri, String method, DigestEchoServer.DigestResponse challenge, String cnonce)
       
   654             throws NoSuchAlgorithmException {
       
   655         int nc = NC.incrementAndGet();
       
   656         DigestEchoServer.DigestResponse response1 = new DigestEchoServer.DigestResponse("earth",
       
   657                 "arthur", challenge.nonce, cnonce, String.valueOf(nc), uri.toASCIIString(),
       
   658                 challenge.algorithm, challenge.qop, challenge.opaque, null);
       
   659         String response = DigestEchoServer.DigestResponse.computeDigest(true, method,
       
   660                 DigestEchoServer.AUTHENTICATOR.getPassword("arthur"), response1);
       
   661         String auth = "Digest username=\"arthur\", realm=\"earth\""
       
   662                 + ", response=\"" + response + "\", uri=\""+uri.toASCIIString()+"\""
       
   663                 + ", qop=\"" + response1.qop + "\", cnonce=\"" + response1.cnonce
       
   664                 + "\", nc=\"" + nc + "\", nonce=\"" + response1.nonce + "\"";
       
   665         if (response1.opaque != null) {
       
   666             auth = auth + ", opaque=\"" + response1.opaque + "\"";
       
   667         }
       
   668         return auth;
       
   669     }
       
   670 
       
   671     static String authenticateKey(DigestEchoServer.HttpAuthType authType) {
       
   672         switch (authType) {
       
   673             case SERVER: return "www-authenticate";
       
   674             case SERVER307: return "www-authenticate";
       
   675             case PROXY: return "proxy-authenticate";
       
   676             case PROXY305: return "proxy-authenticate";
       
   677             default: throw new InternalError("authType: " + authType);
       
   678         }
       
   679     }
       
   680 
       
   681     static String authorizationKey(DigestEchoServer.HttpAuthType authType) {
       
   682         switch (authType) {
       
   683             case SERVER: return "authorization";
       
   684             case SERVER307: return "Authorization";
       
   685             case PROXY: return "Proxy-Authorization";
       
   686             case PROXY305: return "proxy-Authorization";
       
   687             default: throw new InternalError("authType: " + authType);
       
   688         }
       
   689     }
       
   690 
       
   691     static boolean isProxy(DigestEchoServer.HttpAuthType authType) {
       
   692         switch (authType) {
       
   693             case SERVER: return false;
       
   694             case SERVER307: return false;
       
   695             case PROXY: return true;
       
   696             case PROXY305: return true;
       
   697             default: throw new InternalError("authType: " + authType);
       
   698         }
       
   699     }
       
   700 }