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