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