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