diff --git a/jooby/src/main/java/io/jooby/ServerOptions.java b/jooby/src/main/java/io/jooby/ServerOptions.java index 23ee556731..9e2c264058 100644 --- a/jooby/src/main/java/io/jooby/ServerOptions.java +++ b/jooby/src/main/java/io/jooby/ServerOptions.java @@ -536,12 +536,18 @@ public ServerOptions setExpectContinue(@Nullable Boolean expectContinue) { /** * Creates SSL context using the given resource loader. This method attempts to create a - * SSLContext when: + * SSLContext when one of the following is true: * - *

- {@link #getSecurePort()} has been set; or - {@link #getSsl()} has been set. + *

* - *

If secure port is set and there is no SSL options, this method configure a SSL context using + *

+ * If secure port is set and there is no SSL options, this method configure a SSL context using * the a self-signed certificate for localhost. + *

+ * If {@link SslOptions#getCustomSslContext()} is set, it is returned without modification. * * @param loader Resource loader. * @return SSLContext or null when SSL is disabled. @@ -549,33 +555,38 @@ public ServerOptions setExpectContinue(@Nullable Boolean expectContinue) { public @Nullable SSLContext getSSLContext(@NonNull ClassLoader loader) { if (isSSLEnabled()) { setSecurePort(Optional.ofNullable(securePort).orElse(SEVER_SECURE_PORT)); - setSsl(Optional.ofNullable(ssl).orElseGet(SslOptions::selfSigned)); - SslOptions options = getSsl(); - - SslContextProvider sslContextProvider = - Stream.of(SslContextProvider.providers()) - .filter(it -> it.supports(options.getType())) - .findFirst() - .orElseThrow( - () -> new UnsupportedOperationException("SSL Type: " + options.getType())); - - String providerName = - stream( - spliteratorUnknownSize( - ServiceLoader.load(SslProvider.class).iterator(), Spliterator.ORDERED), - false) - .findFirst() - .map( - provider -> { - String name = provider.getName(); - if (Security.getProvider(name) == null) { - Security.addProvider(provider.create()); - } - return name; - }) - .orElse(null); - - SSLContext sslContext = sslContextProvider.create(loader, providerName, options); + SslOptions options = Optional.ofNullable(ssl).orElseGet(SslOptions::selfSigned); + setSsl(options); + + SSLContext sslContext; + if (options.getCustomSslContext() == null) { + SslContextProvider sslContextProvider = + Stream.of(SslContextProvider.providers()) + .filter(it -> it.supports(options.getType())) + .findFirst() + .orElseThrow( + () -> new UnsupportedOperationException("SSL Type: " + options.getType())); + + String providerName = + stream( + spliteratorUnknownSize( + ServiceLoader.load(SslProvider.class).iterator(), Spliterator.ORDERED), + false) + .findFirst() + .map( + provider -> { + String name = provider.getName(); + if (Security.getProvider(name) == null) { + Security.addProvider(provider.create()); + } + return name; + }) + .orElse(null); + + sslContext = sslContextProvider.create(loader, providerName, options); + } else { + sslContext = options.getCustomSslContext(); + } // validate TLS protocol, at least one protocol must be supported Set supportedProtocols = new LinkedHashSet<>(Arrays.asList(sslContext.getDefaultSSLParameters().getProtocols())); diff --git a/jooby/src/main/java/io/jooby/SslOptions.java b/jooby/src/main/java/io/jooby/SslOptions.java index a6f901b860..0692f65b57 100644 --- a/jooby/src/main/java/io/jooby/SslOptions.java +++ b/jooby/src/main/java/io/jooby/SslOptions.java @@ -24,6 +24,8 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; +import javax.net.ssl.SSLContext; + /** * SSL options for enabling HTTPs in Jooby. Jooby supports two certificate formats: * @@ -79,6 +81,8 @@ public enum ClientAuth { private List protocol = Arrays.asList(TLS_V1_3, TLS_V1_2); + private SSLContext customSslContext; + /** * Certificate type. Default is {@link #PKCS12}. * @@ -337,6 +341,28 @@ public void close() { return this; } + /** + * Returns the custom SSL Context if set (default null). + *

+ * If a custom SSL Context is set, all options except for {@link #getClientAuth()} and {@link #getProtocol()} are ignored. + * + * @return the custom SSL Context or null + */ + public @Nullable SSLContext getCustomSslContext() { + return customSslContext; + } + + /** + * Sets a custom SSL context. + *

+ * If a custom SSL Context is set, all options except for {@link #getClientAuth()} and {@link #getProtocol()} are ignored. + * + * @param customSslContext the new context or null to unset it + */ + public void setCustomSslContext(@Nullable SSLContext customSslContext) { + this.customSslContext = customSslContext; + } + @Override public String toString() { return type; diff --git a/tests/src/test/java/io/jooby/test/HttpsTest.java b/tests/src/test/java/io/jooby/test/HttpsTest.java index a47dd22615..d7c35ff454 100644 --- a/tests/src/test/java/io/jooby/test/HttpsTest.java +++ b/tests/src/test/java/io/jooby/test/HttpsTest.java @@ -5,14 +5,14 @@ */ package io.jooby.test; -import static org.junit.jupiter.api.Assertions.assertEquals; - import io.jooby.ServerOptions; import io.jooby.SslOptions; import io.jooby.handler.SSLHandler; import io.jooby.junit.ServerTest; import io.jooby.junit.ServerTestRunner; +import static org.junit.jupiter.api.Assertions.*; + public class HttpsTest { @ServerTest @@ -237,4 +237,30 @@ public void httpsOnly(ServerTestRunner runner) { https.get("/test", rsp -> assertEquals("test", rsp.body().string())); }); } + + @ServerTest + public void customSslContext(ServerTestRunner runner) { + runner + .define( + app -> { + var options = new ServerOptions().setSecurePort(8443).setHttpsOnly(true); + options.setSsl(SslOptions.selfSigned()); + // a fresh context is created every time based on config + var ctx1 = options.getSSLContext(this.getClass().getClassLoader()); + var ctx2 = options.getSSLContext(this.getClass().getClassLoader()); + assertNotSame(ctx1, ctx2); + + // now always the configured context is returned + options.getSsl().setCustomSslContext(ctx1); + assertSame(ctx1, options.getSSLContext(this.getClass().getClassLoader())); + assertSame(ctx1, options.getSSLContext(this.getClass().getClassLoader())); + + app.setServerOptions(options); + app.get("/test", ctx -> "test"); + }) + .ready( + (http, https) -> { + https.get("/test", rsp -> assertEquals("test", rsp.body().string())); + }); + } }