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