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