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