blob: 5147afa4f3278eb713ef8c70a84a1cae434fbabd [file] [log] [blame]
/*
* Copyright (c) 2018, 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.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.
* <p>
* For instance:
* <pre>{@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";
*
* }</pre>
*/
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<String> firstValue(String name);
public abstract Set<String> keySet();
public abstract Set<Map.Entry<String, List<String>>> entrySet();
public abstract List<String> 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<String> firstValue(String name) {
if (headers.containsKey(name)) {
return Optional.ofNullable(headers.getFirst(name));
}
return Optional.empty();
}
@Override
public Set<String> keySet() { return headers.keySet(); }
@Override
public Set<Map.Entry<String, List<String>>> entrySet() {
return headers.entrySet();
}
@Override
public List<String> 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<String> firstValue(String name) {
return headers.firstValue(name);
}
@Override
public Set<String> keySet() { return headers.map().keySet(); }
@Override
public Set<Map.Entry<String, List<String>>> entrySet() {
return headers.map().entrySet();
}
@Override
public List<String> 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 {
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<String> 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<HttpTestFilter> 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<HttpTestFilter> iter;
HttpTestHandler handler;
Http2Chain(List<HttpTestFilter> 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(Http2TestServer server) {
return new Http2TestServerImpl(server);
}
private static class Http1TestServer extends HttpTestServer {
private final HttpServer impl;
Http1TestServer(HttpServer server) {
this.impl = server;
}
@Override
public void start() {
System.out.println("Http1TestServer: start");
impl.start();
}
@Override
public void stop() {
System.out.println("Http1TestServer: stop");
impl.stop(0);
}
@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<HttpTestFilter> 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();
}
}