package io.dropwizard.jetty; import com.codahale.metrics.MetricRegistry; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.io.Resources; import io.dropwizard.configuration.YamlConfigurationFactory; import io.dropwizard.jackson.DiscoverableSubtypeResolver; import io.dropwizard.jackson.Jackson; import io.dropwizard.validation.BaseValidator; import org.apache.commons.lang3.SystemUtils; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.ConnectionFactory; 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.ServerConnector; import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler; import org.eclipse.jetty.util.thread.ThreadPool; import org.junit.Test; import javax.validation.ConstraintViolation; import javax.validation.Validator; import java.io.File; import java.net.URI; import java.security.KeyStore; import java.security.KeyStoreException; import java.util.Collection; import java.util.List; import java.util.Set; import static org.apache.commons.lang3.reflect.FieldUtils.getField; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertNotNull; import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeTrue; public class HttpsConnectorFactoryTest { private static final String WINDOWS_MY_KEYSTORE_NAME = "Windows-MY"; private final Validator validator = BaseValidator.newValidator(); @Test public void isDiscoverable() throws Exception { assertThat(new DiscoverableSubtypeResolver().getDiscoveredSubtypes()) .contains(HttpsConnectorFactory.class); } @Test public void testParsingConfiguration() throws Exception { HttpsConnectorFactory https = new YamlConfigurationFactory<>(HttpsConnectorFactory.class, validator, Jackson.newObjectMapper(), "dw-https") .build(new File(Resources.getResource("yaml/https-connector.yml").toURI())); assertThat(https.getPort()).isEqualTo(8443); assertThat(https.getKeyStorePath()).isEqualTo("/path/to/ks_file"); assertThat(https.getKeyStorePassword()).isEqualTo("changeit"); assertThat(https.getKeyStoreType()).isEqualTo("JKS"); assertThat(https.getTrustStorePath()).isEqualTo("/path/to/ts_file"); assertThat(https.getTrustStorePassword()).isEqualTo("changeit"); assertThat(https.getTrustStoreType()).isEqualTo("JKS"); assertThat(https.getTrustStoreProvider()).isEqualTo("BC"); assertThat(https.getKeyManagerPassword()).isEqualTo("changeit"); assertThat(https.getNeedClientAuth()).isTrue(); assertThat(https.getWantClientAuth()).isTrue(); assertThat(https.getCertAlias()).isEqualTo("http_server"); assertThat(https.getCrlPath()).isEqualTo(new File("/path/to/crl_file")); assertThat(https.getEnableCRLDP()).isTrue(); assertThat(https.getEnableOCSP()).isTrue(); assertThat(https.getMaxCertPathLength()).isEqualTo(3); assertThat(https.getOcspResponderUrl()).isEqualTo(new URI("http://ip.example.com:9443/ca/ocsp")); assertThat(https.getJceProvider()).isEqualTo("BC"); assertThat(https.getValidatePeers()).isTrue(); assertThat(https.getValidatePeers()).isTrue(); assertThat(https.getSupportedProtocols()).containsExactly("TLSv1.1", "TLSv1.2"); assertThat(https.getExcludedProtocols()).isEmpty(); assertThat(https.getSupportedCipherSuites()) .containsExactly("ECDHE-RSA-AES128-GCM-SHA256", "ECDHE-ECDSA-AES128-GCM-SHA256"); assertThat(https.getExcludedCipherSuites()).isEmpty(); assertThat(https.getAllowRenegotiation()).isFalse(); assertThat(https.getEndpointIdentificationAlgorithm()).isEqualTo("HTTPS"); } @Test public void testSupportedProtocols() { List<String> supportedProtocols = ImmutableList.of("SSLv3", "TLS1"); HttpsConnectorFactory factory = new HttpsConnectorFactory(); factory.setKeyStorePassword("password"); // necessary to avoid a prompt for a password factory.setSupportedProtocols(supportedProtocols); SslContextFactory sslContextFactory = factory.configureSslContextFactory(new SslContextFactory()); assertThat(ImmutableList.copyOf(sslContextFactory.getIncludeProtocols())).isEqualTo(supportedProtocols); } @Test public void testExcludedProtocols() { List<String> excludedProtocols = ImmutableList.of("SSLv3", "TLS1"); HttpsConnectorFactory factory = new HttpsConnectorFactory(); factory.setKeyStorePassword("password"); // necessary to avoid a prompt for a password factory.setExcludedProtocols(excludedProtocols); SslContextFactory sslContextFactory = factory.configureSslContextFactory(new SslContextFactory()); assertThat(ImmutableList.copyOf(sslContextFactory.getExcludeProtocols())).isEqualTo(excludedProtocols); } @Test public void nonWindowsKeyStoreValidation() throws Exception { HttpsConnectorFactory factory = new HttpsConnectorFactory(); Collection<String> properties = getViolationProperties(validator.validate(factory)); assertThat(properties.contains("validKeyStorePassword")).isEqualTo(true); assertThat(properties.contains("validKeyStorePath")).isEqualTo(true); } @Test public void windowsKeyStoreValidation() throws Exception { HttpsConnectorFactory factory = new HttpsConnectorFactory(); factory.setKeyStoreType(WINDOWS_MY_KEYSTORE_NAME); Collection<String> properties = getViolationProperties(validator.validate(factory)); assertThat(properties.contains("validKeyStorePassword")).isEqualTo(false); assertThat(properties.contains("validKeyStorePath")).isEqualTo(false); } @Test public void canBuildContextFactoryWhenWindowsKeyStoreAvailable() throws Exception { // ignore test when Windows Keystore unavailable assumeTrue(canAccessWindowsKeyStore()); final HttpsConnectorFactory factory = new HttpsConnectorFactory(); factory.setKeyStoreType(WINDOWS_MY_KEYSTORE_NAME); assertNotNull(factory.configureSslContextFactory(new SslContextFactory())); } @Test(expected = IllegalStateException.class) public void windowsKeyStoreUnavailableThrowsException() throws Exception { assumeFalse(canAccessWindowsKeyStore()); final HttpsConnectorFactory factory = new HttpsConnectorFactory(); factory.setKeyStoreType(WINDOWS_MY_KEYSTORE_NAME); factory.configureSslContextFactory(new SslContextFactory()); } @Test public void testBuild() throws Exception { final HttpsConnectorFactory https = new HttpsConnectorFactory(); https.setBindHost("127.0.0.1"); https.setPort(8443); https.setKeyStorePath("/etc/app/server.ks"); https.setKeyStoreType("JKS"); https.setKeyStorePassword("correct_horse"); https.setKeyStoreProvider("BC"); https.setTrustStorePath("/etc/app/server.ts"); https.setTrustStoreType("JKS"); https.setTrustStorePassword("battery_staple"); https.setTrustStoreProvider("BC"); https.setKeyManagerPassword("new_overlords"); https.setNeedClientAuth(true); https.setWantClientAuth(true); https.setCertAlias("alt_server"); https.setCrlPath(new File("/etc/ctr_list.txt")); https.setEnableCRLDP(true); https.setEnableOCSP(true); https.setMaxCertPathLength(4); https.setOcspResponderUrl(new URI("http://windc1/ocsp")); https.setJceProvider("BC"); https.setAllowRenegotiation(false); https.setEndpointIdentificationAlgorithm("HTTPS"); https.setValidateCerts(true); https.setValidatePeers(true); https.setSupportedProtocols(ImmutableList.of("TLSv1.1", "TLSv1.2")); https.setSupportedCipherSuites(ImmutableList.of("TLS_DHE_RSA.*", "TLS_ECDHE.*")); final Server server = new Server(); final MetricRegistry metrics = new MetricRegistry(); final ThreadPool threadPool = new QueuedThreadPool(); final Connector connector = https.build(server, metrics, "test-https-connector", threadPool); assertThat(connector).isInstanceOf(ServerConnector.class); final ServerConnector serverConnector = (ServerConnector) connector; assertThat(serverConnector.getPort()).isEqualTo(8443); assertThat(serverConnector.getHost()).isEqualTo("127.0.0.1"); assertThat(serverConnector.getName()).isEqualTo("test-https-connector"); assertThat(serverConnector.getServer()).isSameAs(server); assertThat(serverConnector.getScheduler()).isInstanceOf(ScheduledExecutorScheduler.class); assertThat(serverConnector.getExecutor()).isSameAs(threadPool); final Jetty93InstrumentedConnectionFactory jetty93SslConnectionFacttory = (Jetty93InstrumentedConnectionFactory) serverConnector.getConnectionFactory("ssl"); assertThat(jetty93SslConnectionFacttory).isInstanceOf(Jetty93InstrumentedConnectionFactory.class); assertThat(jetty93SslConnectionFacttory.getTimer()).isSameAs( metrics.timer("org.eclipse.jetty.server.HttpConnectionFactory.127.0.0.1.8443.connections")); final SslContextFactory sslContextFactory = ((SslConnectionFactory) jetty93SslConnectionFacttory .getConnectionFactory()).getSslContextFactory(); assertThat(getField(SslContextFactory.class, "_keyStoreResource", true).get(sslContextFactory)) .isEqualTo(Resource.newResource("/etc/app/server.ks")); assertThat(sslContextFactory.getKeyStoreType()).isEqualTo("JKS"); assertThat(getField(SslContextFactory.class, "_keyStorePassword", true).get(sslContextFactory).toString()) .isEqualTo("correct_horse"); assertThat(sslContextFactory.getKeyStoreProvider()).isEqualTo("BC"); assertThat(getField(SslContextFactory.class, "_trustStoreResource", true).get(sslContextFactory)) .isEqualTo(Resource.newResource("/etc/app/server.ts")); assertThat(sslContextFactory.getKeyStoreType()).isEqualTo("JKS"); assertThat(getField(SslContextFactory.class, "_trustStorePassword", true).get(sslContextFactory).toString()) .isEqualTo("battery_staple"); assertThat(sslContextFactory.getKeyStoreProvider()).isEqualTo("BC"); assertThat(getField(SslContextFactory.class, "_keyManagerPassword", true).get(sslContextFactory).toString()) .isEqualTo("new_overlords"); assertThat(sslContextFactory.getNeedClientAuth()).isTrue(); assertThat(sslContextFactory.getWantClientAuth()).isTrue(); assertThat(sslContextFactory.getCertAlias()).isEqualTo("alt_server"); assertThat(sslContextFactory.getCrlPath()).isEqualTo(new File("/etc/ctr_list.txt").getAbsolutePath()); assertThat(sslContextFactory.isEnableCRLDP()).isTrue(); assertThat(sslContextFactory.isEnableOCSP()).isTrue(); assertThat(sslContextFactory.getMaxCertPathLength()).isEqualTo(4); assertThat(sslContextFactory.getOcspResponderURL()).isEqualTo("http://windc1/ocsp"); assertThat(sslContextFactory.getProvider()).isEqualTo("BC"); assertThat(sslContextFactory.isRenegotiationAllowed()).isFalse(); assertThat(getField(SslContextFactory.class, "_endpointIdentificationAlgorithm", true).get(sslContextFactory)) .isEqualTo("HTTPS"); assertThat(sslContextFactory.isValidateCerts()).isTrue(); assertThat(sslContextFactory.isValidatePeerCerts()).isTrue(); assertThat(sslContextFactory.getIncludeProtocols()).containsOnly("TLSv1.1", "TLSv1.2"); assertThat(sslContextFactory.getIncludeCipherSuites()).containsOnly("TLS_DHE_RSA.*", "TLS_ECDHE.*"); final ConnectionFactory httpConnectionFactory = serverConnector.getConnectionFactory("http/1.1"); assertThat(httpConnectionFactory).isInstanceOf(HttpConnectionFactory.class); final HttpConfiguration httpConfiguration = ((HttpConnectionFactory) httpConnectionFactory) .getHttpConfiguration(); assertThat(httpConfiguration.getSecureScheme()).isEqualTo("https"); assertThat(httpConfiguration.getSecurePort()).isEqualTo(8443); assertThat(httpConfiguration.getCustomizers()).hasAtLeastOneElementOfType(SecureRequestCustomizer.class); connector.stop(); server.stop(); } private boolean canAccessWindowsKeyStore() { if (SystemUtils.IS_OS_WINDOWS) { try { KeyStore.getInstance(WINDOWS_MY_KEYSTORE_NAME); return true; } catch (KeyStoreException e) { return false; } } return false; } private <T> Collection<String> getViolationProperties(Set<ConstraintViolation<T>> violations) { return Collections2.transform(violations, input -> input.getPropertyPath().toString()); } }