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