Skip to content

Commit

Permalink
Merge pull request #3446 from U1F984/custom-ssl-context
Browse files Browse the repository at this point in the history
Add custom SSL context option
  • Loading branch information
jknack committed Jun 10, 2024
2 parents 6c222f7 + f74e27d commit 33053ac
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 32 deletions.
71 changes: 41 additions & 30 deletions jooby/src/main/java/io/jooby/ServerOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -536,46 +536,57 @@ 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:
*
* <p>- {@link #getSecurePort()} has been set; or - {@link #getSsl()} has been set.
* <ul>
* <li>{@link #getSecurePort()} has been set</li>
* <li>{@link #getSsl()} has been set.</li>
* </ul>
*
* <p>If secure port is set and there is no SSL options, this method configure a SSL context using
* <p>
* If secure port is set and there is no SSL options, this method configure a SSL context using
* the a self-signed certificate for <code>localhost</code>.
* <p>
* If {@link SslOptions#getCustomSslContext()} is set, it is returned without modification.
*
* @param loader Resource loader.
* @return SSLContext or <code>null</code> when SSL is disabled.
*/
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<String> supportedProtocols =
new LinkedHashSet<>(Arrays.asList(sslContext.getDefaultSSLParameters().getProtocols()));
Expand Down
26 changes: 26 additions & 0 deletions jooby/src/main/java/io/jooby/SslOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -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:
*
Expand Down Expand Up @@ -79,6 +81,8 @@ public enum ClientAuth {

private List<String> protocol = Arrays.asList(TLS_V1_3, TLS_V1_2);

private SSLContext customSslContext;

/**
* Certificate type. Default is {@link #PKCS12}.
*
Expand Down Expand Up @@ -337,6 +341,28 @@ public void close() {
return this;
}

/**
* Returns the custom SSL Context if set (default <code>null</code>).
* <p>
* 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.
* <p>
* 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;
Expand Down
30 changes: 28 additions & 2 deletions tests/src/test/java/io/jooby/test/HttpsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()));
});
}
}

0 comments on commit 33053ac

Please sign in to comment.