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