dropwizard-jetty/src/main/java/io/dropwizard/jetty/HttpsConnectorFactory.java
package io.dropwizard.jetty;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.jetty9.InstrumentedConnectionFactory;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.dropwizard.validation.ValidationMethod;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
import org.eclipse.jetty.util.thread.Scheduler;
import org.eclipse.jetty.util.thread.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import javax.net.ssl.SSLEngine;
import javax.validation.constraints.NotEmpty;
import java.io.File;
import java.net.URI;
import java.security.KeyStore;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* Builds HTTPS connectors (HTTP over TLS/SSL).
* <p/>
* <b>Configuration Parameters:</b>
* <table>
* <tr>
* <td>Name</td>
* <td>Default</td>
* <td>Description</td>
* </tr>
* <tr>
* <td>{@code keyStorePath}</td>
* <td><b>REQUIRED</b></td>
* <td>
* The path to the Java key store which contains the host certificate and private key.
* </td>
* </tr>
* <tr>
* <td>{@code keyStorePassword}</td>
* <td><b>REQUIRED</b></td>
* <td>
* The password used to access the key store.
* </td>
* </tr>
* <tr>
* <td>{@code keyStoreType}</td>
* <td>{@code JKS}</td>
* <td>
* The type of key store (usually {@code JKS}, {@code PKCS12}, {@code JCEKS},
* {@code Windows-MY}, or {@code Windows-ROOT}).
* </td>
* </tr>
* <tr>
* <td>{@code keyStoreProvider}</td>
* <td>(none)</td>
* <td>
* The JCE provider to use to access the key store.
* </td>
* </tr>
* <tr>
* <td>{@code trustStorePath}</td>
* <td>(none)</td>
* <td>
* The path to the Java key store which contains the CA certificates used to establish
* trust.
* </td>
* </tr>
* <tr>
* <td>{@code trustStorePassword}</td>
* <td>(none)</td>
* <td>The password used to access the trust store.</td>
* </tr>
* <tr>
* <td>{@code trustStoreType}</td>
* <td>{@code JKS}</td>
* <td>
* The type of trust store (usually {@code JKS}, {@code PKCS12}, {@code JCEKS},
* {@code Windows-MY}, or {@code Windows-ROOT}).
* </td>
* </tr>
* <tr>
* <td>{@code trustStoreProvider}</td>
* <td>(none)</td>
* <td>
* The JCE provider to use to access the trust store.
* </td>
* </tr>
* <tr>
* <td>{@code keyManagerPassword}</td>
* <td>(none)</td>
* <td>The password, if any, for the key manager.</td>
* </tr>
* <tr>
* <td>{@code needClientAuth}</td>
* <td>(none)</td>
* <td>Whether or not client authentication is required.</td>
* </tr>
* <tr>
* <td>{@code wantClientAuth}</td>
* <td>(none)</td>
* <td>Whether or not client authentication is requested.</td>
* </tr>
* <tr>
* <td>{@code certAlias}</td>
* <td>(none)</td>
* <td>The alias of the certificate to use.</td>
* </tr>
* <tr>
* <td>{@code crlPath}</td>
* <td>(none)</td>
* <td>The path to the file which contains the Certificate Revocation List.</td>
* </tr>
* <tr>
* <td>{@code enableCRLDP}</td>
* <td>false</td>
* <td>Whether or not CRL Distribution Points (CRLDP) support is enabled.</td>
* </tr>
* <tr>
* <td>{@code enableOCSP}</td>
* <td>false</td>
* <td>Whether or not On-Line Certificate Status Protocol (OCSP) support is enabled.</td>
* </tr>
* <tr>
* <td>{@code maxCertPathLength}</td>
* <td>(unlimited)</td>
* <td>The maximum certification path length.</td>
* </tr>
* <tr>
* <td>{@code ocspResponderUrl}</td>
* <td>(none)</td>
* <td>The location of the OCSP responder.</td>
* </tr>
* <tr>
* <td>{@code jceProvider}</td>
* <td>(none)</td>
* <td>The name of the JCE provider to use for cryptographic support.</td>
* </tr>
* <tr>
* <td>{@code validateCerts}</td>
* <td>false</td>
* <td>
* Whether or not to validate TLS certificates before starting. If enabled, Dropwizard
* will refuse to start with expired or otherwise invalid certificates. This option will
* cause unconditional failure in Dropwizard 1.x until a new validation mechanism can be
* implemented.
* </td>
* </tr>
* <tr>
* <td>{@code validatePeers}</td>
* <td>false</td>
* <td>
* Whether or not to validate TLS peer certificates. This option will
* cause unconditional failure in Dropwizard 1.x until a new validation mechanism can be
* implemented.
* </td>
* </tr>
* <tr>
* <td>{@code supportedProtocols}</td>
* <td>JVM default</td>
* <td>
* A list of protocols (e.g., {@code SSLv3}, {@code TLSv1}) which are supported. All
* other protocols will be refused.
* </td>
* </tr>
* <tr>
* <td>{@code excludedProtocols}</td>
* <td>["SSL.*", "TLSv1", "TLSv1\.1"]</td>
* <td>
* A list of protocols (e.g., {@code SSLv3}, {@code TLSv1}) which are excluded. These
* protocols will be refused.
* </td>
* </tr>
* <tr>
* <td>{@code supportedCipherSuites}</td>
* <td>JVM default</td>
* <td>
* A list of cipher suites (e.g., {@code TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256}) which
* are supported. All other cipher suites will be refused.
* </td>
* </tr>
* <tr>
* <td>{@code excludedCipherSuites}</td>
* <td>Jetty's default</td>
* <td>
* A list of cipher suites (e.g., {@code TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256}) which
* are excluded. These cipher suites will be refused.
* </td>
* </tr>
* <tr>
* <td>{@code allowRenegotiation}</td>
* <td>true</td>
* <td>Whether or not TLS renegotiation is allowed.</td>
* </tr>
* <tr>
* <td>{@code endpointIdentificationAlgorithm}</td>
* <td>(none)</td>
* <td>
* Which endpoint identification algorithm, if any, to use during the TLS handshake.
* </td>
* </tr>
* </table>
* <p/>
* For more configuration parameters, see {@link HttpConnectorFactory}.
*
* @see HttpConnectorFactory
*/
@JsonTypeName("https")
public class HttpsConnectorFactory extends HttpConnectorFactory {
private static final Logger LOGGER = LoggerFactory.getLogger(HttpsConnectorFactory.class);
private static final AtomicBoolean LOGGED = new AtomicBoolean(false);
@Nullable
private String keyStorePath;
@Nullable
private String keyStorePassword;
@NotEmpty
private String keyStoreType = "JKS";
@Nullable
private String keyStoreProvider;
@Nullable
private String trustStorePath;
@Nullable
private String trustStorePassword;
@NotEmpty
private String trustStoreType = "JKS";
@Nullable
private String trustStoreProvider;
@Nullable
private String keyManagerPassword;
@Nullable
private Boolean needClientAuth;
@Nullable
private Boolean wantClientAuth;
@Nullable
private String certAlias;
@Nullable
private File crlPath;
@Nullable
private Boolean enableCRLDP;
@Nullable
private Boolean enableOCSP;
@Nullable
private Integer maxCertPathLength;
@Nullable
private URI ocspResponderUrl;
@Nullable
private String jceProvider;
private boolean validateCerts = false;
private boolean validatePeers = false;
@Nullable
private List<String> supportedProtocols;
@Nullable
private List<String> excludedProtocols = Arrays.asList("SSL.*", "TLSv1", "TLSv1\\.1");
@Nullable
private List<String> supportedCipherSuites;
@Nullable
private List<String> excludedCipherSuites;
private boolean allowRenegotiation = true;
@Nullable
private String endpointIdentificationAlgorithm;
@JsonProperty
public boolean getAllowRenegotiation() {
return allowRenegotiation;
}
@JsonProperty
public void setAllowRenegotiation(boolean allowRenegotiation) {
this.allowRenegotiation = allowRenegotiation;
}
@JsonProperty
@Nullable
public String getEndpointIdentificationAlgorithm() {
return endpointIdentificationAlgorithm;
}
@JsonProperty
public void setEndpointIdentificationAlgorithm(@Nullable String endpointIdentificationAlgorithm) {
this.endpointIdentificationAlgorithm = endpointIdentificationAlgorithm;
}
@JsonProperty
@Nullable
public String getKeyStorePath() {
return keyStorePath;
}
@JsonProperty
public void setKeyStorePath(@Nullable String keyStorePath) {
this.keyStorePath = keyStorePath;
}
@JsonProperty
@Nullable
public String getKeyStorePassword() {
return keyStorePassword;
}
@JsonProperty
public void setKeyStorePassword(@Nullable String keyStorePassword) {
this.keyStorePassword = keyStorePassword;
}
@JsonProperty
public String getKeyStoreType() {
return keyStoreType;
}
@JsonProperty
public void setKeyStoreType(String keyStoreType) {
this.keyStoreType = keyStoreType;
}
@JsonProperty
@Nullable
public String getKeyStoreProvider() {
return keyStoreProvider;
}
@JsonProperty
public void setKeyStoreProvider(@Nullable String keyStoreProvider) {
this.keyStoreProvider = keyStoreProvider;
}
@JsonProperty
public String getTrustStoreType() {
return trustStoreType;
}
@JsonProperty
public void setTrustStoreType(String trustStoreType) {
this.trustStoreType = trustStoreType;
}
@JsonProperty
@Nullable
public String getTrustStoreProvider() {
return trustStoreProvider;
}
@JsonProperty
public void setTrustStoreProvider(@Nullable String trustStoreProvider) {
this.trustStoreProvider = trustStoreProvider;
}
@JsonProperty
@Nullable
public String getKeyManagerPassword() {
return keyManagerPassword;
}
@JsonProperty
public void setKeyManagerPassword(@Nullable String keyManagerPassword) {
this.keyManagerPassword = keyManagerPassword;
}
@JsonProperty
@Nullable
public String getTrustStorePath() {
return trustStorePath;
}
@JsonProperty
public void setTrustStorePath(@Nullable String trustStorePath) {
this.trustStorePath = trustStorePath;
}
@JsonProperty
@Nullable
public String getTrustStorePassword() {
return trustStorePassword;
}
@JsonProperty
public void setTrustStorePassword(@Nullable String trustStorePassword) {
this.trustStorePassword = trustStorePassword;
}
@JsonProperty
@Nullable
public Boolean getNeedClientAuth() {
return needClientAuth;
}
@JsonProperty
public void setNeedClientAuth(@Nullable Boolean needClientAuth) {
this.needClientAuth = needClientAuth;
}
@JsonProperty
@Nullable
public Boolean getWantClientAuth() {
return wantClientAuth;
}
@JsonProperty
public void setWantClientAuth(@Nullable Boolean wantClientAuth) {
this.wantClientAuth = wantClientAuth;
}
@JsonProperty
@Nullable
public String getCertAlias() {
return certAlias;
}
@JsonProperty
public void setCertAlias(@Nullable String certAlias) {
this.certAlias = certAlias;
}
@JsonProperty
@Nullable
public File getCrlPath() {
return crlPath;
}
@JsonProperty
public void setCrlPath(@Nullable File crlPath) {
this.crlPath = crlPath;
}
@JsonProperty
@Nullable
public Boolean getEnableCRLDP() {
return enableCRLDP;
}
@JsonProperty
public void setEnableCRLDP(@Nullable Boolean enableCRLDP) {
this.enableCRLDP = enableCRLDP;
}
@JsonProperty
@Nullable
public Boolean getEnableOCSP() {
return enableOCSP;
}
@JsonProperty
public void setEnableOCSP(@Nullable Boolean enableOCSP) {
this.enableOCSP = enableOCSP;
}
@JsonProperty
@Nullable
public Integer getMaxCertPathLength() {
return maxCertPathLength;
}
@JsonProperty
public void setMaxCertPathLength(@Nullable Integer maxCertPathLength) {
this.maxCertPathLength = maxCertPathLength;
}
@JsonProperty
@Nullable
public URI getOcspResponderUrl() {
return ocspResponderUrl;
}
@JsonProperty
public void setOcspResponderUrl(@Nullable URI ocspResponderUrl) {
this.ocspResponderUrl = ocspResponderUrl;
}
@JsonProperty
@Nullable
public String getJceProvider() {
return jceProvider;
}
@JsonProperty
public void setJceProvider(@Nullable String jceProvider) {
this.jceProvider = jceProvider;
}
@JsonProperty
public boolean getValidatePeers() {
return validatePeers;
}
@JsonProperty
public void setValidatePeers(boolean validatePeers) {
this.validatePeers = validatePeers;
}
@JsonProperty
@Nullable
public List<String> getSupportedProtocols() {
return supportedProtocols;
}
@JsonProperty
public void setSupportedProtocols(@Nullable List<String> supportedProtocols) {
this.supportedProtocols = supportedProtocols;
}
@JsonProperty
@Nullable
public List<String> getExcludedProtocols() {
return excludedProtocols;
}
@JsonProperty
public void setExcludedProtocols(@Nullable List<String> excludedProtocols) {
this.excludedProtocols = excludedProtocols;
}
@JsonProperty
@Nullable
public List<String> getSupportedCipherSuites() {
return supportedCipherSuites;
}
@JsonProperty
@Nullable
public List<String> getExcludedCipherSuites() {
return excludedCipherSuites;
}
@JsonProperty
public void setExcludedCipherSuites(@Nullable List<String> excludedCipherSuites) {
this.excludedCipherSuites = excludedCipherSuites;
}
@JsonProperty
public void setSupportedCipherSuites(@Nullable List<String> supportedCipherSuites) {
this.supportedCipherSuites = supportedCipherSuites;
}
@JsonProperty
public boolean isValidateCerts() {
return validateCerts;
}
@JsonProperty
public void setValidateCerts(boolean validateCerts) {
this.validateCerts = validateCerts;
}
@ValidationMethod(message = "keyStorePath should not be null")
public boolean isValidKeyStorePath() {
return keyStoreType.startsWith("Windows-") || keyStorePath != null;
}
@ValidationMethod(message = "keyStorePassword should not be null or empty")
public boolean isValidKeyStorePassword() {
return keyStoreType.startsWith("Windows-") ||
Optional.ofNullable(keyStorePassword).filter(s -> !s.isEmpty()).isPresent();
}
@Override
public Connector build(Server server, MetricRegistry metrics, String name, @Nullable ThreadPool threadPool) {
final HttpConfiguration httpConfig = buildHttpConfiguration();
final HttpConnectionFactory httpConnectionFactory = buildHttpConnectionFactory(httpConfig);
final SslContextFactory sslContextFactory = configureSslContextFactory(new SslContextFactory.Server());
sslContextFactory.addLifeCycleListener(logSslParameters(sslContextFactory));
server.addBean(sslContextFactory);
server.addBean(new SslReload(sslContextFactory, this::configureSslContextFactory));
final SslConnectionFactory sslConnectionFactory =
new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.toString());
final Scheduler scheduler = new ScheduledExecutorScheduler();
final ByteBufferPool bufferPool = buildBufferPool();
return buildConnector(server, scheduler, bufferPool, name, threadPool,
new InstrumentedConnectionFactory(
sslConnectionFactory,
metrics.timer(httpConnections())),
httpConnectionFactory);
}
@Override
protected HttpConfiguration buildHttpConfiguration() {
final HttpConfiguration config = super.buildHttpConfiguration();
config.setSecureScheme("https");
config.setSecurePort(getPort());
config.addCustomizer(new SecureRequestCustomizer());
return config;
}
/**
* Register a listener that waits until the SSL context factory has started. Once it has
* started we can grab the fully initialized context so we can log the parameters.
*
* @since 2.1.0
*/
protected LifeCycle.Listener logSslParameters(final SslContextFactory sslContextFactory) {
// Delegate to the old method as it may have been overridden
return logSslInfoOnStart(sslContextFactory);
}
/**
* @deprecated Use {@link #logSslParameters(SslContextFactory) instead}
*/
@Deprecated
protected AbstractLifeCycle.AbstractLifeCycleListener logSslInfoOnStart(final SslContextFactory sslContextFactory) {
return new AbstractLifeCycle.AbstractLifeCycleListener() {
@Override
public void lifeCycleStarted(LifeCycle event) {
logSupportedParameters(sslContextFactory);
}
};
}
/**
* Given a list of protocols available to the JVM that we can serve up to the client, partition
* this list into two groups: a group of protocols we can serve and a group where we can't. This
* list takes into account protocols that may have been disabled at the JVM level, and also
* protocols that the user explicitly wants to include / exclude. The exclude list (blacklist)
* is stronger than include list (whitelist), so a protocol that is in both lists will be
* excluded. Other than the initial list of available protocols, the other lists are patterns,
* such that one can exclude all SSL protocols with a single exclude entry of "SSL.*". This
* function will handle both cipher suites and protocols, but for the sake of conciseness, this
* documentation only talks about protocols. This implementation is a slimmed down version from
* jetty:
* https://github.com/eclipse/jetty.project/blob/93a8afcc6bd1a6e0af7bd9f967c97ae1bc3eb718/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslSelectionDump.java
*
* @param supportedByJVM protocols available to the JVM.
* @param enabledByJVM protocols enabled by lib/security/java.security.
* @param excludedByConfig protocols the user doesn't want to expose.
* @param includedByConfig the only protocols the user wants to expose.
* @return two entry map of protocols that are enabled (true) and those that have been disabled (false).
*/
static Map<Boolean, List<String>> partitionSupport(
String[] supportedByJVM,
String[] enabledByJVM,
String[] excludedByConfig,
String[] includedByConfig
) {
final List<Pattern> enabled = Arrays.stream(enabledByJVM).map(Pattern::compile).collect(Collectors.toList());
final List<Pattern> disabled = Arrays.stream(excludedByConfig).map(Pattern::compile).collect(Collectors.toList());
final List<Pattern> included = Arrays.stream(includedByConfig).map(Pattern::compile).collect(Collectors.toList());
return Arrays.stream(supportedByJVM)
.sorted(Comparator.naturalOrder())
.collect(Collectors.partitioningBy(x ->
disabled.stream().noneMatch(pat -> pat.matcher(x).matches()) &&
enabled.stream().anyMatch(pat -> pat.matcher(x).matches()) &&
(included.isEmpty() || included.stream().anyMatch(pat -> pat.matcher(x).matches()))
));
}
private void logSupportedParameters(SslContextFactory contextFactory) {
if (LOGGED.compareAndSet(false, true)) {
// When Jetty logs out which protocols are enabled / disabled they include tracing
// information to detect if the protocol was disabled at the
// JRE/lib/security/java.security level. Since we don't log this information we take the
// SSLEngine from our context instead of a pristine version.
//
// For more info from Jetty:
// https://github.com/eclipse/jetty.project/blob/93a8afcc6bd1a6e0af7bd9f967c97ae1bc3eb718/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java#L356-L360
final SSLEngine engine = contextFactory.getSslContext().createSSLEngine();
final Map<Boolean, List<String>> protocols = partitionSupport(
engine.getSupportedProtocols(),
engine.getEnabledProtocols(),
contextFactory.getExcludeProtocols(),
contextFactory.getIncludeProtocols()
);
final Map<Boolean, List<String>> ciphers = partitionSupport(
engine.getSupportedCipherSuites(),
engine.getEnabledCipherSuites(),
contextFactory.getExcludeCipherSuites(),
contextFactory.getIncludeCipherSuites()
);
LOGGER.info("Enabled protocols: {}", protocols.get(true));
LOGGER.info("Disabled protocols: {}", protocols.get(false));
LOGGER.info("Enabled cipher suites: {}", ciphers.get(true));
LOGGER.info("Disabled cipher suites: {}", ciphers.get(false));
}
}
protected SslContextFactory configureSslContextFactory(SslContextFactory factory) {
if (keyStorePath != null) {
factory.setKeyStorePath(keyStorePath);
}
final String realKeyStoreType = getKeyStoreType();
if (realKeyStoreType.startsWith("Windows-")) {
try {
final KeyStore keyStore = KeyStore.getInstance(realKeyStoreType);
keyStore.load(null, null);
factory.setKeyStore(keyStore);
} catch (Exception e) {
throw new IllegalStateException("Windows key store not supported", e);
}
} else {
factory.setKeyStoreType(realKeyStoreType);
factory.setKeyStorePassword(keyStorePassword);
}
if (keyStoreProvider != null) {
factory.setKeyStoreProvider(keyStoreProvider);
}
final String realTrustStoreType = getTrustStoreType();
if (realTrustStoreType.startsWith("Windows-")) {
try {
final KeyStore keyStore = KeyStore.getInstance(realTrustStoreType);
keyStore.load(null, null);
factory.setTrustStore(keyStore);
} catch (Exception e) {
throw new IllegalStateException("Windows key store not supported", e);
}
} else {
if (trustStorePath != null) {
factory.setTrustStorePath(trustStorePath);
}
if (trustStorePassword != null) {
factory.setTrustStorePassword(trustStorePassword);
}
factory.setTrustStoreType(realTrustStoreType);
}
if (trustStoreProvider != null) {
factory.setTrustStoreProvider(trustStoreProvider);
}
if (keyManagerPassword != null) {
factory.setKeyManagerPassword(keyManagerPassword);
}
if (needClientAuth != null && factory instanceof SslContextFactory.Server) {
((SslContextFactory.Server) factory).setNeedClientAuth(needClientAuth);
}
if (wantClientAuth != null && factory instanceof SslContextFactory.Server) {
((SslContextFactory.Server) factory).setWantClientAuth(wantClientAuth);
}
if (certAlias != null) {
factory.setCertAlias(certAlias);
}
if (crlPath != null) {
factory.setCrlPath(crlPath.getAbsolutePath());
}
if (enableCRLDP != null) {
factory.setEnableCRLDP(enableCRLDP);
}
if (enableOCSP != null) {
factory.setEnableOCSP(enableOCSP);
}
if (maxCertPathLength != null) {
factory.setMaxCertPathLength(maxCertPathLength);
}
if (ocspResponderUrl != null) {
factory.setOcspResponderURL(ocspResponderUrl.toASCIIString());
}
if (jceProvider != null) {
factory.setProvider(jceProvider);
}
factory.setRenegotiationAllowed(allowRenegotiation);
factory.setEndpointIdentificationAlgorithm(endpointIdentificationAlgorithm);
factory.setValidateCerts(validateCerts);
factory.setValidatePeerCerts(validatePeers);
if (supportedProtocols != null) {
factory.setIncludeProtocols(supportedProtocols.toArray(new String[0]));
}
if (excludedProtocols != null) {
factory.setExcludeProtocols(excludedProtocols.toArray(new String[0]));
}
if (supportedCipherSuites != null) {
factory.setIncludeCipherSuites(supportedCipherSuites.toArray(new String[0]));
}
if (excludedCipherSuites != null) {
factory.setExcludeCipherSuites(excludedCipherSuites.toArray(new String[0]));
}
return factory;
}
}