1 /*
  2  * Copyright (c) 2018, 2023, 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 package jdk.httpclient.test.lib.common;
 24 
 25 import com.sun.net.httpserver.Authenticator;
 26 import com.sun.net.httpserver.BasicAuthenticator;
 27 import com.sun.net.httpserver.Filter;
 28 import com.sun.net.httpserver.Headers;
 29 import com.sun.net.httpserver.HttpContext;
 30 import com.sun.net.httpserver.HttpExchange;
 31 import com.sun.net.httpserver.HttpHandler;
 32 import com.sun.net.httpserver.HttpServer;
 33 import com.sun.net.httpserver.HttpsConfigurator;
 34 import com.sun.net.httpserver.HttpsServer;
 35 import jdk.httpclient.test.lib.http2.Http2Handler;
 36 import jdk.httpclient.test.lib.http2.Http2TestExchange;
 37 import jdk.httpclient.test.lib.http2.Http2TestServer;
 38 import jdk.internal.net.http.common.HttpHeadersBuilder;
 39 
 40 import java.net.InetAddress;
 41 import java.io.ByteArrayInputStream;
 42 import java.net.http.HttpClient.Version;
 43 import java.io.ByteArrayOutputStream;
 44 import java.io.IOException;
 45 import java.io.InputStream;
 46 import java.io.OutputStream;
 47 import java.io.PrintStream;
 48 import java.io.UncheckedIOException;
 49 import java.math.BigInteger;
 50 import java.net.InetSocketAddress;
 51 import java.net.URI;
 52 import java.net.http.HttpHeaders;
 53 import java.util.Base64;
 54 import java.util.List;
 55 import java.util.ListIterator;
 56 import java.util.Map;
 57 import java.util.Objects;
 58 import java.util.Optional;
 59 import java.util.Set;
 60 import java.util.concurrent.CopyOnWriteArrayList;
 61 import java.util.concurrent.ExecutorService;
 62 import java.util.logging.Level;
 63 import java.util.logging.Logger;
 64 import java.util.stream.Collectors;
 65 import java.util.stream.Stream;
 66 
 67 import javax.net.ssl.SSLContext;
 68 
 69 /**
 70  * Defines an adaptation layers so that a test server handlers and filters
 71  * can be implemented independently of the underlying server version.
 72  * <p>
 73  * For instance:
 74  * <pre>{@code
 75  *
 76  *  URI http1URI, http2URI;
 77  *
 78  *  InetSocketAddress sa = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
 79  *  HttpTestServer server1 = HttpTestServer.of(HttpServer.create(sa, 0));
 80  *  HttpTestContext context = server.addHandler(new HttpTestEchoHandler(), "/http1/echo");
 81  *  http2URI = "http://localhost:" + server1.getAddress().getPort() + "/http1/echo";
 82  *
 83  *  Http2TestServer http2TestServer = new Http2TestServer("localhost", false, 0);
 84  *  HttpTestServer server2 = HttpTestServer.of(http2TestServer);
 85  *  server2.addHandler(new HttpTestEchoHandler(), "/http2/echo");
 86  *  http1URI = "http://localhost:" + server2.getAddress().getPort() + "/http2/echo";
 87  *
 88  *  }</pre>
 89  */
 90 public interface HttpServerAdapters {
 91 
 92     static final boolean PRINTSTACK =
 93             Boolean.getBoolean("jdk.internal.httpclient.debug");
 94 
 95     static void uncheckedWrite(ByteArrayOutputStream baos, byte[] ba) {
 96         try {
 97             baos.write(ba);
 98         } catch (IOException e) {
 99             throw new UncheckedIOException(e);
100         }
101     }
102 
103     static void printBytes(PrintStream out, String prefix, byte[] bytes) {
104         int padding = 4 + 4 - (bytes.length % 4);
105         padding = padding > 4 ? padding - 4 : 4;
106         byte[] bigbytes = new byte[bytes.length + padding];
107         System.arraycopy(bytes, 0, bigbytes, padding, bytes.length);
108         out.println(prefix + bytes.length + " "
109                     + new BigInteger(bigbytes).toString(16));
110     }
111 
112     /**
113      * A version agnostic adapter class for HTTP request Headers.
114      */
115     public static abstract class HttpTestRequestHeaders {
116         public abstract Optional<String> firstValue(String name);
117         public abstract Set<String> keySet();
118         public abstract Set<Map.Entry<String, List<String>>> entrySet();
119         public abstract List<String> get(String name);
120         public abstract boolean containsKey(String name);
121         @Override
122         public boolean equals(Object o) {
123             if (this == o) return true;
124             if (!(o instanceof HttpTestRequestHeaders other)) return false;
125             return Objects.equals(entrySet(), other.entrySet());
126         }
127         @Override
128         public int hashCode() {
129             return Objects.hashCode(entrySet());
130         }
131 
132         public static HttpTestRequestHeaders of(Headers headers) {
133             return new Http1TestRequestHeaders(headers);
134         }
135 
136         public static HttpTestRequestHeaders of(HttpHeaders headers) {
137             return new Http2TestRequestHeaders(headers);
138         }
139 
140         private static final class Http1TestRequestHeaders extends HttpTestRequestHeaders {
141             private final Headers headers;
142             Http1TestRequestHeaders(Headers h) { this.headers = h; }
143             @Override
144             public Optional<String> firstValue(String name) {
145                 if (headers.containsKey(name)) {
146                     return Optional.ofNullable(headers.getFirst(name));
147                 }
148                 return Optional.empty();
149             }
150             @Override
151             public Set<String> keySet() { return headers.keySet(); }
152             @Override
153             public Set<Map.Entry<String, List<String>>> entrySet() {
154                 return headers.entrySet();
155             }
156             @Override
157             public List<String> get(String name) {
158                 return headers.get(name);
159             }
160             @Override
161             public boolean containsKey(String name) {
162                 return headers.containsKey(name);
163             }
164             @Override
165             public String toString() {
166                 return String.valueOf(headers);
167             }
168         }
169         private static final class Http2TestRequestHeaders extends HttpTestRequestHeaders {
170             private final HttpHeaders headers;
171             Http2TestRequestHeaders(HttpHeaders h) { this.headers = h; }
172             @Override
173             public Optional<String> firstValue(String name) {
174                 return headers.firstValue(name);
175             }
176             @Override
177             public Set<String> keySet() { return headers.map().keySet(); }
178             @Override
179             public Set<Map.Entry<String, List<String>>> entrySet() {
180                 return headers.map().entrySet();
181             }
182             @Override
183             public List<String> get(String name) {
184                 return headers.allValues(name);
185             }
186             @Override
187             public boolean containsKey(String name) {
188                 return headers.firstValue(name).isPresent();
189             }
190             @Override
191             public String toString() {
192                 return String.valueOf(headers);
193             }
194         }
195     }
196 
197     /**
198      * A version agnostic adapter class for HTTP response Headers.
199      */
200     public static abstract class HttpTestResponseHeaders {
201         public abstract void addHeader(String name, String value);
202 
203         public static HttpTestResponseHeaders of(Headers headers) {
204             return new Http1TestResponseHeaders(headers);
205         }
206         public static HttpTestResponseHeaders of(HttpHeadersBuilder headersBuilder) {
207             return new Http2TestResponseHeaders(headersBuilder);
208         }
209 
210         private final static class Http1TestResponseHeaders extends HttpTestResponseHeaders {
211             private final Headers headers;
212             Http1TestResponseHeaders(Headers h) { this.headers = h; }
213             @Override
214             public void addHeader(String name, String value) {
215                 headers.add(name, value);
216             }
217         }
218         private final static class Http2TestResponseHeaders extends HttpTestResponseHeaders {
219             private final HttpHeadersBuilder headersBuilder;
220             Http2TestResponseHeaders(HttpHeadersBuilder hb) { this.headersBuilder = hb; }
221             @Override
222             public void addHeader(String name, String value) {
223                 headersBuilder.addHeader(name, value);
224             }
225         }
226     }
227 
228     /**
229      * A version agnostic adapter class for HTTP Server Exchange.
230      */
231     public static abstract class HttpTestExchange implements AutoCloseable {
232         public abstract Version getServerVersion();
233         public abstract Version getExchangeVersion();
234         public abstract InputStream   getRequestBody();
235         public abstract OutputStream  getResponseBody();
236         public abstract HttpTestRequestHeaders getRequestHeaders();
237         public abstract HttpTestResponseHeaders getResponseHeaders();
238         public abstract void sendResponseHeaders(int code, int contentLength) throws IOException;
239         public abstract URI getRequestURI();
240         public abstract String getRequestMethod();
241         public abstract void close();
242         public abstract InetSocketAddress getRemoteAddress();
243         public void serverPush(URI uri, HttpHeaders headers, byte[] body) {
244             ByteArrayInputStream bais = new ByteArrayInputStream(body);
245             serverPush(uri, headers, bais);
246         }
247         public void serverPush(URI uri, HttpHeaders headers, InputStream body) {
248             throw new UnsupportedOperationException("serverPush with " + getExchangeVersion());
249         }
250         public boolean serverPushAllowed() {
251             return false;
252         }
253         public static HttpTestExchange of(HttpExchange exchange) {
254             return new Http1TestExchange(exchange);
255         }
256         public static HttpTestExchange of(Http2TestExchange exchange) {
257             return new Http2TestExchangeImpl(exchange);
258         }
259 
260         abstract void doFilter(Filter.Chain chain) throws IOException;
261 
262         // implementations...
263         private static final class Http1TestExchange extends HttpTestExchange {
264             private final HttpExchange exchange;
265             Http1TestExchange(HttpExchange exch) {
266                 this.exchange = exch;
267             }
268             @Override
269             public Version getServerVersion() { return Version.HTTP_1_1; }
270             @Override
271             public Version getExchangeVersion() { return Version.HTTP_1_1; }
272             @Override
273             public InputStream getRequestBody() {
274                 return exchange.getRequestBody();
275             }
276             @Override
277             public OutputStream getResponseBody() {
278                 return exchange.getResponseBody();
279             }
280             @Override
281             public HttpTestRequestHeaders getRequestHeaders() {
282                 return HttpTestRequestHeaders.of(exchange.getRequestHeaders());
283             }
284             @Override
285             public HttpTestResponseHeaders getResponseHeaders() {
286                 return HttpTestResponseHeaders.of(exchange.getResponseHeaders());
287             }
288             @Override
289             public void sendResponseHeaders(int code, int contentLength) throws IOException {
290                 if (contentLength == 0) contentLength = -1;
291                 else if (contentLength < 0) contentLength = 0;
292                 exchange.sendResponseHeaders(code, contentLength);
293             }
294             @Override
295             void doFilter(Filter.Chain chain) throws IOException {
296                 chain.doFilter(exchange);
297             }
298             @Override
299             public void close() { exchange.close(); }
300 
301             @Override
302             public InetSocketAddress getRemoteAddress() {
303                 return exchange.getRemoteAddress();
304             }
305 
306             @Override
307             public URI getRequestURI() { return exchange.getRequestURI(); }
308             @Override
309             public String getRequestMethod() { return exchange.getRequestMethod(); }
310             @Override
311             public String toString() {
312                 return this.getClass().getSimpleName() + ": " + exchange.toString();
313             }
314         }
315 
316         private static final class Http2TestExchangeImpl extends HttpTestExchange {
317             private final Http2TestExchange exchange;
318             Http2TestExchangeImpl(Http2TestExchange exch) {
319                 this.exchange = exch;
320             }
321             @Override
322             public Version getServerVersion() { return Version.HTTP_2; }
323             @Override
324             public Version getExchangeVersion() { return Version.HTTP_2; }
325             @Override
326             public InputStream getRequestBody() {
327                 return exchange.getRequestBody();
328             }
329             @Override
330             public OutputStream getResponseBody() {
331                 return exchange.getResponseBody();
332             }
333             @Override
334             public HttpTestRequestHeaders getRequestHeaders() {
335                 return HttpTestRequestHeaders.of(exchange.getRequestHeaders());
336             }
337 
338             @Override
339             public HttpTestResponseHeaders getResponseHeaders() {
340                 return HttpTestResponseHeaders.of(exchange.getResponseHeaders());
341             }
342             @Override
343             public void sendResponseHeaders(int code, int contentLength) throws IOException {
344                 if (contentLength == 0) contentLength = -1;
345                 else if (contentLength < 0) contentLength = 0;
346                 exchange.sendResponseHeaders(code, contentLength);
347             }
348             @Override
349             public boolean serverPushAllowed() {
350                 return exchange.serverPushAllowed();
351             }
352             @Override
353             public void serverPush(URI uri, HttpHeaders headers, InputStream body) {
354                 exchange.serverPush(uri, headers, body);
355             }
356             void doFilter(Filter.Chain filter) throws IOException {
357                 throw new IOException("cannot use HTTP/1.1 filter with HTTP/2 server");
358             }
359             @Override
360             public void close() { exchange.close();}
361 
362             @Override
363             public InetSocketAddress getRemoteAddress() {
364                 return exchange.getRemoteAddress();
365             }
366 
367             @Override
368             public URI getRequestURI() { return exchange.getRequestURI(); }
369             @Override
370             public String getRequestMethod() { return exchange.getRequestMethod(); }
371             @Override
372             public String toString() {
373                 return this.getClass().getSimpleName() + ": " + exchange.toString();
374             }
375         }
376 
377     }
378 
379 
380     /**
381      * A version agnostic adapter class for HTTP Server Handlers.
382      */
383     public interface HttpTestHandler {
384         void handle(HttpTestExchange t) throws IOException;
385 
386         default HttpHandler toHttpHandler() {
387             return (t) -> doHandle(HttpTestExchange.of(t));
388         }
389         default Http2Handler toHttp2Handler() {
390             return (t) -> doHandle(HttpTestExchange.of(t));
391         }
392         private void doHandle(HttpTestExchange t) throws IOException {
393             try {
394                 handle(t);
395             } catch (Throwable x) {
396                 System.out.println("WARNING: exception caught in HttpTestHandler::handle " + x);
397                 System.err.println("WARNING: exception caught in HttpTestHandler::handle " + x);
398                 if (PRINTSTACK && !expectException(t)) x.printStackTrace(System.out);
399                 throw x;
400             }
401         }
402     }
403 
404 
405     public static class HttpTestEchoHandler implements HttpTestHandler {
406         @Override
407         public void handle(HttpTestExchange t) throws IOException {
408             try (InputStream is = t.getRequestBody();
409                  OutputStream os = t.getResponseBody()) {
410                 byte[] bytes = is.readAllBytes();
411                 printBytes(System.out,"Echo server got "
412                         + t.getExchangeVersion() + " bytes: ", bytes);
413                 if (t.getRequestHeaders().firstValue("Content-type").isPresent()) {
414                     t.getResponseHeaders().addHeader("Content-type",
415                             t.getRequestHeaders().firstValue("Content-type").get());
416                 }
417                 t.sendResponseHeaders(200, bytes.length);
418                 os.write(bytes);
419             }
420         }
421     }
422 
423     public static boolean expectException(HttpTestExchange e) {
424         HttpTestRequestHeaders h = e.getRequestHeaders();
425         Optional<String> expectException = h.firstValue("X-expect-exception");
426         if (expectException.isPresent()) {
427             return expectException.get().equalsIgnoreCase("true");
428         }
429         return false;
430     }
431 
432     /**
433      * A version agnostic adapter class for HTTP Server Filter Chains.
434      */
435     public abstract class HttpChain {
436 
437         public abstract void doFilter(HttpTestExchange exchange) throws IOException;
438         public static HttpChain of(Filter.Chain chain) {
439             return new Http1Chain(chain);
440         }
441 
442         public static HttpChain of(List<HttpTestFilter> filters, HttpTestHandler handler) {
443             return new Http2Chain(filters, handler);
444         }
445 
446         private static class Http1Chain extends HttpChain {
447             final Filter.Chain chain;
448             Http1Chain(Filter.Chain chain) {
449                 this.chain = chain;
450             }
451             @Override
452             public void doFilter(HttpTestExchange exchange) throws IOException {
453                 try {
454                     exchange.doFilter(chain);
455                 } catch (Throwable t) {
456                     System.out.println("WARNING: exception caught in Http1Chain::doFilter " + t);
457                     System.err.println("WARNING: exception caught in Http1Chain::doFilter " + t);
458                     if (PRINTSTACK && !expectException(exchange)) t.printStackTrace(System.out);
459                     throw t;
460                 }
461             }
462         }
463 
464         private static class Http2Chain extends HttpChain {
465             ListIterator<HttpTestFilter> iter;
466             HttpTestHandler handler;
467             Http2Chain(List<HttpTestFilter> filters, HttpTestHandler handler) {
468                 this.iter = filters.listIterator();
469                 this.handler = handler;
470             }
471             @Override
472             public void doFilter(HttpTestExchange exchange) throws IOException {
473                 try {
474                     if (iter.hasNext()) {
475                         iter.next().doFilter(exchange, this);
476                     } else {
477                         handler.handle(exchange);
478                     }
479                 } catch (Throwable t) {
480                     System.out.println("WARNING: exception caught in Http2Chain::doFilter " + t);
481                     System.err.println("WARNING: exception caught in Http2Chain::doFilter " + t);
482                     if (PRINTSTACK && !expectException(exchange)) t.printStackTrace(System.out);
483                     throw t;
484                 }
485             }
486         }
487 
488     }
489 
490     /**
491      * A version agnostic adapter class for HTTP Server Filters.
492      */
493     public abstract class HttpTestFilter {
494 
495         public abstract String description();
496 
497         public abstract void doFilter(HttpTestExchange exchange, HttpChain chain) throws IOException;
498 
499         public Filter toFilter() {
500             return new Filter() {
501                 @Override
502                 public void doFilter(HttpExchange exchange, Chain chain) throws IOException {
503                     HttpTestFilter.this.doFilter(HttpTestExchange.of(exchange), HttpChain.of(chain));
504                 }
505                 @Override
506                 public String description() {
507                     return HttpTestFilter.this.description();
508                 }
509             };
510         }
511     }
512 
513     static String toString(HttpTestRequestHeaders headers) {
514         return headers.entrySet().stream()
515                 .map((e) -> e.getKey() + ": " + e.getValue())
516                 .collect(Collectors.joining("\n"));
517     }
518 
519     abstract static class AbstractHttpAuthFilter extends HttpTestFilter {
520 
521         public static final int HTTP_PROXY_AUTH = 407;
522         public static final int HTTP_UNAUTHORIZED = 401;
523         public enum HttpAuthMode {PROXY, SERVER}
524         final HttpAuthMode authType;
525         final String type;
526 
527         public AbstractHttpAuthFilter(HttpAuthMode authType, String type) {
528             this.authType = authType;
529             this.type = type;
530         }
531 
532         public final String type() {
533             return type;
534         }
535 
536         protected String getLocation() {
537             return "Location";
538         }
539         protected String getKeepAlive() {
540             return "keep-alive";
541         }
542         protected String getConnection() {
543             return authType == HttpAuthMode.PROXY ? "Proxy-Connection" : "Connection";
544         }
545 
546         protected String getAuthenticate() {
547             return authType == HttpAuthMode.PROXY
548                     ? "Proxy-Authenticate" : "WWW-Authenticate";
549         }
550         protected String getAuthorization() {
551             return authType == HttpAuthMode.PROXY
552                     ? "Proxy-Authorization" : "Authorization";
553         }
554         protected int getUnauthorizedCode() {
555             return authType == HttpAuthMode.PROXY
556                     ? HTTP_PROXY_AUTH
557                     : HTTP_UNAUTHORIZED;
558         }
559         protected abstract boolean isAuthentified(HttpTestExchange he) throws IOException;
560         protected abstract void requestAuthentication(HttpTestExchange he) throws IOException;
561         protected void accept(HttpTestExchange he, HttpChain chain) throws IOException {
562             chain.doFilter(he);
563         }
564 
565         @Override
566         public void doFilter(HttpTestExchange he, HttpChain chain) throws IOException {
567             try {
568                 System.out.println(type + ": Got " + he.getRequestMethod()
569                         + ": " + he.getRequestURI()
570                         + "\n" + HttpServerAdapters.toString(he.getRequestHeaders()));
571 
572                 // Assert only a single value for Expect. Not directly related
573                 // to digest authentication, but verifies good client behaviour.
574                 List<String> expectValues = he.getRequestHeaders().get("Expect");
575                 if (expectValues != null && expectValues.size() > 1) {
576                     throw new IOException("Expect:  " + expectValues);
577                 }
578 
579                 if (!isAuthentified(he)) {
580                     try {
581                         requestAuthentication(he);
582                         he.sendResponseHeaders(getUnauthorizedCode(), -1);
583                         System.out.println(type
584                                 + ": Sent back " + getUnauthorizedCode());
585                     } finally {
586                         he.close();
587                     }
588                 } else {
589                     accept(he, chain);
590                 }
591             } catch (RuntimeException | Error | IOException t) {
592                 System.err.println(type
593                         + ": Unexpected exception while handling request: " + t);
594                 t.printStackTrace(System.err);
595                 he.close();
596                 throw t;
597             }
598         }
599 
600     }
601 
602     public static class HttpBasicAuthFilter extends AbstractHttpAuthFilter {
603 
604             static String type(HttpAuthMode authType) {
605                 String type = authType == HttpAuthMode.SERVER
606                         ? "BasicAuth Server Filter" : "BasicAuth Proxy Filter";
607                 return "["+type+"]";
608             }
609 
610             final BasicAuthenticator auth;
611             public HttpBasicAuthFilter(BasicAuthenticator auth) {
612                 this(auth, HttpAuthMode.SERVER);
613             }
614 
615             public HttpBasicAuthFilter(BasicAuthenticator auth, HttpAuthMode authType) {
616                 this(auth, authType, type(authType));
617             }
618 
619             public HttpBasicAuthFilter(BasicAuthenticator auth, HttpAuthMode authType, String typeDesc) {
620                 super(authType, typeDesc);
621                 this.auth = auth;
622             }
623 
624             protected String getAuthValue() {
625                 return "Basic realm=\"" + auth.getRealm() + "\"";
626             }
627 
628             @Override
629             protected void requestAuthentication(HttpTestExchange he)
630                     throws IOException
631             {
632                 String headerName = getAuthenticate();
633                 String headerValue = getAuthValue();
634                 he.getResponseHeaders().addHeader(headerName, headerValue);
635                 System.out.println(type + ": Requesting Basic Authentication, "
636                         + headerName + " : "+ headerValue);
637             }
638 
639             @Override
640             protected boolean isAuthentified(HttpTestExchange he) {
641                 if (he.getRequestHeaders().containsKey(getAuthorization())) {
642                     List<String> authorization =
643                             he.getRequestHeaders().get(getAuthorization());
644                     for (String a : authorization) {
645                         System.out.println(type + ": processing " + a);
646                         int sp = a.indexOf(' ');
647                         if (sp < 0) return false;
648                         String scheme = a.substring(0, sp);
649                         if (!"Basic".equalsIgnoreCase(scheme)) {
650                             System.out.println(type + ": Unsupported scheme '"
651                                     + scheme +"'");
652                             return false;
653                         }
654                         if (a.length() <= sp+1) {
655                             System.out.println(type + ": value too short for '"
656                                     + scheme +"'");
657                             return false;
658                         }
659                         a = a.substring(sp+1);
660                         return validate(a);
661                     }
662                     return false;
663                 }
664                 return false;
665             }
666 
667             boolean validate(String a) {
668                 byte[] b = Base64.getDecoder().decode(a);
669                 String userpass = new String (b);
670                 int colon = userpass.indexOf (':');
671                 String uname = userpass.substring (0, colon);
672                 String pass = userpass.substring (colon+1);
673                 return auth.checkCredentials(uname, pass);
674             }
675 
676         @Override
677         public String description() {
678             return "HttpBasicAuthFilter";
679         }
680     }
681 
682     /**
683      * A version agnostic adapter class for HTTP Server Context.
684      */
685     public static abstract class HttpTestContext {
686         public abstract String getPath();
687         public abstract void addFilter(HttpTestFilter filter);
688         public abstract Version getVersion();
689 
690         // will throw UOE if the server is HTTP/2 or Authenticator is not a BasicAuthenticator
691         public abstract void setAuthenticator(com.sun.net.httpserver.Authenticator authenticator);
692     }
693 
694     /**
695      * A version agnostic adapter class for HTTP Servers.
696      */
697     public static abstract class HttpTestServer {
698         private static final class ServerLogging {
699             private static final Logger logger = Logger.getLogger("com.sun.net.httpserver");
700             static void enableLogging() {
701                 logger.setLevel(Level.FINE);
702                 Stream.of(Logger.getLogger("").getHandlers())
703                         .forEach(h -> h.setLevel(Level.ALL));
704             }
705         }
706 
707         public abstract void start();
708         public abstract void stop();
709         public abstract HttpTestContext addHandler(HttpTestHandler handler, String root);
710         public abstract InetSocketAddress getAddress();
711         public abstract Version getVersion();
712 
713         public String serverAuthority() {
714             InetSocketAddress address = getAddress();
715             String hostString = address.getHostString();
716             hostString = address.getAddress().isLoopbackAddress() || hostString.equals("localhost")
717                     ? address.getAddress().getHostAddress() // use the raw IP address, if loopback
718                     : hostString; // use whatever host string was used to construct the address
719             hostString = hostString.contains(":")
720                     ? "[" + hostString + "]"
721                     : hostString;
722             return hostString + ":" + address.getPort();
723         }
724 
725         public static HttpTestServer of(HttpServer server) {
726             return new Http1TestServer(server);
727         }
728 
729         public static HttpTestServer of(HttpServer server, ExecutorService executor) {
730             return new Http1TestServer(server, executor);
731         }
732 
733         public static HttpTestServer of(Http2TestServer server) {
734             return new Http2TestServerImpl(server);
735         }
736 
737         /**
738          * Creates a {@link HttpTestServer} which supports the {@code serverVersion}. The server
739          * will only be available on {@code http} protocol. {@code https} will not be supported
740          * by the returned server
741          *
742          * @param serverVersion The HTTP version of the server
743          * @return The newly created server
744          * @throws IllegalArgumentException if {@code serverVersion} is not supported by this method
745          * @throws IOException if any exception occurs during the server creation
746          */
747         public static HttpTestServer create(Version serverVersion) throws IOException {
748             Objects.requireNonNull(serverVersion);
749             return create(serverVersion, null);
750         }
751 
752         /**
753          * Creates a {@link HttpTestServer} which supports the {@code serverVersion}. If the
754          * {@code sslContext} is null, then only {@code http} protocol will be supported by the
755          * server. Else, the server will be configured with the {@code sslContext} and will support
756          * {@code https} protocol.
757          *
758          * @param serverVersion The HTTP version of the server
759          * @param sslContext    The SSLContext to use. Can be null
760          * @return The newly created server
761          * @throws IllegalArgumentException if {@code serverVersion} is not supported by this method
762          * @throws IOException if any exception occurs during the server creation
763          */
764         public static HttpTestServer create(Version serverVersion, SSLContext sslContext)
765                 throws IOException {
766             Objects.requireNonNull(serverVersion);
767             return create(serverVersion, sslContext, null);
768         }
769 
770         /**
771          * Creates a {@link HttpTestServer} which supports the {@code serverVersion}. If the
772          * {@code sslContext} is null, then only {@code http} protocol will be supported by the
773          * server. Else, the server will be configured with the {@code sslContext} and will support
774          * {@code https} protocol.
775          *
776          * @param serverVersion The HTTP version of the server
777          * @param sslContext    The SSLContext to use. Can be null
778          * @param executor      The executor to be used by the server. Can be null
779          * @return The newly created server
780          * @throws IllegalArgumentException if {@code serverVersion} is not supported by this method
781          * @throws IOException if any exception occurs during the server creation
782          */
783         public static HttpTestServer create(Version serverVersion, SSLContext sslContext,
784                                             ExecutorService executor) throws IOException {
785             Objects.requireNonNull(serverVersion);
786             switch (serverVersion) {
787                 case HTTP_2 -> {
788                     Http2TestServer underlying;
789                     try {
790                         underlying = sslContext == null
791                                 ? new Http2TestServer("localhost", false, 0, executor, null) // HTTP
792                                 : new Http2TestServer("localhost", true, 0, executor, sslContext); // HTTPS
793                     } catch (IOException ioe) {
794                         throw ioe;
795                     } catch (Exception e) {
796                         throw new IOException(e);
797                     }
798                     return HttpTestServer.of(underlying);
799                 }
800                 case HTTP_1_1 ->  {
801                     InetSocketAddress sa = new InetSocketAddress(
802                             InetAddress.getLoopbackAddress(), 0);
803                     HttpServer underlying;
804                     if (sslContext == null) {
805                         underlying = HttpServer.create(sa, 0); // HTTP
806                     } else {
807                         HttpsServer https = HttpsServer.create(sa, 0); // HTTPS
808                         https.setHttpsConfigurator(new HttpsConfigurator(sslContext));
809                         underlying = https;
810                     }
811                     if (executor != null) {
812                         underlying.setExecutor(executor);
813                     }
814                     return HttpTestServer.of(underlying);
815                 }
816                 default -> throw new IllegalArgumentException("Unsupported HTTP version "
817                         + serverVersion);
818             }
819         }
820 
821         private static class Http1TestServer extends  HttpTestServer {
822             private final HttpServer impl;
823             private final ExecutorService executor;
824             Http1TestServer(HttpServer server) {
825                 this(server, null);
826             }
827             Http1TestServer(HttpServer server, ExecutorService executor) {
828                 if (executor != null) server.setExecutor(executor);
829                 this.executor = executor;
830                 this.impl = server;
831             }
832             @Override
833             public void start() {
834                 System.out.println("Http1TestServer: start");
835                 impl.start();
836             }
837             @Override
838             public void stop() {
839                 System.out.println("Http1TestServer: stop");
840                 try {
841                     impl.stop(0);
842                 } finally {
843                     if (executor != null) {
844                         executor.shutdownNow();
845                     }
846                 }
847             }
848             @Override
849             public HttpTestContext addHandler(HttpTestHandler handler, String path) {
850                 System.out.println("Http1TestServer[" + getAddress()
851                         + "]::addHandler " + handler + ", " + path);
852                 return new Http1TestContext(impl.createContext(path, handler.toHttpHandler()));
853             }
854             @Override
855             public InetSocketAddress getAddress() {
856                 return new InetSocketAddress(InetAddress.getLoopbackAddress(),
857                         impl.getAddress().getPort());
858             }
859             public Version getVersion() { return Version.HTTP_1_1; }
860         }
861 
862         private static class Http1TestContext extends HttpTestContext {
863             private final HttpContext context;
864             Http1TestContext(HttpContext ctxt) {
865                 this.context = ctxt;
866             }
867             @Override public String getPath() {
868                 return context.getPath();
869             }
870             @Override
871             public void addFilter(HttpTestFilter filter) {
872                 System.out.println("Http1TestContext::addFilter " + filter.description());
873                 context.getFilters().add(filter.toFilter());
874             }
875             @Override
876             public void setAuthenticator(com.sun.net.httpserver.Authenticator authenticator) {
877                 context.setAuthenticator(authenticator);
878             }
879             @Override public Version getVersion() { return Version.HTTP_1_1; }
880         }
881 
882         private static class Http2TestServerImpl extends  HttpTestServer {
883             private final Http2TestServer impl;
884             Http2TestServerImpl(Http2TestServer server) {
885                 this.impl = server;
886             }
887             @Override
888             public void start() {
889                 System.out.println("Http2TestServerImpl: start");
890                 impl.start();
891             }
892             @Override
893             public void stop() {
894                 System.out.println("Http2TestServerImpl: stop");
895                 impl.stop();
896             }
897             @Override
898             public HttpTestContext addHandler(HttpTestHandler handler, String path) {
899                 System.out.println("Http2TestServerImpl[" + getAddress()
900                                    + "]::addHandler " + handler + ", " + path);
901                 Http2TestContext context = new Http2TestContext(handler, path);
902                 impl.addHandler(context.toHttp2Handler(), path);
903                 return context;
904             }
905             @Override
906             public InetSocketAddress getAddress() {
907                 return new InetSocketAddress(InetAddress.getLoopbackAddress(),
908                         impl.getAddress().getPort());
909             }
910             public Version getVersion() { return Version.HTTP_2; }
911         }
912 
913         private static class Http2TestContext
914                 extends HttpTestContext implements HttpTestHandler {
915             private final HttpTestHandler handler;
916             private final String path;
917             private final List<HttpTestFilter> filters = new CopyOnWriteArrayList<>();
918             Http2TestContext(HttpTestHandler hdl, String path) {
919                 this.handler = hdl;
920                 this.path = path;
921             }
922             @Override
923             public String getPath() { return path; }
924             @Override
925             public void addFilter(HttpTestFilter filter) {
926                 System.out.println("Http2TestContext::addFilter " + filter.description());
927                 filters.add(filter);
928             }
929             @Override
930             public void handle(HttpTestExchange exchange) throws IOException {
931                 System.out.println("Http2TestContext::handle " + exchange);
932                 HttpChain.of(filters, handler).doFilter(exchange);
933             }
934             @Override
935             public void setAuthenticator(final Authenticator authenticator) {
936                 if (authenticator instanceof BasicAuthenticator basicAuth) {
937                     addFilter(new HttpBasicAuthFilter(basicAuth));
938                 } else {
939                     throw new UnsupportedOperationException(
940                             "only BasicAuthenticator is supported on HTTP/2 context");
941                 }
942             }
943             @Override public Version getVersion() { return Version.HTTP_2; }
944         }
945     }
946 
947     public static void enableServerLogging() {
948         System.setProperty("java.util.logging.SimpleFormatter.format",
949                 "%4$s [%1$tb %1$td, %1$tl:%1$tM:%1$tS.%1$tN] %2$s: %5$s%6$s%n");
950         HttpTestServer.ServerLogging.enableLogging();
951     }
952 
953 }