diff --git a/src/com/googlecode/utterlyidle/ServerConfiguration.java b/src/com/googlecode/utterlyidle/ServerConfiguration.java index 703a6af1..73455e1a 100644 --- a/src/com/googlecode/utterlyidle/ServerConfiguration.java +++ b/src/com/googlecode/utterlyidle/ServerConfiguration.java @@ -1,12 +1,17 @@ package com.googlecode.utterlyidle; +import com.googlecode.totallylazy.Option; import com.googlecode.totallylazy.io.Uri; import com.googlecode.utterlyidle.httpserver.RestServer; +import com.googlecode.utterlyidle.ssl.SSL; +import javax.net.ssl.SSLContext; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.Properties; +import static com.googlecode.totallylazy.Option.none; +import static com.googlecode.totallylazy.Option.some; import static com.googlecode.totallylazy.Unchecked.cast; import static com.googlecode.totallylazy.io.Uri.uri; import static java.lang.Integer.valueOf; @@ -27,33 +32,34 @@ public class ServerConfiguration { public static final String DEFAULT_PORT = "0"; public static final String DEFAULT_CLASS = RestServer.class.getCanonicalName(); - private final String protocol; private final BasePath basePath; private final int maxThreadNumber; private final InetAddress bindAddress; private final int port; private final Class serverClass; + private final Option sslContext; - public ServerConfiguration(String protocol, BasePath basePath, int maxThreadNumber, InetAddress bindAddress, int port, Class serverClass) { - this.protocol = protocol; + public ServerConfiguration(BasePath basePath, int maxThreadNumber, InetAddress bindAddress, int port, Class serverClass, final Option sslContext) { this.basePath = basePath; this.maxThreadNumber = maxThreadNumber; this.bindAddress = bindAddress; this.port = port; this.serverClass = serverClass; + this.sslContext = sslContext; } public ServerConfiguration() { - this(DEFAULT_PROTOCOL, new BasePath(DEFAULT_BASE_PATH), Integer.parseInt(DEFAULT_THREAD_NUMBER), toInetAddress(DEFAULT_BIND_ADDRESS), Integer.parseInt(DEFAULT_PORT), toServer(DEFAULT_CLASS)); + this(new BasePath(DEFAULT_BASE_PATH), Integer.parseInt(DEFAULT_THREAD_NUMBER), toInetAddress(DEFAULT_BIND_ADDRESS), Integer.parseInt(DEFAULT_PORT), toServer(DEFAULT_CLASS), none()); } public ServerConfiguration(Properties properties) { - this(properties.getProperty(SERVER_PROTOCOL, DEFAULT_PROTOCOL), + this( new BasePath(properties.getProperty(SERVER_BASE_PATH, DEFAULT_BASE_PATH)), valueOf(properties.getProperty(MAX_THREAD_NUM, DEFAULT_THREAD_NUMBER)), toInetAddress(properties.getProperty(SERVER_BIND_ADDRESS, DEFAULT_BIND_ADDRESS)), valueOf(properties.getProperty(SERVER_PORT, DEFAULT_PORT)), - toServer(properties.getProperty(SERVER_CLASS, DEFAULT_CLASS))); + toServer(properties.getProperty(SERVER_CLASS, DEFAULT_CLASS)), + toSSLContext(properties.getProperty(SERVER_PROTOCOL, DEFAULT_PROTOCOL))); } public static ServerConfiguration defaultConfiguration() { @@ -65,7 +71,7 @@ public BasePath basePath() { } public ServerConfiguration basePath(BasePath basePath) { - return new ServerConfiguration(protocol, basePath, maxThreadNumber, bindAddress, port, serverClass); + return new ServerConfiguration(basePath, maxThreadNumber, bindAddress, port, serverClass, sslContext); } public int maxThreadNumber() { @@ -73,7 +79,7 @@ public int maxThreadNumber() { } public ServerConfiguration maxThreadNumber(int maxThreadNumber) { - return new ServerConfiguration(protocol, basePath, maxThreadNumber, bindAddress, port, serverClass); + return new ServerConfiguration(basePath, maxThreadNumber, bindAddress, port, serverClass, sslContext); } public Class serverClass() { @@ -81,7 +87,7 @@ public Class serverClass() { } public ServerConfiguration serverClass(Class serverClass) { - return new ServerConfiguration(protocol, basePath, maxThreadNumber, bindAddress, port, serverClass); + return new ServerConfiguration(basePath, maxThreadNumber, bindAddress, port, serverClass, sslContext); } public InetAddress bindAddress() { @@ -89,7 +95,7 @@ public InetAddress bindAddress() { } public ServerConfiguration bindAddress(InetAddress bindAddress) { - return new ServerConfiguration(protocol, basePath, maxThreadNumber, bindAddress, port, serverClass); + return new ServerConfiguration(basePath, maxThreadNumber, bindAddress, port, serverClass, sslContext); } public int port() { @@ -97,15 +103,28 @@ public int port() { } public ServerConfiguration port(int bindPort) { - return new ServerConfiguration(protocol, basePath, maxThreadNumber, bindAddress, bindPort, serverClass); + return new ServerConfiguration(basePath, maxThreadNumber, bindAddress, bindPort, serverClass, sslContext); } public String protocol() { - return protocol; + return sslContext.isDefined() ? Protocol.HTTPS : Protocol.HTTP; } public ServerConfiguration protocol(final String protocol) { - return new ServerConfiguration(protocol, basePath, maxThreadNumber, bindAddress, port, serverClass); + return new ServerConfiguration(basePath, maxThreadNumber, bindAddress, port, serverClass, toSSLContext(protocol)); + } + + public Option sslContext() { + return sslContext; + } + + public ServerConfiguration sslContext(final SSLContext sslContext) { + return new ServerConfiguration(basePath, maxThreadNumber, bindAddress, port, serverClass, some(sslContext)); + } + + private static Option toSSLContext(String protocol){ + if(protocol.equals(Protocol.HTTPS)) return some(SSL.sslContext()); + return none(); } private static InetAddress toInetAddress(final String address) { diff --git a/src/com/googlecode/utterlyidle/examples/HelloWorld.java b/src/com/googlecode/utterlyidle/examples/HelloWorld.java index a33c97aa..65619fdc 100644 --- a/src/com/googlecode/utterlyidle/examples/HelloWorld.java +++ b/src/com/googlecode/utterlyidle/examples/HelloWorld.java @@ -8,6 +8,8 @@ import com.googlecode.utterlyidle.HttpHeaders; import com.googlecode.utterlyidle.MediaType; import com.googlecode.utterlyidle.QueryParameters; +import com.googlecode.utterlyidle.Request; +import com.googlecode.utterlyidle.Requests; import com.googlecode.utterlyidle.Response; import com.googlecode.utterlyidle.Responses; import com.googlecode.utterlyidle.Status; diff --git a/src/com/googlecode/utterlyidle/handlers/ClientHttpHandler.java b/src/com/googlecode/utterlyidle/handlers/ClientHttpHandler.java index d5537544..1945d6b3 100644 --- a/src/com/googlecode/utterlyidle/handlers/ClientHttpHandler.java +++ b/src/com/googlecode/utterlyidle/handlers/ClientHttpHandler.java @@ -23,6 +23,7 @@ import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import java.io.Closeable; import java.io.File; @@ -101,6 +102,10 @@ public ClientHttpHandler(int connectTimeoutMillis, int readTimeoutMillis, ProxyF this(connectTimeoutMillis, readTimeoutMillis, proxies, HttpsURLConnection.getDefaultHostnameVerifier(), HttpsURLConnection.getDefaultSSLSocketFactory()); } + public ClientHttpHandler(int connectTimeoutMillis, int readTimeoutMillis, ProxyFor proxies, HostnameVerifier hostnameVerifier, final SSLContext sslContext) { + this(connectTimeoutMillis, readTimeoutMillis, proxies, hostnameVerifier, sslContext.getSocketFactory()); + } + public ClientHttpHandler(int connectTimeoutMillis, int readTimeoutMillis, ProxyFor proxies, HostnameVerifier hostnameVerifier, final SSLSocketFactory sslSocketFactory) { this.connectTimeoutMillis = connectTimeoutMillis; this.readTimeoutMillis = readTimeoutMillis; diff --git a/src/com/googlecode/utterlyidle/httpserver/RestHandler.java b/src/com/googlecode/utterlyidle/httpserver/RestHandler.java index 4a029ab7..7fc2226d 100644 --- a/src/com/googlecode/utterlyidle/httpserver/RestHandler.java +++ b/src/com/googlecode/utterlyidle/httpserver/RestHandler.java @@ -4,6 +4,7 @@ import com.googlecode.totallylazy.Value; import com.googlecode.utterlyidle.Application; import com.googlecode.utterlyidle.HttpHeaders; +import com.googlecode.utterlyidle.Protocol; import com.googlecode.utterlyidle.Request; import com.googlecode.utterlyidle.Requests; import com.googlecode.utterlyidle.Response; @@ -11,6 +12,7 @@ import com.googlecode.utterlyidle.Status; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpsExchange; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -63,13 +65,10 @@ private Request request(HttpExchange httpExchange) { ); return requestEnricher( clientAddress(httpExchange.getRemoteAddress().getAddress()), - extractScheme(httpExchange.getProtocol())) + httpExchange instanceof HttpsExchange ? Protocol.HTTPS : Protocol.HTTP) .enrich(request); } - private String extractScheme(final String protocol) { - return protocol.toLowerCase().split("/")[0]; - } private Response exceptionResponse(Request request, final Exception e) throws IOException { System.err.println(String.format("%s %s -> %s", request.method(), request.uri(), e)); diff --git a/src/com/googlecode/utterlyidle/httpserver/RestServer.java b/src/com/googlecode/utterlyidle/httpserver/RestServer.java index dd6c46c4..2dd154a8 100644 --- a/src/com/googlecode/utterlyidle/httpserver/RestServer.java +++ b/src/com/googlecode/utterlyidle/httpserver/RestServer.java @@ -3,11 +3,14 @@ import com.googlecode.totallylazy.io.Uri; import com.googlecode.utterlyidle.Application; import com.googlecode.utterlyidle.ApplicationBuilder; +import com.googlecode.utterlyidle.Protocol; import com.googlecode.utterlyidle.Server; import com.googlecode.utterlyidle.ServerConfiguration; import com.googlecode.utterlyidle.examples.HelloWorldApplication; import com.googlecode.utterlyidle.services.Service; import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.HttpsConfigurator; +import com.sun.net.httpserver.HttpsServer; import java.io.IOException; import java.net.InetSocketAddress; @@ -57,9 +60,8 @@ private HttpServer startApp(Application application, ServerConfiguration configu } private HttpServer startUpServer(Application application, ServerConfiguration configuration) throws Exception { - HttpServer server = HttpServer.create(new InetSocketAddress(configuration.bindAddress(), configuration.port()), 0); - server.createContext(configuration.basePath().toString(), - new RestHandler(application)); + HttpServer server = createServer(configuration); + server.createContext(configuration.basePath().toString(), new RestHandler(application)); executorService = newFixedThreadPool(configuration.maxThreadNumber(), getClass()); server.setExecutor(executorService); server.start(); @@ -67,4 +69,14 @@ private HttpServer startUpServer(Application application, ServerConfiguration co uri = updatedConfiguration.toUrl(); return server; } + + private HttpServer createServer(final ServerConfiguration configuration) throws IOException { + InetSocketAddress address = new InetSocketAddress(configuration.bindAddress(), configuration.port()); + if(configuration.protocol().equals(Protocol.HTTPS)) { + HttpsServer server = HttpsServer.create(address, 0); + server.setHttpsConfigurator(new HttpsConfigurator(configuration.sslContext().get())); + return server; + } + return HttpServer.create(address, 0); + } } diff --git a/src/com/googlecode/utterlyidle/jetty/RestServer.java b/src/com/googlecode/utterlyidle/jetty/RestServer.java index 93aca7c5..3d2bad34 100644 --- a/src/com/googlecode/utterlyidle/jetty/RestServer.java +++ b/src/com/googlecode/utterlyidle/jetty/RestServer.java @@ -5,17 +5,21 @@ import com.googlecode.utterlyidle.Application; import com.googlecode.utterlyidle.ApplicationBuilder; import com.googlecode.utterlyidle.BasePath; +import com.googlecode.utterlyidle.Protocol; import com.googlecode.utterlyidle.ServerConfiguration; import com.googlecode.utterlyidle.examples.HelloWorldApplication; import com.googlecode.utterlyidle.services.Service; import com.googlecode.utterlyidle.servlet.ApplicationServlet; import com.googlecode.utterlyidle.servlet.ServletModule; +import org.mortbay.jetty.Connector; import org.mortbay.jetty.Server; import org.mortbay.jetty.nio.SelectChannelConnector; +import org.mortbay.jetty.security.SslSocketConnector; import org.mortbay.jetty.servlet.Context; import org.mortbay.jetty.webapp.WebAppContext; import org.mortbay.thread.QueuedThreadPool; +import javax.net.ssl.SSLServerSocketFactory; import javax.servlet.ServletContext; import java.io.IOException; @@ -108,14 +112,27 @@ public static Function1 webXmlContext(final Uri webRoot, final private Server createServer(ServerConfiguration serverConfig) { Server server = new Server(); - SelectChannelConnector selectChannelConnector = new SelectChannelConnector(); - selectChannelConnector.setPort(serverConfig.port()); - selectChannelConnector.setHost(serverConfig.bindAddress().getHostAddress()); + org.mortbay.jetty.Connector selectChannelConnector = connector(serverConfig); server.addConnector(selectChannelConnector); server.setThreadPool(new QueuedThreadPool(serverConfig.maxThreadNumber())); return server; } + private Connector connector(final ServerConfiguration serverConfig) { + if(serverConfig.protocol().equals(Protocol.HTTPS)){ + return new SslSocketConnector(){ + @Override + protected SSLServerSocketFactory createFactory() throws Exception { + return configuration.sslContext().get().getServerSocketFactory(); + } + }; + } + SelectChannelConnector selectChannelConnector = new SelectChannelConnector(); + selectChannelConnector.setPort(serverConfig.port()); + selectChannelConnector.setHost(serverConfig.bindAddress().getHostAddress()); + return selectChannelConnector; + } + private int portNumber(Server server) { return sequence(server.getConnectors()).head().getLocalPort(); } diff --git a/src/com/googlecode/utterlyidle/jetty/eclipse/RestServer.java b/src/com/googlecode/utterlyidle/jetty/eclipse/RestServer.java index eeef1b52..90895ff6 100644 --- a/src/com/googlecode/utterlyidle/jetty/eclipse/RestServer.java +++ b/src/com/googlecode/utterlyidle/jetty/eclipse/RestServer.java @@ -1,8 +1,10 @@ package com.googlecode.utterlyidle.jetty.eclipse; +import com.googlecode.totallylazy.Option; import com.googlecode.totallylazy.io.Uri; import com.googlecode.utterlyidle.Application; import com.googlecode.utterlyidle.ApplicationBuilder; +import com.googlecode.utterlyidle.Protocol; import com.googlecode.utterlyidle.ServerConfiguration; import com.googlecode.utterlyidle.examples.HelloWorldApplication; import com.googlecode.utterlyidle.services.Service; @@ -13,6 +15,7 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import javax.servlet.ServletException; @@ -70,13 +73,21 @@ public void handle(String target, Request baseRequest, HttpServletRequest servle protected Server createServer(ServerConfiguration serverConfig) { Server server = new Server(new QueuedThreadPool(serverConfig.maxThreadNumber())); - ServerConnector serverConnector = new ServerConnector(server, new HttpConnectionFactory()); + ServerConnector serverConnector = new ServerConnector(server, sslContextFactory(serverConfig).getOrNull(), new HttpConnectionFactory()); serverConnector.setPort(serverConfig.port()); serverConnector.setHost(serverConfig.bindAddress().getHostAddress()); server.addConnector(serverConnector); return server; } + private Option sslContextFactory(final ServerConfiguration serverConfig) { + return serverConfig.sslContext().map(context -> { + SslContextFactory sslContextFactory = new SslContextFactory(); + sslContextFactory.setSslContext(context); + return sslContextFactory; + }); + } + @Override public Application application() { return application; diff --git a/src/com/googlecode/utterlyidle/simpleframework/RestServer.java b/src/com/googlecode/utterlyidle/simpleframework/RestServer.java index 7a99fc20..472eebbd 100644 --- a/src/com/googlecode/utterlyidle/simpleframework/RestServer.java +++ b/src/com/googlecode/utterlyidle/simpleframework/RestServer.java @@ -58,7 +58,8 @@ private Connection startApp(Application application, ServerConfiguration configu private SocketConnection startUpApp(Application application, ServerConfiguration configuration) throws IOException { Container container = new RestContainer(application); SocketConnection connection = new SocketConnection(new ContainerSocketProcessor(container, configuration.maxThreadNumber())); - InetSocketAddress socketAddress = (InetSocketAddress) connection.connect(new InetSocketAddress(configuration.bindAddress(), configuration.port())); + InetSocketAddress socketAddress = (InetSocketAddress) connection.connect(new InetSocketAddress(configuration.bindAddress(), configuration.port()), + configuration.sslContext().getOrNull()); ServerConfiguration updatedConfiguration = configuration.port(socketAddress.getPort()); uri = updatedConfiguration.toUrl(); diff --git a/src/com/googlecode/utterlyidle/ssl/SSL.java b/src/com/googlecode/utterlyidle/ssl/SSL.java new file mode 100644 index 00000000..1a71c2ad --- /dev/null +++ b/src/com/googlecode/utterlyidle/ssl/SSL.java @@ -0,0 +1,108 @@ +package com.googlecode.utterlyidle.ssl; + +import com.googlecode.totallylazy.Maps; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.util.Map; + +import static com.googlecode.totallylazy.LazyException.lazyException; + +public class SSL { + public static final String DEFAULT_SSL_CONTEXT = "TLSv1.2"; + private static final Map storeType = Maps.map( + "jks", "JKS", + "p12", "PKCS12"); + + public static KeyStore keyStore(SecureString password, File file) { + try { + try (InputStream inputStream = new FileInputStream(file)) { + return keyStore(password, inputStream, storeType.get(extension(file))); + } + } catch (Exception e) { + throw lazyException(e); + } + } + + public static KeyStore keyStore(final SecureString password, final InputStream inputStream) { + return keyStore(password, inputStream, "JKS"); + } + + public static KeyStore keyStore(final SecureString password, final InputStream inputStream, final String type) { + try { + KeyStore keyStore = KeyStore.getInstance(type); + keyStore.load(inputStream, password.characters()); + return keyStore; + } catch (Exception e) { + throw lazyException(e); + } + } + + private static String extension(File file) { + String[] parts = file.getName().split("\\."); + return parts[parts.length - 1]; + } + + public static SSLContext sslContext(KeyStore keyStore) { + return sslContext(keyStore, DEFAULT_SSL_CONTEXT); + } + + public static SSLContext sslContext(final KeyStore keyStore, final SecureString privateKeyPassword) { + return sslContext(keyStore, DEFAULT_SSL_CONTEXT, privateKeyPassword); + } + + public static SSLContext sslContext(final KeyStore keyStore, final String name, final SecureString privateKeyPassword) { + return sslContext(name, keyManagers(keyStore, privateKeyPassword), trustManagers(keyStore)); + } + + public static SSLContext sslContext(final KeyStore keyStore, final String name) { + return sslContext(name, null, trustManagers(keyStore)); + } + + public static SSLContext sslContext(final String name, final KeyManager[] keyManagers, final TrustManager[] trustManagers) { + try { + SSLContext context = SSLContext.getInstance(name); + context.init(keyManagers, trustManagers, null); + return context; + } catch (Exception e) { + throw lazyException(e); + } + } + + public static KeyManager[] keyManagers(final KeyStore keyStore, final SecureString secureString) { + try { + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keyStore, secureString.characters()); + return keyManagerFactory.getKeyManagers(); + } catch (Exception e) { + throw lazyException(e); + } + } + + public static TrustManager[] trustManagers(final KeyStore keyStore) { + try { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(keyStore); + return trustManagerFactory.getTrustManagers(); + } catch (Exception e) { + throw lazyException(e); + } + } + + public static SSLContext sslContext() { + try { + return SSLContext.getDefault(); + } catch (Exception e) { + throw lazyException(e); + } + } +} diff --git a/src/com/googlecode/utterlyidle/ssl/SecureString.java b/src/com/googlecode/utterlyidle/ssl/SecureString.java new file mode 100644 index 00000000..5f2ef2f1 --- /dev/null +++ b/src/com/googlecode/utterlyidle/ssl/SecureString.java @@ -0,0 +1,45 @@ +package com.googlecode.utterlyidle.ssl; + +import java.util.Arrays; + +public class SecureString implements AutoCloseable, CharSequence { + private final char[] value; + + protected SecureString(final char[] value) { + this.value = value; + } + + public static SecureString secureString(final char... value) { + return new SecureString(value); + } + + @Override + public void close() throws Exception { + int length = value.length; + for (int i = 0; i < length; i++) value[i] = 0; + } + + public char[] characters() { + return value; + } + + @Override + public int length() { + return value.length; + } + + @Override + public char charAt(final int index) { + return value[index]; + } + + @Override + public SecureString subSequence(final int start, final int end) { + return secureString(Arrays.copyOfRange(value, start, end)); + } + + @Override + public String toString() { + return "***********"; + } +} diff --git a/src/com/googlecode/utterlyidle/undertow/RestServer.java b/src/com/googlecode/utterlyidle/undertow/RestServer.java index 8b781ed5..368bf100 100644 --- a/src/com/googlecode/utterlyidle/undertow/RestServer.java +++ b/src/com/googlecode/utterlyidle/undertow/RestServer.java @@ -1,10 +1,14 @@ package com.googlecode.utterlyidle.undertow; +import com.googlecode.totallylazy.Classes; +import com.googlecode.totallylazy.Lists; import com.googlecode.totallylazy.Option; import com.googlecode.totallylazy.Sequences; import com.googlecode.totallylazy.io.Uri; +import com.googlecode.totallylazy.reflection.Fields; import com.googlecode.utterlyidle.Application; import com.googlecode.utterlyidle.ApplicationBuilder; +import com.googlecode.utterlyidle.Protocol; import com.googlecode.utterlyidle.ServerConfiguration; import com.googlecode.utterlyidle.examples.HelloWorldApplication; import com.googlecode.utterlyidle.services.Service; @@ -18,6 +22,7 @@ import static com.googlecode.totallylazy.Option.none; import static com.googlecode.totallylazy.Option.option; import static com.googlecode.totallylazy.functions.Time0.calculateMilliseconds; +import static com.googlecode.totallylazy.reflection.Fields.access; import static com.googlecode.utterlyidle.ServerConfiguration.defaultConfiguration; import static java.lang.String.format; import static java.lang.System.nanoTime; @@ -62,8 +67,9 @@ private Undertow startApp(Application application, ServerConfiguration configura } private Undertow startUpServer(Application application, ServerConfiguration configuration) throws Exception { - Undertow server = Undertow.builder() - .addHttpListener(configuration.port(), configuration.bindAddress().getHostAddress()) + Undertow.Builder builder = builder(configuration); + + Undertow server = builder .setWorkerThreads(configuration.maxThreadNumber()) .setHandler(new RestHttpHandler(application)) .build(); @@ -74,6 +80,13 @@ private Undertow startUpServer(Application application, ServerConfiguration conf return server; } + private Undertow.Builder builder(final ServerConfiguration configuration) { + if(configuration.protocol().equals(Protocol.HTTPS)) { + return Undertow.builder().addHttpsListener(configuration.port(), configuration.bindAddress().getHostAddress(), configuration.sslContext().get()); + } + return Undertow.builder().addHttpListener(configuration.port(), configuration.bindAddress().getHostAddress()); + } + private int findPortInUse(Undertow server) { return declaredField(server, "channels") .map(field -> listValueOf(field, server)) @@ -81,10 +94,18 @@ private int findPortInUse(Undertow server) { .flatMap(channels -> channels .flatMap(this::portFrom) .headOption()) - .getOrThrow(new IllegalStateException("Cannot find port from Undertow")); + .getOrThrow(new IllegalStateException("Cannot find port from Undertow using reflection!")); } + private static Class sslChannel = Classes.forName("io.undertow.protocols.ssl.UndertowAcceptingSslChannel").get(); private Option portFrom(Object channel) throws Exception { + if(sslChannel.isInstance(channel)) { + channel = declaredField(channel, "tcpServer").map(Fields.value(channel)).get(); + } + return socket(channel); + } + + private Option socket(final Object channel) throws IllegalAccessException { return declaredField(channel, "socket") .map(socketField -> (ServerSocket) socketField.get(channel)) .map(ServerSocket::getLocalPort); @@ -92,17 +113,15 @@ private Option portFrom(Object channel) throws Exception { private List listValueOf(Field field, Object object) { try { - return (List) field.get(object); + return Fields.get(field, object); } catch (IllegalAccessException e) { - return null; + return Lists.list(); } } private Option declaredField(Object object, String fieldName) { try { - Field field = object.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - return option(field); + return option(access(object.getClass().getDeclaredField(fieldName))); } catch (NoSuchFieldException e) { return none(); } diff --git a/test/com/googlecode/utterlyidle/ServerContract.java b/test/com/googlecode/utterlyidle/ServerContract.java index f3469ba2..a32fb5af 100644 --- a/test/com/googlecode/utterlyidle/ServerContract.java +++ b/test/com/googlecode/utterlyidle/ServerContract.java @@ -4,15 +4,23 @@ import com.googlecode.totallylazy.predicates.Predicates; import com.googlecode.totallylazy.time.Dates; import com.googlecode.utterlyidle.examples.HelloWorldApplication; +import com.googlecode.utterlyidle.handlers.ClientHttpHandler; import com.googlecode.utterlyidle.rendering.exceptions.LastExceptions; import com.googlecode.utterlyidle.rendering.exceptions.StoredException; +import com.googlecode.utterlyidle.ssl.SecureString; +import com.googlecode.utterlyidle.ssl.SecureStringTest; import org.hamcrest.CoreMatchers; import org.hamcrest.Matchers; import org.junit.After; import org.junit.Before; import org.junit.Test; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import java.io.InputStream; + import static com.googlecode.totallylazy.Option.none; +import static com.googlecode.totallylazy.Sequences.repeat; import static com.googlecode.totallylazy.Sequences.sequence; import static com.googlecode.totallylazy.Unchecked.cast; import static com.googlecode.totallylazy.predicates.Predicates.not; @@ -37,6 +45,9 @@ import static com.googlecode.utterlyidle.ServerConfiguration.defaultConfiguration; import static com.googlecode.utterlyidle.Status.NOT_FOUND; import static com.googlecode.utterlyidle.handlers.ClientHttpHandlerTest.handle; +import static com.googlecode.utterlyidle.ssl.SSL.keyStore; +import static com.googlecode.utterlyidle.ssl.SSL.sslContext; +import static com.googlecode.utterlyidle.ssl.SecureString.secureString; import static org.hamcrest.CoreMatchers.allOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -50,7 +61,11 @@ public abstract class ServerContract { @Before public void start() throws Exception { - server = cast(application(HelloWorldApplication.class).start(defaultConfiguration().basePath(basePath("base/path")).serverClass(server()))); + server = configureServer(defaultConfiguration()); + } + + private T configureServer(final ServerConfiguration configuration) throws Exception { + return cast(application(HelloWorldApplication.class).start(configuration.basePath(basePath("base/path")).serverClass(server()))); } @After @@ -217,4 +232,20 @@ public void canHandleOptionalDate() throws Exception { assertThat(handle(get("optionalDate?date="), server).entity().toString(), is("no date")); assertThat(handle(get("optionalDate?date=" + LEXICAL().format(Dates.date(1974, 10, 29))), server).entity().toString(), is("19741029000000000")); } + + @Test + public void supportsHttps() throws Exception { + try(InputStream resource = SecureStringTest.class.getResourceAsStream("localhost.jks"); + SecureString password = secureString('p', 'a', 's', 's', 'w', 'o', 'r', 'd')) { + SSLContext sslContext = sslContext(keyStore(password, resource), password); + server.close(); + server = configureServer(defaultConfiguration().sslContext(sslContext)); + Response response = handle(new ClientHttpHandler(1000, 1000, ClientHttpHandler.DEFAULT_PROXY, HttpsURLConnection.getDefaultHostnameVerifier(), sslContext), get("helloworld/x-forwarded-proto"), server); + + assertThat(response.status(), is(Status.OK)); + assertThat(response.entity().toString(), is(Protocol.HTTPS)); + } + + } + } \ No newline at end of file diff --git a/test/com/googlecode/utterlyidle/handlers/ClientHttpHandlerTest.java b/test/com/googlecode/utterlyidle/handlers/ClientHttpHandlerTest.java index bbdaa23e..8e594198 100644 --- a/test/com/googlecode/utterlyidle/handlers/ClientHttpHandlerTest.java +++ b/test/com/googlecode/utterlyidle/handlers/ClientHttpHandlerTest.java @@ -191,7 +191,7 @@ public void canPostToAResource() throws Exception { } public static Response handle(final RequestBuilder request, final Server server) throws Exception { - return handle(new ClientHttpHandler(0), request, server); + return handle(0, request, server); } public static Response handle(int timeout, final RequestBuilder request, final Server server) throws Exception { diff --git a/test/com/googlecode/utterlyidle/ssl/SecureStringTest.java b/test/com/googlecode/utterlyidle/ssl/SecureStringTest.java new file mode 100644 index 00000000..988ccab2 --- /dev/null +++ b/test/com/googlecode/utterlyidle/ssl/SecureStringTest.java @@ -0,0 +1,18 @@ +package com.googlecode.utterlyidle.ssl; + +import org.junit.Test; + +import static com.googlecode.totallylazy.Assert.assertThat; +import static com.googlecode.totallylazy.predicates.Predicates.is; +import static com.googlecode.utterlyidle.ssl.SecureString.secureString; + +public class SecureStringTest { + @Test + public void supportsSecureWipingOfValue() throws Exception { + char[] password = new char[]{'p', 'a', 's', 's', 'w', 'o', 'r', 'd'}; + try(SecureString secureString = secureString(password)) { + for (int i = 0; i < password.length; i++) assertThat(secureString.charAt(i), is(password[i])); + } + for (final char c : password) assertThat(c, is((char) 0)); + } +} \ No newline at end of file diff --git a/test/com/googlecode/utterlyidle/ssl/generate.keystore.sh b/test/com/googlecode/utterlyidle/ssl/generate.keystore.sh new file mode 100644 index 00000000..1a1f49c7 --- /dev/null +++ b/test/com/googlecode/utterlyidle/ssl/generate.keystore.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +keytool -genkey -noprompt -trustcacerts -alias localhost -dname "cn=localhost" -validity 10000 -keypass password -keystore localhost.jks -storepass password \ No newline at end of file diff --git a/test/com/googlecode/utterlyidle/ssl/localhost.jks b/test/com/googlecode/utterlyidle/ssl/localhost.jks new file mode 100644 index 00000000..7a1e2708 Binary files /dev/null and b/test/com/googlecode/utterlyidle/ssl/localhost.jks differ