1 /* 2 * Copyright (c) 2016, 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. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 import com.sun.net.httpserver.BasicAuthenticator; 27 import com.sun.net.httpserver.Filter; 28 import com.sun.net.httpserver.Headers; 29 import com.sun.net.httpserver.HttpContext; 30 import com.sun.net.httpserver.HttpExchange; 31 import com.sun.net.httpserver.HttpHandler; 32 import com.sun.net.httpserver.HttpServer; 33 import com.sun.net.httpserver.HttpsConfigurator; 34 import com.sun.net.httpserver.HttpsParameters; 35 import com.sun.net.httpserver.HttpsServer; 36 import java.io.IOException; 37 import java.io.InputStream; 38 import java.io.OutputStream; 39 import java.io.OutputStreamWriter; 40 import java.io.PrintWriter; 41 import java.io.Writer; 42 import java.math.BigInteger; 43 import java.net.HttpURLConnection; 44 import java.net.InetAddress; 45 import java.net.InetSocketAddress; 46 import java.net.MalformedURLException; 47 import java.net.ServerSocket; 48 import java.net.Socket; 49 import java.net.URL; 50 import java.security.MessageDigest; 51 import java.security.NoSuchAlgorithmException; 52 import java.time.Instant; 53 import java.util.Arrays; 54 import java.util.Base64; 55 import java.util.List; 56 import java.util.Objects; 57 import java.util.Random; 58 import java.util.stream.Collectors; 59 import javax.net.ssl.SSLContext; 60 import sun.net.www.HeaderParser; 61 62 /** 63 * A simple HTTP server that supports Digest authentication. 64 * By default this server will echo back whatever is present 65 * in the request body. 66 * @author danielfuchs 67 */ 68 public class HTTPTestServer extends HTTPTest { 69 70 final HttpServer serverImpl; // this server endpoint 71 final HTTPTestServer redirect; // the target server where to redirect 3xx 72 final HttpHandler delegate; // unused 73 74 private HTTPTestServer(HttpServer server, HTTPTestServer target, 75 HttpHandler delegate) { 76 this.serverImpl = server; 77 this.redirect = target; 78 this.delegate = delegate; 79 } 80 81 public static void main(String[] args) 82 throws IOException { 83 84 HTTPTestServer server = create(HTTPTest.DEFAULT_PROTOCOL_TYPE, 85 HTTPTest.DEFAULT_HTTP_AUTH_TYPE, 86 HTTPTest.AUTHENTICATOR, 87 HTTPTest.DEFAULT_SCHEME_TYPE); 88 try { 89 System.out.println("Server created at " + server.getAddress()); 90 System.out.println("Strike <Return> to exit"); 91 System.in.read(); 92 } finally { 93 System.out.println("stopping server"); 94 server.stop(); 95 } 96 } 97 98 private static String toString(Headers headers) { 99 return headers.entrySet().stream() 100 .map((e) -> e.getKey() + ": " + e.getValue()) 101 .collect(Collectors.joining("\n")); 102 } 103 104 public static HTTPTestServer create(HttpProtocolType protocol, 105 HttpAuthType authType, 106 HttpTestAuthenticator auth, 107 HttpSchemeType schemeType) 108 throws IOException { 109 return create(protocol, authType, auth, schemeType, null); 110 } 111 112 public static HTTPTestServer create(HttpProtocolType protocol, 113 HttpAuthType authType, 114 HttpTestAuthenticator auth, 115 HttpSchemeType schemeType, 116 HttpHandler delegate) 117 throws IOException { 118 Objects.requireNonNull(authType); 119 Objects.requireNonNull(auth); 120 switch(authType) { 121 // A server that performs Server Digest authentication. 122 case SERVER: return createServer(protocol, authType, auth, 123 schemeType, delegate, "/"); 124 // A server that pretends to be a Proxy and performs 125 // Proxy Digest authentication. If protocol is HTTPS, 126 // then this will create a HttpsProxyTunnel that will 127 // handle the CONNECT request for tunneling. 128 case PROXY: return createProxy(protocol, authType, auth, 129 schemeType, delegate, "/"); 130 // A server that sends 307 redirect to a server that performs 131 // Digest authentication. 132 // Note: 301 doesn't work here because it transforms POST into GET. 133 case SERVER307: return createServerAndRedirect(protocol, 134 HttpAuthType.SERVER, 135 auth, schemeType, 136 delegate, 307); 137 // A server that sends 305 redirect to a proxy that performs 138 // Digest authentication. 139 case PROXY305: return createServerAndRedirect(protocol, 140 HttpAuthType.PROXY, 141 auth, schemeType, 142 delegate, 305); 143 default: 144 throw new InternalError("Unknown server type: " + authType); 145 } 146 } 147 148 static HttpServer createHttpServer(HttpProtocolType protocol) throws IOException { 149 switch (protocol) { 150 case HTTP: return HttpServer.create(); 151 case HTTPS: return configure(HttpsServer.create()); 152 default: throw new InternalError("Unsupported protocol " + protocol); 153 } 154 } 155 156 static HttpsServer configure(HttpsServer server) throws IOException { 157 try { 158 SSLContext ctx = SSLContext.getDefault(); 159 server.setHttpsConfigurator(new Configurator(ctx)); 160 } catch (NoSuchAlgorithmException ex) { 161 throw new IOException(ex); 162 } 163 return server; 164 } 165 166 167 static void setContextAuthenticator(HttpContext ctxt, 168 HttpTestAuthenticator auth) { 169 final String realm = auth.getRealm(); 170 com.sun.net.httpserver.Authenticator authenticator = 171 new BasicAuthenticator(realm) { 172 @Override 173 public boolean checkCredentials(String username, String pwd) { 174 return auth.getUserName().equals(username) 175 && new String(auth.getPassword(username)).equals(pwd); 176 } 177 }; 178 ctxt.setAuthenticator(authenticator); 179 } 180 181 public static HTTPTestServer createServer(HttpProtocolType protocol, 182 HttpAuthType authType, 183 HttpTestAuthenticator auth, 184 HttpSchemeType schemeType, 185 HttpHandler delegate, 186 String path) 187 throws IOException { 188 Objects.requireNonNull(authType); 189 Objects.requireNonNull(auth); 190 191 HttpServer impl = createHttpServer(protocol); 192 final HTTPTestServer server = new HTTPTestServer(impl, null, delegate); 193 final HttpHandler hh = server.createHandler(schemeType, auth, authType); 194 HttpContext ctxt = impl.createContext(path, hh); 195 server.configureAuthentication(ctxt, schemeType, auth, authType); 196 impl.bind(new InetSocketAddress("127.0.0.1", 0), 0); 197 impl.start(); 198 return server; 199 } 200 201 public static HTTPTestServer createProxy(HttpProtocolType protocol, 202 HttpAuthType authType, 203 HttpTestAuthenticator auth, 204 HttpSchemeType schemeType, 205 HttpHandler delegate, 206 String path) 207 throws IOException { 208 Objects.requireNonNull(authType); 209 Objects.requireNonNull(auth); 210 211 HttpServer impl = createHttpServer(protocol); 212 final HTTPTestServer server = protocol == HttpProtocolType.HTTPS 213 ? new HttpsProxyTunnel(impl, null, delegate) 214 : new HTTPTestServer(impl, null, delegate); 215 final HttpHandler hh = server.createHandler(schemeType, auth, authType); 216 HttpContext ctxt = impl.createContext(path, hh); 217 server.configureAuthentication(ctxt, schemeType, auth, authType); 218 219 impl.bind(new InetSocketAddress("127.0.0.1", 0), 0); 220 impl.start(); 221 222 return server; 223 } 224 225 public static HTTPTestServer createServerAndRedirect( 226 HttpProtocolType protocol, 227 HttpAuthType targetAuthType, 228 HttpTestAuthenticator auth, 229 HttpSchemeType schemeType, 230 HttpHandler targetDelegate, 231 int code300) 232 throws IOException { 233 Objects.requireNonNull(targetAuthType); 234 Objects.requireNonNull(auth); 235 236 // The connection between client and proxy can only 237 // be a plain connection: SSL connection to proxy 238 // is not supported by our client connection. 239 HttpProtocolType targetProtocol = targetAuthType == HttpAuthType.PROXY 240 ? HttpProtocolType.HTTP 241 : protocol; 242 HTTPTestServer redirectTarget = 243 (targetAuthType == HttpAuthType.PROXY) 244 ? createProxy(protocol, targetAuthType, 245 auth, schemeType, targetDelegate, "/") 246 : createServer(targetProtocol, targetAuthType, 247 auth, schemeType, targetDelegate, "/"); 248 HttpServer impl = createHttpServer(protocol); 249 final HTTPTestServer redirectingServer = 250 new HTTPTestServer(impl, redirectTarget, null); 251 InetSocketAddress redirectAddr = redirectTarget.getAddress(); 252 URL locationURL = url(targetProtocol, redirectAddr, "/"); 253 final HttpHandler hh = redirectingServer.create300Handler(locationURL, 254 HttpAuthType.SERVER, code300); 255 impl.createContext("/", hh); 256 impl.bind(new InetSocketAddress("127.0.0.1", 0), 0); 257 impl.start(); 258 return redirectingServer; 259 } 260 261 public InetSocketAddress getAddress() { 262 return serverImpl.getAddress(); 263 } 264 265 public void stop() { 266 serverImpl.stop(0); 267 if (redirect != null) { 268 redirect.stop(); 269 } 270 } 271 272 protected void writeResponse(HttpExchange he) throws IOException { 273 if (delegate == null) { 274 he.sendResponseHeaders(HttpURLConnection.HTTP_OK, 0); 275 he.getResponseBody().write(he.getRequestBody().readAllBytes()); 276 } else { 277 delegate.handle(he); 278 } 279 } 280 281 private HttpHandler createHandler(HttpSchemeType schemeType, 282 HttpTestAuthenticator auth, 283 HttpAuthType authType) { 284 return new HttpNoAuthHandler(authType); 285 } 286 287 private void configureAuthentication(HttpContext ctxt, 288 HttpSchemeType schemeType, 289 HttpTestAuthenticator auth, 290 HttpAuthType authType) { 291 switch(schemeType) { 292 case DIGEST: 293 // DIGEST authentication is handled by the handler. 294 ctxt.getFilters().add(new HttpDigestFilter(auth, authType)); 295 break; 296 case BASIC: 297 // BASIC authentication is handled by the filter. 298 ctxt.getFilters().add(new HttpBasicFilter(auth, authType)); 299 break; 300 case BASICSERVER: 301 switch(authType) { 302 case PROXY: case PROXY305: 303 // HttpServer can't support Proxy-type authentication 304 // => we do as if BASIC had been specified, and we will 305 // handle authentication in the handler. 306 ctxt.getFilters().add(new HttpBasicFilter(auth, authType)); 307 break; 308 case SERVER: case SERVER307: 309 // Basic authentication is handled by HttpServer 310 // directly => the filter should not perform 311 // authentication again. 312 setContextAuthenticator(ctxt, auth); 313 ctxt.getFilters().add(new HttpNoAuthFilter(authType)); 314 break; 315 default: 316 throw new InternalError("Invalid combination scheme=" 317 + schemeType + " authType=" + authType); 318 } 319 case NONE: 320 // No authentication at all. 321 ctxt.getFilters().add(new HttpNoAuthFilter(authType)); 322 break; 323 default: 324 throw new InternalError("No such scheme: " + schemeType); 325 } 326 } 327 328 private HttpHandler create300Handler(URL proxyURL, 329 HttpAuthType type, int code300) throws MalformedURLException { 330 return new Http3xxHandler(proxyURL, type, code300); 331 } 332 333 // Abstract HTTP filter class. 334 private abstract static class AbstractHttpFilter extends Filter { 335 336 final HttpAuthType authType; 337 final String type; 338 public AbstractHttpFilter(HttpAuthType authType, String type) { 339 this.authType = authType; 340 this.type = type; 341 } 342 343 String getLocation() { 344 return "Location"; 345 } 346 String getAuthenticate() { 347 return authType == HttpAuthType.PROXY 348 ? "Proxy-Authenticate" : "WWW-Authenticate"; 349 } 350 String getAuthorization() { 351 return authType == HttpAuthType.PROXY 352 ? "Proxy-Authorization" : "Authorization"; 353 } 354 int getUnauthorizedCode() { 355 return authType == HttpAuthType.PROXY 356 ? HttpURLConnection.HTTP_PROXY_AUTH 357 : HttpURLConnection.HTTP_UNAUTHORIZED; 358 } 359 String getKeepAlive() { 360 return "keep-alive"; 361 } 362 String getConnection() { 363 return authType == HttpAuthType.PROXY 364 ? "Proxy-Connection" : "Connection"; 365 } 366 protected abstract boolean isAuthentified(HttpExchange he) throws IOException; 367 protected abstract void requestAuthentication(HttpExchange he) throws IOException; 368 protected void accept(HttpExchange he, Chain chain) throws IOException { 369 chain.doFilter(he); 370 } 371 372 @Override 373 public String description() { 374 return "Filter for " + type; 375 } 376 @Override 377 public void doFilter(HttpExchange he, Chain chain) throws IOException { 378 try { 379 System.out.println(type + ": Got " + he.getRequestMethod() 380 + ": " + he.getRequestURI() 381 + "\n" + HTTPTestServer.toString(he.getRequestHeaders())); 382 if (!isAuthentified(he)) { 383 try { 384 requestAuthentication(he); 385 he.sendResponseHeaders(getUnauthorizedCode(), 0); 386 System.out.println(type 387 + ": Sent back " + getUnauthorizedCode()); 388 } finally { 389 he.close(); 390 } 391 } else { 392 accept(he, chain); 393 } 394 } catch (RuntimeException | Error | IOException t) { 395 System.err.println(type 396 + ": Unexpected exception while handling request: " + t); 397 t.printStackTrace(System.err); 398 he.close(); 399 throw t; 400 } 401 } 402 403 } 404 405 private final static class DigestResponse { 406 final String realm; 407 final String username; 408 final String nonce; 409 final String cnonce; 410 final String nc; 411 final String uri; 412 final String algorithm; 413 final String response; 414 final String qop; 415 final String opaque; 416 417 public DigestResponse(String realm, String username, String nonce, 418 String cnonce, String nc, String uri, 419 String algorithm, String qop, String opaque, 420 String response) { 421 this.realm = realm; 422 this.username = username; 423 this.nonce = nonce; 424 this.cnonce = cnonce; 425 this.nc = nc; 426 this.uri = uri; 427 this.algorithm = algorithm; 428 this.qop = qop; 429 this.opaque = opaque; 430 this.response = response; 431 } 432 433 String getAlgorithm(String defval) { 434 return algorithm == null ? defval : algorithm; 435 } 436 String getQoP(String defval) { 437 return qop == null ? defval : qop; 438 } 439 440 // Code stolen from DigestAuthentication: 441 442 private static final char charArray[] = { 443 '0', '1', '2', '3', '4', '5', '6', '7', 444 '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' 445 }; 446 447 private static String encode(String src, char[] passwd, MessageDigest md) { 448 try { 449 md.update(src.getBytes("ISO-8859-1")); 450 } catch (java.io.UnsupportedEncodingException uee) { 451 assert false; 452 } 453 if (passwd != null) { 454 byte[] passwdBytes = new byte[passwd.length]; 455 for (int i=0; i<passwd.length; i++) 456 passwdBytes[i] = (byte)passwd[i]; 457 md.update(passwdBytes); 458 Arrays.fill(passwdBytes, (byte)0x00); 459 } 460 byte[] digest = md.digest(); 461 462 StringBuilder res = new StringBuilder(digest.length * 2); 463 for (int i = 0; i < digest.length; i++) { 464 int hashchar = ((digest[i] >>> 4) & 0xf); 465 res.append(charArray[hashchar]); 466 hashchar = (digest[i] & 0xf); 467 res.append(charArray[hashchar]); 468 } 469 return res.toString(); 470 } 471 472 public static String computeDigest(boolean isRequest, 473 String reqMethod, 474 char[] password, 475 DigestResponse params) 476 throws NoSuchAlgorithmException 477 { 478 479 String A1, HashA1; 480 String algorithm = params.getAlgorithm("MD5"); 481 boolean md5sess = algorithm.equalsIgnoreCase ("MD5-sess"); 482 483 MessageDigest md = MessageDigest.getInstance(md5sess?"MD5":algorithm); 484 485 if (params.username == null) { 486 throw new IllegalArgumentException("missing username"); 487 } 488 if (params.realm == null) { 489 throw new IllegalArgumentException("missing realm"); 490 } 491 if (params.uri == null) { 492 throw new IllegalArgumentException("missing uri"); 493 } 494 if (params.nonce == null) { 495 throw new IllegalArgumentException("missing nonce"); 496 } 497 498 A1 = params.username + ":" + params.realm + ":"; 499 HashA1 = encode(A1, password, md); 500 501 String A2; 502 if (isRequest) { 503 A2 = reqMethod + ":" + params.uri; 504 } else { 505 A2 = ":" + params.uri; 506 } 507 String HashA2 = encode(A2, null, md); 508 String combo, finalHash; 509 510 if ("auth".equals(params.qop)) { /* RRC2617 when qop=auth */ 511 if (params.cnonce == null) { 512 throw new IllegalArgumentException("missing nonce"); 513 } 514 if (params.nc == null) { 515 throw new IllegalArgumentException("missing nonce"); 516 } 517 combo = HashA1+ ":" + params.nonce + ":" + params.nc + ":" + 518 params.cnonce + ":auth:" +HashA2; 519 520 } else { /* for compatibility with RFC2069 */ 521 combo = HashA1 + ":" + 522 params.nonce + ":" + 523 HashA2; 524 } 525 finalHash = encode(combo, null, md); 526 return finalHash; 527 } 528 529 public static DigestResponse create(String raw) { 530 String username, realm, nonce, nc, uri, response, cnonce, 531 algorithm, qop, opaque; 532 HeaderParser parser = new HeaderParser(raw); 533 username = parser.findValue("username"); 534 realm = parser.findValue("realm"); 535 nonce = parser.findValue("nonce"); 536 nc = parser.findValue("nc"); 537 uri = parser.findValue("uri"); 538 cnonce = parser.findValue("cnonce"); 539 response = parser.findValue("response"); 540 algorithm = parser.findValue("algorithm"); 541 qop = parser.findValue("qop"); 542 opaque = parser.findValue("opaque"); 543 return new DigestResponse(realm, username, nonce, cnonce, nc, uri, 544 algorithm, qop, opaque, response); 545 } 546 547 } 548 549 private class HttpNoAuthFilter extends AbstractHttpFilter { 550 551 public HttpNoAuthFilter(HttpAuthType authType) { 552 super(authType, authType == HttpAuthType.SERVER 553 ? "NoAuth Server" : "NoAuth Proxy"); 554 } 555 556 @Override 557 protected boolean isAuthentified(HttpExchange he) throws IOException { 558 return true; 559 } 560 561 @Override 562 protected void requestAuthentication(HttpExchange he) throws IOException { 563 throw new InternalError("Should not com here"); 564 } 565 566 @Override 567 public String description() { 568 return "Passthrough Filter"; 569 } 570 571 } 572 573 // An HTTP Filter that performs Basic authentication 574 private class HttpBasicFilter extends AbstractHttpFilter { 575 576 private final HttpTestAuthenticator auth; 577 public HttpBasicFilter(HttpTestAuthenticator auth, HttpAuthType authType) { 578 super(authType, authType == HttpAuthType.SERVER 579 ? "Basic Server" : "Basic Proxy"); 580 this.auth = auth; 581 } 582 583 @Override 584 protected void requestAuthentication(HttpExchange he) 585 throws IOException { 586 he.getResponseHeaders().add(getAuthenticate(), 587 "Basic realm=\"" + auth.getRealm() + "\""); 588 System.out.println(type + ": Requesting Basic Authentication " 589 + he.getResponseHeaders().getFirst(getAuthenticate())); 590 } 591 592 @Override 593 protected boolean isAuthentified(HttpExchange he) { 594 if (he.getRequestHeaders().containsKey(getAuthorization())) { 595 List<String> authorization = 596 he.getRequestHeaders().get(getAuthorization()); 597 for (String a : authorization) { 598 System.out.println(type + ": processing " + a); 599 int sp = a.indexOf(' '); 600 if (sp < 0) return false; 601 String scheme = a.substring(0, sp); 602 if (!"Basic".equalsIgnoreCase(scheme)) { 603 System.out.println(type + ": Unsupported scheme '" 604 + scheme +"'"); 605 return false; 606 } 607 if (a.length() <= sp+1) { 608 System.out.println(type + ": value too short for '" 609 + scheme +"'"); 610 return false; 611 } 612 a = a.substring(sp+1); 613 return validate(a); 614 } 615 return false; 616 } 617 return false; 618 } 619 620 boolean validate(String a) { 621 byte[] b = Base64.getDecoder().decode(a); 622 String userpass = new String (b); 623 int colon = userpass.indexOf (':'); 624 String uname = userpass.substring (0, colon); 625 String pass = userpass.substring (colon+1); 626 return auth.getUserName().equals(uname) && 627 new String(auth.getPassword(uname)).equals(pass); 628 } 629 630 @Override 631 public String description() { 632 return "Filter for " + type; 633 } 634 635 } 636 637 638 // An HTTP Filter that performs Digest authentication 639 private class HttpDigestFilter extends AbstractHttpFilter { 640 641 // This is a very basic DIGEST - used only for the purpose of testing 642 // the client implementation. Therefore we can get away with never 643 // updating the server nonce as it makes the implementation of the 644 // server side digest simpler. 645 private final HttpTestAuthenticator auth; 646 private final byte[] nonce; 647 private final String ns; 648 public HttpDigestFilter(HttpTestAuthenticator auth, HttpAuthType authType) { 649 super(authType, authType == HttpAuthType.SERVER 650 ? "Digest Server" : "Digest Proxy"); 651 this.auth = auth; 652 nonce = new byte[16]; 653 new Random(Instant.now().toEpochMilli()).nextBytes(nonce); 654 ns = new BigInteger(1, nonce).toString(16); 655 } 656 657 @Override 658 protected void requestAuthentication(HttpExchange he) 659 throws IOException { 660 he.getResponseHeaders().add(getAuthenticate(), 661 "Digest realm=\"" + auth.getRealm() + "\"," 662 + "\r\n qop=\"auth\"," 663 + "\r\n nonce=\"" + ns +"\""); 664 System.out.println(type + ": Requesting Digest Authentication " 665 + he.getResponseHeaders().getFirst(getAuthenticate())); 666 } 667 668 @Override 669 protected boolean isAuthentified(HttpExchange he) { 670 if (he.getRequestHeaders().containsKey(getAuthorization())) { 671 List<String> authorization = he.getRequestHeaders().get(getAuthorization()); 672 for (String a : authorization) { 673 System.out.println(type + ": processing " + a); 674 int sp = a.indexOf(' '); 675 if (sp < 0) return false; 676 String scheme = a.substring(0, sp); 677 if (!"Digest".equalsIgnoreCase(scheme)) { 678 System.out.println(type + ": Unsupported scheme '" + scheme +"'"); 679 return false; 680 } 681 if (a.length() <= sp+1) { 682 System.out.println(type + ": value too short for '" + scheme +"'"); 683 return false; 684 } 685 a = a.substring(sp+1); 686 DigestResponse dgr = DigestResponse.create(a); 687 return validate(he.getRequestMethod(), dgr); 688 } 689 return false; 690 } 691 return false; 692 } 693 694 boolean validate(String reqMethod, DigestResponse dg) { 695 if (!"MD5".equalsIgnoreCase(dg.getAlgorithm("MD5"))) { 696 System.out.println(type + ": Unsupported algorithm " 697 + dg.algorithm); 698 return false; 699 } 700 if (!"auth".equalsIgnoreCase(dg.getQoP("auth"))) { 701 System.out.println(type + ": Unsupported qop " 702 + dg.qop); 703 return false; 704 } 705 try { 706 if (!dg.nonce.equals(ns)) { 707 System.out.println(type + ": bad nonce returned by client: " 708 + nonce + " expected " + ns); 709 return false; 710 } 711 if (dg.response == null) { 712 System.out.println(type + ": missing digest response."); 713 return false; 714 } 715 char[] pa = auth.getPassword(dg.username); 716 return verify(reqMethod, dg, pa); 717 } catch(IllegalArgumentException | SecurityException 718 | NoSuchAlgorithmException e) { 719 System.out.println(type + ": " + e.getMessage()); 720 return false; 721 } 722 } 723 724 boolean verify(String reqMethod, DigestResponse dg, char[] pw) 725 throws NoSuchAlgorithmException { 726 String response = DigestResponse.computeDigest(true, reqMethod, pw, dg); 727 if (!dg.response.equals(response)) { 728 System.out.println(type + ": bad response returned by client: " 729 + dg.response + " expected " + response); 730 return false; 731 } else { 732 System.out.println(type + ": verified response " + response); 733 } 734 return true; 735 } 736 737 @Override 738 public String description() { 739 return "Filter for DIGEST authentication"; 740 } 741 } 742 743 // Abstract HTTP handler class. 744 private abstract static class AbstractHttpHandler implements HttpHandler { 745 746 final HttpAuthType authType; 747 final String type; 748 public AbstractHttpHandler(HttpAuthType authType, String type) { 749 this.authType = authType; 750 this.type = type; 751 } 752 753 String getLocation() { 754 return "Location"; 755 } 756 757 @Override 758 public void handle(HttpExchange he) throws IOException { 759 try { 760 sendResponse(he); 761 } catch (RuntimeException | Error | IOException t) { 762 System.err.println(type 763 + ": Unexpected exception while handling request: " + t); 764 t.printStackTrace(System.err); 765 throw t; 766 } finally { 767 he.close(); 768 } 769 } 770 771 protected abstract void sendResponse(HttpExchange he) throws IOException; 772 773 } 774 775 private class HttpNoAuthHandler extends AbstractHttpHandler { 776 777 public HttpNoAuthHandler(HttpAuthType authType) { 778 super(authType, authType == HttpAuthType.SERVER 779 ? "NoAuth Server" : "NoAuth Proxy"); 780 } 781 782 @Override 783 protected void sendResponse(HttpExchange he) throws IOException { 784 HTTPTestServer.this.writeResponse(he); 785 } 786 787 } 788 789 // A dummy HTTP Handler that redirects all incoming requests 790 // by sending a back 3xx response code (301, 305, 307 etc..) 791 private class Http3xxHandler extends AbstractHttpHandler { 792 793 private final URL redirectTargetURL; 794 private final int code3XX; 795 public Http3xxHandler(URL proxyURL, HttpAuthType authType, int code300) { 796 super(authType, "Server" + code300); 797 this.redirectTargetURL = proxyURL; 798 this.code3XX = code300; 799 } 800 801 int get3XX() { 802 return code3XX; 803 } 804 805 @Override 806 public void sendResponse(HttpExchange he) throws IOException { 807 System.out.println(type + ": Got " + he.getRequestMethod() 808 + ": " + he.getRequestURI() 809 + "\n" + HTTPTestServer.toString(he.getRequestHeaders())); 810 System.out.println(type + ": Redirecting to " 811 + (authType == HttpAuthType.PROXY305 812 ? "proxy" : "server")); 813 he.getResponseHeaders().add(getLocation(), 814 redirectTargetURL.toExternalForm().toString()); 815 he.sendResponseHeaders(get3XX(), 0); 816 System.out.println(type + ": Sent back " + get3XX() + " " 817 + getLocation() + ": " + redirectTargetURL.toExternalForm().toString()); 818 } 819 } 820 821 static class Configurator extends HttpsConfigurator { 822 public Configurator(SSLContext ctx) { 823 super(ctx); 824 } 825 826 @Override 827 public void configure (HttpsParameters params) { 828 params.setSSLParameters (getSSLContext().getSupportedSSLParameters()); 829 } 830 } 831 832 // This is a bit hacky: HttpsProxyTunnel is an HTTPTestServer hidden 833 // behind a fake proxy that only understands CONNECT requests. 834 // The fake proxy is just a server socket that intercept the 835 // CONNECT and then redirect streams to the real server. 836 static class HttpsProxyTunnel extends HTTPTestServer 837 implements Runnable { 838 839 final ServerSocket ss; 840 public HttpsProxyTunnel(HttpServer server, HTTPTestServer target, 841 HttpHandler delegate) 842 throws IOException { 843 super(server, target, delegate); 844 System.out.flush(); 845 System.err.println("WARNING: HttpsProxyTunnel is an experimental test class"); 846 ss = new ServerSocket(0, 0, InetAddress.getByName("127.0.0.1")); 847 start(); 848 } 849 850 final void start() throws IOException { 851 Thread t = new Thread(this, "ProxyThread"); 852 t.setDaemon(true); 853 t.start(); 854 } 855 856 @Override 857 public void stop() { 858 super.stop(); 859 try { 860 ss.close(); 861 } catch (IOException ex) { 862 if (DEBUG) ex.printStackTrace(System.out); 863 } 864 } 865 866 // Pipe the input stream to the output stream. 867 private synchronized Thread pipe(InputStream is, OutputStream os, char tag) { 868 return new Thread("TunnelPipe("+tag+")") { 869 @Override 870 public void run() { 871 try { 872 try { 873 int c; 874 while ((c = is.read()) != -1) { 875 os.write(c); 876 os.flush(); 877 // if DEBUG prints a + or a - for each transferred 878 // character. 879 if (DEBUG) System.out.print(tag); 880 } 881 is.close(); 882 } finally { 883 os.close(); 884 } 885 } catch (IOException ex) { 886 if (DEBUG) ex.printStackTrace(System.out); 887 } 888 } 889 }; 890 } 891 892 @Override 893 public InetSocketAddress getAddress() { 894 return new InetSocketAddress(ss.getInetAddress(), ss.getLocalPort()); 895 } 896 897 // This is a bit shaky. It doesn't handle continuation 898 // lines, but our client shouldn't send any. 899 // Read a line from the input stream, swallowing the final 900 // \r\n sequence. Stops at the first \n, doesn't complain 901 // if it wasn't preceded by '\r'. 902 // 903 String readLine(InputStream r) throws IOException { 904 StringBuilder b = new StringBuilder(); 905 int c; 906 while ((c = r.read()) != -1) { 907 if (c == '\n') break; 908 b.appendCodePoint(c); 909 } 910 if (b.codePointAt(b.length() -1) == '\r') { 911 b.delete(b.length() -1, b.length()); 912 } 913 return b.toString(); 914 } 915 916 @Override 917 public void run() { 918 Socket clientConnection = null; 919 try { 920 while (true) { 921 System.out.println("Tunnel: Waiting for client"); 922 Socket previous = clientConnection; 923 try { 924 clientConnection = ss.accept(); 925 } catch (IOException io) { 926 if (DEBUG) io.printStackTrace(System.out); 927 break; 928 } finally { 929 // close the previous connection 930 if (previous != null) previous.close(); 931 } 932 System.out.println("Tunnel: Client accepted"); 933 Socket targetConnection = null; 934 InputStream ccis = clientConnection.getInputStream(); 935 OutputStream ccos = clientConnection.getOutputStream(); 936 Writer w = new OutputStreamWriter( 937 clientConnection.getOutputStream(), "UTF-8"); 938 PrintWriter pw = new PrintWriter(w); 939 System.out.println("Tunnel: Reading request line"); 940 String requestLine = readLine(ccis); 941 System.out.println("Tunnel: Request line: " + requestLine); 942 if (requestLine.startsWith("CONNECT ")) { 943 // We should probably check that the next word following 944 // CONNECT is the host:port of our HTTPS serverImpl. 945 // Some improvement for a followup! 946 947 // Read all headers until we find the empty line that 948 // signals the end of all headers. 949 while(!requestLine.equals("")) { 950 System.out.println("Tunnel: Reading header: " 951 + (requestLine = readLine(ccis))); 952 } 953 954 targetConnection = new Socket( 955 serverImpl.getAddress().getAddress(), 956 serverImpl.getAddress().getPort()); 957 958 // Then send the 200 OK response to the client 959 System.out.println("Tunnel: Sending " 960 + "HTTP/1.1 200 OK\r\n\r\n"); 961 pw.print("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); 962 pw.flush(); 963 } else { 964 // This should not happen. If it does let our serverImpl 965 // deal with it. 966 throw new IOException("Tunnel: Unexpected status line: " 967 + requestLine); 968 } 969 970 // Pipe the input stream of the client connection to the 971 // output stream of the target connection and conversely. 972 // Now the client and target will just talk to each other. 973 System.out.println("Tunnel: Starting tunnel pipes"); 974 Thread t1 = pipe(ccis, targetConnection.getOutputStream(), '+'); 975 Thread t2 = pipe(targetConnection.getInputStream(), ccos, '-'); 976 t1.start(); 977 t2.start(); 978 979 // We have only 1 client... wait until it has finished before 980 // accepting a new connection request. 981 t1.join(); 982 t2.join(); 983 } 984 } catch (Throwable ex) { 985 try { 986 ss.close(); 987 } catch (IOException ex1) { 988 ex.addSuppressed(ex1); 989 } 990 ex.printStackTrace(System.err); 991 } 992 } 993 994 } 995 }