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