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