diff options
author | Bjørn Christian Seime <bjorncs@verizonmedia.com> | 2021-07-05 16:13:50 +0200 |
---|---|---|
committer | Bjørn Christian Seime <bjorncs@verizonmedia.com> | 2021-07-05 16:24:11 +0200 |
commit | e6cbfe9c57766f61eac4e38607c6d9a5077a13ed (patch) | |
tree | 9f63d5eb49ec8651966fa3ed1bad3d5b69a4d173 /container-core/src/test/java/com/yahoo | |
parent | 25c571d2e724e1a38a8c3528964f6e7eab4ac64f (diff) |
Move the most expensive tests in HttpServerTest to separate test classes
Rewrite test methods to reuse generated crypto material.
Rewrite moved tests to use Junit 5.
Diffstat (limited to 'container-core/src/test/java/com/yahoo')
5 files changed, 481 insertions, 388 deletions
diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/EchoRequestHandler.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/EchoRequestHandler.java new file mode 100644 index 00000000000..9610648ad41 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/EchoRequestHandler.java @@ -0,0 +1,24 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; + +import static com.yahoo.jdisc.Response.Status.OK; + +/** + * @author bjorncs + */ +class EchoRequestHandler extends AbstractRequestHandler { + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + int port = request.getUri().getPort(); + Response response = new Response(OK); + response.headers().put("Jdisc-Local-Port", Integer.toString(port)); + return handler.handleResponse(response); + } +} + diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerTest.java index 0f625b5c3df..bd3000a0188 100644 --- a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerTest.java +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerTest.java @@ -5,14 +5,12 @@ import com.google.inject.AbstractModule; import com.google.inject.Module; import com.yahoo.container.logging.ConnectionLog; import com.yahoo.container.logging.ConnectionLogEntry; -import com.yahoo.container.logging.ConnectionLogEntry.SslHandshakeFailure.ExceptionEntry; import com.yahoo.container.logging.RequestLog; import com.yahoo.container.logging.RequestLogEntry; import com.yahoo.jdisc.References; import com.yahoo.jdisc.Request; import com.yahoo.jdisc.Response; import com.yahoo.jdisc.application.BindingSetSelector; -import com.yahoo.jdisc.application.MetricConsumer; import com.yahoo.jdisc.handler.AbstractRequestHandler; import com.yahoo.jdisc.handler.CompletionHandler; import com.yahoo.jdisc.handler.ContentChannel; @@ -28,12 +26,7 @@ import com.yahoo.jdisc.http.HttpResponse; import com.yahoo.jdisc.http.ServerConfig; import com.yahoo.jdisc.http.server.jetty.JettyTestDriver.TlsClientAuth; import com.yahoo.jdisc.service.BindingSetNotFoundException; -import com.yahoo.security.KeyUtils; -import com.yahoo.security.Pkcs10Csr; -import com.yahoo.security.Pkcs10CsrBuilder; import com.yahoo.security.SslContextBuilder; -import com.yahoo.security.X509CertificateBuilder; -import com.yahoo.security.X509CertificateUtils; import com.yahoo.security.tls.TlsContext; import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; @@ -42,39 +35,22 @@ import org.apache.hc.client5.http.entity.mime.FormBodyPartBuilder; import org.apache.hc.client5.http.entity.mime.StringBody; import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; import org.apache.hc.client5.http.impl.async.H2AsyncClientBuilder; -import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder; -import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder; -import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.nio.ssl.TlsStrategy; -import org.apache.hc.core5.http2.HttpVersionPolicy; import org.assertj.core.api.Assertions; -import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.client.ProxyProtocolClientConnectionFactory.V1; -import org.eclipse.jetty.client.ProxyProtocolClientConnectionFactory.V2; -import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.server.handler.AbstractHandlerContainer; -import org.eclipse.jetty.util.ssl.SslContextFactory; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLHandshakeException; -import javax.security.auth.x500.X500Principal; import java.io.IOException; -import java.math.BigInteger; import java.net.BindException; import java.net.URI; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.nio.file.Path; -import java.security.KeyPair; -import java.security.PrivateKey; -import java.security.cert.X509Certificate; import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -85,10 +61,7 @@ import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.UUID; -import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; import java.util.regex.Pattern; import static com.yahoo.jdisc.Response.Status.GATEWAY_TIMEOUT; @@ -105,8 +78,8 @@ import static com.yahoo.jdisc.http.HttpHeaders.Names.X_DISABLE_CHUNKING; import static com.yahoo.jdisc.http.HttpHeaders.Values.APPLICATION_X_WWW_FORM_URLENCODED; import static com.yahoo.jdisc.http.HttpHeaders.Values.CLOSE; import static com.yahoo.jdisc.http.server.jetty.SimpleHttpClient.ResponseValidator; -import static com.yahoo.security.KeyAlgorithm.EC; -import static com.yahoo.security.SignatureAlgorithm.SHA256_WITH_ECDSA; +import static com.yahoo.jdisc.http.server.jetty.Utils.createSslTestDriver; +import static com.yahoo.jdisc.http.server.jetty.Utils.generatePrivateKeyAndCertificate; import static org.cthul.matchers.CthulMatchers.containsPattern; import static org.cthul.matchers.CthulMatchers.matchesPattern; import static org.hamcrest.CoreMatchers.containsString; @@ -114,13 +87,10 @@ import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.startsWith; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.anyOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.junit.Assume.assumeTrue; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -133,8 +103,6 @@ import static org.mockito.Mockito.when; */ public class HttpServerTest { - private static final Logger log = Logger.getLogger(HttpServerTest.class.getName()); - @Rule public TemporaryFolder tmpFolder = new TemporaryFolder(); @@ -666,205 +634,6 @@ public class HttpServerTest { } @Test - public void requireThatMetricIsIncrementedWhenClientIsMissingCertificateOnHandshake() throws IOException { - Path privateKeyFile = tmpFolder.newFile().toPath(); - Path certificateFile = tmpFolder.newFile().toPath(); - generatePrivateKeyAndCertificate(privateKeyFile, certificateFile); - var metricConsumer = new MetricConsumerMock(); - InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); - JettyTestDriver driver = createSslTestDriver(certificateFile, privateKeyFile, metricConsumer, connectionLog); - - SSLContext clientCtx = new SslContextBuilder() - .withTrustStore(certificateFile) - .build(); - assertHttpsRequestTriggersSslHandshakeException( - driver, clientCtx, null, null, "Received fatal alert: bad_certificate"); - verify(metricConsumer.mockitoMock(), atLeast(1)) - .add(MetricDefinitions.SSL_HANDSHAKE_FAILURE_MISSING_CLIENT_CERT, 1L, MetricConsumerMock.STATIC_CONTEXT); - assertTrue(driver.close()); - Assertions.assertThat(connectionLog.logEntries()).hasSize(1); - assertSslHandshakeFailurePresent( - connectionLog.logEntries().get(0), SSLHandshakeException.class, SslHandshakeFailure.MISSING_CLIENT_CERT.failureType()); - } - - @Test - public void requireThatMetricIsIncrementedWhenClientUsesIncompatibleTlsVersion() throws IOException { - Path privateKeyFile = tmpFolder.newFile().toPath(); - Path certificateFile = tmpFolder.newFile().toPath(); - generatePrivateKeyAndCertificate(privateKeyFile, certificateFile); - var metricConsumer = new MetricConsumerMock(); - InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); - JettyTestDriver driver = createSslTestDriver(certificateFile, privateKeyFile, metricConsumer, connectionLog); - - SSLContext clientCtx = new SslContextBuilder() - .withTrustStore(certificateFile) - .withKeyStore(privateKeyFile, certificateFile) - .build(); - - boolean tlsv11Enabled = List.of(clientCtx.getDefaultSSLParameters().getProtocols()).contains("TLSv1.1"); - assumeTrue("TLSv1.1 must be enabled in installed JDK", tlsv11Enabled); - - assertHttpsRequestTriggersSslHandshakeException(driver, clientCtx, "TLSv1.1", null, "protocol"); - verify(metricConsumer.mockitoMock(), atLeast(1)) - .add(MetricDefinitions.SSL_HANDSHAKE_FAILURE_INCOMPATIBLE_PROTOCOLS, 1L, MetricConsumerMock.STATIC_CONTEXT); - assertTrue(driver.close()); - Assertions.assertThat(connectionLog.logEntries()).hasSize(1); - assertSslHandshakeFailurePresent( - connectionLog.logEntries().get(0), SSLHandshakeException.class, SslHandshakeFailure.INCOMPATIBLE_PROTOCOLS.failureType()); - } - - @Test - public void requireThatMetricIsIncrementedWhenClientUsesIncompatibleCiphers() throws IOException { - Path privateKeyFile = tmpFolder.newFile().toPath(); - Path certificateFile = tmpFolder.newFile().toPath(); - generatePrivateKeyAndCertificate(privateKeyFile, certificateFile); - var metricConsumer = new MetricConsumerMock(); - InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); - JettyTestDriver driver = createSslTestDriver(certificateFile, privateKeyFile, metricConsumer, connectionLog); - - SSLContext clientCtx = new SslContextBuilder() - .withTrustStore(certificateFile) - .withKeyStore(privateKeyFile, certificateFile) - .build(); - - assertHttpsRequestTriggersSslHandshakeException( - driver, clientCtx, null, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "Received fatal alert: handshake_failure"); - verify(metricConsumer.mockitoMock(), atLeast(1)) - .add(MetricDefinitions.SSL_HANDSHAKE_FAILURE_INCOMPATIBLE_CIPHERS, 1L, MetricConsumerMock.STATIC_CONTEXT); - assertTrue(driver.close()); - Assertions.assertThat(connectionLog.logEntries()).hasSize(1); - assertSslHandshakeFailurePresent( - connectionLog.logEntries().get(0), SSLHandshakeException.class, SslHandshakeFailure.INCOMPATIBLE_CIPHERS.failureType()); - } - - @Test - public void requireThatMetricIsIncrementedWhenClientUsesInvalidCertificateInHandshake() throws IOException { - Path serverPrivateKeyFile = tmpFolder.newFile().toPath(); - Path serverCertificateFile = tmpFolder.newFile().toPath(); - generatePrivateKeyAndCertificate(serverPrivateKeyFile, serverCertificateFile); - var metricConsumer = new MetricConsumerMock(); - InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); - JettyTestDriver driver = createSslTestDriver(serverCertificateFile, serverPrivateKeyFile, metricConsumer, connectionLog); - - Path clientPrivateKeyFile = tmpFolder.newFile().toPath(); - Path clientCertificateFile = tmpFolder.newFile().toPath(); - generatePrivateKeyAndCertificate(clientPrivateKeyFile, clientCertificateFile); - - SSLContext clientCtx = new SslContextBuilder() - .withKeyStore(clientPrivateKeyFile, clientCertificateFile) - .withTrustStore(serverCertificateFile) - .build(); - - assertHttpsRequestTriggersSslHandshakeException( - driver, clientCtx, null, null, "Received fatal alert: certificate_unknown"); - verify(metricConsumer.mockitoMock(), atLeast(1)) - .add(MetricDefinitions.SSL_HANDSHAKE_FAILURE_INVALID_CLIENT_CERT, 1L, MetricConsumerMock.STATIC_CONTEXT); - assertTrue(driver.close()); - Assertions.assertThat(connectionLog.logEntries()).hasSize(1); - assertSslHandshakeFailurePresent( - connectionLog.logEntries().get(0), SSLHandshakeException.class, SslHandshakeFailure.INVALID_CLIENT_CERT.failureType()); - } - - @Test - public void requireThatMetricIsIncrementedWhenClientUsesExpiredCertificateInHandshake() throws IOException { - Path rootPrivateKeyFile = tmpFolder.newFile().toPath(); - Path rootCertificateFile = tmpFolder.newFile().toPath(); - Path privateKeyFile = tmpFolder.newFile().toPath(); - Path certificateFile = tmpFolder.newFile().toPath(); - Instant notAfter = Instant.now().minus(100, ChronoUnit.DAYS); - generatePrivateKeyAndCertificate(rootPrivateKeyFile, rootCertificateFile, privateKeyFile, certificateFile, notAfter); - var metricConsumer = new MetricConsumerMock(); - InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); - JettyTestDriver driver = createSslTestDriver(rootCertificateFile, rootPrivateKeyFile, metricConsumer, connectionLog); - - SSLContext clientCtx = new SslContextBuilder() - .withTrustStore(rootCertificateFile) - .withKeyStore(privateKeyFile, certificateFile) - .build(); - - assertHttpsRequestTriggersSslHandshakeException( - driver, clientCtx, null, null, "Received fatal alert: certificate_unknown"); - verify(metricConsumer.mockitoMock(), atLeast(1)) - .add(MetricDefinitions.SSL_HANDSHAKE_FAILURE_EXPIRED_CLIENT_CERT, 1L, MetricConsumerMock.STATIC_CONTEXT); - assertTrue(driver.close()); - Assertions.assertThat(connectionLog.logEntries()).hasSize(1); - - } - - @Test - public void requireThatProxyProtocolIsAcceptedAndActualRemoteAddressStoredInAccessLog() throws Exception { - Path privateKeyFile = tmpFolder.newFile().toPath(); - Path certificateFile = tmpFolder.newFile().toPath(); - generatePrivateKeyAndCertificate(privateKeyFile, certificateFile); - InMemoryRequestLog requestLogMock = new InMemoryRequestLog(); - InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); - JettyTestDriver driver = createSslWithProxyProtocolTestDriver(certificateFile, privateKeyFile, requestLogMock, /*mixedMode*/connectionLog, false); - - String proxiedRemoteAddress = "192.168.0.100"; - int proxiedRemotePort = 12345; - sendJettyClientRequest(driver, certificateFile, new V1.Tag(proxiedRemoteAddress, proxiedRemotePort)); - sendJettyClientRequest(driver, certificateFile, new V2.Tag(proxiedRemoteAddress, proxiedRemotePort)); - assertTrue(driver.close()); - - assertEquals(2, requestLogMock.entries().size()); - assertLogEntryHasRemote(requestLogMock.entries().get(0), proxiedRemoteAddress, proxiedRemotePort); - assertLogEntryHasRemote(requestLogMock.entries().get(1), proxiedRemoteAddress, proxiedRemotePort); - Assertions.assertThat(connectionLog.logEntries()).hasSize(2); - assertLogEntryHasRemote(connectionLog.logEntries().get(0), proxiedRemoteAddress, proxiedRemotePort); - assertEquals("v1", connectionLog.logEntries().get(0).proxyProtocolVersion().get()); - assertLogEntryHasRemote(connectionLog.logEntries().get(1), proxiedRemoteAddress, proxiedRemotePort); - assertEquals("v2", connectionLog.logEntries().get(1).proxyProtocolVersion().get()); - } - - @Test - public void requireThatConnectorWithProxyProtocolMixedEnabledAcceptsBothProxyProtocolAndHttps() throws Exception { - Path privateKeyFile = tmpFolder.newFile().toPath(); - Path certificateFile = tmpFolder.newFile().toPath(); - generatePrivateKeyAndCertificate(privateKeyFile, certificateFile); - InMemoryRequestLog requestLogMock = new InMemoryRequestLog(); - InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); - JettyTestDriver driver = createSslWithProxyProtocolTestDriver(certificateFile, privateKeyFile, requestLogMock, /*mixedMode*/connectionLog, true); - - String proxiedRemoteAddress = "192.168.0.100"; - sendJettyClientRequest(driver, certificateFile, null); - sendJettyClientRequest(driver, certificateFile, new V1.Tag(proxiedRemoteAddress, 12345)); - sendJettyClientRequest(driver, certificateFile, new V2.Tag(proxiedRemoteAddress, 12345)); - assertTrue(driver.close()); - - assertEquals(3, requestLogMock.entries().size()); - assertLogEntryHasRemote(requestLogMock.entries().get(0), "127.0.0.1", 0); - assertLogEntryHasRemote(requestLogMock.entries().get(1), proxiedRemoteAddress, 0); - assertLogEntryHasRemote(requestLogMock.entries().get(2), proxiedRemoteAddress, 0); - Assertions.assertThat(connectionLog.logEntries()).hasSize(3); - assertLogEntryHasRemote(connectionLog.logEntries().get(0), null, 0); - assertLogEntryHasRemote(connectionLog.logEntries().get(1), proxiedRemoteAddress, 12345); - assertLogEntryHasRemote(connectionLog.logEntries().get(2), proxiedRemoteAddress, 12345); - } - - @Test - public void requireThatJdiscLocalPortPropertyIsNotOverriddenByProxyProtocol() throws Exception { - Path privateKeyFile = tmpFolder.newFile().toPath(); - Path certificateFile = tmpFolder.newFile().toPath(); - generatePrivateKeyAndCertificate(privateKeyFile, certificateFile); - InMemoryRequestLog requestLogMock = new InMemoryRequestLog(); - InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); - JettyTestDriver driver = createSslWithProxyProtocolTestDriver(certificateFile, privateKeyFile, requestLogMock, connectionLog, /*mixedMode*/false); - - String proxiedRemoteAddress = "192.168.0.100"; - int proxiedRemotePort = 12345; - String proxyLocalAddress = "10.0.0.10"; - int proxyLocalPort = 23456; - V2.Tag v2Tag = new V2.Tag(V2.Tag.Command.PROXY, null, V2.Tag.Protocol.STREAM, - proxiedRemoteAddress, proxiedRemotePort, proxyLocalAddress, proxyLocalPort, null); - ContentResponse response = sendJettyClientRequest(driver, certificateFile, v2Tag); - assertTrue(driver.close()); - - int clientPort = Integer.parseInt(response.getHeaders().get("Jdisc-Local-Port")); - assertNotEquals(proxyLocalPort, clientPort); - assertNotEquals(proxyLocalPort, connectionLog.logEntries().get(0).localPort().get().intValue()); - } - - @Test public void requireThatConnectionIsTrackedInConnectionLog() throws Exception { Path privateKeyFile = tmpFolder.newFile().toPath(); Path certificateFile = tmpFolder.newFile().toPath(); @@ -932,41 +701,6 @@ public class HttpServerTest { .set(MetricDefinitions.REQUESTS_PER_CONNECTION, 1L, MetricConsumerMock.STATIC_CONTEXT); } - private ContentResponse sendJettyClientRequest(JettyTestDriver testDriver, Path certificateFile, Object tag) - throws Exception { - HttpClient client = createJettyHttpClient(certificateFile); - try { - int maxAttempts = 3; - for (int attempt = 0; attempt < maxAttempts; attempt++) { - try { - ContentResponse response = client.newRequest(URI.create("https://localhost:" + testDriver.server().getListenPort() + "/")) - .tag(tag) - .send(); - assertEquals(200, response.getStatus()); - return response; - } catch (ExecutionException e) { - // Retry when the server closes the connection before the TLS handshake is completed. This have been observed in CI. - // We have been unable to reproduce this locally. The cause is therefor currently unknown. - log.log(Level.WARNING, String.format("Attempt %d failed: %s", attempt, e.getMessage()), e); - Thread.sleep(10); - } - } - throw new AssertionError("Failed to send request, see log for details"); - } finally { - client.stop(); - } - } - - // Using Jetty's http client as Apache httpclient does not support the proxy-protocol v1/v2. - private static HttpClient createJettyHttpClient(Path certificateFile) throws Exception { - SslContextFactory.Client clientSslCtxFactory = new SslContextFactory.Client(); - clientSslCtxFactory.setHostnameVerifier(NoopHostnameVerifier.INSTANCE); - clientSslCtxFactory.setSslContext(new SslContextBuilder().withTrustStore(certificateFile).build()); - - HttpClient client = new HttpClient(clientSslCtxFactory); - client.start(); - return client; - } private static CloseableHttpAsyncClient createHttp2Client(JettyTestDriver driver) { TlsStrategy tlsStrategy = ClientTlsStrategyBuilder.create() @@ -980,58 +714,6 @@ public class HttpServerTest { return client; } - private static void assertLogEntryHasRemote(RequestLogEntry entry, String expectedAddress, int expectedPort) { - assertEquals(expectedAddress, entry.peerAddress().get()); - if (expectedPort > 0) { - assertEquals(expectedPort, entry.peerPort().getAsInt()); - } - } - - private static void assertLogEntryHasRemote(ConnectionLogEntry entry, String expectedAddress, int expectedPort) { - if (expectedAddress != null) { - Assertions.assertThat(entry.remoteAddress()).hasValue(expectedAddress); - } else { - Assertions.assertThat(entry.remoteAddress()).isEmpty(); - } - if (expectedPort > 0) { - Assertions.assertThat(entry.remotePort()).hasValue(expectedPort); - } else { - Assertions.assertThat(entry.remotePort()).isEmpty(); - } - } - - private static void assertSslHandshakeFailurePresent( - ConnectionLogEntry entry, Class<? extends SSLHandshakeException> expectedException, String expectedType) { - Assertions.assertThat(entry.sslHandshakeFailure()).isPresent(); - ConnectionLogEntry.SslHandshakeFailure failure = entry.sslHandshakeFailure().get(); - assertEquals(expectedType, failure.type()); - ExceptionEntry exceptionEntry = failure.exceptionChain().get(0); - assertEquals(expectedException.getName(), exceptionEntry.name()); - } - - private static JettyTestDriver createSslWithProxyProtocolTestDriver( - Path certificateFile, Path privateKeyFile, RequestLog requestLog, - ConnectionLog connectionLog, boolean mixedMode) { - ConnectorConfig.Builder connectorConfig = new ConnectorConfig.Builder() - .http2Enabled(true) - .proxyProtocol(new ConnectorConfig.ProxyProtocol.Builder() - .enabled(true) - .mixedMode(mixedMode)) - .ssl(new ConnectorConfig.Ssl.Builder() - .enabled(true) - .privateKeyFile(privateKeyFile.toString()) - .certificateFile(certificateFile.toString()) - .caCertificateFile(certificateFile.toString())); - return JettyTestDriver.newConfiguredInstance( - new EchoRequestHandler(), - new ServerConfig.Builder().connectionLog(new ServerConfig.ConnectionLog.Builder().enabled(true)), - connectorConfig, - binder -> { - binder.bind(RequestLog.class).toInstance(requestLog); - binder.bind(ConnectionLog.class).toInstance(connectionLog); - }); - } - private static JettyTestDriver createSslWithTlsClientAuthenticationEnforcer(Path certificateFile, Path privateKeyFile) { ConnectorConfig.Builder connectorConfig = new ConnectorConfig.Builder() .tlsClientAuthEnforcer( @@ -1051,63 +733,6 @@ public class HttpServerTest { binder -> {}); } - private static JettyTestDriver createSslTestDriver( - Path serverCertificateFile, Path serverPrivateKeyFile, MetricConsumerMock metricConsumer, InMemoryConnectionLog connectionLog) throws IOException { - Module extraModule = binder -> { - binder.bind(MetricConsumer.class).toInstance(metricConsumer.mockitoMock()); - binder.bind(ConnectionLog.class).toInstance(connectionLog); - }; - return JettyTestDriver.newInstanceWithSsl( - new EchoRequestHandler(), serverCertificateFile, serverPrivateKeyFile, TlsClientAuth.NEED, extraModule); - } - - private static void assertHttpsRequestTriggersSslHandshakeException( - JettyTestDriver testDriver, - SSLContext sslContext, - String protocolOverride, - String cipherOverride, - String expectedExceptionSubstring) throws IOException { - List<String> protocols = protocolOverride != null ? List.of(protocolOverride) : null; - List<String> ciphers = cipherOverride != null ? List.of(cipherOverride) : null; - try (var client = new SimpleHttpClient(sslContext, protocols, ciphers, testDriver.server().getListenPort(), false)) { - client.get("/status.html"); - fail("SSLHandshakeException expected"); - } catch (SSLHandshakeException e) { - assertThat(e.getMessage(), containsString(expectedExceptionSubstring)); - } catch (SSLException e) { - // This exception is thrown if Apache httpclient's write thread detects the handshake failure before the read thread. - log.log(Level.WARNING, "Client failed to get a proper TLS handshake response: " + e.getMessage(), e); - // Only ignore a subset of exceptions - assertThat(e.getMessage(), anyOf(containsString("readHandshakeRecord"), containsString("Broken pipe"))); - } - } - - private static void generatePrivateKeyAndCertificate(Path privateKeyFile, Path certificateFile) throws IOException { - KeyPair keyPair = KeyUtils.generateKeypair(EC); - Files.writeString(privateKeyFile, KeyUtils.toPem(keyPair.getPrivate())); - - X509Certificate certificate = X509CertificateBuilder - .fromKeypair( - keyPair, new X500Principal("CN=localhost"), Instant.EPOCH, Instant.EPOCH.plus(100_000, ChronoUnit.DAYS), SHA256_WITH_ECDSA, BigInteger.ONE) - .build(); - Files.writeString(certificateFile, X509CertificateUtils.toPem(certificate)); - } - - private static void generatePrivateKeyAndCertificate(Path rootPrivateKeyFile, Path rootCertificateFile, - Path privateKeyFile, Path certificateFile, Instant notAfter) throws IOException { - generatePrivateKeyAndCertificate(rootPrivateKeyFile, rootCertificateFile); - X509Certificate rootCertificate = X509CertificateUtils.fromPem(Files.readString(rootCertificateFile)); - PrivateKey privateKey = KeyUtils.fromPemEncodedPrivateKey(Files.readString(rootPrivateKeyFile)); - - KeyPair keyPair = KeyUtils.generateKeypair(EC); - Files.writeString(privateKeyFile, KeyUtils.toPem(keyPair.getPrivate())); - Pkcs10Csr csr = Pkcs10CsrBuilder.fromKeypair(new X500Principal("CN=myclient"), keyPair, SHA256_WITH_ECDSA).build(); - X509Certificate certificate = X509CertificateBuilder - .fromCsr(csr, rootCertificate.getSubjectX500Principal(), Instant.EPOCH, notAfter, privateKey, SHA256_WITH_ECDSA, BigInteger.ONE) - .build(); - Files.writeString(certificateFile, X509CertificateUtils.toPem(certificate)); - } - private static RequestHandler mockRequestHandler() { final RequestHandler mockRequestHandler = mock(RequestHandler.class); when(mockRequestHandler.refer()).thenReturn(References.NOOP_REFERENCE); @@ -1237,17 +862,6 @@ public class HttpServerTest { } } - private static class EchoRequestHandler extends AbstractRequestHandler { - - @Override - public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { - int port = request.getUri().getPort(); - Response response = new Response(OK); - response.headers().put("Jdisc-Local-Port", Integer.toString(port)); - return handler.handleResponse(response); - } - } - private static class OkRequestHandler extends AbstractRequestHandler { @Override public ContentChannel handleRequest(Request request, ResponseHandler handler) { diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/ProxyProtocolTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/ProxyProtocolTest.java new file mode 100644 index 00000000000..d29abea024e --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/ProxyProtocolTest.java @@ -0,0 +1,198 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.container.logging.ConnectionLog; +import com.yahoo.container.logging.ConnectionLogEntry; +import com.yahoo.container.logging.RequestLog; +import com.yahoo.container.logging.RequestLogEntry; +import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.jdisc.http.ServerConfig; +import com.yahoo.security.SslContextBuilder; +import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.assertj.core.api.Assertions; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.ProxyProtocolClientConnectionFactory; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Path; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static com.yahoo.jdisc.http.server.jetty.Utils.generatePrivateKeyAndCertificate; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * @author bjorncs + */ +class ProxyProtocolTest { + + private static final Logger log = Logger.getLogger(ProxyProtocolTest.class.getName()); + + private static Path privateKeyFile; + private static Path certificateFile; + private InMemoryConnectionLog connectionLog; + private InMemoryRequestLog requestLogMock; + private JettyTestDriver driver; + + @BeforeAll + static void generateCrypto(@TempDir Path tmpFolder) throws IOException { + privateKeyFile = tmpFolder.resolve("key.pem"); + certificateFile = tmpFolder.resolve("cert.pem"); + generatePrivateKeyAndCertificate(privateKeyFile, certificateFile); + } + + @BeforeEach + void initializeServer() { + requestLogMock = new InMemoryRequestLog(); + connectionLog = new InMemoryConnectionLog(); + driver = createSslWithProxyProtocolTestDriver(certificateFile, privateKeyFile, requestLogMock, /*mixedMode*/connectionLog, false); + } + + @Test + void requireThatProxyProtocolIsAcceptedAndActualRemoteAddressStoredInAccessLog() throws Exception { + String proxiedRemoteAddress = "192.168.0.100"; + int proxiedRemotePort = 12345; + sendJettyClientRequest(driver, certificateFile, new ProxyProtocolClientConnectionFactory.V1.Tag(proxiedRemoteAddress, proxiedRemotePort)); + sendJettyClientRequest(driver, certificateFile, new ProxyProtocolClientConnectionFactory.V2.Tag(proxiedRemoteAddress, proxiedRemotePort)); + assertTrue(driver.close()); + + assertEquals(2, requestLogMock.entries().size()); + assertLogEntryHasRemote(requestLogMock.entries().get(0), proxiedRemoteAddress, proxiedRemotePort); + assertLogEntryHasRemote(requestLogMock.entries().get(1), proxiedRemoteAddress, proxiedRemotePort); + Assertions.assertThat(connectionLog.logEntries()).hasSize(2); + assertLogEntryHasRemote(connectionLog.logEntries().get(0), proxiedRemoteAddress, proxiedRemotePort); + assertEquals("v1", connectionLog.logEntries().get(0).proxyProtocolVersion().get()); + assertLogEntryHasRemote(connectionLog.logEntries().get(1), proxiedRemoteAddress, proxiedRemotePort); + assertEquals("v2", connectionLog.logEntries().get(1).proxyProtocolVersion().get()); + } + + @Test + void requireThatConnectorWithProxyProtocolMixedEnabledAcceptsBothProxyProtocolAndHttps() throws Exception { + generatePrivateKeyAndCertificate(privateKeyFile, certificateFile); + InMemoryRequestLog requestLogMock = new InMemoryRequestLog(); + InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); + JettyTestDriver driver = createSslWithProxyProtocolTestDriver(certificateFile, privateKeyFile, requestLogMock, /*mixedMode*/connectionLog, true); + + String proxiedRemoteAddress = "192.168.0.100"; + sendJettyClientRequest(driver, certificateFile, null); + sendJettyClientRequest(driver, certificateFile, new ProxyProtocolClientConnectionFactory.V1.Tag(proxiedRemoteAddress, 12345)); + sendJettyClientRequest(driver, certificateFile, new ProxyProtocolClientConnectionFactory.V2.Tag(proxiedRemoteAddress, 12345)); + assertTrue(driver.close()); + + assertEquals(3, requestLogMock.entries().size()); + assertLogEntryHasRemote(requestLogMock.entries().get(0), "127.0.0.1", 0); + assertLogEntryHasRemote(requestLogMock.entries().get(1), proxiedRemoteAddress, 0); + assertLogEntryHasRemote(requestLogMock.entries().get(2), proxiedRemoteAddress, 0); + Assertions.assertThat(connectionLog.logEntries()).hasSize(3); + assertLogEntryHasRemote(connectionLog.logEntries().get(0), null, 0); + assertLogEntryHasRemote(connectionLog.logEntries().get(1), proxiedRemoteAddress, 12345); + assertLogEntryHasRemote(connectionLog.logEntries().get(2), proxiedRemoteAddress, 12345); + } + + @Test + void requireThatJdiscLocalPortPropertyIsNotOverriddenByProxyProtocol() throws Exception { + String proxiedRemoteAddress = "192.168.0.100"; + int proxiedRemotePort = 12345; + String proxyLocalAddress = "10.0.0.10"; + int proxyLocalPort = 23456; + ProxyProtocolClientConnectionFactory.V2.Tag v2Tag = new ProxyProtocolClientConnectionFactory.V2.Tag(ProxyProtocolClientConnectionFactory.V2.Tag.Command.PROXY, null, ProxyProtocolClientConnectionFactory.V2.Tag.Protocol.STREAM, + proxiedRemoteAddress, proxiedRemotePort, proxyLocalAddress, proxyLocalPort, null); + ContentResponse response = sendJettyClientRequest(driver, certificateFile, v2Tag); + assertTrue(driver.close()); + + int clientPort = Integer.parseInt(response.getHeaders().get("Jdisc-Local-Port")); + assertNotEquals(proxyLocalPort, clientPort); + assertNotEquals(proxyLocalPort, connectionLog.logEntries().get(0).localPort().get().intValue()); + } + + private static JettyTestDriver createSslWithProxyProtocolTestDriver( + Path certificateFile, Path privateKeyFile, RequestLog requestLog, + ConnectionLog connectionLog, boolean mixedMode) { + ConnectorConfig.Builder connectorConfig = new ConnectorConfig.Builder() + .http2Enabled(true) + .proxyProtocol(new ConnectorConfig.ProxyProtocol.Builder() + .enabled(true) + .mixedMode(mixedMode)) + .ssl(new ConnectorConfig.Ssl.Builder() + .enabled(true) + .privateKeyFile(privateKeyFile.toString()) + .certificateFile(certificateFile.toString()) + .caCertificateFile(certificateFile.toString())); + return JettyTestDriver.newConfiguredInstance( + new EchoRequestHandler(), + new ServerConfig.Builder().connectionLog(new ServerConfig.ConnectionLog.Builder().enabled(true)), + connectorConfig, + binder -> { + binder.bind(RequestLog.class).toInstance(requestLog); + binder.bind(ConnectionLog.class).toInstance(connectionLog); + }); + } + + private ContentResponse sendJettyClientRequest(JettyTestDriver testDriver, Path certificateFile, Object tag) + throws Exception { + HttpClient client = createJettyHttpClient(certificateFile); + try { + int maxAttempts = 3; + for (int attempt = 0; attempt < maxAttempts; attempt++) { + try { + ContentResponse response = client.newRequest(URI.create("https://localhost:" + testDriver.server().getListenPort() + "/")) + .tag(tag) + .send(); + assertEquals(200, response.getStatus()); + return response; + } catch (ExecutionException e) { + // Retry when the server closes the connection before the TLS handshake is completed. This have been observed in CI. + // We have been unable to reproduce this locally. The cause is therefor currently unknown. + log.log(Level.WARNING, String.format("Attempt %d failed: %s", attempt, e.getMessage()), e); + Thread.sleep(10); + } + } + throw new AssertionError("Failed to send request, see log for details"); + } finally { + client.stop(); + } + } + + // Using Jetty's http client as Apache httpclient does not support the proxy-protocol v1/v2. + private static HttpClient createJettyHttpClient(Path certificateFile) throws Exception { + SslContextFactory.Client clientSslCtxFactory = new SslContextFactory.Client(); + clientSslCtxFactory.setHostnameVerifier(NoopHostnameVerifier.INSTANCE); + clientSslCtxFactory.setSslContext(new SslContextBuilder().withTrustStore(certificateFile).build()); + + HttpClient client = new HttpClient(clientSslCtxFactory); + client.start(); + return client; + } + + private static void assertLogEntryHasRemote(RequestLogEntry entry, String expectedAddress, int expectedPort) { + assertEquals(expectedAddress, entry.peerAddress().get()); + if (expectedPort > 0) { + assertEquals(expectedPort, entry.peerPort().getAsInt()); + } + } + + private static void assertLogEntryHasRemote(ConnectionLogEntry entry, String expectedAddress, int expectedPort) { + if (expectedAddress != null) { + Assertions.assertThat(entry.remoteAddress()).hasValue(expectedAddress); + } else { + Assertions.assertThat(entry.remoteAddress()).isEmpty(); + } + if (expectedPort > 0) { + Assertions.assertThat(entry.remotePort()).hasValue(expectedPort); + } else { + Assertions.assertThat(entry.remotePort()).isEmpty(); + } + } + + +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/SslHandshakeMetricsTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/SslHandshakeMetricsTest.java new file mode 100644 index 00000000000..0f71dd87d00 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/SslHandshakeMetricsTest.java @@ -0,0 +1,189 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.container.logging.ConnectionLogEntry; +import com.yahoo.security.SslContextBuilder; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLHandshakeException; +import java.io.IOException; +import java.nio.file.Path; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static com.yahoo.jdisc.http.server.jetty.Utils.createSslTestDriver; +import static com.yahoo.jdisc.http.server.jetty.Utils.generatePrivateKeyAndCertificate; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.verify; + +/** + * @author bjorncs + */ +class SslHandshakeMetricsTest { + + private static final Logger log = Logger.getLogger(SslHandshakeMetricsTest.class.getName()); + private static Path privateKeyFile; + private static Path certificateFile; + + @BeforeAll + static void generateCrypto(@TempDir Path tmpFolder) throws IOException { + privateKeyFile = tmpFolder.resolve("private-key.pem"); + certificateFile = tmpFolder.resolve("certificate.pem"); + generatePrivateKeyAndCertificate(privateKeyFile, certificateFile); + } + + @Test + void requireThatMetricIsIncrementedWhenClientIsMissingCertificateOnHandshake() throws IOException { + var metricConsumer = new MetricConsumerMock(); + InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); + JettyTestDriver driver = createSslTestDriver(certificateFile, privateKeyFile, metricConsumer, connectionLog); + + SSLContext clientCtx = new SslContextBuilder() + .withTrustStore(certificateFile) + .build(); + assertHttpsRequestTriggersSslHandshakeException( + driver, clientCtx, null, null, "Received fatal alert: bad_certificate"); + verify(metricConsumer.mockitoMock(), atLeast(1)) + .add(MetricDefinitions.SSL_HANDSHAKE_FAILURE_MISSING_CLIENT_CERT, 1L, MetricConsumerMock.STATIC_CONTEXT); + assertTrue(driver.close()); + assertThat(connectionLog.logEntries()).hasSize(1); + assertSslHandshakeFailurePresent( + connectionLog.logEntries().get(0), SSLHandshakeException.class, SslHandshakeFailure.MISSING_CLIENT_CERT.failureType()); + } + + @Test + void requireThatMetricIsIncrementedWhenClientUsesIncompatibleTlsVersion() throws IOException { + var metricConsumer = new MetricConsumerMock(); + InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); + JettyTestDriver driver = createSslTestDriver(certificateFile, privateKeyFile, metricConsumer, connectionLog); + + SSLContext clientCtx = new SslContextBuilder() + .withTrustStore(certificateFile) + .withKeyStore(privateKeyFile, certificateFile) + .build(); + + boolean tlsv11Enabled = List.of(clientCtx.getDefaultSSLParameters().getProtocols()).contains("TLSv1.1"); + assumeTrue(tlsv11Enabled, "TLSv1.1 must be enabled in installed JDK"); + + assertHttpsRequestTriggersSslHandshakeException(driver, clientCtx, "TLSv1.1", null, "protocol"); + verify(metricConsumer.mockitoMock(), atLeast(1)) + .add(MetricDefinitions.SSL_HANDSHAKE_FAILURE_INCOMPATIBLE_PROTOCOLS, 1L, MetricConsumerMock.STATIC_CONTEXT); + assertTrue(driver.close()); + assertThat(connectionLog.logEntries()).hasSize(1); + assertSslHandshakeFailurePresent( + connectionLog.logEntries().get(0), SSLHandshakeException.class, SslHandshakeFailure.INCOMPATIBLE_PROTOCOLS.failureType()); + } + + @Test + void requireThatMetricIsIncrementedWhenClientUsesIncompatibleCiphers() throws IOException { + var metricConsumer = new MetricConsumerMock(); + InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); + JettyTestDriver driver = createSslTestDriver(certificateFile, privateKeyFile, metricConsumer, connectionLog); + + SSLContext clientCtx = new SslContextBuilder() + .withTrustStore(certificateFile) + .withKeyStore(privateKeyFile, certificateFile) + .build(); + + assertHttpsRequestTriggersSslHandshakeException( + driver, clientCtx, null, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "Received fatal alert: handshake_failure"); + verify(metricConsumer.mockitoMock(), atLeast(1)) + .add(MetricDefinitions.SSL_HANDSHAKE_FAILURE_INCOMPATIBLE_CIPHERS, 1L, MetricConsumerMock.STATIC_CONTEXT); + assertTrue(driver.close()); + assertThat(connectionLog.logEntries()).hasSize(1); + assertSslHandshakeFailurePresent( + connectionLog.logEntries().get(0), SSLHandshakeException.class, SslHandshakeFailure.INCOMPATIBLE_CIPHERS.failureType()); + } + + @Test + void requireThatMetricIsIncrementedWhenClientUsesInvalidCertificateInHandshake(@TempDir Path tmpFolder) throws IOException { + var metricConsumer = new MetricConsumerMock(); + InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); + JettyTestDriver driver = createSslTestDriver(certificateFile, privateKeyFile, metricConsumer, connectionLog); + + Path clientPrivateKeyFile = tmpFolder.resolve("client-key.pem"); + Path clientCertificateFile = tmpFolder.resolve("client-cert.pem"); + generatePrivateKeyAndCertificate(clientPrivateKeyFile, clientCertificateFile); + + SSLContext clientCtx = new SslContextBuilder() + .withKeyStore(clientPrivateKeyFile, clientCertificateFile) + .withTrustStore(certificateFile) + .build(); + + assertHttpsRequestTriggersSslHandshakeException( + driver, clientCtx, null, null, "Received fatal alert: certificate_unknown"); + verify(metricConsumer.mockitoMock(), atLeast(1)) + .add(MetricDefinitions.SSL_HANDSHAKE_FAILURE_INVALID_CLIENT_CERT, 1L, MetricConsumerMock.STATIC_CONTEXT); + assertTrue(driver.close()); + assertThat(connectionLog.logEntries()).hasSize(1); + assertSslHandshakeFailurePresent( + connectionLog.logEntries().get(0), SSLHandshakeException.class, SslHandshakeFailure.INVALID_CLIENT_CERT.failureType()); + } + + @Test + void requireThatMetricIsIncrementedWhenClientUsesExpiredCertificateInHandshake(@TempDir Path tmpFolder) throws IOException { + Path endEntityKeyFile = tmpFolder.resolve("client-key.pem"); + Path endEntitycertificateFile = tmpFolder.resolve("client-cert.pem"); + Instant notAfter = Instant.now().minus(100, ChronoUnit.DAYS); + generatePrivateKeyAndCertificate(privateKeyFile, certificateFile, endEntityKeyFile, endEntitycertificateFile, notAfter); + var metricConsumer = new MetricConsumerMock(); + InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); + JettyTestDriver driver = createSslTestDriver(certificateFile, privateKeyFile, metricConsumer, connectionLog); + + SSLContext clientCtx = new SslContextBuilder() + .withTrustStore(certificateFile) + .withKeyStore(endEntityKeyFile, endEntitycertificateFile) + .build(); + + assertHttpsRequestTriggersSslHandshakeException( + driver, clientCtx, null, null, "Received fatal alert: certificate_unknown"); + verify(metricConsumer.mockitoMock(), atLeast(1)) + .add(MetricDefinitions.SSL_HANDSHAKE_FAILURE_EXPIRED_CLIENT_CERT, 1L, MetricConsumerMock.STATIC_CONTEXT); + assertTrue(driver.close()); + assertThat(connectionLog.logEntries()).hasSize(1); + } + + + private static void assertHttpsRequestTriggersSslHandshakeException( + JettyTestDriver testDriver, + SSLContext sslContext, + String protocolOverride, + String cipherOverride, + String expectedExceptionSubstring) throws IOException { + List<String> protocols = protocolOverride != null ? List.of(protocolOverride) : null; + List<String> ciphers = cipherOverride != null ? List.of(cipherOverride) : null; + try (var client = new SimpleHttpClient(sslContext, protocols, ciphers, testDriver.server().getListenPort(), false)) { + client.get("/status.html"); + fail("SSLHandshakeException expected"); + } catch (SSLHandshakeException e) { + assertThat(e.getMessage()).contains(expectedExceptionSubstring); + } catch (SSLException e) { + // This exception is thrown if Apache httpclient's write thread detects the handshake failure before the read thread. + log.log(Level.WARNING, "Client failed to get a proper TLS handshake response: " + e.getMessage(), e); + // Only ignore a subset of exceptions + assertTrue(e.getMessage().contains("readHandshakeRecord") || e.getMessage().contains("Broken pipe"), e.getMessage()); + } + } + + private static void assertSslHandshakeFailurePresent( + ConnectionLogEntry entry, Class<? extends SSLHandshakeException> expectedException, String expectedType) { + assertThat(entry.sslHandshakeFailure()).isPresent(); + ConnectionLogEntry.SslHandshakeFailure failure = entry.sslHandshakeFailure().get(); + assertEquals(expectedType, failure.type()); + ConnectionLogEntry.SslHandshakeFailure.ExceptionEntry exceptionEntry = failure.exceptionChain().get(0); + assertEquals(expectedException.getName(), exceptionEntry.name()); + } + +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/Utils.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/Utils.java new file mode 100644 index 00000000000..626ab521773 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/Utils.java @@ -0,0 +1,68 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.inject.Module; +import com.yahoo.container.logging.ConnectionLog; +import com.yahoo.jdisc.application.MetricConsumer; +import com.yahoo.security.KeyUtils; +import com.yahoo.security.Pkcs10Csr; +import com.yahoo.security.Pkcs10CsrBuilder; +import com.yahoo.security.X509CertificateBuilder; +import com.yahoo.security.X509CertificateUtils; + +import javax.security.auth.x500.X500Principal; +import java.io.IOException; +import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +import static com.yahoo.security.KeyAlgorithm.EC; +import static com.yahoo.security.SignatureAlgorithm.SHA256_WITH_ECDSA; + +/** + * @author bjorncs + */ +class Utils { + + private Utils() {} + + static JettyTestDriver createSslTestDriver( + Path serverCertificateFile, Path serverPrivateKeyFile, MetricConsumerMock metricConsumer, InMemoryConnectionLog connectionLog) { + Module extraModule = binder -> { + binder.bind(MetricConsumer.class).toInstance(metricConsumer.mockitoMock()); + binder.bind(ConnectionLog.class).toInstance(connectionLog); + }; + return JettyTestDriver.newInstanceWithSsl( + new EchoRequestHandler(), serverCertificateFile, serverPrivateKeyFile, JettyTestDriver.TlsClientAuth.NEED, extraModule); + } + + static void generatePrivateKeyAndCertificate(Path privateKeyFile, Path certificateFile) throws IOException { + KeyPair keyPair = KeyUtils.generateKeypair(EC); + Files.writeString(privateKeyFile, KeyUtils.toPem(keyPair.getPrivate())); + + X509Certificate certificate = X509CertificateBuilder + .fromKeypair( + keyPair, new X500Principal("CN=localhost"), Instant.EPOCH, Instant.EPOCH.plus(100_000, ChronoUnit.DAYS), SHA256_WITH_ECDSA, BigInteger.ONE) + .build(); + Files.writeString(certificateFile, X509CertificateUtils.toPem(certificate)); + } + + static void generatePrivateKeyAndCertificate(Path rootPrivateKeyFile, Path rootCertificateFile, + Path privateKeyFile, Path certificateFile, Instant notAfter) throws IOException { + X509Certificate rootCertificate = X509CertificateUtils.fromPem(Files.readString(rootCertificateFile)); + PrivateKey privateKey = KeyUtils.fromPemEncodedPrivateKey(Files.readString(rootPrivateKeyFile)); + + KeyPair keyPair = KeyUtils.generateKeypair(EC); + Files.writeString(privateKeyFile, KeyUtils.toPem(keyPair.getPrivate())); + Pkcs10Csr csr = Pkcs10CsrBuilder.fromKeypair(new X500Principal("CN=myclient"), keyPair, SHA256_WITH_ECDSA).build(); + X509Certificate certificate = X509CertificateBuilder + .fromCsr(csr, rootCertificate.getSubjectX500Principal(), Instant.EPOCH, notAfter, privateKey, SHA256_WITH_ECDSA, BigInteger.ONE) + .build(); + Files.writeString(certificateFile, X509CertificateUtils.toPem(certificate)); + } +} |