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.test.lib.net.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 /test/lib http2/server
66  * @build jdk.test.lib.net.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 
EchoServers(DigestEchoServer server, Version version, String protocolScheme, DigestEchoServer.HttpAuthType authType, DigestEchoServer.HttpAuthSchemeType authScheme)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 
key(Version version, String protocolScheme, DigestEchoServer.HttpAuthType authType, DigestEchoServer.HttpAuthSchemeType authScheme)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 
create(Version version, String protocolScheme, DigestEchoServer.HttpAuthType authType, DigestEchoServer.HttpAuthSchemeType authScheme)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 
of(Version version, String protocolScheme, DigestEchoServer.HttpAuthType authType, DigestEchoServer.HttpAuthSchemeType authScheme)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 
stop()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;
DigestEchoClient(boolean useSSL, DigestEchoServer.HttpAuthSchemeType authScheme, 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;
newHttpClient(DigestEchoServer server)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 
serverVersions(Version clientVersion)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 
clientVersions()236     public static List<Version> clientVersions() {
237         return List.of(Version.values());
238     }
239 
expectContinue(Version serverVersion)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 
main(String[] args)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 
isSchemeDisabled()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
testBasic(Version clientVersion, Version serverVersion, boolean async, boolean expectContinue, boolean preemptive)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 
getBasicAuth(String username)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
testDigest(Version clientVersion, Version serverVersion, boolean async, boolean expectContinue)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.
digestResponse(URI uri, String method, DigestEchoServer.DigestResponse challenge, String cnonce)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 
authenticateKey(DigestEchoServer.HttpAuthType authType)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 
authorizationKey(DigestEchoServer.HttpAuthType authType)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 
isProxy(DigestEchoServer.HttpAuthType authType)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 }
701