1 /* 2 * Copyright (c) 2018, 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.HttpServer; 26 import com.sun.net.httpserver.HttpsConfigurator; 27 import com.sun.net.httpserver.HttpsParameters; 28 import com.sun.net.httpserver.HttpsServer; 29 import java.io.IOException; 30 import java.io.InputStream; 31 import java.io.OutputStream; 32 import java.io.OutputStreamWriter; 33 import java.io.PrintWriter; 34 import java.io.Writer; 35 import java.math.BigInteger; 36 import java.net.Authenticator; 37 import java.net.HttpURLConnection; 38 import java.net.InetAddress; 39 import java.net.InetSocketAddress; 40 import java.net.MalformedURLException; 41 import java.net.PasswordAuthentication; 42 import java.net.ServerSocket; 43 import java.net.Socket; 44 import java.net.StandardSocketOptions; 45 import java.net.URI; 46 import java.net.URISyntaxException; 47 import java.net.URL; 48 import java.nio.charset.StandardCharsets; 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.Locale; 57 import java.util.Objects; 58 import java.util.Optional; 59 import java.util.Random; 60 import java.util.StringTokenizer; 61 import java.util.concurrent.CompletableFuture; 62 import java.util.concurrent.CopyOnWriteArrayList; 63 import java.util.concurrent.atomic.AtomicInteger; 64 import java.util.stream.Collectors; 65 import java.util.stream.Stream; 66 import javax.net.ssl.SSLContext; 67 import sun.net.www.HeaderParser; 68 import java.net.http.HttpClient.Version; 69 70 /** 71 * A simple HTTP server that supports Basic or Digest authentication. 72 * By default this server will echo back whatever is present 73 * in the request body. Note that the Digest authentication is 74 * a test implementation implemented only for tests purposes. 75 * @author danielfuchs 76 */ 77 public abstract class DigestEchoServer implements HttpServerAdapters { 78 79 public static final boolean DEBUG = 80 Boolean.parseBoolean(System.getProperty("test.debug", "false")); 81 public static final boolean NO_LINGER = 82 Boolean.parseBoolean(System.getProperty("test.nolinger", "false")); 83 public enum HttpAuthType { 84 SERVER, PROXY, SERVER307, PROXY305 85 /* add PROXY_AND_SERVER and SERVER_PROXY_NONE */ 86 }; 87 public enum HttpAuthSchemeType { NONE, BASICSERVER, BASIC, DIGEST }; 88 public static final HttpAuthType DEFAULT_HTTP_AUTH_TYPE = HttpAuthType.SERVER; 89 public static final String DEFAULT_PROTOCOL_TYPE = "https"; 90 public static final HttpAuthSchemeType DEFAULT_SCHEME_TYPE = HttpAuthSchemeType.DIGEST; 91 92 public static class HttpTestAuthenticator extends Authenticator { 93 private final String realm; 94 private final String username; 95 // Used to prevent incrementation of 'count' when calling the 96 // authenticator from the server side. 97 private final ThreadLocal<Boolean> skipCount = new ThreadLocal<>(); 98 // count will be incremented every time getPasswordAuthentication() 99 // is called from the client side. 100 final AtomicInteger count = new AtomicInteger(); 101 HttpTestAuthenticator(String realm, String username)102 public HttpTestAuthenticator(String realm, String username) { 103 this.realm = realm; 104 this.username = username; 105 } 106 @Override getPasswordAuthentication()107 protected PasswordAuthentication getPasswordAuthentication() { 108 if (skipCount.get() == null || skipCount.get().booleanValue() == false) { 109 System.out.println("Authenticator called: " + count.incrementAndGet()); 110 } 111 return new PasswordAuthentication(getUserName(), 112 new char[] {'d','e','n', 't'}); 113 } 114 // Called by the server side to get the password of the user 115 // being authentified. getPassword(String user)116 public final char[] getPassword(String user) { 117 if (user.equals(username)) { 118 skipCount.set(Boolean.TRUE); 119 try { 120 return getPasswordAuthentication().getPassword(); 121 } finally { 122 skipCount.set(Boolean.FALSE); 123 } 124 } 125 throw new SecurityException("User unknown: " + user); 126 } getUserName()127 public final String getUserName() { 128 return username; 129 } getRealm()130 public final String getRealm() { 131 return realm; 132 } 133 } 134 135 public static final HttpTestAuthenticator AUTHENTICATOR; 136 static { 137 AUTHENTICATOR = new HttpTestAuthenticator("earth", "arthur"); 138 } 139 140 141 final HttpTestServer serverImpl; // this server endpoint 142 final DigestEchoServer redirect; // the target server where to redirect 3xx 143 final HttpTestHandler delegate; // unused 144 final String key; 145 DigestEchoServer(String key, HttpTestServer server, DigestEchoServer target, HttpTestHandler delegate)146 DigestEchoServer(String key, 147 HttpTestServer server, 148 DigestEchoServer target, 149 HttpTestHandler delegate) { 150 this.key = key; 151 this.serverImpl = server; 152 this.redirect = target; 153 this.delegate = delegate; 154 } 155 main(String[] args)156 public static void main(String[] args) 157 throws IOException { 158 159 DigestEchoServer server = create(Version.HTTP_1_1, 160 DEFAULT_PROTOCOL_TYPE, 161 DEFAULT_HTTP_AUTH_TYPE, 162 AUTHENTICATOR, 163 DEFAULT_SCHEME_TYPE); 164 try { 165 System.out.println("Server created at " + server.getAddress()); 166 System.out.println("Strike <Return> to exit"); 167 System.in.read(); 168 } finally { 169 System.out.println("stopping server"); 170 server.stop(); 171 } 172 } 173 toString(HttpTestRequestHeaders headers)174 private static String toString(HttpTestRequestHeaders headers) { 175 return headers.entrySet().stream() 176 .map((e) -> e.getKey() + ": " + e.getValue()) 177 .collect(Collectors.joining("\n")); 178 } 179 create(Version version, String protocol, HttpAuthType authType, HttpAuthSchemeType schemeType)180 public static DigestEchoServer create(Version version, 181 String protocol, 182 HttpAuthType authType, 183 HttpAuthSchemeType schemeType) 184 throws IOException { 185 return create(version, protocol, authType, AUTHENTICATOR, schemeType); 186 } 187 create(Version version, String protocol, HttpAuthType authType, HttpTestAuthenticator auth, HttpAuthSchemeType schemeType)188 public static DigestEchoServer create(Version version, 189 String protocol, 190 HttpAuthType authType, 191 HttpTestAuthenticator auth, 192 HttpAuthSchemeType schemeType) 193 throws IOException { 194 return create(version, protocol, authType, auth, schemeType, null); 195 } 196 create(Version version, String protocol, HttpAuthType authType, HttpTestAuthenticator auth, HttpAuthSchemeType schemeType, HttpTestHandler delegate)197 public static DigestEchoServer create(Version version, 198 String protocol, 199 HttpAuthType authType, 200 HttpTestAuthenticator auth, 201 HttpAuthSchemeType schemeType, 202 HttpTestHandler delegate) 203 throws IOException { 204 Objects.requireNonNull(authType); 205 Objects.requireNonNull(auth); 206 switch(authType) { 207 // A server that performs Server Digest authentication. 208 case SERVER: return createServer(version, protocol, authType, auth, 209 schemeType, delegate, "/"); 210 // A server that pretends to be a Proxy and performs 211 // Proxy Digest authentication. If protocol is HTTPS, 212 // then this will create a HttpsProxyTunnel that will 213 // handle the CONNECT request for tunneling. 214 case PROXY: return createProxy(version, protocol, authType, auth, 215 schemeType, delegate, "/"); 216 // A server that sends 307 redirect to a server that performs 217 // Digest authentication. 218 // Note: 301 doesn't work here because it transforms POST into GET. 219 case SERVER307: return createServerAndRedirect(version, 220 protocol, 221 HttpAuthType.SERVER, 222 auth, schemeType, 223 delegate, 307); 224 // A server that sends 305 redirect to a proxy that performs 225 // Digest authentication. 226 // Note: this is not correctly stubbed/implemented in this test. 227 case PROXY305: return createServerAndRedirect(version, 228 protocol, 229 HttpAuthType.PROXY, 230 auth, schemeType, 231 delegate, 305); 232 default: 233 throw new InternalError("Unknown server type: " + authType); 234 } 235 } 236 237 238 /** 239 * The SocketBindableFactory ensures that the local port used by an HttpServer 240 * or a proxy ServerSocket previously created by the current test/VM will not 241 * get reused by a subsequent test in the same VM. 242 * This is to avoid having the test client trying to reuse cached connections. 243 */ 244 private static abstract class SocketBindableFactory<B> { 245 private static final int MAX = 10; 246 private static final CopyOnWriteArrayList<String> addresses = 247 new CopyOnWriteArrayList<>(); createInternal()248 protected B createInternal() throws IOException { 249 final int max = addresses.size() + MAX; 250 final List<B> toClose = new ArrayList<>(); 251 try { 252 for (int i = 1; i <= max; i++) { 253 B bindable = createBindable(); 254 InetSocketAddress address = getAddress(bindable); 255 String key = "localhost:" + address.getPort(); 256 if (addresses.addIfAbsent(key)) { 257 System.out.println("Socket bound to: " + key 258 + " after " + i + " attempt(s)"); 259 return bindable; 260 } 261 System.out.println("warning: address " + key 262 + " already used. Retrying bind."); 263 // keep the port bound until we get a port that we haven't 264 // used already 265 toClose.add(bindable); 266 } 267 } finally { 268 // if we had to retry, then close the socket we're not 269 // going to use. 270 for (B b : toClose) { 271 try { close(b); } catch (Exception x) { /* ignore */ } 272 } 273 } 274 throw new IOException("Couldn't bind socket after " + max + " attempts: " 275 + "addresses used before: " + addresses); 276 } 277 createBindable()278 protected abstract B createBindable() throws IOException; 279 getAddress(B bindable)280 protected abstract InetSocketAddress getAddress(B bindable); 281 close(B bindable)282 protected abstract void close(B bindable) throws IOException; 283 } 284 285 /* 286 * Used to create ServerSocket for a proxy. 287 */ 288 private static final class ServerSocketFactory 289 extends SocketBindableFactory<ServerSocket> { 290 private static final ServerSocketFactory instance = new ServerSocketFactory(); 291 create()292 static ServerSocket create() throws IOException { 293 return instance.createInternal(); 294 } 295 296 @Override createBindable()297 protected ServerSocket createBindable() throws IOException { 298 ServerSocket ss = new ServerSocket(); 299 ss.setReuseAddress(false); 300 ss.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); 301 return ss; 302 } 303 304 @Override getAddress(ServerSocket socket)305 protected InetSocketAddress getAddress(ServerSocket socket) { 306 return new InetSocketAddress(socket.getInetAddress(), socket.getLocalPort()); 307 } 308 309 @Override close(ServerSocket socket)310 protected void close(ServerSocket socket) throws IOException { 311 socket.close(); 312 } 313 } 314 315 /* 316 * Used to create HttpServer 317 */ 318 private static abstract class H1ServerFactory<S extends HttpServer> 319 extends SocketBindableFactory<S> { 320 @Override createBindable()321 protected S createBindable() throws IOException { 322 S server = newHttpServer(); 323 server.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0); 324 return server; 325 } 326 327 @Override getAddress(S server)328 protected InetSocketAddress getAddress(S server) { 329 return server.getAddress(); 330 } 331 332 @Override close(S server)333 protected void close(S server) throws IOException { 334 server.stop(1); 335 } 336 337 /* 338 * Returns a HttpServer or a HttpsServer in different subclasses. 339 */ newHttpServer()340 protected abstract S newHttpServer() throws IOException; 341 } 342 343 /* 344 * Used to create Http2TestServer 345 */ 346 private static abstract class H2ServerFactory<S extends Http2TestServer> 347 extends SocketBindableFactory<S> { 348 @Override createBindable()349 protected S createBindable() throws IOException { 350 final S server; 351 try { 352 server = newHttpServer(); 353 } catch (IOException io) { 354 throw io; 355 } catch (Exception x) { 356 throw new IOException(x); 357 } 358 return server; 359 } 360 361 @Override getAddress(S server)362 protected InetSocketAddress getAddress(S server) { 363 return server.getAddress(); 364 } 365 366 @Override close(S server)367 protected void close(S server) throws IOException { 368 server.stop(); 369 } 370 371 /* 372 * Returns a HttpServer or a HttpsServer in different subclasses. 373 */ newHttpServer()374 protected abstract S newHttpServer() throws Exception; 375 } 376 377 private static final class Http2ServerFactory extends H2ServerFactory<Http2TestServer> { 378 private static final Http2ServerFactory instance = new Http2ServerFactory(); 379 create()380 static Http2TestServer create() throws IOException { 381 return instance.createInternal(); 382 } 383 384 @Override newHttpServer()385 protected Http2TestServer newHttpServer() throws Exception { 386 return new Http2TestServer("localhost", false, 0); 387 } 388 } 389 390 private static final class Https2ServerFactory extends H2ServerFactory<Http2TestServer> { 391 private static final Https2ServerFactory instance = new Https2ServerFactory(); 392 create()393 static Http2TestServer create() throws IOException { 394 return instance.createInternal(); 395 } 396 397 @Override newHttpServer()398 protected Http2TestServer newHttpServer() throws Exception { 399 return new Http2TestServer("localhost", true, 0); 400 } 401 } 402 403 private static final class Http1ServerFactory extends H1ServerFactory<HttpServer> { 404 private static final Http1ServerFactory instance = new Http1ServerFactory(); 405 create()406 static HttpServer create() throws IOException { 407 return instance.createInternal(); 408 } 409 410 @Override newHttpServer()411 protected HttpServer newHttpServer() throws IOException { 412 return HttpServer.create(); 413 } 414 } 415 416 private static final class Https1ServerFactory extends H1ServerFactory<HttpsServer> { 417 private static final Https1ServerFactory instance = new Https1ServerFactory(); 418 create()419 static HttpsServer create() throws IOException { 420 return instance.createInternal(); 421 } 422 423 @Override newHttpServer()424 protected HttpsServer newHttpServer() throws IOException { 425 return HttpsServer.create(); 426 } 427 } 428 createHttp2Server(String protocol)429 static Http2TestServer createHttp2Server(String protocol) throws IOException { 430 final Http2TestServer server; 431 if ("http".equalsIgnoreCase(protocol)) { 432 server = Http2ServerFactory.create(); 433 } else if ("https".equalsIgnoreCase(protocol)) { 434 server = Https2ServerFactory.create(); 435 } else { 436 throw new InternalError("unsupported protocol: " + protocol); 437 } 438 return server; 439 } 440 createHttpServer(Version version, String protocol)441 static HttpTestServer createHttpServer(Version version, String protocol) 442 throws IOException 443 { 444 switch(version) { 445 case HTTP_1_1: 446 return HttpTestServer.of(createHttp1Server(protocol)); 447 case HTTP_2: 448 return HttpTestServer.of(createHttp2Server(protocol)); 449 default: 450 throw new InternalError("Unexpected version: " + version); 451 } 452 } 453 createHttp1Server(String protocol)454 static HttpServer createHttp1Server(String protocol) throws IOException { 455 final HttpServer server; 456 if ("http".equalsIgnoreCase(protocol)) { 457 server = Http1ServerFactory.create(); 458 } else if ("https".equalsIgnoreCase(protocol)) { 459 server = configure(Https1ServerFactory.create()); 460 } else { 461 throw new InternalError("unsupported protocol: " + protocol); 462 } 463 return server; 464 } 465 configure(HttpsServer server)466 static HttpsServer configure(HttpsServer server) throws IOException { 467 try { 468 SSLContext ctx = SSLContext.getDefault(); 469 server.setHttpsConfigurator(new Configurator(ctx)); 470 } catch (NoSuchAlgorithmException ex) { 471 throw new IOException(ex); 472 } 473 return server; 474 } 475 476 setContextAuthenticator(HttpTestContext ctxt, HttpTestAuthenticator auth)477 static void setContextAuthenticator(HttpTestContext ctxt, 478 HttpTestAuthenticator auth) { 479 final String realm = auth.getRealm(); 480 com.sun.net.httpserver.Authenticator authenticator = 481 new BasicAuthenticator(realm) { 482 @Override 483 public boolean checkCredentials(String username, String pwd) { 484 return auth.getUserName().equals(username) 485 && new String(auth.getPassword(username)).equals(pwd); 486 } 487 }; 488 ctxt.setAuthenticator(authenticator); 489 } 490 createServer(Version version, String protocol, HttpAuthType authType, HttpTestAuthenticator auth, HttpAuthSchemeType schemeType, HttpTestHandler delegate, String path)491 public static DigestEchoServer createServer(Version version, 492 String protocol, 493 HttpAuthType authType, 494 HttpTestAuthenticator auth, 495 HttpAuthSchemeType schemeType, 496 HttpTestHandler delegate, 497 String path) 498 throws IOException { 499 Objects.requireNonNull(authType); 500 Objects.requireNonNull(auth); 501 502 HttpTestServer impl = createHttpServer(version, protocol); 503 String key = String.format("DigestEchoServer[PID=%s,PORT=%s]:%s:%s:%s:%s", 504 ProcessHandle.current().pid(), 505 impl.getAddress().getPort(), 506 version, protocol, authType, schemeType); 507 final DigestEchoServer server = new DigestEchoServerImpl(key, impl, null, delegate); 508 final HttpTestHandler handler = 509 server.createHandler(schemeType, auth, authType, false); 510 HttpTestContext context = impl.addHandler(handler, path); 511 server.configureAuthentication(context, schemeType, auth, authType); 512 impl.start(); 513 return server; 514 } 515 createProxy(Version version, String protocol, HttpAuthType authType, HttpTestAuthenticator auth, HttpAuthSchemeType schemeType, HttpTestHandler delegate, String path)516 public static DigestEchoServer createProxy(Version version, 517 String protocol, 518 HttpAuthType authType, 519 HttpTestAuthenticator auth, 520 HttpAuthSchemeType schemeType, 521 HttpTestHandler delegate, 522 String path) 523 throws IOException { 524 Objects.requireNonNull(authType); 525 Objects.requireNonNull(auth); 526 527 if (version == Version.HTTP_2 && protocol.equalsIgnoreCase("http")) { 528 System.out.println("WARNING: can't use HTTP/1.1 proxy with unsecure HTTP/2 server"); 529 version = Version.HTTP_1_1; 530 } 531 HttpTestServer impl = createHttpServer(version, protocol); 532 String key = String.format("DigestEchoServer[PID=%s,PORT=%s]:%s:%s:%s:%s", 533 ProcessHandle.current().pid(), 534 impl.getAddress().getPort(), 535 version, protocol, authType, schemeType); 536 final DigestEchoServer server = "https".equalsIgnoreCase(protocol) 537 ? new HttpsProxyTunnel(key, impl, null, delegate) 538 : new DigestEchoServerImpl(key, impl, null, delegate); 539 540 final HttpTestHandler hh = server.createHandler(HttpAuthSchemeType.NONE, 541 null, HttpAuthType.SERVER, 542 server instanceof HttpsProxyTunnel); 543 HttpTestContext ctxt = impl.addHandler(hh, path); 544 server.configureAuthentication(ctxt, schemeType, auth, authType); 545 impl.start(); 546 547 return server; 548 } 549 createServerAndRedirect( Version version, String protocol, HttpAuthType targetAuthType, HttpTestAuthenticator auth, HttpAuthSchemeType schemeType, HttpTestHandler targetDelegate, int code300)550 public static DigestEchoServer createServerAndRedirect( 551 Version version, 552 String protocol, 553 HttpAuthType targetAuthType, 554 HttpTestAuthenticator auth, 555 HttpAuthSchemeType schemeType, 556 HttpTestHandler targetDelegate, 557 int code300) 558 throws IOException { 559 Objects.requireNonNull(targetAuthType); 560 Objects.requireNonNull(auth); 561 562 // The connection between client and proxy can only 563 // be a plain connection: SSL connection to proxy 564 // is not supported by our client connection. 565 String targetProtocol = targetAuthType == HttpAuthType.PROXY 566 ? "http" 567 : protocol; 568 DigestEchoServer redirectTarget = 569 (targetAuthType == HttpAuthType.PROXY) 570 ? createProxy(version, protocol, targetAuthType, 571 auth, schemeType, targetDelegate, "/") 572 : createServer(version, targetProtocol, targetAuthType, 573 auth, schemeType, targetDelegate, "/"); 574 HttpTestServer impl = createHttpServer(version, protocol); 575 String key = String.format("RedirectingServer[PID=%s,PORT=%s]:%s:%s:%s:%s", 576 ProcessHandle.current().pid(), 577 impl.getAddress().getPort(), 578 version, protocol, 579 HttpAuthType.SERVER, code300) 580 + "->" + redirectTarget.key; 581 final DigestEchoServer redirectingServer = 582 new DigestEchoServerImpl(key, impl, redirectTarget, null); 583 InetSocketAddress redirectAddr = redirectTarget.getAddress(); 584 URL locationURL = url(targetProtocol, redirectAddr, "/"); 585 final HttpTestHandler hh = redirectingServer.create300Handler(key, locationURL, 586 HttpAuthType.SERVER, code300); 587 impl.addHandler(hh,"/"); 588 impl.start(); 589 return redirectingServer; 590 } 591 getServerAddress()592 public abstract InetSocketAddress getServerAddress(); getProxyAddress()593 public abstract InetSocketAddress getProxyAddress(); getAddress()594 public abstract InetSocketAddress getAddress(); stop()595 public abstract void stop(); getServerVersion()596 public abstract Version getServerVersion(); 597 598 private static class DigestEchoServerImpl extends DigestEchoServer { DigestEchoServerImpl(String key, HttpTestServer server, DigestEchoServer target, HttpTestHandler delegate)599 DigestEchoServerImpl(String key, 600 HttpTestServer server, 601 DigestEchoServer target, 602 HttpTestHandler delegate) { 603 super(key, Objects.requireNonNull(server), target, delegate); 604 } 605 getAddress()606 public InetSocketAddress getAddress() { 607 return new InetSocketAddress(InetAddress.getLoopbackAddress(), 608 serverImpl.getAddress().getPort()); 609 } 610 getServerAddress()611 public InetSocketAddress getServerAddress() { 612 return new InetSocketAddress(InetAddress.getLoopbackAddress(), 613 serverImpl.getAddress().getPort()); 614 } 615 getProxyAddress()616 public InetSocketAddress getProxyAddress() { 617 return new InetSocketAddress(InetAddress.getLoopbackAddress(), 618 serverImpl.getAddress().getPort()); 619 } 620 getServerVersion()621 public Version getServerVersion() { 622 return serverImpl.getVersion(); 623 } 624 stop()625 public void stop() { 626 serverImpl.stop(); 627 if (redirect != null) { 628 redirect.stop(); 629 } 630 } 631 } 632 writeResponse(HttpTestExchange he)633 protected void writeResponse(HttpTestExchange he) throws IOException { 634 if (delegate == null) { 635 he.sendResponseHeaders(HttpURLConnection.HTTP_OK, -1); 636 he.getResponseBody().write(he.getRequestBody().readAllBytes()); 637 } else { 638 delegate.handle(he); 639 } 640 } 641 createHandler(HttpAuthSchemeType schemeType, HttpTestAuthenticator auth, HttpAuthType authType, boolean tunelled)642 private HttpTestHandler createHandler(HttpAuthSchemeType schemeType, 643 HttpTestAuthenticator auth, 644 HttpAuthType authType, 645 boolean tunelled) { 646 return new HttpNoAuthHandler(key, authType, tunelled); 647 } 648 configureAuthentication(HttpTestContext ctxt, HttpAuthSchemeType schemeType, HttpTestAuthenticator auth, HttpAuthType authType)649 void configureAuthentication(HttpTestContext ctxt, 650 HttpAuthSchemeType schemeType, 651 HttpTestAuthenticator auth, 652 HttpAuthType authType) { 653 switch(schemeType) { 654 case DIGEST: 655 // DIGEST authentication is handled by the handler. 656 ctxt.addFilter(new HttpDigestFilter(key, auth, authType)); 657 break; 658 case BASIC: 659 // BASIC authentication is handled by the filter. 660 ctxt.addFilter(new HttpBasicFilter(key, auth, authType)); 661 break; 662 case BASICSERVER: 663 switch(authType) { 664 case PROXY: case PROXY305: 665 // HttpServer can't support Proxy-type authentication 666 // => we do as if BASIC had been specified, and we will 667 // handle authentication in the handler. 668 ctxt.addFilter(new HttpBasicFilter(key, auth, authType)); 669 break; 670 case SERVER: case SERVER307: 671 if (ctxt.getVersion() == Version.HTTP_1_1) { 672 // Basic authentication is handled by HttpServer 673 // directly => the filter should not perform 674 // authentication again. 675 setContextAuthenticator(ctxt, auth); 676 ctxt.addFilter(new HttpNoAuthFilter(key, authType)); 677 } else { 678 ctxt.addFilter(new HttpBasicFilter(key, auth, authType)); 679 } 680 break; 681 default: 682 throw new InternalError(key + ": Invalid combination scheme=" 683 + schemeType + " authType=" + authType); 684 } 685 case NONE: 686 // No authentication at all. 687 ctxt.addFilter(new HttpNoAuthFilter(key, authType)); 688 break; 689 default: 690 throw new InternalError(key + ": No such scheme: " + schemeType); 691 } 692 } 693 create300Handler(String key, URL proxyURL, HttpAuthType type, int code300)694 private HttpTestHandler create300Handler(String key, URL proxyURL, 695 HttpAuthType type, int code300) 696 throws MalformedURLException 697 { 698 return new Http3xxHandler(key, proxyURL, type, code300); 699 } 700 701 // Abstract HTTP filter class. 702 private abstract static class AbstractHttpFilter extends HttpTestFilter { 703 704 final HttpAuthType authType; 705 final String type; AbstractHttpFilter(HttpAuthType authType, String type)706 public AbstractHttpFilter(HttpAuthType authType, String type) { 707 this.authType = authType; 708 this.type = type; 709 } 710 getLocation()711 String getLocation() { 712 return "Location"; 713 } getAuthenticate()714 String getAuthenticate() { 715 return authType == HttpAuthType.PROXY 716 ? "Proxy-Authenticate" : "WWW-Authenticate"; 717 } getAuthorization()718 String getAuthorization() { 719 return authType == HttpAuthType.PROXY 720 ? "Proxy-Authorization" : "Authorization"; 721 } getUnauthorizedCode()722 int getUnauthorizedCode() { 723 return authType == HttpAuthType.PROXY 724 ? HttpURLConnection.HTTP_PROXY_AUTH 725 : HttpURLConnection.HTTP_UNAUTHORIZED; 726 } getKeepAlive()727 String getKeepAlive() { 728 return "keep-alive"; 729 } getConnection()730 String getConnection() { 731 return authType == HttpAuthType.PROXY 732 ? "Proxy-Connection" : "Connection"; 733 } isAuthentified(HttpTestExchange he)734 protected abstract boolean isAuthentified(HttpTestExchange he) throws IOException; requestAuthentication(HttpTestExchange he)735 protected abstract void requestAuthentication(HttpTestExchange he) throws IOException; accept(HttpTestExchange he, HttpChain chain)736 protected void accept(HttpTestExchange he, HttpChain chain) throws IOException { 737 chain.doFilter(he); 738 } 739 740 @Override description()741 public String description() { 742 return "Filter for " + type; 743 } 744 @Override doFilter(HttpTestExchange he, HttpChain chain)745 public void doFilter(HttpTestExchange he, HttpChain chain) throws IOException { 746 try { 747 System.out.println(type + ": Got " + he.getRequestMethod() 748 + ": " + he.getRequestURI() 749 + "\n" + DigestEchoServer.toString(he.getRequestHeaders())); 750 751 // Assert only a single value for Expect. Not directly related 752 // to digest authentication, but verifies good client behaviour. 753 List<String> expectValues = he.getRequestHeaders().get("Expect"); 754 if (expectValues != null && expectValues.size() > 1) { 755 throw new IOException("Expect: " + expectValues); 756 } 757 758 if (!isAuthentified(he)) { 759 try { 760 requestAuthentication(he); 761 he.sendResponseHeaders(getUnauthorizedCode(), -1); 762 System.out.println(type 763 + ": Sent back " + getUnauthorizedCode()); 764 } finally { 765 he.close(); 766 } 767 } else { 768 accept(he, chain); 769 } 770 } catch (RuntimeException | Error | IOException t) { 771 System.err.println(type 772 + ": Unexpected exception while handling request: " + t); 773 t.printStackTrace(System.err); 774 he.close(); 775 throw t; 776 } 777 } 778 779 } 780 781 // WARNING: This is not a full fledged implementation of DIGEST. 782 // It does contain bugs and inaccuracy. 783 final static class DigestResponse { 784 final String realm; 785 final String username; 786 final String nonce; 787 final String cnonce; 788 final String nc; 789 final String uri; 790 final String algorithm; 791 final String response; 792 final String qop; 793 final String opaque; 794 DigestResponse(String realm, String username, String nonce, String cnonce, String nc, String uri, String algorithm, String qop, String opaque, String response)795 public DigestResponse(String realm, String username, String nonce, 796 String cnonce, String nc, String uri, 797 String algorithm, String qop, String opaque, 798 String response) { 799 this.realm = realm; 800 this.username = username; 801 this.nonce = nonce; 802 this.cnonce = cnonce; 803 this.nc = nc; 804 this.uri = uri; 805 this.algorithm = algorithm; 806 this.qop = qop; 807 this.opaque = opaque; 808 this.response = response; 809 } 810 getAlgorithm(String defval)811 String getAlgorithm(String defval) { 812 return algorithm == null ? defval : algorithm; 813 } getQoP(String defval)814 String getQoP(String defval) { 815 return qop == null ? defval : qop; 816 } 817 818 // Code stolen from DigestAuthentication: 819 820 private static final char charArray[] = { 821 '0', '1', '2', '3', '4', '5', '6', '7', 822 '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' 823 }; 824 encode(String src, char[] passwd, MessageDigest md)825 private static String encode(String src, char[] passwd, MessageDigest md) { 826 try { 827 md.update(src.getBytes("ISO-8859-1")); 828 } catch (java.io.UnsupportedEncodingException uee) { 829 assert false; 830 } 831 if (passwd != null) { 832 byte[] passwdBytes = new byte[passwd.length]; 833 for (int i=0; i<passwd.length; i++) 834 passwdBytes[i] = (byte)passwd[i]; 835 md.update(passwdBytes); 836 Arrays.fill(passwdBytes, (byte)0x00); 837 } 838 byte[] digest = md.digest(); 839 840 StringBuilder res = new StringBuilder(digest.length * 2); 841 for (int i = 0; i < digest.length; i++) { 842 int hashchar = ((digest[i] >>> 4) & 0xf); 843 res.append(charArray[hashchar]); 844 hashchar = (digest[i] & 0xf); 845 res.append(charArray[hashchar]); 846 } 847 return res.toString(); 848 } 849 computeDigest(boolean isRequest, String reqMethod, char[] password, DigestResponse params)850 public static String computeDigest(boolean isRequest, 851 String reqMethod, 852 char[] password, 853 DigestResponse params) 854 throws NoSuchAlgorithmException 855 { 856 857 String A1, HashA1; 858 String algorithm = params.getAlgorithm("MD5"); 859 boolean md5sess = algorithm.equalsIgnoreCase ("MD5-sess"); 860 861 MessageDigest md = MessageDigest.getInstance(md5sess?"MD5":algorithm); 862 863 if (params.username == null) { 864 throw new IllegalArgumentException("missing username"); 865 } 866 if (params.realm == null) { 867 throw new IllegalArgumentException("missing realm"); 868 } 869 if (params.uri == null) { 870 throw new IllegalArgumentException("missing uri"); 871 } 872 if (params.nonce == null) { 873 throw new IllegalArgumentException("missing nonce"); 874 } 875 876 A1 = params.username + ":" + params.realm + ":"; 877 HashA1 = encode(A1, password, md); 878 879 String A2; 880 if (isRequest) { 881 A2 = reqMethod + ":" + params.uri; 882 } else { 883 A2 = ":" + params.uri; 884 } 885 String HashA2 = encode(A2, null, md); 886 String combo, finalHash; 887 888 if ("auth".equals(params.qop)) { /* RRC2617 when qop=auth */ 889 if (params.cnonce == null) { 890 throw new IllegalArgumentException("missing nonce"); 891 } 892 if (params.nc == null) { 893 throw new IllegalArgumentException("missing nonce"); 894 } 895 combo = HashA1+ ":" + params.nonce + ":" + params.nc + ":" + 896 params.cnonce + ":auth:" +HashA2; 897 898 } else { /* for compatibility with RFC2069 */ 899 combo = HashA1 + ":" + 900 params.nonce + ":" + 901 HashA2; 902 } 903 finalHash = encode(combo, null, md); 904 return finalHash; 905 } 906 create(String raw)907 public static DigestResponse create(String raw) { 908 String username, realm, nonce, nc, uri, response, cnonce, 909 algorithm, qop, opaque; 910 HeaderParser parser = new HeaderParser(raw); 911 username = parser.findValue("username"); 912 realm = parser.findValue("realm"); 913 nonce = parser.findValue("nonce"); 914 nc = parser.findValue("nc"); 915 uri = parser.findValue("uri"); 916 cnonce = parser.findValue("cnonce"); 917 response = parser.findValue("response"); 918 algorithm = parser.findValue("algorithm"); 919 qop = parser.findValue("qop"); 920 opaque = parser.findValue("opaque"); 921 return new DigestResponse(realm, username, nonce, cnonce, nc, uri, 922 algorithm, qop, opaque, response); 923 } 924 925 } 926 927 private static class HttpNoAuthFilter extends AbstractHttpFilter { 928 type(String key, HttpAuthType authType)929 static String type(String key, HttpAuthType authType) { 930 String type = authType == HttpAuthType.SERVER 931 ? "NoAuth Server Filter" : "NoAuth Proxy Filter"; 932 return "["+type+"]:"+key; 933 } 934 HttpNoAuthFilter(String key, HttpAuthType authType)935 public HttpNoAuthFilter(String key, HttpAuthType authType) { 936 super(authType, type(key, authType)); 937 } 938 939 @Override isAuthentified(HttpTestExchange he)940 protected boolean isAuthentified(HttpTestExchange he) throws IOException { 941 return true; 942 } 943 944 @Override requestAuthentication(HttpTestExchange he)945 protected void requestAuthentication(HttpTestExchange he) throws IOException { 946 throw new InternalError("Should not com here"); 947 } 948 949 @Override description()950 public String description() { 951 return "Passthrough Filter"; 952 } 953 954 } 955 956 // An HTTP Filter that performs Basic authentication 957 private static class HttpBasicFilter extends AbstractHttpFilter { 958 type(String key, HttpAuthType authType)959 static String type(String key, HttpAuthType authType) { 960 String type = authType == HttpAuthType.SERVER 961 ? "Basic Server Filter" : "Basic Proxy Filter"; 962 return "["+type+"]:"+key; 963 } 964 965 private final HttpTestAuthenticator auth; HttpBasicFilter(String key, HttpTestAuthenticator auth, HttpAuthType authType)966 public HttpBasicFilter(String key, HttpTestAuthenticator auth, 967 HttpAuthType authType) { 968 super(authType, type(key, authType)); 969 this.auth = auth; 970 } 971 972 @Override requestAuthentication(HttpTestExchange he)973 protected void requestAuthentication(HttpTestExchange he) 974 throws IOException 975 { 976 String headerName = getAuthenticate(); 977 String headerValue = "Basic realm=\"" + auth.getRealm() + "\""; 978 he.getResponseHeaders().addHeader(headerName, headerValue); 979 System.out.println(type + ": Requesting Basic Authentication, " 980 + headerName + " : "+ headerValue); 981 } 982 983 @Override isAuthentified(HttpTestExchange he)984 protected boolean isAuthentified(HttpTestExchange he) { 985 if (he.getRequestHeaders().containsKey(getAuthorization())) { 986 List<String> authorization = 987 he.getRequestHeaders().get(getAuthorization()); 988 for (String a : authorization) { 989 System.out.println(type + ": processing " + a); 990 int sp = a.indexOf(' '); 991 if (sp < 0) return false; 992 String scheme = a.substring(0, sp); 993 if (!"Basic".equalsIgnoreCase(scheme)) { 994 System.out.println(type + ": Unsupported scheme '" 995 + scheme +"'"); 996 return false; 997 } 998 if (a.length() <= sp+1) { 999 System.out.println(type + ": value too short for '" 1000 + scheme +"'"); 1001 return false; 1002 } 1003 a = a.substring(sp+1); 1004 return validate(a); 1005 } 1006 return false; 1007 } 1008 return false; 1009 } 1010 validate(String a)1011 boolean validate(String a) { 1012 byte[] b = Base64.getDecoder().decode(a); 1013 String userpass = new String (b); 1014 int colon = userpass.indexOf (':'); 1015 String uname = userpass.substring (0, colon); 1016 String pass = userpass.substring (colon+1); 1017 return auth.getUserName().equals(uname) && 1018 new String(auth.getPassword(uname)).equals(pass); 1019 } 1020 1021 @Override description()1022 public String description() { 1023 return "Filter for BASIC authentication: " + type; 1024 } 1025 1026 } 1027 1028 1029 // An HTTP Filter that performs Digest authentication 1030 // WARNING: This is not a full fledged implementation of DIGEST. 1031 // It does contain bugs and inaccuracy. 1032 private static class HttpDigestFilter extends AbstractHttpFilter { 1033 type(String key, HttpAuthType authType)1034 static String type(String key, HttpAuthType authType) { 1035 String type = authType == HttpAuthType.SERVER 1036 ? "Digest Server Filter" : "Digest Proxy Filter"; 1037 return "["+type+"]:"+key; 1038 } 1039 1040 // This is a very basic DIGEST - used only for the purpose of testing 1041 // the client implementation. Therefore we can get away with never 1042 // updating the server nonce as it makes the implementation of the 1043 // server side digest simpler. 1044 private final HttpTestAuthenticator auth; 1045 private final byte[] nonce; 1046 private final String ns; HttpDigestFilter(String key, HttpTestAuthenticator auth, HttpAuthType authType)1047 public HttpDigestFilter(String key, HttpTestAuthenticator auth, HttpAuthType authType) { 1048 super(authType, type(key, authType)); 1049 this.auth = auth; 1050 nonce = new byte[16]; 1051 new Random(Instant.now().toEpochMilli()).nextBytes(nonce); 1052 ns = new BigInteger(1, nonce).toString(16); 1053 } 1054 1055 @Override requestAuthentication(HttpTestExchange he)1056 protected void requestAuthentication(HttpTestExchange he) 1057 throws IOException { 1058 String separator; 1059 Version v = he.getExchangeVersion(); 1060 if (v == Version.HTTP_1_1) { 1061 separator = "\r\n "; 1062 } else if (v == Version.HTTP_2) { 1063 separator = " "; 1064 } else { 1065 throw new InternalError(String.valueOf(v)); 1066 } 1067 String headerName = getAuthenticate(); 1068 String headerValue = "Digest realm=\"" + auth.getRealm() + "\"," 1069 + separator + "qop=\"auth\"," 1070 + separator + "nonce=\"" + ns +"\""; 1071 he.getResponseHeaders().addHeader(headerName, headerValue); 1072 System.out.println(type + ": Requesting Digest Authentication, " 1073 + headerName + " : " + headerValue); 1074 } 1075 1076 @Override isAuthentified(HttpTestExchange he)1077 protected boolean isAuthentified(HttpTestExchange he) { 1078 if (he.getRequestHeaders().containsKey(getAuthorization())) { 1079 List<String> authorization = he.getRequestHeaders().get(getAuthorization()); 1080 for (String a : authorization) { 1081 System.out.println(type + ": processing " + a); 1082 int sp = a.indexOf(' '); 1083 if (sp < 0) return false; 1084 String scheme = a.substring(0, sp); 1085 if (!"Digest".equalsIgnoreCase(scheme)) { 1086 System.out.println(type + ": Unsupported scheme '" + scheme +"'"); 1087 return false; 1088 } 1089 if (a.length() <= sp+1) { 1090 System.out.println(type + ": value too short for '" + scheme +"'"); 1091 return false; 1092 } 1093 a = a.substring(sp+1); 1094 DigestResponse dgr = DigestResponse.create(a); 1095 return validate(he.getRequestURI(), he.getRequestMethod(), dgr); 1096 } 1097 return false; 1098 } 1099 return false; 1100 } 1101 validate(URI uri, String reqMethod, DigestResponse dg)1102 boolean validate(URI uri, String reqMethod, DigestResponse dg) { 1103 if (!"MD5".equalsIgnoreCase(dg.getAlgorithm("MD5"))) { 1104 System.out.println(type + ": Unsupported algorithm " 1105 + dg.algorithm); 1106 return false; 1107 } 1108 if (!"auth".equalsIgnoreCase(dg.getQoP("auth"))) { 1109 System.out.println(type + ": Unsupported qop " 1110 + dg.qop); 1111 return false; 1112 } 1113 try { 1114 if (!dg.nonce.equals(ns)) { 1115 System.out.println(type + ": bad nonce returned by client: " 1116 + nonce + " expected " + ns); 1117 return false; 1118 } 1119 if (dg.response == null) { 1120 System.out.println(type + ": missing digest response."); 1121 return false; 1122 } 1123 char[] pa = auth.getPassword(dg.username); 1124 return verify(uri, reqMethod, dg, pa); 1125 } catch(IllegalArgumentException | SecurityException 1126 | NoSuchAlgorithmException e) { 1127 System.out.println(type + ": " + e.getMessage()); 1128 return false; 1129 } 1130 } 1131 1132 verify(URI uri, String reqMethod, DigestResponse dg, char[] pw)1133 boolean verify(URI uri, String reqMethod, DigestResponse dg, char[] pw) 1134 throws NoSuchAlgorithmException { 1135 String response = DigestResponse.computeDigest(true, reqMethod, pw, dg); 1136 if (!dg.response.equals(response)) { 1137 System.out.println(type + ": bad response returned by client: " 1138 + dg.response + " expected " + response); 1139 return false; 1140 } else { 1141 // A real server would also verify the uri=<request-uri> 1142 // parameter - but this is just a test... 1143 System.out.println(type + ": verified response " + response); 1144 } 1145 return true; 1146 } 1147 1148 1149 @Override description()1150 public String description() { 1151 return "Filter for DIGEST authentication: " + type; 1152 } 1153 } 1154 1155 // Abstract HTTP handler class. 1156 private abstract static class AbstractHttpHandler implements HttpTestHandler { 1157 1158 final HttpAuthType authType; 1159 final String type; AbstractHttpHandler(HttpAuthType authType, String type)1160 public AbstractHttpHandler(HttpAuthType authType, String type) { 1161 this.authType = authType; 1162 this.type = type; 1163 } 1164 getLocation()1165 String getLocation() { 1166 return "Location"; 1167 } 1168 1169 @Override handle(HttpTestExchange he)1170 public void handle(HttpTestExchange he) throws IOException { 1171 try { 1172 sendResponse(he); 1173 } catch (RuntimeException | Error | IOException t) { 1174 System.err.println(type 1175 + ": Unexpected exception while handling request: " + t); 1176 t.printStackTrace(System.err); 1177 throw t; 1178 } finally { 1179 he.close(); 1180 } 1181 } 1182 sendResponse(HttpTestExchange he)1183 protected abstract void sendResponse(HttpTestExchange he) throws IOException; 1184 1185 } 1186 stype(String type, String key, HttpAuthType authType, boolean tunnelled)1187 static String stype(String type, String key, HttpAuthType authType, boolean tunnelled) { 1188 type = type + (authType == HttpAuthType.SERVER 1189 ? " Server" : " Proxy") 1190 + (tunnelled ? " Tunnelled" : ""); 1191 return "["+type+"]:"+key; 1192 } 1193 1194 private class HttpNoAuthHandler extends AbstractHttpHandler { 1195 1196 // true if this server is behind a proxy tunnel. 1197 final boolean tunnelled; HttpNoAuthHandler(String key, HttpAuthType authType, boolean tunnelled)1198 public HttpNoAuthHandler(String key, HttpAuthType authType, boolean tunnelled) { 1199 super(authType, stype("NoAuth", key, authType, tunnelled)); 1200 this.tunnelled = tunnelled; 1201 } 1202 1203 @Override sendResponse(HttpTestExchange he)1204 protected void sendResponse(HttpTestExchange he) throws IOException { 1205 if (DEBUG) { 1206 System.out.println(type + ": headers are: " 1207 + DigestEchoServer.toString(he.getRequestHeaders())); 1208 } 1209 if (authType == HttpAuthType.SERVER && tunnelled) { 1210 // Verify that the client doesn't send us proxy-* headers 1211 // used to establish the proxy tunnel 1212 Optional<String> proxyAuth = he.getRequestHeaders() 1213 .keySet().stream() 1214 .filter("proxy-authorization"::equalsIgnoreCase) 1215 .findAny(); 1216 if (proxyAuth.isPresent()) { 1217 System.out.println(type + " found " 1218 + proxyAuth.get() + ": failing!"); 1219 throw new IOException(proxyAuth.get() 1220 + " found by " + type + " for " 1221 + he.getRequestURI()); 1222 } 1223 } 1224 DigestEchoServer.this.writeResponse(he); 1225 } 1226 1227 } 1228 1229 // A dummy HTTP Handler that redirects all incoming requests 1230 // by sending a back 3xx response code (301, 305, 307 etc..) 1231 private class Http3xxHandler extends AbstractHttpHandler { 1232 1233 private final URL redirectTargetURL; 1234 private final int code3XX; Http3xxHandler(String key, URL proxyURL, HttpAuthType authType, int code300)1235 public Http3xxHandler(String key, URL proxyURL, HttpAuthType authType, int code300) { 1236 super(authType, stype("Server" + code300, key, authType, false)); 1237 this.redirectTargetURL = proxyURL; 1238 this.code3XX = code300; 1239 } 1240 get3XX()1241 int get3XX() { 1242 return code3XX; 1243 } 1244 1245 @Override sendResponse(HttpTestExchange he)1246 public void sendResponse(HttpTestExchange he) throws IOException { 1247 System.out.println(type + ": Got " + he.getRequestMethod() 1248 + ": " + he.getRequestURI() 1249 + "\n" + DigestEchoServer.toString(he.getRequestHeaders())); 1250 System.out.println(type + ": Redirecting to " 1251 + (authType == HttpAuthType.PROXY305 1252 ? "proxy" : "server")); 1253 he.getResponseHeaders().addHeader(getLocation(), 1254 redirectTargetURL.toExternalForm().toString()); 1255 he.sendResponseHeaders(get3XX(), -1); 1256 System.out.println(type + ": Sent back " + get3XX() + " " 1257 + getLocation() + ": " + redirectTargetURL.toExternalForm().toString()); 1258 } 1259 } 1260 1261 static class Configurator extends HttpsConfigurator { Configurator(SSLContext ctx)1262 public Configurator(SSLContext ctx) { 1263 super(ctx); 1264 } 1265 1266 @Override configure(HttpsParameters params)1267 public void configure (HttpsParameters params) { 1268 params.setSSLParameters (getSSLContext().getSupportedSSLParameters()); 1269 } 1270 } 1271 1272 static final long start = System.nanoTime(); now()1273 public static String now() { 1274 long now = System.nanoTime() - start; 1275 long secs = now / 1000_000_000; 1276 long mill = (now % 1000_000_000) / 1000_000; 1277 long nan = now % 1000_000; 1278 return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan); 1279 } 1280 1281 static class ProxyAuthorization { 1282 final HttpAuthSchemeType schemeType; 1283 final HttpTestAuthenticator authenticator; 1284 private final byte[] nonce; 1285 private final String ns; 1286 private final String key; 1287 ProxyAuthorization(String key, HttpAuthSchemeType schemeType, HttpTestAuthenticator auth)1288 ProxyAuthorization(String key, HttpAuthSchemeType schemeType, HttpTestAuthenticator auth) { 1289 this.key = key; 1290 this.schemeType = schemeType; 1291 this.authenticator = auth; 1292 nonce = new byte[16]; 1293 new Random(Instant.now().toEpochMilli()).nextBytes(nonce); 1294 ns = new BigInteger(1, nonce).toString(16); 1295 } 1296 doBasic(Optional<String> authorization)1297 String doBasic(Optional<String> authorization) { 1298 String offset = "proxy-authorization: basic "; 1299 String authstring = authorization.orElse(""); 1300 if (!authstring.toLowerCase(Locale.US).startsWith(offset)) { 1301 return "Proxy-Authenticate: BASIC " + "realm=\"" 1302 + authenticator.getRealm() +"\""; 1303 } 1304 authstring = authstring 1305 .substring(offset.length()) 1306 .trim(); 1307 byte[] base64 = Base64.getDecoder().decode(authstring); 1308 String up = new String(base64, StandardCharsets.UTF_8); 1309 int colon = up.indexOf(':'); 1310 if (colon < 1) { 1311 return "Proxy-Authenticate: BASIC " + "realm=\"" 1312 + authenticator.getRealm() +"\""; 1313 } 1314 String u = up.substring(0, colon); 1315 String p = up.substring(colon+1); 1316 char[] pw = authenticator.getPassword(u); 1317 if (!p.equals(new String(pw))) { 1318 return "Proxy-Authenticate: BASIC " + "realm=\"" 1319 + authenticator.getRealm() +"\""; 1320 } 1321 System.out.println(now() + key + " Proxy basic authentication success"); 1322 return null; 1323 } 1324 doDigest(Optional<String> authorization)1325 String doDigest(Optional<String> authorization) { 1326 String offset = "proxy-authorization: digest "; 1327 String authstring = authorization.orElse(""); 1328 if (!authstring.toLowerCase(Locale.US).startsWith(offset)) { 1329 return "Proxy-Authenticate: " + 1330 "Digest realm=\"" + authenticator.getRealm() + "\"," 1331 + "\r\n qop=\"auth\"," 1332 + "\r\n nonce=\"" + ns +"\""; 1333 } 1334 authstring = authstring 1335 .substring(offset.length()) 1336 .trim(); 1337 boolean validated = false; 1338 try { 1339 DigestResponse dgr = DigestResponse.create(authstring); 1340 validated = validate("CONNECT", dgr); 1341 } catch (Throwable t) { 1342 t.printStackTrace(); 1343 } 1344 if (!validated) { 1345 return "Proxy-Authenticate: " + 1346 "Digest realm=\"" + authenticator.getRealm() + "\"," 1347 + "\r\n qop=\"auth\"," 1348 + "\r\n nonce=\"" + ns +"\""; 1349 } 1350 return null; 1351 } 1352 1353 1354 1355 validate(String reqMethod, DigestResponse dg)1356 boolean validate(String reqMethod, DigestResponse dg) { 1357 String type = now() + this.getClass().getSimpleName() + ":" + key; 1358 if (!"MD5".equalsIgnoreCase(dg.getAlgorithm("MD5"))) { 1359 System.out.println(type + ": Unsupported algorithm " 1360 + dg.algorithm); 1361 return false; 1362 } 1363 if (!"auth".equalsIgnoreCase(dg.getQoP("auth"))) { 1364 System.out.println(type + ": Unsupported qop " 1365 + dg.qop); 1366 return false; 1367 } 1368 try { 1369 if (!dg.nonce.equals(ns)) { 1370 System.out.println(type + ": bad nonce returned by client: " 1371 + nonce + " expected " + ns); 1372 return false; 1373 } 1374 if (dg.response == null) { 1375 System.out.println(type + ": missing digest response."); 1376 return false; 1377 } 1378 char[] pa = authenticator.getPassword(dg.username); 1379 return verify(type, reqMethod, dg, pa); 1380 } catch(IllegalArgumentException | SecurityException 1381 | NoSuchAlgorithmException e) { 1382 System.out.println(type + ": " + e.getMessage()); 1383 return false; 1384 } 1385 } 1386 1387 verify(String type, String reqMethod, DigestResponse dg, char[] pw)1388 boolean verify(String type, String reqMethod, DigestResponse dg, char[] pw) 1389 throws NoSuchAlgorithmException { 1390 String response = DigestResponse.computeDigest(true, reqMethod, pw, dg); 1391 if (!dg.response.equals(response)) { 1392 System.out.println(type + ": bad response returned by client: " 1393 + dg.response + " expected " + response); 1394 return false; 1395 } else { 1396 // A real server would also verify the uri=<request-uri> 1397 // parameter - but this is just a test... 1398 System.out.println(type + ": verified response " + response); 1399 } 1400 return true; 1401 } 1402 authorize(StringBuilder response, String requestLine, String headers)1403 public boolean authorize(StringBuilder response, String requestLine, String headers) { 1404 String message = "<html><body><p>Authorization Failed%s</p></body></html>\r\n"; 1405 if (authenticator == null && schemeType != HttpAuthSchemeType.NONE) { 1406 message = String.format(message, " No Authenticator Set"); 1407 response.append("HTTP/1.1 407 Proxy Authentication Failed\r\n"); 1408 response.append("Content-Length: ") 1409 .append(message.getBytes(StandardCharsets.UTF_8).length) 1410 .append("\r\n\r\n"); 1411 response.append(message); 1412 return false; 1413 } 1414 Optional<String> authorization = Stream.of(headers.split("\r\n")) 1415 .filter((k) -> k.toLowerCase(Locale.US).startsWith("proxy-authorization:")) 1416 .findFirst(); 1417 String authenticate = null; 1418 switch(schemeType) { 1419 case BASIC: 1420 case BASICSERVER: 1421 authenticate = doBasic(authorization); 1422 break; 1423 case DIGEST: 1424 authenticate = doDigest(authorization); 1425 break; 1426 case NONE: 1427 response.append("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); 1428 return true; 1429 default: 1430 throw new InternalError("Unknown scheme type: " + schemeType); 1431 } 1432 if (authenticate != null) { 1433 message = String.format(message, ""); 1434 response.append("HTTP/1.1 407 Proxy Authentication Required\r\n"); 1435 response.append("Content-Length: ") 1436 .append(message.getBytes(StandardCharsets.UTF_8).length) 1437 .append("\r\n") 1438 .append(authenticate) 1439 .append("\r\n\r\n"); 1440 response.append(message); 1441 return false; 1442 } 1443 response.append("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); 1444 return true; 1445 } 1446 } 1447 1448 public interface TunnelingProxy { getProxyAddress()1449 InetSocketAddress getProxyAddress(); stop()1450 void stop(); 1451 } 1452 1453 // This is a bit hacky: HttpsProxyTunnel is an HTTPTestServer hidden 1454 // behind a fake proxy that only understands CONNECT requests. 1455 // The fake proxy is just a server socket that intercept the 1456 // CONNECT and then redirect streams to the real server. 1457 static class HttpsProxyTunnel extends DigestEchoServer 1458 implements Runnable, TunnelingProxy { 1459 1460 final ServerSocket ss; 1461 final CopyOnWriteArrayList<CompletableFuture<Void>> connectionCFs 1462 = new CopyOnWriteArrayList<>(); 1463 volatile ProxyAuthorization authorization; 1464 volatile boolean stopped; HttpsProxyTunnel(String key, HttpTestServer server, DigestEchoServer target, HttpTestHandler delegate)1465 public HttpsProxyTunnel(String key, HttpTestServer server, DigestEchoServer target, 1466 HttpTestHandler delegate) 1467 throws IOException { 1468 this(key, server, target, delegate, ServerSocketFactory.create()); 1469 } HttpsProxyTunnel(String key, HttpTestServer server, DigestEchoServer target, HttpTestHandler delegate, ServerSocket ss)1470 private HttpsProxyTunnel(String key, HttpTestServer server, DigestEchoServer target, 1471 HttpTestHandler delegate, ServerSocket ss) 1472 throws IOException { 1473 super("HttpsProxyTunnel:" + ss.getLocalPort() + ":" + key, 1474 server, target, delegate); 1475 System.out.flush(); 1476 System.err.println("WARNING: HttpsProxyTunnel is an experimental test class"); 1477 this.ss = ss; 1478 start(); 1479 } 1480 start()1481 final void start() throws IOException { 1482 Thread t = new Thread(this, "ProxyThread"); 1483 t.setDaemon(true); 1484 t.start(); 1485 } 1486 1487 @Override getServerVersion()1488 public Version getServerVersion() { 1489 // serverImpl is not null when this proxy 1490 // serves a single server. It will be null 1491 // if this proxy can serve multiple servers. 1492 if (serverImpl != null) return serverImpl.getVersion(); 1493 return null; 1494 } 1495 1496 @Override stop()1497 public void stop() { 1498 stopped = true; 1499 if (serverImpl != null) { 1500 serverImpl.stop(); 1501 } 1502 if (redirect != null) { 1503 redirect.stop(); 1504 } 1505 try { 1506 ss.close(); 1507 } catch (IOException ex) { 1508 if (DEBUG) ex.printStackTrace(System.out); 1509 } 1510 } 1511 1512 1513 @Override configureAuthentication(HttpTestContext ctxt, HttpAuthSchemeType schemeType, HttpTestAuthenticator auth, HttpAuthType authType)1514 void configureAuthentication(HttpTestContext ctxt, 1515 HttpAuthSchemeType schemeType, 1516 HttpTestAuthenticator auth, 1517 HttpAuthType authType) { 1518 if (authType == HttpAuthType.PROXY || authType == HttpAuthType.PROXY305) { 1519 authorization = new ProxyAuthorization(key, schemeType, auth); 1520 } else { 1521 super.configureAuthentication(ctxt, schemeType, auth, authType); 1522 } 1523 } 1524 authorize(StringBuilder response, String requestLine, String headers)1525 boolean authorize(StringBuilder response, String requestLine, String headers) { 1526 if (authorization != null) { 1527 return authorization.authorize(response, requestLine, headers); 1528 } 1529 response.append("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); 1530 return true; 1531 } 1532 1533 // Pipe the input stream to the output stream. pipe(InputStream is, OutputStream os, char tag, CompletableFuture<Void> end)1534 private synchronized Thread pipe(InputStream is, OutputStream os, char tag, CompletableFuture<Void> end) { 1535 return new Thread("TunnelPipe("+tag+")") { 1536 @Override 1537 public void run() { 1538 try { 1539 try { 1540 int c; 1541 while ((c = is.read()) != -1) { 1542 os.write(c); 1543 os.flush(); 1544 // if DEBUG prints a + or a - for each transferred 1545 // character. 1546 if (DEBUG) System.out.print(tag); 1547 } 1548 is.close(); 1549 } finally { 1550 os.close(); 1551 } 1552 } catch (IOException ex) { 1553 if (DEBUG) ex.printStackTrace(System.out); 1554 } finally { 1555 end.complete(null); 1556 } 1557 } 1558 }; 1559 } 1560 1561 @Override 1562 public InetSocketAddress getAddress() { 1563 return new InetSocketAddress(InetAddress.getLoopbackAddress(), 1564 ss.getLocalPort()); 1565 } 1566 @Override 1567 public InetSocketAddress getProxyAddress() { 1568 return getAddress(); 1569 } 1570 @Override 1571 public InetSocketAddress getServerAddress() { 1572 // serverImpl can be null if this proxy can serve 1573 // multiple servers. 1574 if (serverImpl != null) { 1575 return serverImpl.getAddress(); 1576 } 1577 return null; 1578 } 1579 1580 1581 // This is a bit shaky. It doesn't handle continuation 1582 // lines, but our client shouldn't send any. 1583 // Read a line from the input stream, swallowing the final 1584 // \r\n sequence. Stops at the first \n, doesn't complain 1585 // if it wasn't preceded by '\r'. 1586 // 1587 String readLine(InputStream r) throws IOException { 1588 StringBuilder b = new StringBuilder(); 1589 int c; 1590 while ((c = r.read()) != -1) { 1591 if (c == '\n') break; 1592 b.appendCodePoint(c); 1593 } 1594 if (b.codePointAt(b.length() -1) == '\r') { 1595 b.delete(b.length() -1, b.length()); 1596 } 1597 return b.toString(); 1598 } 1599 1600 @Override 1601 public void run() { 1602 Socket clientConnection = null; 1603 try { 1604 while (!stopped) { 1605 System.out.println(now() + "Tunnel: Waiting for client"); 1606 Socket toClose; 1607 try { 1608 toClose = clientConnection = ss.accept(); 1609 if (NO_LINGER) { 1610 // can be useful to trigger "Connection reset by peer" 1611 // errors on the client side. 1612 clientConnection.setOption(StandardSocketOptions.SO_LINGER, 0); 1613 } 1614 } catch (IOException io) { 1615 if (DEBUG || !stopped) io.printStackTrace(System.out); 1616 break; 1617 } 1618 System.out.println(now() + "Tunnel: Client accepted"); 1619 StringBuilder headers = new StringBuilder(); 1620 Socket targetConnection = null; 1621 InputStream ccis = clientConnection.getInputStream(); 1622 OutputStream ccos = clientConnection.getOutputStream(); 1623 Writer w = new OutputStreamWriter( 1624 clientConnection.getOutputStream(), "UTF-8"); 1625 PrintWriter pw = new PrintWriter(w); 1626 System.out.println(now() + "Tunnel: Reading request line"); 1627 String requestLine = readLine(ccis); 1628 System.out.println(now() + "Tunnel: Request line: " + requestLine); 1629 if (requestLine.startsWith("CONNECT ")) { 1630 // We should probably check that the next word following 1631 // CONNECT is the host:port of our HTTPS serverImpl. 1632 // Some improvement for a followup! 1633 StringTokenizer tokenizer = new StringTokenizer(requestLine); 1634 String connect = tokenizer.nextToken(); 1635 assert connect.equalsIgnoreCase("connect"); 1636 String hostport = tokenizer.nextToken(); 1637 InetSocketAddress targetAddress; 1638 try { 1639 URI uri = new URI("https", hostport, "/", null, null); 1640 int port = uri.getPort(); 1641 port = port == -1 ? 443 : port; 1642 targetAddress = new InetSocketAddress(uri.getHost(), port); 1643 if (serverImpl != null) { 1644 assert targetAddress.getHostString() 1645 .equalsIgnoreCase(serverImpl.getAddress().getHostString()); 1646 assert targetAddress.getPort() == serverImpl.getAddress().getPort(); 1647 } 1648 } catch (Throwable x) { 1649 System.err.printf("Bad target address: \"%s\" in \"%s\"%n", 1650 hostport, requestLine); 1651 toClose.close(); 1652 continue; 1653 } 1654 1655 // Read all headers until we find the empty line that 1656 // signals the end of all headers. 1657 String line = requestLine; 1658 while(!line.equals("")) { 1659 System.out.println(now() + "Tunnel: Reading header: " 1660 + (line = readLine(ccis))); 1661 headers.append(line).append("\r\n"); 1662 } 1663 1664 StringBuilder response = new StringBuilder(); 1665 final boolean authorize = authorize(response, requestLine, headers.toString()); 1666 if (!authorize) { 1667 System.out.println(now() + "Tunnel: Sending " 1668 + response); 1669 // send the 407 response 1670 pw.print(response.toString()); 1671 pw.flush(); 1672 toClose.close(); 1673 continue; 1674 } 1675 System.out.println(now() 1676 + "Tunnel connecting to target server at " 1677 + targetAddress.getAddress() + ":" + targetAddress.getPort()); 1678 targetConnection = new Socket( 1679 targetAddress.getAddress(), 1680 targetAddress.getPort()); 1681 1682 // Then send the 200 OK response to the client 1683 System.out.println(now() + "Tunnel: Sending " 1684 + response); 1685 pw.print(response); 1686 pw.flush(); 1687 } else { 1688 // This should not happen. If it does then just print an 1689 // error - both on out and err, and close the accepted 1690 // socket 1691 System.out.println("WARNING: Tunnel: Unexpected status line: " 1692 + requestLine + " received by " 1693 + ss.getLocalSocketAddress() 1694 + " from " 1695 + toClose.getRemoteSocketAddress() 1696 + " - closing accepted socket"); 1697 // Print on err 1698 System.err.println("WARNING: Tunnel: Unexpected status line: " 1699 + requestLine + " received by " 1700 + ss.getLocalSocketAddress() 1701 + " from " 1702 + toClose.getRemoteSocketAddress()); 1703 // close accepted socket. 1704 toClose.close(); 1705 System.err.println("Tunnel: accepted socket closed."); 1706 continue; 1707 } 1708 1709 // Pipe the input stream of the client connection to the 1710 // output stream of the target connection and conversely. 1711 // Now the client and target will just talk to each other. 1712 System.out.println(now() + "Tunnel: Starting tunnel pipes"); 1713 CompletableFuture<Void> end, end1, end2; 1714 Thread t1 = pipe(ccis, targetConnection.getOutputStream(), '+', 1715 end1 = new CompletableFuture<>()); 1716 Thread t2 = pipe(targetConnection.getInputStream(), ccos, '-', 1717 end2 = new CompletableFuture<>()); 1718 end = CompletableFuture.allOf(end1, end2); 1719 end.whenComplete( 1720 (r,t) -> { 1721 try { toClose.close(); } catch (IOException x) { } 1722 finally {connectionCFs.remove(end);} 1723 }); 1724 connectionCFs.add(end); 1725 t1.start(); 1726 t2.start(); 1727 } 1728 } catch (Throwable ex) { 1729 try { 1730 ss.close(); 1731 } catch (IOException ex1) { 1732 ex.addSuppressed(ex1); 1733 } 1734 ex.printStackTrace(System.err); 1735 } finally { 1736 System.out.println(now() + "Tunnel: exiting (stopped=" + stopped + ")"); 1737 connectionCFs.forEach(cf -> cf.complete(null)); 1738 } 1739 } 1740 } 1741 1742 /** 1743 * Creates a TunnelingProxy that can serve multiple servers. 1744 * The server address is extracted from the CONNECT request line. 1745 * @param authScheme The authentication scheme supported by the proxy. 1746 * Typically one of DIGEST, BASIC, NONE. 1747 * @return A new TunnelingProxy able to serve multiple servers. 1748 * @throws IOException If the proxy could not be created. 1749 */ 1750 public static TunnelingProxy createHttpsProxyTunnel(HttpAuthSchemeType authScheme) 1751 throws IOException { 1752 HttpsProxyTunnel result = new HttpsProxyTunnel("", null, null, null); 1753 if (authScheme != HttpAuthSchemeType.NONE) { 1754 result.configureAuthentication(null, 1755 authScheme, 1756 AUTHENTICATOR, 1757 HttpAuthType.PROXY); 1758 } 1759 return result; 1760 } 1761 1762 private static String protocol(String protocol) { 1763 if ("http".equalsIgnoreCase(protocol)) return "http"; 1764 else if ("https".equalsIgnoreCase(protocol)) return "https"; 1765 else throw new InternalError("Unsupported protocol: " + protocol); 1766 } 1767 1768 public static URL url(String protocol, InetSocketAddress address, 1769 String path) throws MalformedURLException { 1770 return new URL(protocol(protocol), 1771 address.getHostString(), 1772 address.getPort(), path); 1773 } 1774 1775 public static URI uri(String protocol, InetSocketAddress address, 1776 String path) throws URISyntaxException { 1777 return new URI(protocol(protocol) + "://" + 1778 address.getHostString() + ":" + 1779 address.getPort() + path); 1780 } 1781 } 1782