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