package io.dropwizard.jetty; import com.codahale.metrics.MetricRegistry; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeName; import com.google.common.base.Strings; import com.google.common.collect.Iterables; 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.hibernate.validator.constraints.NotEmpty; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import java.io.File; import java.net.URI; import java.security.KeyStore; import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; /** * 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>Jetty's default</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); private String keyStorePath; private String keyStorePassword; @NotEmpty private String keyStoreType = "JKS"; private String keyStoreProvider; private String trustStorePath; private String trustStorePassword; @NotEmpty private String trustStoreType = "JKS"; private String trustStoreProvider; private String keyManagerPassword; private Boolean needClientAuth; private Boolean wantClientAuth; private String certAlias; private File crlPath; private Boolean enableCRLDP; private Boolean enableOCSP; private Integer maxCertPathLength; private URI ocspResponderUrl; private String jceProvider; private boolean validateCerts = false; private boolean validatePeers = false; private List<String> supportedProtocols; private List<String> excludedProtocols; private List<String> supportedCipherSuites; private List<String> excludedCipherSuites; private boolean allowRenegotiation = true; private String endpointIdentificationAlgorithm; @JsonProperty public boolean getAllowRenegotiation() { return allowRenegotiation; } @JsonProperty public void setAllowRenegotiation(boolean allowRenegotiation) { this.allowRenegotiation = allowRenegotiation; } @JsonProperty public String getEndpointIdentificationAlgorithm() { return endpointIdentificationAlgorithm; } @JsonProperty public void setEndpointIdentificationAlgorithm(String endpointIdentificationAlgorithm) { this.endpointIdentificationAlgorithm = endpointIdentificationAlgorithm; } @JsonProperty public String getKeyStorePath() { return keyStorePath; } @JsonProperty public void setKeyStorePath(String keyStorePath) { this.keyStorePath = keyStorePath; } @JsonProperty public String getKeyStorePassword() { return keyStorePassword; } @JsonProperty public void setKeyStorePassword(String keyStorePassword) { this.keyStorePassword = keyStorePassword; } @JsonProperty public String getKeyStoreType() { return keyStoreType; } @JsonProperty public void setKeyStoreType(String keyStoreType) { this.keyStoreType = keyStoreType; } @JsonProperty public String getKeyStoreProvider() { return keyStoreProvider; } @JsonProperty public void setKeyStoreProvider(String keyStoreProvider) { this.keyStoreProvider = keyStoreProvider; } @JsonProperty public String getTrustStoreType() { return trustStoreType; } @JsonProperty public void setTrustStoreType(String trustStoreType) { this.trustStoreType = trustStoreType; } @JsonProperty public String getTrustStoreProvider() { return trustStoreProvider; } @JsonProperty public void setTrustStoreProvider(String trustStoreProvider) { this.trustStoreProvider = trustStoreProvider; } @JsonProperty public String getKeyManagerPassword() { return keyManagerPassword; } @JsonProperty public void setKeyManagerPassword(String keyManagerPassword) { this.keyManagerPassword = keyManagerPassword; } @JsonProperty public String getTrustStorePath() { return trustStorePath; } @JsonProperty public void setTrustStorePath(String trustStorePath) { this.trustStorePath = trustStorePath; } @JsonProperty public String getTrustStorePassword() { return trustStorePassword; } @JsonProperty public void setTrustStorePassword(String trustStorePassword) { this.trustStorePassword = trustStorePassword; } @JsonProperty public Boolean getNeedClientAuth() { return needClientAuth; } @JsonProperty public void setNeedClientAuth(Boolean needClientAuth) { this.needClientAuth = needClientAuth; } @JsonProperty public Boolean getWantClientAuth() { return wantClientAuth; } @JsonProperty public void setWantClientAuth(Boolean wantClientAuth) { this.wantClientAuth = wantClientAuth; } @JsonProperty public String getCertAlias() { return certAlias; } @JsonProperty public void setCertAlias(String certAlias) { this.certAlias = certAlias; } @JsonProperty public File getCrlPath() { return crlPath; } @JsonProperty public void setCrlPath(File crlPath) { this.crlPath = crlPath; } @JsonProperty public Boolean getEnableCRLDP() { return enableCRLDP; } @JsonProperty public void setEnableCRLDP(Boolean enableCRLDP) { this.enableCRLDP = enableCRLDP; } @JsonProperty public Boolean getEnableOCSP() { return enableOCSP; } @JsonProperty public void setEnableOCSP(Boolean enableOCSP) { this.enableOCSP = enableOCSP; } @JsonProperty public Integer getMaxCertPathLength() { return maxCertPathLength; } @JsonProperty public void setMaxCertPathLength(Integer maxCertPathLength) { this.maxCertPathLength = maxCertPathLength; } @JsonProperty public URI getOcspResponderUrl() { return ocspResponderUrl; } @JsonProperty public void setOcspResponderUrl(URI ocspResponderUrl) { this.ocspResponderUrl = ocspResponderUrl; } @JsonProperty public String getJceProvider() { return jceProvider; } @JsonProperty public void setJceProvider(String jceProvider) { this.jceProvider = jceProvider; } @JsonProperty public boolean getValidatePeers() { return validatePeers; } @JsonProperty public void setValidatePeers(boolean validatePeers) { this.validatePeers = validatePeers; } @JsonProperty public List<String> getSupportedProtocols() { return supportedProtocols; } @JsonProperty public void setSupportedProtocols(List<String> supportedProtocols) { this.supportedProtocols = supportedProtocols; } @JsonProperty public List<String> getExcludedProtocols() { return excludedProtocols; } @JsonProperty public void setExcludedProtocols(List<String> excludedProtocols) { this.excludedProtocols = excludedProtocols; } @JsonProperty public List<String> getSupportedCipherSuites() { return supportedCipherSuites; } @JsonProperty public List<String> getExcludedCipherSuites() { return excludedCipherSuites; } @JsonProperty public void setExcludedCipherSuites(List<String> excludedCipherSuites) { this.excludedCipherSuites = excludedCipherSuites; } @JsonProperty public void setSupportedCipherSuites(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-") || !Strings.isNullOrEmpty(keyStorePassword); } @Override public Connector build(Server server, MetricRegistry metrics, String name, ThreadPool threadPool) { final HttpConfiguration httpConfig = buildHttpConfiguration(); final HttpConnectionFactory httpConnectionFactory = buildHttpConnectionFactory(httpConfig); final SslContextFactory sslContextFactory = configureSslContextFactory(new SslContextFactory()); sslContextFactory.addLifeCycleListener(logSslInfoOnStart(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 Jetty93InstrumentedConnectionFactory( 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. */ protected AbstractLifeCycle.AbstractLifeCycleListener logSslInfoOnStart(final SslContextFactory sslContextFactory) { return new AbstractLifeCycle.AbstractLifeCycleListener() { @Override public void lifeCycleStarted(LifeCycle event) { logSupportedParameters(sslContextFactory.getSslContext()); } }; } private void logSupportedParameters(SSLContext context) { if (LOGGED.compareAndSet(false, true)) { final String[] protocols = context.getSupportedSSLParameters().getProtocols(); final SSLSocketFactory factory = context.getSocketFactory(); final String[] cipherSuites = factory.getSupportedCipherSuites(); LOGGER.info("Supported protocols: {}", Arrays.toString(protocols)); LOGGER.info("Supported cipher suites: {}", Arrays.toString(cipherSuites)); if (getSupportedProtocols() != null) { LOGGER.info("Configured protocols: {}", getSupportedProtocols()); } if (getExcludedProtocols() != null) { LOGGER.info("Excluded protocols: {}", getExcludedProtocols()); } if (getSupportedCipherSuites() != null) { LOGGER.info("Configured cipher suites: {}", getSupportedCipherSuites()); } if (getExcludedCipherSuites() != null) { LOGGER.info("Excluded cipher suites: {}", getExcludedCipherSuites()); } } } protected SslContextFactory configureSslContextFactory(SslContextFactory factory) { if (keyStorePath != null) { factory.setKeyStorePath(keyStorePath); } final String keyStoreType = getKeyStoreType(); if (keyStoreType.startsWith("Windows-")) { try { final KeyStore keyStore = KeyStore.getInstance(keyStoreType); keyStore.load(null, null); factory.setKeyStore(keyStore); } catch (Exception e) { throw new IllegalStateException("Windows key store not supported", e); } } else { factory.setKeyStoreType(keyStoreType); factory.setKeyStorePassword(keyStorePassword); } if (keyStoreProvider != null) { factory.setKeyStoreProvider(keyStoreProvider); } final String trustStoreType = getTrustStoreType(); if (trustStoreType.startsWith("Windows-")) { try { final KeyStore keyStore = KeyStore.getInstance(trustStoreType); 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(trustStoreType); } if (trustStoreProvider != null) { factory.setTrustStoreProvider(trustStoreProvider); } if (keyManagerPassword != null) { factory.setKeyManagerPassword(keyManagerPassword); } if (needClientAuth != null) { factory.setNeedClientAuth(needClientAuth); } if (wantClientAuth != null) { 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(Iterables.toArray(supportedProtocols, String.class)); } if (excludedProtocols != null) { factory.setExcludeProtocols(Iterables.toArray(excludedProtocols, String.class)); } if (supportedCipherSuites != null) { factory.setIncludeCipherSuites(Iterables.toArray(supportedCipherSuites, String.class)); } if (excludedCipherSuites != null) { factory.setExcludeCipherSuites(Iterables.toArray(excludedCipherSuites, String.class)); } return factory; } }