/* * Copyright (c) 2018, 2020, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ import com.sun.net.httpserver.Filter; import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.HttpContext; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import jdk.internal.net.http.common.HttpHeadersBuilder; import java.net.InetAddress; import java.io.ByteArrayInputStream; import java.net.http.HttpClient.Version; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintStream; import java.io.UncheckedIOException; import java.math.BigInteger; import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpHeaders; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Stream; /** * Defines an adaptation layers so that a test server handlers and filters * can be implemented independently of the underlying server version. *

* For instance: *

{@code
 *
 *  URI http1URI, http2URI;
 *
 *  InetSocketAddress sa = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
 *  HttpTestServer server1 = HttpTestServer.of(HttpServer.create(sa, 0));
 *  HttpTestContext context = server.addHandler(new HttpTestEchoHandler(), "/http1/echo");
 *  http2URI = "http://localhost:" + server1.getAddress().getPort() + "/http1/echo";
 *
 *  Http2TestServer http2TestServer = new Http2TestServer("localhost", false, 0);
 *  HttpTestServer server2 = HttpTestServer.of(http2TestServer);
 *  server2.addHandler(new HttpTestEchoHandler(), "/http2/echo");
 *  http1URI = "http://localhost:" + server2.getAddress().getPort() + "/http2/echo";
 *
 *  }
*/ public interface HttpServerAdapters { static final boolean PRINTSTACK = Boolean.getBoolean("jdk.internal.httpclient.debug"); static void uncheckedWrite(ByteArrayOutputStream baos, byte[] ba) { try { baos.write(ba); } catch (IOException e) { throw new UncheckedIOException(e); } } static void printBytes(PrintStream out, String prefix, byte[] bytes) { int padding = 4 + 4 - (bytes.length % 4); padding = padding > 4 ? padding - 4 : 4; byte[] bigbytes = new byte[bytes.length + padding]; System.arraycopy(bytes, 0, bigbytes, padding, bytes.length); out.println(prefix + bytes.length + " " + new BigInteger(bigbytes).toString(16)); } /** * A version agnostic adapter class for HTTP request Headers. */ public static abstract class HttpTestRequestHeaders { public abstract Optional firstValue(String name); public abstract Set keySet(); public abstract Set>> entrySet(); public abstract List get(String name); public abstract boolean containsKey(String name); public static HttpTestRequestHeaders of(Headers headers) { return new Http1TestRequestHeaders(headers); } public static HttpTestRequestHeaders of(HttpHeaders headers) { return new Http2TestRequestHeaders(headers); } private static final class Http1TestRequestHeaders extends HttpTestRequestHeaders { private final Headers headers; Http1TestRequestHeaders(Headers h) { this.headers = h; } @Override public Optional firstValue(String name) { if (headers.containsKey(name)) { return Optional.ofNullable(headers.getFirst(name)); } return Optional.empty(); } @Override public Set keySet() { return headers.keySet(); } @Override public Set>> entrySet() { return headers.entrySet(); } @Override public List get(String name) { return headers.get(name); } @Override public boolean containsKey(String name) { return headers.containsKey(name); } } private static final class Http2TestRequestHeaders extends HttpTestRequestHeaders { private final HttpHeaders headers; Http2TestRequestHeaders(HttpHeaders h) { this.headers = h; } @Override public Optional firstValue(String name) { return headers.firstValue(name); } @Override public Set keySet() { return headers.map().keySet(); } @Override public Set>> entrySet() { return headers.map().entrySet(); } @Override public List get(String name) { return headers.allValues(name); } @Override public boolean containsKey(String name) { return headers.firstValue(name).isPresent(); } } } /** * A version agnostic adapter class for HTTP response Headers. */ public static abstract class HttpTestResponseHeaders { public abstract void addHeader(String name, String value); public static HttpTestResponseHeaders of(Headers headers) { return new Http1TestResponseHeaders(headers); } public static HttpTestResponseHeaders of(HttpHeadersBuilder headersBuilder) { return new Http2TestResponseHeaders(headersBuilder); } private final static class Http1TestResponseHeaders extends HttpTestResponseHeaders { private final Headers headers; Http1TestResponseHeaders(Headers h) { this.headers = h; } @Override public void addHeader(String name, String value) { headers.add(name, value); } } private final static class Http2TestResponseHeaders extends HttpTestResponseHeaders { private final HttpHeadersBuilder headersBuilder; Http2TestResponseHeaders(HttpHeadersBuilder hb) { this.headersBuilder = hb; } @Override public void addHeader(String name, String value) { headersBuilder.addHeader(name, value); } } } /** * A version agnostic adapter class for HTTP Server Exchange. */ public static abstract class HttpTestExchange implements AutoCloseable { public abstract Version getServerVersion(); public abstract Version getExchangeVersion(); public abstract InputStream getRequestBody(); public abstract OutputStream getResponseBody(); public abstract HttpTestRequestHeaders getRequestHeaders(); public abstract HttpTestResponseHeaders getResponseHeaders(); public abstract void sendResponseHeaders(int code, int contentLength) throws IOException; public abstract URI getRequestURI(); public abstract String getRequestMethod(); public abstract void close(); public void serverPush(URI uri, HttpHeaders headers, byte[] body) { ByteArrayInputStream bais = new ByteArrayInputStream(body); serverPush(uri, headers, bais); } public void serverPush(URI uri, HttpHeaders headers, InputStream body) { throw new UnsupportedOperationException("serverPush with " + getExchangeVersion()); } public boolean serverPushAllowed() { return false; } public static HttpTestExchange of(HttpExchange exchange) { return new Http1TestExchange(exchange); } public static HttpTestExchange of(Http2TestExchange exchange) { return new Http2TestExchangeImpl(exchange); } abstract void doFilter(Filter.Chain chain) throws IOException; // implementations... private static final class Http1TestExchange extends HttpTestExchange { private final HttpExchange exchange; Http1TestExchange(HttpExchange exch) { this.exchange = exch; } @Override public Version getServerVersion() { return Version.HTTP_1_1; } @Override public Version getExchangeVersion() { return Version.HTTP_1_1; } @Override public InputStream getRequestBody() { return exchange.getRequestBody(); } @Override public OutputStream getResponseBody() { return exchange.getResponseBody(); } @Override public HttpTestRequestHeaders getRequestHeaders() { return HttpTestRequestHeaders.of(exchange.getRequestHeaders()); } @Override public HttpTestResponseHeaders getResponseHeaders() { return HttpTestResponseHeaders.of(exchange.getResponseHeaders()); } @Override public void sendResponseHeaders(int code, int contentLength) throws IOException { if (contentLength == 0) contentLength = -1; else if (contentLength < 0) contentLength = 0; exchange.sendResponseHeaders(code, contentLength); } @Override void doFilter(Filter.Chain chain) throws IOException { chain.doFilter(exchange); } @Override public void close() { exchange.close(); } @Override public URI getRequestURI() { return exchange.getRequestURI(); } @Override public String getRequestMethod() { return exchange.getRequestMethod(); } @Override public String toString() { return this.getClass().getSimpleName() + ": " + exchange.toString(); } } private static final class Http2TestExchangeImpl extends HttpTestExchange { private final Http2TestExchange exchange; Http2TestExchangeImpl(Http2TestExchange exch) { this.exchange = exch; } @Override public Version getServerVersion() { return Version.HTTP_2; } @Override public Version getExchangeVersion() { return Version.HTTP_2; } @Override public InputStream getRequestBody() { return exchange.getRequestBody(); } @Override public OutputStream getResponseBody() { return exchange.getResponseBody(); } @Override public HttpTestRequestHeaders getRequestHeaders() { return HttpTestRequestHeaders.of(exchange.getRequestHeaders()); } @Override public HttpTestResponseHeaders getResponseHeaders() { return HttpTestResponseHeaders.of(exchange.getResponseHeaders()); } @Override public void sendResponseHeaders(int code, int contentLength) throws IOException { if (contentLength == 0) contentLength = -1; else if (contentLength < 0) contentLength = 0; exchange.sendResponseHeaders(code, contentLength); } @Override public boolean serverPushAllowed() { return exchange.serverPushAllowed(); } @Override public void serverPush(URI uri, HttpHeaders headers, InputStream body) { exchange.serverPush(uri, headers, body); } void doFilter(Filter.Chain filter) throws IOException { throw new IOException("cannot use HTTP/1.1 filter with HTTP/2 server"); } @Override public void close() { exchange.close();} @Override public URI getRequestURI() { return exchange.getRequestURI(); } @Override public String getRequestMethod() { return exchange.getRequestMethod(); } @Override public String toString() { return this.getClass().getSimpleName() + ": " + exchange.toString(); } } } /** * A version agnostic adapter class for HTTP Server Handlers. */ public interface HttpTestHandler { void handle(HttpTestExchange t) throws IOException; default HttpHandler toHttpHandler() { return (t) -> doHandle(HttpTestExchange.of(t)); } default Http2Handler toHttp2Handler() { return (t) -> doHandle(HttpTestExchange.of(t)); } private void doHandle(HttpTestExchange t) throws IOException { try { handle(t); } catch (Throwable x) { System.out.println("WARNING: exception caught in HttpTestHandler::handle " + x); System.err.println("WARNING: exception caught in HttpTestHandler::handle " + x); if (PRINTSTACK && !expectException(t)) x.printStackTrace(System.out); throw x; } } } public static class HttpTestEchoHandler implements HttpTestHandler { @Override public void handle(HttpTestExchange t) throws IOException { try (InputStream is = t.getRequestBody(); OutputStream os = t.getResponseBody()) { byte[] bytes = is.readAllBytes(); printBytes(System.out,"Echo server got " + t.getExchangeVersion() + " bytes: ", bytes); if (t.getRequestHeaders().firstValue("Content-type").isPresent()) { t.getResponseHeaders().addHeader("Content-type", t.getRequestHeaders().firstValue("Content-type").get()); } t.sendResponseHeaders(200, bytes.length); os.write(bytes); } } } public static boolean expectException(HttpTestExchange e) { HttpTestRequestHeaders h = e.getRequestHeaders(); Optional expectException = h.firstValue("X-expect-exception"); if (expectException.isPresent()) { return expectException.get().equalsIgnoreCase("true"); } return false; } /** * A version agnostic adapter class for HTTP Server Filter Chains. */ public abstract class HttpChain { public abstract void doFilter(HttpTestExchange exchange) throws IOException; public static HttpChain of(Filter.Chain chain) { return new Http1Chain(chain); } public static HttpChain of(List filters, HttpTestHandler handler) { return new Http2Chain(filters, handler); } private static class Http1Chain extends HttpChain { final Filter.Chain chain; Http1Chain(Filter.Chain chain) { this.chain = chain; } @Override public void doFilter(HttpTestExchange exchange) throws IOException { try { exchange.doFilter(chain); } catch (Throwable t) { System.out.println("WARNING: exception caught in Http1Chain::doFilter " + t); System.err.println("WARNING: exception caught in Http1Chain::doFilter " + t); if (PRINTSTACK && !expectException(exchange)) t.printStackTrace(System.out); throw t; } } } private static class Http2Chain extends HttpChain { ListIterator iter; HttpTestHandler handler; Http2Chain(List filters, HttpTestHandler handler) { this.iter = filters.listIterator(); this.handler = handler; } @Override public void doFilter(HttpTestExchange exchange) throws IOException { try { if (iter.hasNext()) { iter.next().doFilter(exchange, this); } else { handler.handle(exchange); } } catch (Throwable t) { System.out.println("WARNING: exception caught in Http2Chain::doFilter " + t); System.err.println("WARNING: exception caught in Http2Chain::doFilter " + t); if (PRINTSTACK && !expectException(exchange)) t.printStackTrace(System.out); throw t; } } } } /** * A version agnostic adapter class for HTTP Server Filters. */ public abstract class HttpTestFilter { public abstract String description(); public abstract void doFilter(HttpTestExchange exchange, HttpChain chain) throws IOException; public Filter toFilter() { return new Filter() { @Override public void doFilter(HttpExchange exchange, Chain chain) throws IOException { HttpTestFilter.this.doFilter(HttpTestExchange.of(exchange), HttpChain.of(chain)); } @Override public String description() { return HttpTestFilter.this.description(); } }; } } /** * A version agnostic adapter class for HTTP Server Context. */ public static abstract class HttpTestContext { public abstract String getPath(); public abstract void addFilter(HttpTestFilter filter); public abstract Version getVersion(); // will throw UOE if the server is HTTP/2 public abstract void setAuthenticator(com.sun.net.httpserver.Authenticator authenticator); } /** * A version agnostic adapter class for HTTP Servers. */ public static abstract class HttpTestServer { private static final class ServerLogging { private static final Logger logger = Logger.getLogger("com.sun.net.httpserver"); static void enableLogging() { logger.setLevel(Level.FINE); Stream.of(Logger.getLogger("").getHandlers()) .forEach(h -> h.setLevel(Level.ALL)); } } public abstract void start(); public abstract void stop(); public abstract HttpTestContext addHandler(HttpTestHandler handler, String root); public abstract InetSocketAddress getAddress(); public abstract Version getVersion(); public String serverAuthority() { return InetAddress.getLoopbackAddress().getHostName() + ":" + getAddress().getPort(); } public static HttpTestServer of(HttpServer server) { return new Http1TestServer(server); } public static HttpTestServer of(HttpServer server, ExecutorService executor) { return new Http1TestServer(server, executor); } public static HttpTestServer of(Http2TestServer server) { return new Http2TestServerImpl(server); } private static class Http1TestServer extends HttpTestServer { private final HttpServer impl; private final ExecutorService executor; Http1TestServer(HttpServer server) { this(server, null); } Http1TestServer(HttpServer server, ExecutorService executor) { if (executor != null) server.setExecutor(executor); this.executor = executor; this.impl = server; } @Override public void start() { System.out.println("Http1TestServer: start"); impl.start(); } @Override public void stop() { System.out.println("Http1TestServer: stop"); try { impl.stop(0); } finally { if (executor != null) { executor.shutdownNow(); } } } @Override public HttpTestContext addHandler(HttpTestHandler handler, String path) { System.out.println("Http1TestServer[" + getAddress() + "]::addHandler " + handler + ", " + path); return new Http1TestContext(impl.createContext(path, handler.toHttpHandler())); } @Override public InetSocketAddress getAddress() { return new InetSocketAddress(InetAddress.getLoopbackAddress(), impl.getAddress().getPort()); } public Version getVersion() { return Version.HTTP_1_1; } } private static class Http1TestContext extends HttpTestContext { private final HttpContext context; Http1TestContext(HttpContext ctxt) { this.context = ctxt; } @Override public String getPath() { return context.getPath(); } @Override public void addFilter(HttpTestFilter filter) { System.out.println("Http1TestContext::addFilter " + filter.description()); context.getFilters().add(filter.toFilter()); } @Override public void setAuthenticator(com.sun.net.httpserver.Authenticator authenticator) { context.setAuthenticator(authenticator); } @Override public Version getVersion() { return Version.HTTP_1_1; } } private static class Http2TestServerImpl extends HttpTestServer { private final Http2TestServer impl; Http2TestServerImpl(Http2TestServer server) { this.impl = server; } @Override public void start() { System.out.println("Http2TestServerImpl: start"); impl.start(); } @Override public void stop() { System.out.println("Http2TestServerImpl: stop"); impl.stop(); } @Override public HttpTestContext addHandler(HttpTestHandler handler, String path) { System.out.println("Http2TestServerImpl[" + getAddress() + "]::addHandler " + handler + ", " + path); Http2TestContext context = new Http2TestContext(handler, path); impl.addHandler(context.toHttp2Handler(), path); return context; } @Override public InetSocketAddress getAddress() { return new InetSocketAddress(InetAddress.getLoopbackAddress(), impl.getAddress().getPort()); } public Version getVersion() { return Version.HTTP_2; } } private static class Http2TestContext extends HttpTestContext implements HttpTestHandler { private final HttpTestHandler handler; private final String path; private final List filters = new CopyOnWriteArrayList<>(); Http2TestContext(HttpTestHandler hdl, String path) { this.handler = hdl; this.path = path; } @Override public String getPath() { return path; } @Override public void addFilter(HttpTestFilter filter) { System.out.println("Http2TestContext::addFilter " + filter.description()); filters.add(filter); } @Override public void handle(HttpTestExchange exchange) throws IOException { System.out.println("Http2TestContext::handle " + exchange); HttpChain.of(filters, handler).doFilter(exchange); } @Override public void setAuthenticator(com.sun.net.httpserver.Authenticator authenticator) { throw new UnsupportedOperationException("Can't set HTTP/1.1 authenticator on HTTP/2 context"); } @Override public Version getVersion() { return Version.HTTP_2; } } } public static void enableServerLogging() { System.setProperty("java.util.logging.SimpleFormatter.format", "%4$s [%1$tb %1$td, %1$tl:%1$tM:%1$tS.%1$tN] %2$s: %5$s%6$s%n"); HttpTestServer.ServerLogging.enableLogging(); } }