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