From 549e498e15e9429d250c599988a0dc509b0d04fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccol=C3=B2=20Maltoni?= Date: Wed, 2 Oct 2024 15:51:48 +0200 Subject: [PATCH 1/8] test: rework ApplyConfigurationTest --- .../org/carapaceproxy/core/Listeners.java | 2 +- .../DynamicCertificatesManager.java | 2 +- .../carapaceproxy/ApplyConfigurationTest.java | 471 +++++++++--------- 3 files changed, 239 insertions(+), 236 deletions(-) diff --git a/carapace-server/src/main/java/org/carapaceproxy/core/Listeners.java b/carapace-server/src/main/java/org/carapaceproxy/core/Listeners.java index d25973fa4..ec25f6538 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/core/Listeners.java +++ b/carapace-server/src/main/java/org/carapaceproxy/core/Listeners.java @@ -398,7 +398,7 @@ private SslContext bootSslContext(NetworkListenerConfiguration listener, SSLCert try { // Try to find certificate data on db byte[] keystoreContent = parent.getDynamicCertificatesManager().getCertificateForDomain(certificate.getId()); - KeyStore keystore; + final KeyStore keystore; if (keystoreContent != null) { LOG.debug("start SSL with dynamic certificate id {}, on listener {}:{}", certificate.getId(), listener.getHost(), port); keystore = loadKeyStoreData(keystoreContent, certificate.getPassword()); diff --git a/carapace-server/src/main/java/org/carapaceproxy/server/certificates/DynamicCertificatesManager.java b/carapace-server/src/main/java/org/carapaceproxy/server/certificates/DynamicCertificatesManager.java index ee0c938ac..0d165e91b 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/server/certificates/DynamicCertificatesManager.java +++ b/carapace-server/src/main/java/org/carapaceproxy/server/certificates/DynamicCertificatesManager.java @@ -606,7 +606,7 @@ private RuntimeServerConfiguration getConfig() { * @return PKCS12 Keystore content */ public byte[] getCertificateForDomain(String domain) { - CertificateData cert = certificates.get(domain); // certs always retrived from cache + CertificateData cert = certificates.get(domain); // certs always retrieved from cache if (cert == null || cert.getKeystoreData() == null || cert.getKeystoreData().length == 0) { LOG.error("No dynamic certificate available for domain {}", domain); return null; diff --git a/carapace-server/src/test/java/org/carapaceproxy/ApplyConfigurationTest.java b/carapace-server/src/test/java/org/carapaceproxy/ApplyConfigurationTest.java index 40f9af3ae..e49458a39 100644 --- a/carapace-server/src/test/java/org/carapaceproxy/ApplyConfigurationTest.java +++ b/carapace-server/src/test/java/org/carapaceproxy/ApplyConfigurationTest.java @@ -23,18 +23,35 @@ import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.anEmptyMap; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; import com.github.tomakehurst.wiremock.junit.WireMockRule; import java.io.IOException; import java.net.URI; -import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.util.Map; import java.util.Properties; -import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.ssl.SSLContextBuilder; import org.carapaceproxy.configstore.PropertiesConfigurationStore; import org.carapaceproxy.core.HttpProxyServer; import org.carapaceproxy.server.config.ConfigurationChangeInProgressException; @@ -46,6 +63,7 @@ import org.carapaceproxy.user.UserRealm; import org.carapaceproxy.utils.TestEndpointMapper; import org.carapaceproxy.utils.TestUserRealm; +import org.carapaceproxy.utils.TestUtils; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Rule; @@ -53,13 +71,14 @@ import org.junit.rules.TemporaryFolder; /** - * * @author enrico.olivelli */ public class ApplyConfigurationTest { @ClassRule public static WireMockRule wireMockRule = new WireMockRule(0); + @Rule + public TemporaryFolder tmpDir = new TemporaryFolder(); @BeforeClass public static void setupWireMock() { @@ -73,236 +92,197 @@ public static void setupWireMock() { } - @Rule - public TemporaryFolder tmpDir = new TemporaryFolder(); - - /** - * Static mapper, so that it can be references by configuration - */ - public static final class StaticEndpointMapper extends TestEndpointMapper { - - public StaticEndpointMapper(final HttpProxyServer ignoredServer) { - this(); // required for reflective construction - } - - public StaticEndpointMapper() { - super("localhost", wireMockRule.port()); - } + private static CloseableHttpClient createHttpClientWithDisabledSSLValidation() throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + return HttpClients.custom() + .setSSLContext(SSLContextBuilder.create() + .loadTrustMaterial((chain, authType) -> true) // Trust all certificates + .build()) + .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE) // Disable hostname verification + .build(); } @Test public void testChangeListenersConfig() throws Exception { - try (HttpProxyServer server = new HttpProxyServer(StandardEndpointMapper::new, tmpDir.newFolder());) { - - { - Properties configuration = new Properties(); - configuration.put("mapper.class", StaticEndpointMapper.class.getName()); - configuration.put("aws.accesskey", "accesskey"); - configuration.put("aws.secretkey", "secretkey"); - server.configureAtBoot(new PropertiesConfigurationStore(configuration)); - } + server.configureAtBoot(new PropertiesConfigurationStore(propsWithMapper(Map.of( + "aws.accesskey", "accesskey", + "aws.secretkey", "secretkey" + )))); // start without listeners server.start(); // start two listeners - { - Properties configuration = new Properties(); - configuration.put("mapper.class", StaticEndpointMapper.class.getName()); - configuration.put("listener.1.host", "localhost"); - configuration.put("listener.1.port", "1423"); - configuration.put("listener.2.host", "localhost"); - configuration.put("listener.2.port", "1426"); - reloadConfiguration(configuration, server); - } + reloadConfiguration(server, propsWithMapper(Map.of( + "listener.1.host", "localhost", + "listener.1.port", "1423", + "listener.2.host", "localhost", + "listener.2.port", "1426" + ))); testIt(1423, true); testIt(1426, true); // restart listener 1 - { - Properties configuration = new Properties(); - configuration.put("mapper.class", StaticEndpointMapper.class.getName()); - configuration.put("listener.1.host", "localhost"); - configuration.put("listener.1.port", "1425"); - configuration.put("listener.2.host", "localhost"); - configuration.put("listener.2.port", "1426"); - reloadConfiguration(configuration, server); - } + reloadConfiguration(server, propsWithMapper(Map.of( + "listener.1.host", "localhost", + "listener.1.port", "1425", + "listener.2.host", "localhost", + "listener.2.port", "1426" + ))); testIt(1425, true); testIt(1426, true); // stop listener 2 - { - Properties configuration = new Properties(); - configuration.put("mapper.class", StaticEndpointMapper.class.getName()); - configuration.put("listener.1.host", "localhost"); - configuration.put("listener.1.port", "1425"); - reloadConfiguration(configuration, server); - } + reloadConfiguration(server, propsWithMapper(Map.of( + "listener.1.host", "localhost", + "listener.1.port", "1425" + ))); testIt(1425, true); testIt(1426, false); - // restart listerer 2 - { - Properties configuration = new Properties(); - configuration.put("mapper.class", StaticEndpointMapper.class.getName()); - configuration.put("listener.1.host", "localhost"); - configuration.put("listener.1.port", "1425"); - configuration.put("listener.2.host", "localhost"); - configuration.put("listener.2.port", "1426"); - reloadConfiguration(configuration, server); - } + // restart listener 2 + reloadConfiguration(server, propsWithMapper(Map.of( + "listener.1.host", "localhost", + "listener.1.port", "1425", + "listener.2.host", "localhost", + "listener.2.port", "1426" + ))); testIt(1425, true); testIt(1426, true); // no more listeners - { - Properties configuration = new Properties(); - configuration.put("mapper.class", StaticEndpointMapper.class.getName()); - reloadConfiguration(configuration, server); - } + reloadConfiguration(server, propsWithMapper(Map.of())); testIt(1425, false); testIt(1426, false); // listener with correct tls version - { - Properties configuration = new Properties(); - configuration.put("mapper.class", StaticEndpointMapper.class.getName()); - configuration.put("certificate.1.hostname", "*"); - configuration.put("certificate.1.mode", "manual"); - - configuration.put("listener.1.host", "localhost"); - configuration.put("listener.1.port", "1423"); - configuration.put("listener.1.ssl", "true"); - configuration.put("listener.1.sslprotocols", "TLSv1.2"); - - configuration.put("listener.2.host", "localhost"); - configuration.put("listener.2.port", "1426"); - configuration.put("listener.2.ssl", "true"); - configuration.put("listener.2.sslprotocols", "TLSv1.2,TLSv1.3"); - reloadConfiguration(configuration, server); - } + String defaultCertificate = TestUtils.deployResource("ia.p12", tmpDir.getRoot()); + reloadConfiguration(server, propsWithMapperAndCertificate(defaultCertificate, Map.of( + "listener.1.host", "localhost", + "listener.1.port", "1423", + "listener.1.ssl", "true", + "listener.1.sslprotocols", "TLSv1.2", + "listener.2.host", "localhost", + "listener.2.port", "1426", + "listener.2.ssl", "true", + "listener.2.sslprotocols", "TLSv1.2,TLSv1.3" + ))); + + // Test HTTPS for listener 1 + testIt(1423, true, true); // Expecting valid HTTPS connection + // Test HTTPS for listener 2 + testIt(1426, true, true); // Expecting valid HTTPS connection + // listener with default tls version - { - Properties configuration = new Properties(); - configuration.put("mapper.class", StaticEndpointMapper.class.getName()); - configuration.put("certificate.1.hostname", "*"); - configuration.put("certificate.1.mode", "manual"); - configuration.put("listener.1.host", "localhost"); - configuration.put("listener.1.port", "1423"); - configuration.put("listener.1.ssl", "true"); - reloadConfiguration(configuration, server); - } + reloadConfiguration(server, propsWithMapperAndCertificate(defaultCertificate, Map.of( + "listener.1.host", "localhost", + "listener.1.port", "1423", + "listener.1.ssl", "true" + ))); + // Test HTTPS for listener 1 + testIt(1423, true, true); // Expecting valid HTTPS connection + // listener with wrong tls version - try { - Properties configuration = new Properties(); - configuration.put("mapper.class", StaticEndpointMapper.class.getName()); - configuration.put("certificate.1.hostname", "*"); - configuration.put("certificate.1.mode", "manual"); - configuration.put("listener.1.host", "localhost"); - configuration.put("listener.1.port", "1423"); - configuration.put("listener.1.ssl", "true"); - configuration.put("listener.1.sslprotocols", "TLSUNKNOWN"); - reloadConfiguration(configuration, server); - } catch (IllegalStateException e) { - Throwable cause = e.getCause(); - assertTrue(cause instanceof ConfigurationNotValidException && cause.getMessage().contains("Unsupported SSL Protocols")); - } + final IllegalStateException e = assertThrows(IllegalStateException.class, () -> + reloadConfiguration(server, propsWithMapperAndCertificate(defaultCertificate, Map.of( + "listener.1.host", "localhost", + "listener.1.port", "1423", + "listener.1.ssl", "true", + "listener.1.sslprotocols", "TLSUNKNOWN" + )))); + Throwable cause = e.getCause(); + assertThat(cause, instanceOf(ConfigurationNotValidException.class)); + assertThat(cause.getMessage(), containsString("Unsupported SSL Protocols")); } } @Test public void testReloadMapper() throws Exception { - try (HttpProxyServer server = new HttpProxyServer(StandardEndpointMapper::new, tmpDir.newFolder());) { - - { - Properties configuration = new Properties(); - server.configureAtBoot(new PropertiesConfigurationStore(configuration)); - } + server.configureAtBoot(new PropertiesConfigurationStore(new Properties())); server.start(); - { - StandardEndpointMapper mapper = (StandardEndpointMapper) server.getMapper(); - assertEquals(0, mapper.getBackends().size()); - } + assertThat(server.getMapper(), instanceOf(StandardEndpointMapper.class)); + assertThat(server.getMapper().getBackends(), is(anEmptyMap())); // add backend - { - Properties configuration = new Properties(); - configuration.put("backend.1.id", "foo"); - configuration.put("backend.1.host", "my-host1"); - configuration.put("backend.1.port", "4213"); - configuration.put("backend.1.enabled", "true"); - reloadConfiguration(configuration, server); - - StandardEndpointMapper mapper = (StandardEndpointMapper) server.getMapper(); - assertEquals(1, mapper.getBackends().size()); - System.out.println("backends:" + mapper.getBackends()); - assertNotNull(mapper.getBackends().get("foo")); - } + reloadConfiguration(server, props(Map.of( + "backend.1.id", "foo", + "backend.1.host", "my-host1", + "backend.1.port", "4213", + "backend.1.enabled", "true" + ))); + assertThat(server.getMapper(), instanceOf(StandardEndpointMapper.class)); + assertThat(server.getMapper().getBackends(), allOf( + is(aMapWithSize(1)), + hasKey("foo") + )); // add second backend - { - Properties configuration = new Properties(); - configuration.put("backend.1.id", "foo"); - configuration.put("backend.1.host", "my-host1"); - configuration.put("backend.1.port", "4213"); - configuration.put("backend.1.enabled", "true"); - - configuration.put("backend.2.id", "bar"); - configuration.put("backend.2.host", "my-host2"); - configuration.put("backend.2.port", "4213"); - configuration.put("backend.2.enabled", "true"); - reloadConfiguration(configuration, server); - - StandardEndpointMapper mapper = (StandardEndpointMapper) server.getMapper(); - - assertEquals(2, mapper.getBackends().size()); - assertNotNull(mapper.getBackends().get("foo")); - assertNotNull(mapper.getBackends().get("bar")); - } + reloadConfiguration(server, props(Map.of( + "backend.1.id", "foo", + "backend.1.host", "my-host1", + "backend.1.port", "4213", + "backend.1.enabled", "true", + "backend.2.id", "bar", + "backend.2.host", "my-host2", + "backend.2.port", "4213", + "backend.2.enabled", "true" + ))); + + assertThat(server.getMapper(), instanceOf(StandardEndpointMapper.class)); + assertThat(server.getMapper().getBackends(), allOf( + is(aMapWithSize(2)), + hasKey("foo"), + hasKey("bar") + )); // remove first backend - { - Properties configuration = new Properties(); - - configuration.put("backend.2.id", "bar"); - configuration.put("backend.2.host", "my-host2"); - configuration.put("backend.2.port", "4213"); - configuration.put("backend.2.enabled", "true"); - reloadConfiguration(configuration, server); - - StandardEndpointMapper mapper = (StandardEndpointMapper) server.getMapper(); - assertEquals(1, mapper.getBackends().size()); - assertNull(mapper.getBackends().get("foo")); - assertNotNull(mapper.getBackends().get("bar")); - } - + reloadConfiguration(server, props(Map.of( + "backend.2.id", "bar", + "backend.2.host", "my-host2", + "backend.2.port", "4213", + "backend.2.enabled", "true" + ))); + + assertThat(server.getMapper(), instanceOf(StandardEndpointMapper.class)); + assertThat(server.getMapper().getBackends(), allOf( + is(aMapWithSize(1)), + hasKey("bar") + )); } } + private void reloadConfiguration(final HttpProxyServer server, final Properties configuration) throws ConfigurationChangeInProgressException, InterruptedException { + PropertiesConfigurationStore config = new PropertiesConfigurationStore(configuration); + server.applyDynamicConfigurationFromAPI(config); + } + + private Properties props(final Map props) { + final var configuration = new Properties(props.size() + 1); + configuration.putAll(props); + return configuration; + } + @Test public void testUserRealm() throws Exception { // Default UserRealm try (HttpProxyServer server = new HttpProxyServer(StandardEndpointMapper::new, tmpDir.newFolder())) { - Properties configuration = new Properties(); - server.configureAtBoot(new PropertiesConfigurationStore(configuration)); + server.configureAtBoot(new PropertiesConfigurationStore(new Properties())); server.start(); UserRealm realm = server.getRealm(); - assertTrue(realm instanceof SimpleUserRealm); + assertThat(realm, is(instanceOf(SimpleUserRealm.class))); // default user with auth always valid SimpleUserRealm userRealm = (SimpleUserRealm) server.getRealm(); - assertEquals(1, userRealm.listUsers().size()); + assertThat(userRealm.listUsers(), hasSize(1)); assertNotNull(userRealm.login("test_0", "anypass0")); assertNotNull(userRealm.login("test_1", "anypass1")); @@ -311,26 +291,32 @@ public void testUserRealm() throws Exception { // TestUserRealm try (HttpProxyServer server = new HttpProxyServer(StandardEndpointMapper::new, tmpDir.newFolder())) { - Properties configuration = new Properties(); - configuration.put("userrealm.class", "org.carapaceproxy.utils.TestUserRealm"); - configuration.put("user.test1", "pass1"); - configuration.put("user.test2", "pass2"); - server.configureAtBoot(new PropertiesConfigurationStore(configuration)); + server.configureAtBoot(new PropertiesConfigurationStore(props(Map.of( + "userrealm.class", "org.carapaceproxy.utils.TestUserRealm", + "user.test1", "pass1", + "user.test2", "pass2" + )))); server.start(); UserRealm realm = server.getRealm(); - assertTrue(realm instanceof TestUserRealm); + assertThat(realm, is(instanceOf(TestUserRealm.class))); + TestUserRealm userRealm = (TestUserRealm) server.getRealm(); - assertEquals(2, userRealm.listUsers().size()); + assertThat(userRealm.listUsers(), hasSize(2)); assertNotNull(userRealm.login("test1", "pass1")); assertNotNull(userRealm.login("test2", "pass2")); assertNull(userRealm.login("test1", "pass3")); // wrong pass // Add new user - configuration.put("user.test3", "pass3"); - reloadConfiguration(configuration, server); + reloadConfiguration(server, props(Map.of( + "userrealm.class", "org.carapaceproxy.utils.TestUserRealm", + "user.test1", "pass1", + "user.test2", "pass2", + "user.test3", "pass3" + ))); + userRealm = (TestUserRealm) server.getRealm(); // realm re-created at each configuration reload - assertEquals(3, userRealm.listUsers().size()); + assertThat(userRealm.listUsers(), hasSize(3)); assertNotNull(userRealm.login("test3", "pass3")); } } @@ -338,81 +324,68 @@ public void testUserRealm() throws Exception { @SuppressWarnings("deprecation") @Test public void testChangeFiltersConfiguration() throws Exception { - try (HttpProxyServer server = new HttpProxyServer(StandardEndpointMapper::new, tmpDir.newFolder());) { - - { - Properties configuration = new Properties(); - configuration.put("filter.1.type", "add-x-forwarded-for"); - server.configureAtBoot(new PropertiesConfigurationStore(configuration)); - } + server.configureAtBoot(new PropertiesConfigurationStore(props("filter.1.type", "add-x-forwarded-for"))); server.start(); - assertEquals(1, server.getFilters().size()); - assertTrue(server.getFilters().get(0) instanceof XForwardedForRequestFilter); + assertThat(server.getFilters(), hasSize(1)); + assertThat(server.getFilters().get(0), instanceOf(XForwardedForRequestFilter.class)); // add a filter - { - Properties configuration = new Properties(); - configuration.put("filter.1.type", "add-x-forwarded-for"); - configuration.put("filter.2.type", "match-user-regexp"); - reloadConfiguration(configuration, server); - - assertEquals(2, server.getFilters().size()); - assertTrue(server.getFilters().get(0) instanceof XForwardedForRequestFilter); - assertTrue(server.getFilters().get(1) instanceof RegexpMapUserIdFilter); - } + reloadConfiguration(server, props(Map.of( + "filter.1.type", "add-x-forwarded-for", + "filter.2.type", "match-user-regexp" + ))); - // remove a filter - { - Properties configuration = new Properties(); - configuration.put("filter.2.type", "match-user-regexp"); - reloadConfiguration(configuration, server); - - assertEquals(1, server.getFilters().size()); - assertTrue(server.getFilters().get(0) instanceof RegexpMapUserIdFilter); - } + assertThat(server.getFilters(), hasSize(2)); + assertThat(server.getFilters().get(0), is(instanceOf(XForwardedForRequestFilter.class))); + assertThat(server.getFilters().get(1), is(instanceOf(RegexpMapUserIdFilter.class))); + // remove a filter + reloadConfiguration(server, props("filter.2.type", "match-user-regexp")); + assertThat(server.getFilters(), hasSize(1)); + assertThat(server.getFilters().get(0), is(instanceOf(RegexpMapUserIdFilter.class))); } } + private Properties props(final String key, final String value) { + return props(Map.of(key, value)); + } + @Test public void testChangeBackendHealthManagerConfiguration() throws Exception { - try (HttpProxyServer server = new HttpProxyServer(StandardEndpointMapper::new, tmpDir.newFolder());) { - - { - Properties configuration = new Properties(); - configuration.put("healthmanager.connecttimeout", "9479"); - server.configureAtBoot(new PropertiesConfigurationStore(configuration)); - } + server.configureAtBoot(new PropertiesConfigurationStore(props("healthmanager.connecttimeout", "9479"))); server.start(); assertEquals(9479, server.getBackendHealthManager().getConnectTimeout()); // change configuration - { - Properties configuration = new Properties(); - configuration.put("healthmanager.connecttimeout", "9233"); - reloadConfiguration(configuration, server); - - assertEquals(9233, server.getBackendHealthManager().getConnectTimeout()); - } - + reloadConfiguration(server, props("healthmanager.connecttimeout", "9233")); + assertEquals(9233, server.getBackendHealthManager().getConnectTimeout()); } } - private void reloadConfiguration(Properties configuration, final HttpProxyServer server) throws ConfigurationNotValidException, ConfigurationChangeInProgressException, InterruptedException { - PropertiesConfigurationStore config = new PropertiesConfigurationStore(configuration); - server.applyDynamicConfigurationFromAPI(config); + private void testIt(int port, boolean ok) throws Exception { + testIt(port, false, ok); } - private void testIt(int port, boolean ok) throws URISyntaxException, IOException { - try { - String url = "http://localhost:" + port + "/index.html?redir"; - String s = IOUtils.toString(URI.create(url), StandardCharsets.UTF_8); - System.out.println("RES FOR: " + url + " -> " + s); - assertEquals("it works !!", s); - if (!ok) { - fail("Expecting an error for port " + port); + private void testIt(int port, final boolean https, boolean ok) throws Exception { + try (CloseableHttpClient client = createHttpClientWithDisabledSSLValidation()) { + final String protocol = https ? "https" : "http"; + String url = protocol + "://localhost:" + port + "/index.html?redir"; + + HttpGet request = new HttpGet(new URI(url)); + try (CloseableHttpResponse response = client.execute(request)) { + int statusCode = response.getStatusLine().getStatusCode(); + String responseBody = new String(response.getEntity().getContent().readAllBytes(), StandardCharsets.UTF_8); + + System.out.println("RES FOR: " + url + " -> " + responseBody); + + // Check that the response body matches what we expect + assertEquals("it works !!", responseBody); + + if (!ok && statusCode == 200) { + fail("Expecting an error for port " + port); + } } } catch (IOException err) { if (ok) { @@ -421,4 +394,34 @@ private void testIt(int port, boolean ok) throws URISyntaxException, IOException } } + private Properties propsWithMapper(final Map props) { + final var configuration = new Properties(props.size()); + configuration.put("mapper.class", StaticEndpointMapper.class.getName()); + configuration.putAll(props); + return configuration; + } + + private Properties propsWithMapperAndCertificate(final String defaultCertificate, final Map props) { + final var configuration = new Properties(props.size()); + configuration.put("mapper.class", StaticEndpointMapper.class.getName()); + configuration.put("certificate.1.hostname", "*"); + configuration.put("certificate.1.file", defaultCertificate); + configuration.put("certificate.1.password", "changeit"); + configuration.putAll(props); + return configuration; + } + + /** + * Static mapper, so that it can be references by configuration + */ + public static final class StaticEndpointMapper extends TestEndpointMapper { + + public StaticEndpointMapper(final HttpProxyServer ignoredServer) { + this(); // required for reflective construction + } + + public StaticEndpointMapper() { + super("localhost", wireMockRule.port()); + } + } } From 1ed56618ff720721adcf49892cc2f22dfa639901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccol=C3=B2=20Maltoni?= Date: Wed, 4 Dec 2024 13:49:11 +0100 Subject: [PATCH 2/8] refactor: move ListeningChannel out of Listeners --- .../carapaceproxy/api/ListenersResource.java | 35 +- .../configstore/ConfigurationStoreUtils.java | 4 +- .../org/carapaceproxy/core/EndpointKey.java | 4 + .../org/carapaceproxy/core/Listeners.java | 348 +++++------------- .../carapaceproxy/core/ListeningChannel.java | 180 +++++++++ .../core/RuntimeServerConfiguration.java | 15 +- .../cache/CacheByteBufMemoryUsageMetric.java | 2 +- .../ocsp/OcspStaplingManager.java | 1 - .../config/NetworkListenerConfiguration.java | 7 +- .../config/SSLCertificateConfiguration.java | 2 +- .../utils/CertificatesUtils.java | 137 ++++++- .../carapaceproxy/ApplyConfigurationTest.java | 50 +-- .../carapaceproxy/core/MaxHeaderSizeTest.java | 33 +- .../listeners/ListenerConfigurationTest.java | 8 +- .../carapaceproxy/listeners/SSLSNITest.java | 41 ++- .../certificates/CertificatesUtilsTest.java | 4 +- .../carapaceproxy/utils/ApacheHttpUtils.java | 18 + 17 files changed, 492 insertions(+), 397 deletions(-) create mode 100644 carapace-server/src/main/java/org/carapaceproxy/core/ListeningChannel.java create mode 100644 carapace-server/src/test/java/org/carapaceproxy/utils/ApacheHttpUtils.java diff --git a/carapace-server/src/main/java/org/carapaceproxy/api/ListenersResource.java b/carapace-server/src/main/java/org/carapaceproxy/api/ListenersResource.java index 3fe946871..68fab7a3d 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/api/ListenersResource.java +++ b/carapace-server/src/main/java/org/carapaceproxy/api/ListenersResource.java @@ -27,9 +27,7 @@ import javax.ws.rs.Path; import javax.ws.rs.Produces; import lombok.Data; -import org.carapaceproxy.core.EndpointKey; import org.carapaceproxy.core.HttpProxyServer; -import org.carapaceproxy.server.config.NetworkListenerConfiguration; /** * Access to listeners @@ -58,23 +56,22 @@ public static final class ListenerBean { @GET public Map getAllListeners() { - HttpProxyServer server = (HttpProxyServer) context.getAttribute("server"); - - return server.getListeners().getListeningChannels().entrySet().stream().map(listener -> { - NetworkListenerConfiguration config = listener.getValue().getConfig(); - int port = listener.getKey().port(); - ListenerBean bean = new ListenerBean( - config.getHost(), - port, - config.isSsl(), - config.getSslCiphers(), - config.getSslProtocols(), - config.getDefaultCertificate(), - (int) listener.getValue().getTotalRequests().get() - ); - EndpointKey endpointKey = EndpointKey.make(config.getHost(), port); - return Map.entry(endpointKey.toString(), bean); - }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + final HttpProxyServer server = (HttpProxyServer) context.getAttribute("server"); + return server.getListeners() + .getListeningChannels() + .values() + .stream() + .collect(Collectors.toMap( + channel -> channel.getHostPort().toString(), + channel -> new ListenerBean( + channel.getHostPort().host(), + channel.getHostPort().port(), + channel.getConfig().isSsl(), + channel.getConfig().getSslCiphers(), + channel.getConfig().getSslProtocols(), + channel.getConfig().getDefaultCertificate(), + (int) channel.getTotalRequests().get() + ))); } } diff --git a/carapace-server/src/main/java/org/carapaceproxy/configstore/ConfigurationStoreUtils.java b/carapace-server/src/main/java/org/carapaceproxy/configstore/ConfigurationStoreUtils.java index 5ec039691..a623eaa04 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/configstore/ConfigurationStoreUtils.java +++ b/carapace-server/src/main/java/org/carapaceproxy/configstore/ConfigurationStoreUtils.java @@ -19,6 +19,8 @@ */ package org.carapaceproxy.configstore; +import static org.carapaceproxy.utils.CertificatesUtils.createKeystore; +import static org.carapaceproxy.utils.CertificatesUtils.readChainFromKeystore; import java.security.GeneralSecurityException; import java.security.Key; import java.security.KeyFactory; @@ -28,8 +30,6 @@ import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; -import static org.carapaceproxy.utils.CertificatesUtils.createKeystore; -import static org.carapaceproxy.utils.CertificatesUtils.readChainFromKeystore; public final class ConfigurationStoreUtils { diff --git a/carapace-server/src/main/java/org/carapaceproxy/core/EndpointKey.java b/carapace-server/src/main/java/org/carapaceproxy/core/EndpointKey.java index 66b487f50..92a80c345 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/core/EndpointKey.java +++ b/carapace-server/src/main/java/org/carapaceproxy/core/EndpointKey.java @@ -64,4 +64,8 @@ public static EndpointKey make(String hostAndPort) { public String toString() { return host + ":" + port; } + + public EndpointKey offsetPort(final int offsetPort) { + return make(host(), port() + offsetPort); + } } diff --git a/carapace-server/src/main/java/org/carapaceproxy/core/Listeners.java b/carapace-server/src/main/java/org/carapaceproxy/core/Listeners.java index ec25f6538..7b379fd21 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/core/Listeners.java +++ b/carapace-server/src/main/java/org/carapaceproxy/core/Listeners.java @@ -19,9 +19,6 @@ */ package org.carapaceproxy.core; -import static org.carapaceproxy.utils.CertificatesUtils.loadKeyStoreData; -import static org.carapaceproxy.utils.CertificatesUtils.loadKeyStoreFromFile; -import static org.carapaceproxy.utils.CertificatesUtils.readChainFromKeystore; import static reactor.netty.ConnectionObserver.State.CONNECTED; import io.netty.buffer.ByteBufAllocator; import io.netty.channel.ChannelOption; @@ -29,48 +26,32 @@ import io.netty.channel.epoll.EpollChannelOption; import io.netty.channel.socket.nio.NioChannelOption; import io.netty.handler.ssl.OpenSsl; -import io.netty.handler.ssl.OpenSslCachingX509KeyManagerFactory; import io.netty.handler.ssl.ReferenceCountedOpenSslEngine; import io.netty.handler.ssl.SniHandler; import io.netty.handler.ssl.SslContext; -import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SslHandler; -import io.netty.handler.ssl.SslProvider; import io.netty.handler.timeout.IdleStateHandler; -import io.netty.util.Attribute; import io.netty.util.AttributeKey; -import io.netty.util.concurrent.Future; -import io.netty.util.concurrent.Promise; -import io.prometheus.client.Counter; import io.prometheus.client.Gauge; import java.io.File; import java.io.IOException; -import java.net.InetSocketAddress; -import java.security.GeneralSecurityException; -import java.security.KeyStore; import java.security.cert.Certificate; -import java.time.Duration; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.function.Function; -import javax.net.ssl.KeyManagerFactory; import jdk.net.ExtendedSocketOptions; -import lombok.Data; +import org.carapaceproxy.server.certificates.ocsp.OcspStaplingManager; import org.carapaceproxy.server.config.ConfigurationNotValidException; import org.carapaceproxy.server.config.NetworkListenerConfiguration; -import org.carapaceproxy.server.config.SSLCertificateConfiguration; -import org.carapaceproxy.utils.CertificatesUtils; import org.carapaceproxy.utils.PrometheusUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.netty.DisposableServer; -import reactor.netty.FutureMono; import reactor.netty.http.HttpProtocol; import reactor.netty.http.server.HttpServer; @@ -78,7 +59,8 @@ * Collection of listeners waiting for incoming clients requests on the configured HTTP ports. *
* While the {@link RuntimeServerConfiguration} is actually mutable, this class won't watch it for updates; - * the caller should request a {@link #reloadCurrentConfiguration() reload of the configuration} manually instead. + * the caller should instead + * request a {@link #reloadConfiguration(RuntimeServerConfiguration) reload of the configuration} manually. * * @author enrico.olivelli */ @@ -92,16 +74,11 @@ public class Listeners { "clients", "current_connected", "currently connected clients" ).register(); - private static final Counter TOTAL_REQUESTS_PER_LISTENER_COUNTER = PrometheusUtils.createCounter( - "listeners", "requests_total", "total requests", "listener" - ).register(); - private final HttpProxyServer parent; - private final Map sslContexts = new ConcurrentHashMap<>(); - private final Map listeningChannels = new ConcurrentHashMap<>(); + private final ConcurrentMap sslContexts = new ConcurrentHashMap<>(); + private final ConcurrentMap listeningChannels = new ConcurrentHashMap<>(); private final File basePath; private boolean started; - private RuntimeServerConfiguration currentConfiguration; public Listeners(HttpProxyServer parent) { @@ -110,10 +87,13 @@ public Listeners(HttpProxyServer parent) { this.basePath = parent.getBasePath(); } + public RuntimeServerConfiguration getCurrentConfiguration() { + return currentConfiguration; + } + public int getLocalPort() { - for (ListeningChannel c : listeningChannels.values()) { - InetSocketAddress addr = (InetSocketAddress) c.getChannel().address(); - return addr.getPort(); + for (final ListeningChannel listeningChannel : listeningChannels.values()) { + return listeningChannel.getLocalPort(); } return -1; } @@ -127,34 +107,6 @@ public void start() throws InterruptedException, ConfigurationNotValidException reloadConfiguration(currentConfiguration); } - public void stop() { - for (EndpointKey key : listeningChannels.keySet()) { - try { - stopListener(key); - } catch (InterruptedException ex) { - LOG.error("Interrupted while stopping a listener", ex); - Thread.currentThread().interrupt(); - } - } - } - - private void stopListener(EndpointKey hostport) throws InterruptedException { - ListeningChannel channel = listeningChannels.remove(hostport); - if (channel != null) { - channel.channel.disposeNow(Duration.ofSeconds(10)); - FutureMono.from(channel.getConfig().getGroup().close()).block(Duration.ofSeconds(10)); - } - } - - /** - * Re-apply the current configuration; it should be invoked after editing it. - * - * @throws InterruptedException if it is interrupted while starting or stopping a listener - */ - public void reloadCurrentConfiguration() throws InterruptedException { - reloadConfiguration(this.currentConfiguration); - } - /** * Apply a new configuration and refresh the listeners according to it. * @@ -162,7 +114,7 @@ public void reloadCurrentConfiguration() throws InterruptedException { * @throws InterruptedException if it is interrupted while starting or stopping a listener * @see #reloadCurrentConfiguration() */ - void reloadConfiguration(RuntimeServerConfiguration newConfiguration) throws InterruptedException { + void reloadConfiguration(final RuntimeServerConfiguration newConfiguration) throws InterruptedException { if (!started) { this.currentConfiguration = newConfiguration; return; @@ -171,26 +123,26 @@ void reloadConfiguration(RuntimeServerConfiguration newConfiguration) throws Int sslContexts.clear(); // stop dropped listeners, start new one - List listenersToStop = new ArrayList<>(); - List listenersToRestart = new ArrayList<>(); - for (Map.Entry channel : listeningChannels.entrySet()) { - EndpointKey key = channel.getKey(); - NetworkListenerConfiguration actualListenerConfig = currentConfiguration.getListener(key); - NetworkListenerConfiguration newConfigurationForListener = newConfiguration.getListener(key); + final List listenersToStop = new ArrayList<>(); + final List listenersToRestart = new ArrayList<>(); + for (final Map.Entry channel : listeningChannels.entrySet()) { + final EndpointKey hostPort = channel.getKey(); + final NetworkListenerConfiguration actualListenerConfig = currentConfiguration.getListener(hostPort); + final NetworkListenerConfiguration newConfigurationForListener = newConfiguration.getListener(hostPort); if (newConfigurationForListener == null) { - LOG.info("listener: {} is to be shut down", key); - listenersToStop.add(key); + LOG.info("listener: {} is to be shut down", hostPort); + listenersToStop.add(hostPort); } else if (!newConfigurationForListener.equals(actualListenerConfig) - || newConfiguration.getResponseCompressionThreshold() != currentConfiguration.getResponseCompressionThreshold() - || newConfiguration.getMaxHeaderSize() != currentConfiguration.getMaxHeaderSize()) { - LOG.info("listener: {} is to be restarted", key); - listenersToRestart.add(key); + || newConfiguration.getResponseCompressionThreshold() != currentConfiguration.getResponseCompressionThreshold() + || newConfiguration.getMaxHeaderSize() != currentConfiguration.getMaxHeaderSize()) { + LOG.info("listener: {} is to be restarted", hostPort); + listenersToRestart.add(hostPort); } channel.getValue().clear(); } - List listenersToStart = new ArrayList<>(); - for (NetworkListenerConfiguration config : newConfiguration.getListeners()) { - EndpointKey key = config.getKey(); + final List listenersToStart = new ArrayList<>(); + for (final NetworkListenerConfiguration config : newConfiguration.getListeners()) { + final EndpointKey key = config.getKey(); if (!listeningChannels.containsKey(key)) { LOG.info("listener: {} is to be started", key); listenersToStart.add(key); @@ -201,33 +153,40 @@ void reloadConfiguration(RuntimeServerConfiguration newConfiguration) throws Int currentConfiguration = newConfiguration; try { - for (final EndpointKey hostport : listenersToStop) { - LOG.info("Stopping {}", hostport); - stopListener(hostport); + for (final EndpointKey hostPort : listenersToStop) { + LOG.info("Stopping {}", hostPort); + stopListener(hostPort); } - for (final EndpointKey hostport : listenersToRestart) { - LOG.info("Restart {}", hostport); - stopListener(hostport); - NetworkListenerConfiguration newConfigurationForListener = currentConfiguration.getListener(hostport); + for (final EndpointKey hostPort : listenersToRestart) { + LOG.info("Restart {}", hostPort); + stopListener(hostPort); + final var newConfigurationForListener = currentConfiguration.getListener(hostPort); bootListener(newConfigurationForListener); } - for (final EndpointKey hostport : listenersToStart) { - LOG.info("Starting {}", hostport); - NetworkListenerConfiguration newConfigurationForListener = currentConfiguration.getListener(hostport); + for (final EndpointKey hostPort : listenersToStart) { + LOG.info("Starting {}", hostPort); + final var newConfigurationForListener = currentConfiguration.getListener(hostPort); bootListener(newConfigurationForListener); } - } catch (InterruptedException stopMe) { + } catch (final InterruptedException stopMe) { Thread.currentThread().interrupt(); throw stopMe; } } - private void bootListener(NetworkListenerConfiguration config) throws InterruptedException { - EndpointKey hostPort = new EndpointKey(config.getHost(), config.getPort() + parent.getListenersOffsetPort()); - ListeningChannel listeningChannel = new ListeningChannel(hostPort, config); - LOG.info("Starting listener at {}:{} ssl:{}", hostPort.host(), String.valueOf(hostPort.port()), config.isSsl()); + private void stopListener(EndpointKey hostPort) throws InterruptedException { + final var channel = listeningChannels.remove(hostPort); + if (channel != null) { + channel.disposeChannel(); + } + } + + private void bootListener(final NetworkListenerConfiguration config) throws InterruptedException { + final EndpointKey hostPort = new EndpointKey(config.getHost(), config.getPort()).offsetPort(parent.getListenersOffsetPort()); + final ListeningChannel listeningChannel = new ListeningChannel(basePath, currentConfiguration, parent, sslContexts, hostPort, config); + LOG.info("Starting listener at {}:{} ssl:{}", hostPort.host(), hostPort.port(), config.isSsl()); // Listener setup HttpServer httpServer = HttpServer.create() @@ -262,16 +221,23 @@ private void bootListener(NetworkListenerConfiguration config) throws Interrupte protected SslHandler newSslHandler(SslContext context, ByteBufAllocator allocator) { SslHandler handler = super.newSslHandler(context, allocator); if (currentConfiguration.isOcspEnabled() && OpenSsl.isOcspSupported()) { - Certificate cert = (Certificate) context.attributes().attr(AttributeKey.valueOf(OCSP_CERTIFICATE_CHAIN)).get(); - if (cert != null) { - try { - ReferenceCountedOpenSslEngine engine = (ReferenceCountedOpenSslEngine) handler.engine(); - engine.setOcspResponse(parent.getOcspStaplingManager().getOcspResponseForCertificate(cert)); // setting proper ocsp response - } catch (IOException ex) { - LOG.error("Error setting OCSP response.", ex); - } - } else { + final Certificate cert = (Certificate) context.attributes() + .attr(AttributeKey.valueOf(OCSP_CERTIFICATE_CHAIN)) + .get(); + if (cert == null) { LOG.error("Cannot set OCSP response without the certificate"); + return handler; + } + if (!(handler.engine() instanceof ReferenceCountedOpenSslEngine engine)) { + LOG.error("Unexpected OpenSSL Engine used; cannot set OCSP response."); + return handler; + } + try { + final OcspStaplingManager ocspStaplingManager = parent.getOcspStaplingManager(); + final byte[] response = ocspStaplingManager.getOcspResponseForCertificate(cert); + engine.setOcspResponse(response); + } catch (final IOException ex) { + LOG.error("Error setting OCSP response.", ex); } } return handler; @@ -294,16 +260,18 @@ protected SslHandler newSslHandler(SslContext context, ByteBufAllocator allocato .handle((request, response) -> { // Custom request-response handling if (LOG.isDebugEnabled()) { LOG.debug( - "Receive request {} From {} Timestamp {}", request.uri(), request.remoteAddress(), + "Receive request {} From {} Timestamp {}", + request.uri(), + request.remoteAddress(), LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss.SSS")) ); } - ListeningChannel channel = listeningChannels.get(hostPort); + final ListeningChannel channel = listeningChannels.get(hostPort); if (channel != null) { channel.incRequests(); } - ProxyRequest proxyRequest = new ProxyRequest(request, response, hostPort); + final ProxyRequest proxyRequest = new ProxyRequest(request, response, hostPort); return parent.getProxyRequestsManager().processRequest(proxyRequest); }); @@ -320,176 +288,30 @@ protected SslHandler newSslHandler(SslContext context, ByteBufAllocator allocato httpServer.warmup().block(); // Listener startup - DisposableServer channel = httpServer.bindNow(); // blocking + final DisposableServer channel = httpServer.bindNow(); // blocking listeningChannel.setChannel(channel); listeningChannels.put(hostPort, listeningChannel); LOG.info("started listener at {}: {}", hostPort, channel); } - @Data - public final class ListeningChannel implements io.netty.util.AsyncMapping { - - private final EndpointKey hostPort; - private final NetworkListenerConfiguration config; - private final Counter.Child totalRequests; - private final Map listenerSslContexts = new HashMap<>(); - DisposableServer channel; - - public ListeningChannel(EndpointKey hostPort, NetworkListenerConfiguration config) { - this.hostPort = hostPort; - this.config = config; - totalRequests = TOTAL_REQUESTS_PER_LISTENER_COUNTER.labels(hostPort.host() + "_" + hostPort.port()); - } - - public void incRequests() { - totalRequests.inc(); - } - - public void clear() { - this.listenerSslContexts.clear(); - } - - @Override - public Future map(String sniHostname, Promise promise) { - try { - String key = config.getHost() + ":" + hostPort.port() + "+" + sniHostname; - if (LOG.isTraceEnabled()) { - LOG.trace("resolve SNI mapping {}, key: {}", sniHostname, key); - } - try { - SslContext sslContext = listenerSslContexts.get(key); - if (sslContext != null) { - return promise.setSuccess(sslContext); - } - - sslContext = sslContexts.computeIfAbsent(key, (k) -> { - try { - SSLCertificateConfiguration chosen = chooseCertificate(sniHostname, config.getDefaultCertificate()); - if (chosen == null) { - throw new ConfigurationNotValidException("cannot find a certificate for snihostname " + sniHostname - + ", with default cert for listener as '" + config.getDefaultCertificate() - + "', available " + currentConfiguration.getCertificates().keySet()); - } - return bootSslContext(config, chosen); - } catch (ConfigurationNotValidException ex) { - throw new RuntimeException(ex); - } - }); - listenerSslContexts.put(key, sslContext); - - return promise.setSuccess(sslContext); - } catch (RuntimeException err) { - if (err.getCause() instanceof ConfigurationNotValidException) { - throw (ConfigurationNotValidException) err.getCause(); - } else { - throw new ConfigurationNotValidException(err); - } - } - } catch (ConfigurationNotValidException err) { - LOG.error("Error booting certificate for SNI hostname {}, on listener {}", sniHostname, config); - return promise.setFailure(err); - } - } - - private SslContext bootSslContext(NetworkListenerConfiguration listener, SSLCertificateConfiguration certificate) throws ConfigurationNotValidException { - int port = listener.getPort() + parent.getListenersOffsetPort(); - String sslCiphers = listener.getSslCiphers(); - + public void stop() { + for (var key : listeningChannels.keySet()) { try { - // Try to find certificate data on db - byte[] keystoreContent = parent.getDynamicCertificatesManager().getCertificateForDomain(certificate.getId()); - final KeyStore keystore; - if (keystoreContent != null) { - LOG.debug("start SSL with dynamic certificate id {}, on listener {}:{}", certificate.getId(), listener.getHost(), port); - keystore = loadKeyStoreData(keystoreContent, certificate.getPassword()); - } else { - if (certificate.isDynamic()) { // fallback to default certificate - certificate = currentConfiguration.getCertificates().get(listener.getDefaultCertificate()); - if (certificate == null) { - throw new ConfigurationNotValidException("Unable to boot SSL context for listener " + listener.getHost() + ": no default certificate setup."); - } - } - LOG.debug("start SSL with certificate id {}, on listener {}:{} file={}", certificate.getId(), listener.getHost(), port, certificate.getFile()); - keystore = loadKeyStoreFromFile(certificate.getFile(), certificate.getPassword(), basePath); - } - KeyManagerFactory keyFactory = new OpenSslCachingX509KeyManagerFactory(KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())); - keyFactory.init(keystore, certificate.getPassword().toCharArray()); - - List ciphers = null; - if (sslCiphers != null && !sslCiphers.isEmpty()) { - LOG.debug("required sslCiphers {}", sslCiphers); - ciphers = Arrays.asList(sslCiphers.split(",")); - } - SslContext sslContext = SslContextBuilder - .forServer(keyFactory) - .enableOcsp(currentConfiguration.isOcspEnabled() && OpenSsl.isOcspSupported()) - .trustManager(parent.getTrustStoreManager().getTrustManagerFactory()) - .sslProvider(SslProvider.OPENSSL) - .protocols(listener.getSslProtocols()) - .ciphers(ciphers).build(); - - Certificate[] chain = readChainFromKeystore(keystore); - if (currentConfiguration.isOcspEnabled() && OpenSsl.isOcspSupported() && chain != null && chain.length > 0) { - parent.getOcspStaplingManager().addCertificateForStapling(chain); - Attribute attr = sslContext.attributes().attr(AttributeKey.valueOf(OCSP_CERTIFICATE_CHAIN)); - attr.set(chain[0]); - } - - return sslContext; - } catch (IOException | GeneralSecurityException err) { - LOG.error("ERROR booting listener {}", err, err); - throw new ConfigurationNotValidException(err); - } - } - } - - public SSLCertificateConfiguration chooseCertificate(String sniHostname, String defaultCertificate) { - if (sniHostname == null) { - sniHostname = ""; - } - Map certificates = currentConfiguration.getCertificates(); - SSLCertificateConfiguration certificateMatchExact = null; - SSLCertificateConfiguration certificateMatchNoExact = null; - for (SSLCertificateConfiguration c : certificates.values()) { - if (certificateMatches(sniHostname, c, true)) { - certificateMatchExact = c; - } else if (certificateMatches(sniHostname, c, false)) { - if (certificateMatchNoExact == null || c.isMoreSpecific(certificateMatchNoExact)) { - certificateMatchNoExact = c; - } + stopListener(key); + } catch (InterruptedException ex) { + LOG.error("Interrupted while stopping a listener", ex); + Thread.currentThread().interrupt(); } } - SSLCertificateConfiguration chosen = null; - if (certificateMatchExact != null) { - chosen = certificateMatchExact; - } else if (certificateMatchNoExact != null) { - chosen = certificateMatchNoExact; - } - if (chosen == null) { - chosen = certificates.get(defaultCertificate); - } - return chosen; } - private static boolean certificateMatches(String hostname, SSLCertificateConfiguration c, boolean exact) { - if (c.getSubjectAltNames() == null || c.getSubjectAltNames().isEmpty()) { - if (exact) { - return !c.isWildcard() && hostname.equals(c.getHostname()); - } else { - return c.isWildcard() && hostname.endsWith(c.getHostname()); - } - } else { - for (var name: c.getNames()) { - final var wildcard = CertificatesUtils.isWildcard(name); - if (exact && !wildcard && hostname.equals(name)) { - return true; - } - if (!exact && wildcard && hostname.endsWith(CertificatesUtils.removeWildcard(name))) { - return true; - } - } - return false; - } + /** + * Re-apply the current configuration; it should be invoked after editing it. + * + * @throws InterruptedException if it is interrupted while starting or stopping a listener + */ + public void reloadCurrentConfiguration() throws InterruptedException { + reloadConfiguration(this.currentConfiguration); } } diff --git a/carapace-server/src/main/java/org/carapaceproxy/core/ListeningChannel.java b/carapace-server/src/main/java/org/carapaceproxy/core/ListeningChannel.java new file mode 100644 index 000000000..d62f3b6d7 --- /dev/null +++ b/carapace-server/src/main/java/org/carapaceproxy/core/ListeningChannel.java @@ -0,0 +1,180 @@ +package org.carapaceproxy.core; + +import static org.carapaceproxy.utils.CertificatesUtils.loadKeyStoreData; +import static org.carapaceproxy.utils.CertificatesUtils.loadKeyStoreFromFile; +import static org.carapaceproxy.utils.CertificatesUtils.readChainFromKeystore; +import io.netty.handler.ssl.OpenSsl; +import io.netty.handler.ssl.OpenSslCachingX509KeyManagerFactory; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslProvider; +import io.netty.util.AttributeKey; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.Promise; +import io.prometheus.client.Counter; +import java.io.File; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.time.Duration; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentMap; +import javax.net.ssl.KeyManagerFactory; +import lombok.Data; +import org.carapaceproxy.server.config.ConfigurationNotValidException; +import org.carapaceproxy.server.config.NetworkListenerConfiguration; +import org.carapaceproxy.server.config.SSLCertificateConfiguration; +import org.carapaceproxy.utils.CertificatesUtils; +import org.carapaceproxy.utils.PrometheusUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.netty.DisposableServer; +import reactor.netty.FutureMono; + +@Data +public class ListeningChannel implements io.netty.util.AsyncMapping { + + private static final Logger LOG = LoggerFactory.getLogger(ListeningChannel.class); + + private static final Counter TOTAL_REQUESTS_PER_LISTENER_COUNTER = PrometheusUtils.createCounter( + "listeners", "requests_total", "total requests", "listener" + ).register(); + + private final EndpointKey hostPort; + private final NetworkListenerConfiguration config; + private final Counter.Child totalRequests; + private final Map listenerSslContexts = new HashMap<>(); + private final File basePath; + private final RuntimeServerConfiguration currentConfiguration; + private final HttpProxyServer parent; + private final ConcurrentMap sslContexts; + private DisposableServer channel; + + public ListeningChannel(final File basePath, final RuntimeServerConfiguration currentConfiguration, final HttpProxyServer parent, final ConcurrentMap sslContexts, EndpointKey hostPort, NetworkListenerConfiguration config) { + this.hostPort = hostPort; + this.config = config; + this.totalRequests = TOTAL_REQUESTS_PER_LISTENER_COUNTER.labels(hostPort.host() + "_" + hostPort.port()); + this.basePath = basePath; + this.currentConfiguration = currentConfiguration; + this.parent = parent; + this.sslContexts = sslContexts; + } + + public int getLocalPort() { + return ((InetSocketAddress) this.channel.address()).getPort(); + } + + public void disposeChannel() { + this.channel.disposeNow(Duration.ofSeconds(10)); + FutureMono.from(this.config.getGroup().close()).block(Duration.ofSeconds(10)); + } + + public void incRequests() { + totalRequests.inc(); + } + + public void clear() { + this.listenerSslContexts.clear(); + } + + @Override + public Future map(String sniHostname, Promise promise) { + try { + var key = config.getHost() + ":" + hostPort.port() + "+" + sniHostname; + if (LOG.isDebugEnabled()) { + LOG.debug("resolve SNI mapping {}, key: {}", sniHostname, key); + } + try { + var sslContext = listenerSslContexts.get(key); + if (sslContext != null) { + return promise.setSuccess(sslContext); + } + + sslContext = sslContexts.computeIfAbsent(key, (k) -> { + try { + var chosen = chooseCertificate(sniHostname); + if (chosen == null) { + throw new ConfigurationNotValidException("cannot find a certificate for snihostname " + sniHostname + + ", with default cert for listener as '" + config.getDefaultCertificate() + + "', available " + currentConfiguration.getCertificates().keySet()); + } + return bootSslContext(config, chosen); + } catch (ConfigurationNotValidException ex) { + throw new RuntimeException(ex); + } + }); + listenerSslContexts.put(key, sslContext); + + return promise.setSuccess(sslContext); + } catch (RuntimeException err) { + if (err.getCause() instanceof ConfigurationNotValidException) { + throw (ConfigurationNotValidException) err.getCause(); + } else { + throw new ConfigurationNotValidException(err); + } + } + } catch (ConfigurationNotValidException err) { + LOG.error("Error booting certificate for SNI hostname {}, on listener {}", sniHostname, config, err); + return promise.setFailure(err); + } + } + + private SSLCertificateConfiguration chooseCertificate(final String sniHostname) { + return CertificatesUtils.chooseCertificate(currentConfiguration, sniHostname, config.getDefaultCertificate()); + } + + private SslContext bootSslContext(NetworkListenerConfiguration listener, SSLCertificateConfiguration certificate) throws ConfigurationNotValidException { + var port = listener.getPort() + parent.getListenersOffsetPort(); + var sslCiphers = listener.getSslCiphers(); + + try { + // Try to find certificate data on db + var keystoreContent = parent.getDynamicCertificatesManager().getCertificateForDomain(certificate.getId()); + final KeyStore keystore; + if (keystoreContent == null) { + if (certificate.isDynamic()) { // fallback to default certificate + certificate = currentConfiguration.getCertificates().get(listener.getDefaultCertificate()); + if (certificate == null) { + throw new ConfigurationNotValidException("Unable to boot SSL context for listener " + listener.getHost() + ": no default certificate setup."); + } + } + LOG.debug("start SSL with certificate id {}, on listener {}:{} file={}", certificate.getId(), listener.getHost(), port, certificate.getFile()); + keystore = loadKeyStoreFromFile(certificate.getFile(), certificate.getPassword(), basePath); + } else { + LOG.debug("start SSL with dynamic certificate id {}, on listener {}:{}", certificate.getId(), listener.getHost(), port); + keystore = loadKeyStoreData(keystoreContent, certificate.getPassword()); + } + KeyManagerFactory keyFactory = new OpenSslCachingX509KeyManagerFactory(KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())); + keyFactory.init(keystore, certificate.getPassword().toCharArray()); + + List ciphers = null; + if (sslCiphers != null && !sslCiphers.isEmpty()) { + LOG.debug("required sslCiphers {}", sslCiphers); + ciphers = Arrays.asList(sslCiphers.split(",")); + } + var sslContext = SslContextBuilder + .forServer(keyFactory) + .enableOcsp(currentConfiguration.isOcspEnabled() && OpenSsl.isOcspSupported()) + .trustManager(parent.getTrustStoreManager().getTrustManagerFactory()) + .sslProvider(SslProvider.OPENSSL) + .protocols(listener.getSslProtocols()) + .ciphers(ciphers).build(); + + var chain = readChainFromKeystore(keystore); + if (currentConfiguration.isOcspEnabled() && OpenSsl.isOcspSupported() && chain.length > 0) { + parent.getOcspStaplingManager().addCertificateForStapling(chain); + var attr = sslContext.attributes().attr(AttributeKey.valueOf(Listeners.OCSP_CERTIFICATE_CHAIN)); + attr.set(chain[0]); + } + + return sslContext; + } catch (IOException | GeneralSecurityException err) { + LOG.error("ERROR booting listener", err); + throw new ConfigurationNotValidException(err); + } + } +} diff --git a/carapace-server/src/main/java/org/carapaceproxy/core/RuntimeServerConfiguration.java b/carapace-server/src/main/java/org/carapaceproxy/core/RuntimeServerConfiguration.java index ed8d55e31..6e7b607f6 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/core/RuntimeServerConfiguration.java +++ b/carapace-server/src/main/java/org/carapaceproxy/core/RuntimeServerConfiguration.java @@ -43,6 +43,7 @@ import java.time.Duration; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -78,7 +79,7 @@ public class RuntimeServerConfiguration { private static final int DEFAULT_PROBE_PERIOD = 0; public static final long DEFAULT_WARMUP_PERIOD = Duration.ofSeconds(30).toMillis(); - private final List listeners = new ArrayList<>(); + private final Map listeners = new LinkedHashMap<>(); private final Map certificates = new HashMap<>(); private final List requestFilters = new ArrayList<>(); private final Map connectionPools = new HashMap<>(); @@ -471,7 +472,7 @@ public void addListener(NetworkListenerConfiguration listener) throws Configurat throw new ConfigurationNotValidException(ex); } } - listeners.add(listener); + listeners.put(listener.getKey(), listener); } public void addCertificate(SSLCertificateConfiguration certificate) throws ConfigurationNotValidException { @@ -486,7 +487,7 @@ void addRequestFilter(RequestFilterConfiguration config) { } public List getListeners() { - return listeners; + return List.copyOf(listeners.values()); } public Map getCertificates() { @@ -497,11 +498,7 @@ public List getRequestFilters() { return requestFilters; } - NetworkListenerConfiguration getListener(EndpointKey hostPort) { - return listeners - .stream() - .filter(s -> s.getHost().equalsIgnoreCase(hostPort.host()) && s.getPort() == hostPort.port()) - .findFirst() - .orElse(null); + NetworkListenerConfiguration getListener(final EndpointKey hostPort) { + return listeners.getOrDefault(hostPort, null); } } diff --git a/carapace-server/src/main/java/org/carapaceproxy/server/cache/CacheByteBufMemoryUsageMetric.java b/carapace-server/src/main/java/org/carapaceproxy/server/cache/CacheByteBufMemoryUsageMetric.java index 23a4b1d97..ecc8fa54a 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/server/cache/CacheByteBufMemoryUsageMetric.java +++ b/carapace-server/src/main/java/org/carapaceproxy/server/cache/CacheByteBufMemoryUsageMetric.java @@ -73,7 +73,7 @@ public synchronized void stop() { @Override public void run() { - if(parent.getCachePoolAllocator() instanceof PooledByteBufAllocator) { + if (parent.getCachePoolAllocator() instanceof PooledByteBufAllocator) { CACHE_POOLED_BYTEBUF_ALLOCATOR.set(((PooledByteBufAllocator) parent.getCachePoolAllocator()).metric().usedDirectMemory()); } else { CACHE_UNPOOLED_BYTEBUF_ALLOCATOR.set(((UnpooledByteBufAllocator) parent.getCachePoolAllocator()).metric().usedDirectMemory()); diff --git a/carapace-server/src/main/java/org/carapaceproxy/server/certificates/ocsp/OcspStaplingManager.java b/carapace-server/src/main/java/org/carapaceproxy/server/certificates/ocsp/OcspStaplingManager.java index 21019b18c..455fa142b 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/server/certificates/ocsp/OcspStaplingManager.java +++ b/carapace-server/src/main/java/org/carapaceproxy/server/certificates/ocsp/OcspStaplingManager.java @@ -21,7 +21,6 @@ import com.google.common.annotations.VisibleForTesting; import io.netty.handler.ssl.OpenSsl; - import java.io.IOException; import java.math.BigInteger; import java.net.URI; diff --git a/carapace-server/src/main/java/org/carapaceproxy/server/config/NetworkListenerConfiguration.java b/carapace-server/src/main/java/org/carapaceproxy/server/config/NetworkListenerConfiguration.java index 3dead41df..22f05551b 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/server/config/NetworkListenerConfiguration.java +++ b/carapace-server/src/main/java/org/carapaceproxy/server/config/NetworkListenerConfiguration.java @@ -160,11 +160,6 @@ public EndpointKey getKey() { } public static Set getDefaultHttpProtocols(final boolean ssl) { - // return Set.of(HTTP11.name(), (ssl ? H2 : H2C).name()); - if (ssl) { - // todo: until #410 is done, we will allow HTTP/2 only in clear-text - return Set.of(HTTP11.name()); - } - return Set.of(HTTP11.name(), H2C.name()); + return Set.of(HTTP11.name(), (ssl ? H2 : H2C).name()); } } diff --git a/carapace-server/src/main/java/org/carapaceproxy/server/config/SSLCertificateConfiguration.java b/carapace-server/src/main/java/org/carapaceproxy/server/config/SSLCertificateConfiguration.java index b258b77b2..56690dde4 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/server/config/SSLCertificateConfiguration.java +++ b/carapace-server/src/main/java/org/carapaceproxy/server/config/SSLCertificateConfiguration.java @@ -38,7 +38,7 @@ @Data public class SSLCertificateConfiguration { - public static enum CertificateMode { + public enum CertificateMode { STATIC, ACME, MANUAL } diff --git a/carapace-server/src/main/java/org/carapaceproxy/utils/CertificatesUtils.java b/carapace-server/src/main/java/org/carapaceproxy/utils/CertificatesUtils.java index 7e72e824b..5929e07ac 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/utils/CertificatesUtils.java +++ b/carapace-server/src/main/java/org/carapaceproxy/utils/CertificatesUtils.java @@ -19,19 +19,36 @@ */ package org.carapaceproxy.utils; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; -import java.io.*; -import java.security.*; +import io.netty.handler.ssl.SslContext; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; -import java.util.*; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; +import org.carapaceproxy.core.EndpointKey; +import org.carapaceproxy.core.RuntimeServerConfiguration; +import org.carapaceproxy.server.config.SSLCertificateConfiguration; /** - * Utilitis for Certificates storing as Keystores + * Utilities for Certificates storing as Keystores * * @author paolo */ @@ -47,7 +64,13 @@ public final class CertificatesUtils { * @param chain to store into a keystore * @param key private key for the chain * @return keystore data - * @throws GeneralSecurityException + * @throws KeyStoreException if no provider supports a {@code KeyStoreSpi} implementation for the specified type, + * if the keystore has not been initialized, + * the given key cannot be protected, + * or this operation fails for some other reason + * @throws NoSuchAlgorithmException if the algorithm used to check the integrity of the keystore cannot be found + * @throws CertificateException if any of the certificates in the keystore could not be loaded + * @throws GeneralSecurityException if something else goes wrong, i.e., because of a {@link IOException} */ public static byte[] createKeystore(Certificate[] chain, PrivateKey key) throws GeneralSecurityException { try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { @@ -66,7 +89,11 @@ public static byte[] createKeystore(Certificate[] chain, PrivateKey key) throws * * @param data keystore data. * @return certificate chain contained into the keystore. - * @throws GeneralSecurityException + * @throws KeyStoreException if no provider supports a {@code KeyStoreSpi} implementation for the specified type, + * or if the keystore has not been initialized + * @throws NoSuchAlgorithmException if the algorithm used to check the integrity of the keystore cannot be found + * @throws CertificateException if any of the certificates in the keystore could not be loaded + * @throws GeneralSecurityException if something else goes wrong, i.e., because of a {@link IOException} */ public static Certificate[] readChainFromKeystore(byte[] data) throws GeneralSecurityException { try (ByteArrayInputStream is = new ByteArrayInputStream(data)) { @@ -84,7 +111,10 @@ public static Certificate[] readChainFromKeystore(byte[] data) throws GeneralSec * * @param keystore keystore. * @return certificate chain contained into the keystore. - * @throws GeneralSecurityException + * @throws KeyStoreException if no provider supports a {@code KeyStoreSpi} implementation for the specified type, + * or if the keystore has not been initialized + * @throws NoSuchAlgorithmException if the algorithm used to check the integrity of the keystore cannot be found + * @throws CertificateException if any of the certificates in the keystore could not be loaded */ public static Certificate[] readChainFromKeystore(KeyStore keystore) throws GeneralSecurityException { Iterator iter = keystore.aliases().asIterator(); @@ -100,7 +130,11 @@ public static Certificate[] readChainFromKeystore(KeyStore keystore) throws Gene /** * @param data keystore data. - * @return whether a valid keystore can be retrived from data. + * @return whether a valid keystore can be retrieved from data. + * @throws KeyStoreException if no provider supports a {@code KeyStoreSpi} implementation for the specified type, + * or if the keystore has not been initialized + * @throws NoSuchAlgorithmException if the algorithm used to check the integrity of the keystore cannot be found + * @throws CertificateException if any of the certificates in the keystore could not be loaded */ public static boolean validateKeystore(byte[] data) throws GeneralSecurityException { try (ByteArrayInputStream is = new ByteArrayInputStream(data)) { @@ -160,17 +194,20 @@ public static boolean isCertificateExpired(Date expiringDate, int daysBeforeRene /** * Extract certificate private key - * @param data Certificate data. + * + * @param data Certificate data. * @param password Private key password. * @return PrivateKey. - * @throws Exception + * @throws GeneralSecurityException if something goes wrong with the keystore + * @throws IOException if there is an I/O or format problem with the keystore data, + * if a password is required but not given, + * or if the given password was incorrect. */ - public static PrivateKey loadPrivateKey(byte[] data, String password) throws Exception { + public static PrivateKey loadPrivateKey(byte[] data, String password) throws GeneralSecurityException, IOException { KeyStore ks = KeyStore.getInstance(KEYSTORE_FORMAT); ks.load(new ByteArrayInputStream(data), password.trim().toCharArray()); String alias = ks.aliases().nextElement(); - PrivateKey key = (PrivateKey) ks.getKey(alias, password.trim().toCharArray()); - return key; + return (PrivateKey) ks.getKey(alias, password.trim().toCharArray()); } /** @@ -184,6 +221,9 @@ public static boolean compareChains(Certificate[] c1, Certificate[] c2) { if (c1 == null && c2 != null || c2 == null && c1 != null) { return false; } + if (c1 == null /* && c2 == null */) { + return true; + } if (c1.length != c2.length) { return false; } @@ -211,4 +251,69 @@ public static String addWildcard(String name) { return WILDCARD_PREFIX + Objects.requireNonNull(name); } + /** + * {@link SslContext}s are cached at listener level. + * This method computes the key from hostname, port, and SNI. + * + * @param hostPort the host and port tuple + * @param sniHostname the Server Name Indication (SNI) indication + * @return the cache key + */ + public static String computeKey(final EndpointKey hostPort, final String sniHostname) { + return hostPort.host() + ":" + hostPort.port() + "+" + sniHostname; + } + + public static boolean certificateMatches( + final String hostname, final SSLCertificateConfiguration certificate, final boolean exact) { + if (certificate.getSubjectAltNames() == null || certificate.getSubjectAltNames().isEmpty()) { + if (exact) { + return !certificate.isWildcard() && hostname.equals(certificate.getHostname()); + } else { + return certificate.isWildcard() && hostname.endsWith(certificate.getHostname()); + } + } + for (final var name : certificate.getNames()) { + final var wildcard = isWildcard(name); + if (exact) { + return !wildcard && hostname.equals(name); + } else { + return wildcard && hostname.endsWith(removeWildcard(name)); + } + } + return false; + } + + public static SSLCertificateConfiguration chooseCertificate(final RuntimeServerConfiguration currentConfiguration, String sniHostname, final String defaultCertificate) { + if (sniHostname == null) { + sniHostname = ""; + } + final var certificates = currentConfiguration.getCertificates(); + SSLCertificateConfiguration certificateMatchExact = null; + SSLCertificateConfiguration certificateMatchNoExact = null; + for (final var c : certificates.values()) { + if (certificateMatches(sniHostname, c, true)) { + certificateMatchExact = c; + } else if (certificateMatches(sniHostname, c, false)) { + if (certificateMatchNoExact == null || c.isMoreSpecific(certificateMatchNoExact)) { + certificateMatchNoExact = c; + } + } + } + SSLCertificateConfiguration chosen = null; + if (certificateMatchExact != null) { + chosen = certificateMatchExact; + } else if (certificateMatchNoExact != null) { + chosen = certificateMatchNoExact; + } + if (chosen == null) { + chosen = certificates.get(defaultCertificate); + } + /* LOG.info("Resolving SNI for hostname: {}", sniHostname); + if (chosen == null) { + LOG.error("No certificate found for SNI hostname: {}", sniHostname); + } else { + LOG.info("Using certificate: {}", chosen.getId()); + } */ + return chosen; + } } diff --git a/carapace-server/src/test/java/org/carapaceproxy/ApplyConfigurationTest.java b/carapace-server/src/test/java/org/carapaceproxy/ApplyConfigurationTest.java index e49458a39..41c1f2bbd 100644 --- a/carapace-server/src/test/java/org/carapaceproxy/ApplyConfigurationTest.java +++ b/carapace-server/src/test/java/org/carapaceproxy/ApplyConfigurationTest.java @@ -23,6 +23,8 @@ import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.carapaceproxy.utils.ApacheHttpUtils.createHttpClientWithDisabledSSLValidation; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.aMapWithSize; import static org.hamcrest.Matchers.allOf; @@ -40,18 +42,12 @@ import com.github.tomakehurst.wiremock.junit.WireMockRule; import java.io.IOException; import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.security.KeyManagementException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; import java.util.Map; import java.util.Properties; +import org.apache.http.HttpStatus; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; -import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; -import org.apache.http.ssl.SSLContextBuilder; import org.carapaceproxy.configstore.PropertiesConfigurationStore; import org.carapaceproxy.core.HttpProxyServer; import org.carapaceproxy.server.config.ConfigurationChangeInProgressException; @@ -84,7 +80,7 @@ public class ApplyConfigurationTest { public static void setupWireMock() { stubFor(get(urlEqualTo("/index.html?redir")) .willReturn(aResponse() - .withStatus(200) + .withStatus(HttpStatus.SC_OK) .withHeader("Content-Type", "text/html") .withHeader("Pragma", "no-cache") .withHeader("Connection", "close") @@ -92,15 +88,6 @@ public static void setupWireMock() { } - private static CloseableHttpClient createHttpClientWithDisabledSSLValidation() throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException { - return HttpClients.custom() - .setSSLContext(SSLContextBuilder.create() - .loadTrustMaterial((chain, authType) -> true) // Trust all certificates - .build()) - .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE) // Disable hostname verification - .build(); - } - @Test public void testChangeListenersConfig() throws Exception { try (HttpProxyServer server = new HttpProxyServer(StandardEndpointMapper::new, tmpDir.newFolder());) { @@ -173,10 +160,9 @@ public void testChangeListenersConfig() throws Exception { "listener.2.sslprotocols", "TLSv1.2,TLSv1.3" ))); - // Test HTTPS for listener 1 - testIt(1423, true, true); // Expecting valid HTTPS connection - // Test HTTPS for listener 2 - testIt(1426, true, true); // Expecting valid HTTPS connection + // Expecting valid HTTPS connection + testIt(1423, true, true); + testIt(1426, true, true); // listener with default tls version reloadConfiguration(server, propsWithMapperAndCertificate(defaultCertificate, Map.of( @@ -184,8 +170,9 @@ public void testChangeListenersConfig() throws Exception { "listener.1.port", "1423", "listener.1.ssl", "true" ))); - // Test HTTPS for listener 1 - testIt(1423, true, true); // Expecting valid HTTPS connection + + // Expecting valid HTTPS connection + testIt(1423, true, true); // listener with wrong tls version final IllegalStateException e = assertThrows(IllegalStateException.class, () -> @@ -369,21 +356,19 @@ private void testIt(int port, boolean ok) throws Exception { } private void testIt(int port, final boolean https, boolean ok) throws Exception { - try (CloseableHttpClient client = createHttpClientWithDisabledSSLValidation()) { + try (final CloseableHttpClient client = createHttpClientWithDisabledSSLValidation()) { final String protocol = https ? "https" : "http"; - String url = protocol + "://localhost:" + port + "/index.html?redir"; + final String url = protocol + "://localhost:" + port + "/index.html?redir"; - HttpGet request = new HttpGet(new URI(url)); - try (CloseableHttpResponse response = client.execute(request)) { - int statusCode = response.getStatusLine().getStatusCode(); - String responseBody = new String(response.getEntity().getContent().readAllBytes(), StandardCharsets.UTF_8); + final HttpGet request = new HttpGet(new URI(url)); + try (final CloseableHttpResponse response = client.execute(request)) { + final int statusCode = response.getStatusLine().getStatusCode(); + final String responseBody = new String(response.getEntity().getContent().readAllBytes(), UTF_8); System.out.println("RES FOR: " + url + " -> " + responseBody); - - // Check that the response body matches what we expect assertEquals("it works !!", responseBody); - if (!ok && statusCode == 200) { + if (!ok && statusCode == HttpStatus.SC_OK) { fail("Expecting an error for port " + port); } } @@ -407,6 +392,7 @@ private Properties propsWithMapperAndCertificate(final String defaultCertificate configuration.put("certificate.1.hostname", "*"); configuration.put("certificate.1.file", defaultCertificate); configuration.put("certificate.1.password", "changeit"); + configuration.put("certificate.1.mode", "static"); configuration.putAll(props); return configuration; } diff --git a/carapace-server/src/test/java/org/carapaceproxy/core/MaxHeaderSizeTest.java b/carapace-server/src/test/java/org/carapaceproxy/core/MaxHeaderSizeTest.java index a91e74c1e..94b1bdfba 100644 --- a/carapace-server/src/test/java/org/carapaceproxy/core/MaxHeaderSizeTest.java +++ b/carapace-server/src/test/java/org/carapaceproxy/core/MaxHeaderSizeTest.java @@ -21,11 +21,8 @@ public class MaxHeaderSizeTest extends UseAdminServer { @Rule public WireMockRule wireMockRule = new WireMockRule(0); - private Properties config; - @Test public void test() throws Exception { - stubFor(get(urlEqualTo("/index.html")) .willReturn(aResponse() .withStatus(200) @@ -33,12 +30,12 @@ public void test() throws Exception { .withHeader("Content-Length", "it works !!".length() + "") .withBody("it works !!"))); - config = new Properties(HTTP_ADMIN_SERVER_CONFIG); + final Properties config = new Properties(HTTP_ADMIN_SERVER_CONFIG); config.put("healthmanager.tolerant", "true"); startServer(config); // Default certificate - String defaultCertificate = TestUtils.deployResource("ia.p12", tmpDir.getRoot()); + final String defaultCertificate = TestUtils.deployResource("ia.p12", tmpDir.getRoot()); config.put("certificate.1.hostname", "*"); config.put("certificate.1.file", defaultCertificate); config.put("certificate.1.password", "changeit"); @@ -73,13 +70,9 @@ public void test() throws Exception { changeDynamicConfiguration(config); - HttpClient httpClient = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_1_1) - .build(); - - HttpRequest request = HttpRequest.newBuilder() + final HttpRequest request = HttpRequest.newBuilder() .GET() - .uri(URI.create("http://localhost:" + 8086 +"/index.html")) + .uri(URI.create("http://localhost:" + 8086 + "/index.html")) .setHeader("custom-header", "test") .setHeader("token", "eyJhbGciOiJIUzI1NiJ9.eyJSb") .setHeader("token1", "eyJhbGciOiJIUzI1NiJ9.eyJSb") @@ -87,19 +80,17 @@ public void test() throws Exception { .setHeader("token3", "eyJhbGciOiJIUzI1NiJ9.eyJSb") .build(); - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - - assertEquals(200, response.statusCode()); + try (final HttpClient httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build()) { + final HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + assertEquals(200, response.statusCode()); + } config.put("carapace.maxheadersize", "1"); changeDynamicConfiguration(config); - HttpClient httpClient2 = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_1_1) - .build(); - - HttpResponse response2 = httpClient2.send(request, HttpResponse.BodyHandlers.ofString()); - - assertEquals(431, response2.statusCode()); + try (final HttpClient httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build()) { + final HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + assertEquals(431, response.statusCode()); + } } } diff --git a/carapace-server/src/test/java/org/carapaceproxy/listeners/ListenerConfigurationTest.java b/carapace-server/src/test/java/org/carapaceproxy/listeners/ListenerConfigurationTest.java index 5a955eb6a..1b2335efb 100644 --- a/carapace-server/src/test/java/org/carapaceproxy/listeners/ListenerConfigurationTest.java +++ b/carapace-server/src/test/java/org/carapaceproxy/listeners/ListenerConfigurationTest.java @@ -8,7 +8,7 @@ import org.carapaceproxy.configstore.PropertiesConfigurationStore; import org.carapaceproxy.core.EndpointKey; import org.carapaceproxy.core.HttpProxyServer; -import org.carapaceproxy.core.Listeners; +import org.carapaceproxy.core.ListeningChannel; import org.carapaceproxy.server.config.ConfigurationChangeInProgressException; import org.carapaceproxy.server.mapper.StandardEndpointMapper; import org.junit.Rule; @@ -37,7 +37,7 @@ public void testListenerKeepAliveConfiguration() throws Exception { EndpointKey listenerKey = new EndpointKey("localhost", 8080); { - Map listeners = server.getListeners().getListeningChannels(); + Map listeners = server.getListeners().getListeningChannels(); //check default configuration assertTrue(listeners.get(listenerKey).getConfig().isKeepAlive()); @@ -57,7 +57,7 @@ public void testListenerKeepAliveConfiguration() throws Exception { reloadConfiguration(configuration, server); - Map listeners = server.getListeners().getListeningChannels(); + Map listeners = server.getListeners().getListeningChannels(); assertEquals(1, listeners.size()); assertFalse(listeners.get(listenerKey).getConfig().isKeepAlive()); @@ -78,7 +78,7 @@ public void testListenerKeepAliveConfiguration() throws Exception { configuration.put("listener.1.enabled", "true"); reloadConfiguration(configuration, server); - Map listeners = server.getListeners().getListeningChannels(); + Map listeners = server.getListeners().getListeningChannels(); assertTrue(listeners.get(listenerKey).getConfig().isKeepAlive()); assertEquals(10, listeners.get(listenerKey).getConfig().getSoBacklog()); diff --git a/carapace-server/src/test/java/org/carapaceproxy/listeners/SSLSNITest.java b/carapace-server/src/test/java/org/carapaceproxy/listeners/SSLSNITest.java index 685e1e88b..5cd092a52 100644 --- a/carapace-server/src/test/java/org/carapaceproxy/listeners/SSLSNITest.java +++ b/carapace-server/src/test/java/org/carapaceproxy/listeners/SSLSNITest.java @@ -39,6 +39,7 @@ import org.carapaceproxy.server.config.ConfigurationNotValidException; import org.carapaceproxy.server.config.NetworkListenerConfiguration; import org.carapaceproxy.server.config.SSLCertificateConfiguration; +import org.carapaceproxy.utils.CertificatesUtils; import org.carapaceproxy.utils.RawHttpClient; import org.carapaceproxy.utils.TestEndpointMapper; import org.carapaceproxy.utils.TestUtils; @@ -99,38 +100,38 @@ public void testChooseCertificate() throws Exception { // client requests bad SNI, bad default in listener - assertNull(server.getListeners().chooseCertificate("no", "no-default")); + assertNull(CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "no", "no-default")); - assertEquals("*.qatest.pexample.it", server.getListeners().chooseCertificate("test2.qatest.pexample.it", "no-default").getId()); + assertEquals("*.qatest.pexample.it", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "test2.qatest.pexample.it", "no-default").getId()); // client requests SNI, bad default in listener - assertEquals("other", server.getListeners().chooseCertificate("other", "no-default").getId()); + assertEquals("other", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "other", "no-default").getId()); - assertEquals("www.example.com", server.getListeners().chooseCertificate("unkn-other", "www.example.com").getId()); + assertEquals("www.example.com", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "unkn-other", "www.example.com").getId()); // client without SNI - assertEquals("www.example.com", server.getListeners().chooseCertificate(null, "www.example.com").getId()); + assertEquals("www.example.com", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), null, "www.example.com").getId()); // exact match - assertEquals("www.example.com", server.getListeners().chooseCertificate("www.example.com", "no-default").getId()); + assertEquals("www.example.com", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "www.example.com", "no-default").getId()); // wildcard - assertEquals("*.example.com", server.getListeners().chooseCertificate("test.example.com", "no-default").getId()); + assertEquals("*.example.com", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "test.example.com", "no-default").getId()); // san - assertEquals("*.example.com", server.getListeners().chooseCertificate("example.com", "no-default").getId()); - assertEquals("*.example.com", server.getListeners().chooseCertificate("test.example2.com", "no-default").getId()); + assertEquals("*.example.com", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "example.com", "no-default").getId()); + assertEquals("*.example.com", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "test.example2.com", "no-default").getId()); // full wildcard server.addCertificate(new SSLCertificateConfiguration("*", null, "cert", "pwd", STATIC)); // full wildcard has not to hide more specific wildcard one - assertEquals("*.example.com", server.getListeners().chooseCertificate("test.example.com", "no-default").getId()); + assertEquals("*.example.com", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "test.example.com", "no-default").getId()); // san - assertEquals("*.example.com", server.getListeners().chooseCertificate("example.com", "no-default").getId()); - assertEquals("*.example.com", server.getListeners().chooseCertificate("test.example2.com", "no-default").getId()); + assertEquals("*.example.com", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "example.com", "no-default").getId()); + assertEquals("*.example.com", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "test.example2.com", "no-default").getId()); // more specific wildcard server.addCertificate(new SSLCertificateConfiguration("*.test.example.com", null, "cert", "pwd", STATIC)); // more specific wildcard has to hide less specific one (*.example.com) - assertEquals("*.test.example.com", server.getListeners().chooseCertificate("pippo.test.example.com", "no-default").getId()); + assertEquals("*.test.example.com", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "pippo.test.example.com", "no-default").getId()); // san - assertEquals("*.example.com", server.getListeners().chooseCertificate("example.com", "no-default").getId()); - assertEquals("*.example.com", server.getListeners().chooseCertificate("test.example2.com", "no-default").getId()); + assertEquals("*.example.com", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "example.com", "no-default").getId()); + assertEquals("*.example.com", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "test.example2.com", "no-default").getId()); } try (HttpProxyServer server = new HttpProxyServer(mapper, tmpDir.getRoot())) { @@ -138,11 +139,11 @@ public void testChooseCertificate() throws Exception { // full wildcard server.addCertificate(new SSLCertificateConfiguration("*", null, "cert", "pwd", STATIC)); - assertEquals("*", server.getListeners().chooseCertificate(null, "www.example.com").getId()); - assertEquals("*", server.getListeners().chooseCertificate("www.example.com", null).getId()); - assertEquals("*", server.getListeners().chooseCertificate(null, null).getId()); - assertEquals("*", server.getListeners().chooseCertificate("", null).getId()); - assertEquals("*", server.getListeners().chooseCertificate(null, "").getId()); + assertEquals("*", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), null, "www.example.com").getId()); + assertEquals("*", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "www.example.com", null).getId()); + assertEquals("*", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), null, null).getId()); + assertEquals("*", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "", null).getId()); + assertEquals("*", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), null, "").getId()); } } diff --git a/carapace-server/src/test/java/org/carapaceproxy/server/certificates/CertificatesUtilsTest.java b/carapace-server/src/test/java/org/carapaceproxy/server/certificates/CertificatesUtilsTest.java index 195baf99f..c5fdff846 100644 --- a/carapace-server/src/test/java/org/carapaceproxy/server/certificates/CertificatesUtilsTest.java +++ b/carapace-server/src/test/java/org/carapaceproxy/server/certificates/CertificatesUtilsTest.java @@ -23,6 +23,7 @@ import static org.carapaceproxy.configstore.ConfigurationStoreUtils.base64EncodeCertificateChain; import static org.carapaceproxy.server.certificates.DynamicCertificatesManager.DEFAULT_KEYPAIRS_SIZE; import static org.carapaceproxy.utils.CertificatesTestUtils.generateSampleChain; +import static org.carapaceproxy.utils.CertificatesUtils.compareChains; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -30,12 +31,11 @@ import java.security.KeyPair; import java.security.cert.Certificate; import java.util.Arrays; +import org.carapaceproxy.utils.CertificatesUtils; import org.junit.Test; import org.shredzone.acme4j.util.KeyPairUtils; -import static org.carapaceproxy.utils.CertificatesUtils.compareChains; import java.security.cert.X509Certificate; import java.util.Date; -import org.carapaceproxy.utils.CertificatesUtils; /** * diff --git a/carapace-server/src/test/java/org/carapaceproxy/utils/ApacheHttpUtils.java b/carapace-server/src/test/java/org/carapaceproxy/utils/ApacheHttpUtils.java new file mode 100644 index 000000000..d94897172 --- /dev/null +++ b/carapace-server/src/test/java/org/carapaceproxy/utils/ApacheHttpUtils.java @@ -0,0 +1,18 @@ +package org.carapaceproxy.utils; + +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.ssl.SSLContextBuilder; + +public class ApacheHttpUtils { + + public static CloseableHttpClient createHttpClientWithDisabledSSLValidation() throws Exception { + return HttpClients.custom() + .setSSLContext(SSLContextBuilder.create() + .loadTrustMaterial((chain, authType) -> true) // Trust all certificates + .build()) + .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE) // Disable hostname verification + .build(); + } +} From 39f89c9a155985fe233eeb3b5ed76a87878471aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccol=C3=B2=20Maltoni?= Date: Mon, 7 Oct 2024 11:55:04 +0200 Subject: [PATCH 3/8] refactor: move out ListenersSniHandler out of Listeners --- .../org/carapaceproxy/core/Listeners.java | 41 ++------------ .../core/ListenersSniHandler.java | 53 +++++++++++++++++++ 2 files changed, 57 insertions(+), 37 deletions(-) create mode 100644 carapace-server/src/main/java/org/carapaceproxy/core/ListenersSniHandler.java diff --git a/carapace-server/src/main/java/org/carapaceproxy/core/Listeners.java b/carapace-server/src/main/java/org/carapaceproxy/core/Listeners.java index 7b379fd21..920af7496 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/core/Listeners.java +++ b/carapace-server/src/main/java/org/carapaceproxy/core/Listeners.java @@ -20,22 +20,15 @@ package org.carapaceproxy.core; import static reactor.netty.ConnectionObserver.State.CONNECTED; -import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelOption; import io.netty.channel.epoll.Epoll; import io.netty.channel.epoll.EpollChannelOption; import io.netty.channel.socket.nio.NioChannelOption; -import io.netty.handler.ssl.OpenSsl; -import io.netty.handler.ssl.ReferenceCountedOpenSslEngine; -import io.netty.handler.ssl.SniHandler; import io.netty.handler.ssl.SslContext; -import io.netty.handler.ssl.SslHandler; import io.netty.handler.timeout.IdleStateHandler; -import io.netty.util.AttributeKey; import io.prometheus.client.Gauge; import java.io.File; -import java.io.IOException; -import java.security.cert.Certificate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; @@ -45,7 +38,6 @@ import java.util.concurrent.ConcurrentMap; import java.util.function.Function; import jdk.net.ExtendedSocketOptions; -import org.carapaceproxy.server.certificates.ocsp.OcspStaplingManager; import org.carapaceproxy.server.config.ConfigurationNotValidException; import org.carapaceproxy.server.config.NetworkListenerConfiguration; import org.carapaceproxy.utils.PrometheusUtils; @@ -214,35 +206,10 @@ private void bootListener(final NetworkListenerConfiguration config) throws Inte : NioChannelOption.of(ExtendedSocketOptions.TCP_KEEPCOUNT), config.getKeepAliveCount()) .maxKeepAliveRequests(config.getMaxKeepAliveRequests()) .doOnChannelInit((observer, channel, remoteAddress) -> { - channel.pipeline().addFirst("idleStateHandler", new IdleStateHandler(0, 0, currentConfiguration.getClientsIdleTimeoutSeconds())); + final ChannelHandler idle = new IdleStateHandler(0, 0, currentConfiguration.getClientsIdleTimeoutSeconds()); + channel.pipeline().addFirst("idleStateHandler", idle); if (config.isSsl()) { - SniHandler sni = new SniHandler(listeningChannel) { - @Override - protected SslHandler newSslHandler(SslContext context, ByteBufAllocator allocator) { - SslHandler handler = super.newSslHandler(context, allocator); - if (currentConfiguration.isOcspEnabled() && OpenSsl.isOcspSupported()) { - final Certificate cert = (Certificate) context.attributes() - .attr(AttributeKey.valueOf(OCSP_CERTIFICATE_CHAIN)) - .get(); - if (cert == null) { - LOG.error("Cannot set OCSP response without the certificate"); - return handler; - } - if (!(handler.engine() instanceof ReferenceCountedOpenSslEngine engine)) { - LOG.error("Unexpected OpenSSL Engine used; cannot set OCSP response."); - return handler; - } - try { - final OcspStaplingManager ocspStaplingManager = parent.getOcspStaplingManager(); - final byte[] response = ocspStaplingManager.getOcspResponseForCertificate(cert); - engine.setOcspResponse(response); - } catch (final IOException ex) { - LOG.error("Error setting OCSP response.", ex); - } - } - return handler; - } - }; + final ChannelHandler sni = new ListenersSniHandler(currentConfiguration, parent, listeningChannel); channel.pipeline().addFirst(sni); } }) diff --git a/carapace-server/src/main/java/org/carapaceproxy/core/ListenersSniHandler.java b/carapace-server/src/main/java/org/carapaceproxy/core/ListenersSniHandler.java new file mode 100644 index 000000000..6ff6a49f7 --- /dev/null +++ b/carapace-server/src/main/java/org/carapaceproxy/core/ListenersSniHandler.java @@ -0,0 +1,53 @@ +package org.carapaceproxy.core; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.ssl.OpenSsl; +import io.netty.handler.ssl.ReferenceCountedOpenSslEngine; +import io.netty.handler.ssl.SniHandler; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslHandler; +import io.netty.util.AttributeKey; +import java.io.IOException; +import java.security.cert.Certificate; +import org.carapaceproxy.server.certificates.ocsp.OcspStaplingManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class ListenersSniHandler extends SniHandler { + private static final Logger LOG = LoggerFactory.getLogger(ListenersSniHandler.class); + + private final RuntimeServerConfiguration currentConfiguration; + private final HttpProxyServer parent; + + public ListenersSniHandler(final RuntimeServerConfiguration currentConfiguration, final HttpProxyServer parent, final ListeningChannel listeningChannel) { + super(listeningChannel); + this.currentConfiguration = currentConfiguration; + this.parent = parent; + } + + @Override + protected SslHandler newSslHandler(final SslContext context, final ByteBufAllocator allocator) { + final SslHandler handler = super.newSslHandler(context, allocator); + if (currentConfiguration.isOcspEnabled() && OpenSsl.isOcspSupported()) { + final Certificate cert = (Certificate) context.attributes() + .attr(AttributeKey.valueOf(Listeners.OCSP_CERTIFICATE_CHAIN)) + .get(); + if (cert == null) { + LOG.error("Cannot set OCSP response without the certificate"); + return handler; + } + if (!(handler.engine() instanceof ReferenceCountedOpenSslEngine engine)) { + LOG.error("Unexpected OpenSSL Engine used; cannot set OCSP response."); + return handler; + } + try { + final OcspStaplingManager ocspStaplingManager = parent.getOcspStaplingManager(); + final byte[] response = ocspStaplingManager.getOcspResponseForCertificate(cert); + engine.setOcspResponse(response); + } catch (final IOException ex) { + LOG.error("Error setting OCSP response.", ex); + } + } + return handler; + } +} From 228da1f8cef0a2bf73348f0e18ab3befb87b761e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccol=C3=B2=20Maltoni?= Date: Mon, 7 Oct 2024 12:41:42 +0200 Subject: [PATCH 4/8] refactor: make NetworkListenerConfiguration a record --- .../carapaceproxy/api/ListenersResource.java | 8 +- .../carapaceproxy/core/HttpProxyServer.java | 2 +- .../org/carapaceproxy/core/Listeners.java | 24 ++-- .../carapaceproxy/core/ListeningChannel.java | 22 ++-- .../core/RuntimeServerConfiguration.java | 27 +++-- .../config/NetworkListenerConfiguration.java | 106 ++++++------------ .../java/org/carapaceproxy/RawClientTest.java | 4 +- .../carapaceproxy/SimpleHTTPProxyTest.java | 4 +- .../backends/StuckRequestsTest.java | 2 +- .../backends/UnreachableBackendTest.java | 4 +- .../core/ForwardedStrategyTest.java | 6 +- .../carapaceproxy/core/Http2HeadersTest.java | 7 +- .../org/carapaceproxy/core/Http2Test.java | 11 +- .../listeners/ListenerConfigurationTest.java | 26 ++--- .../listeners/MultiListeningEndpointTest.java | 2 +- .../carapaceproxy/listeners/SSLSNITest.java | 12 +- .../carapaceproxy/server/cache/CacheTest.java | 10 +- .../server/filters/XTlsCipherFilterTest.java | 6 +- .../filters/XTlsProtocolFilterTest.java | 6 +- 19 files changed, 138 insertions(+), 151 deletions(-) diff --git a/carapace-server/src/main/java/org/carapaceproxy/api/ListenersResource.java b/carapace-server/src/main/java/org/carapaceproxy/api/ListenersResource.java index 68fab7a3d..f8a0c39aa 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/api/ListenersResource.java +++ b/carapace-server/src/main/java/org/carapaceproxy/api/ListenersResource.java @@ -66,10 +66,10 @@ public Map getAllListeners() { channel -> new ListenerBean( channel.getHostPort().host(), channel.getHostPort().port(), - channel.getConfig().isSsl(), - channel.getConfig().getSslCiphers(), - channel.getConfig().getSslProtocols(), - channel.getConfig().getDefaultCertificate(), + channel.getConfig().ssl(), + channel.getConfig().sslCiphers(), + channel.getConfig().sslProtocols(), + channel.getConfig().defaultCertificate(), (int) channel.getTotalRequests().get() ))); } diff --git a/carapace-server/src/main/java/org/carapaceproxy/core/HttpProxyServer.java b/carapace-server/src/main/java/org/carapaceproxy/core/HttpProxyServer.java index d8237031d..93e120cb3 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/core/HttpProxyServer.java +++ b/carapace-server/src/main/java/org/carapaceproxy/core/HttpProxyServer.java @@ -235,7 +235,7 @@ public static HttpProxyServer buildForTests( ) throws ConfigurationNotValidException { final HttpProxyServer server = new HttpProxyServer(mapperFactory, baseDir.getAbsoluteFile()); final EndpointMapper mapper = server.getMapper(); - server.currentConfiguration.addListener(new NetworkListenerConfiguration(host, port)); + server.currentConfiguration.addListener(NetworkListenerConfiguration.withDefault(host, port)); server.proxyRequestsManager.reloadConfiguration(server.currentConfiguration, mapper.getBackends().values()); return server; } diff --git a/carapace-server/src/main/java/org/carapaceproxy/core/Listeners.java b/carapace-server/src/main/java/org/carapaceproxy/core/Listeners.java index 920af7496..c6ffa8aab 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/core/Listeners.java +++ b/carapace-server/src/main/java/org/carapaceproxy/core/Listeners.java @@ -176,39 +176,39 @@ private void stopListener(EndpointKey hostPort) throws InterruptedException { } private void bootListener(final NetworkListenerConfiguration config) throws InterruptedException { - final EndpointKey hostPort = new EndpointKey(config.getHost(), config.getPort()).offsetPort(parent.getListenersOffsetPort()); + final EndpointKey hostPort = new EndpointKey(config.host(), config.port()).offsetPort(parent.getListenersOffsetPort()); final ListeningChannel listeningChannel = new ListeningChannel(basePath, currentConfiguration, parent, sslContexts, hostPort, config); - LOG.info("Starting listener at {}:{} ssl:{}", hostPort.host(), hostPort.port(), config.isSsl()); + LOG.info("Starting listener at {}:{} ssl:{}", hostPort.host(), hostPort.port(), config.ssl()); // Listener setup HttpServer httpServer = HttpServer.create() .host(hostPort.host()) .port(hostPort.port()) - .protocol(config.getProtocols().toArray(HttpProtocol[]::new)) + .protocol(config.protocols().toArray(HttpProtocol[]::new)) /* // .secure() todo: to enable H2, see config.isSsl() & snimappings see https://projectreactor.io/docs/netty/release/reference/index.html#_server_name_indication_3 */ .metrics(true, Function.identity()) - .forwarded(ForwardedStrategy.of(config.getForwardedStrategy(), config.getTrustedIps())) - .option(ChannelOption.SO_BACKLOG, config.getSoBacklog()) - .childOption(ChannelOption.SO_KEEPALIVE, config.isKeepAlive()) + .forwarded(ForwardedStrategy.of(config.forwardedStrategy(), config.trustedIps())) + .option(ChannelOption.SO_BACKLOG, config.soBacklog()) + .childOption(ChannelOption.SO_KEEPALIVE, config.keepAlive()) .runOn(parent.getEventLoopGroup()) .childOption(Epoll.isAvailable() ? EpollChannelOption.TCP_KEEPIDLE - : NioChannelOption.of(ExtendedSocketOptions.TCP_KEEPIDLE), config.getKeepAliveIdle()) + : NioChannelOption.of(ExtendedSocketOptions.TCP_KEEPIDLE), config.keepAliveIdle()) .childOption(Epoll.isAvailable() ? EpollChannelOption.TCP_KEEPINTVL - : NioChannelOption.of(ExtendedSocketOptions.TCP_KEEPINTERVAL), config.getKeepAliveInterval()) + : NioChannelOption.of(ExtendedSocketOptions.TCP_KEEPINTERVAL), config.keepAliveInterval()) .childOption(Epoll.isAvailable() ? EpollChannelOption.TCP_KEEPCNT - : NioChannelOption.of(ExtendedSocketOptions.TCP_KEEPCOUNT), config.getKeepAliveCount()) - .maxKeepAliveRequests(config.getMaxKeepAliveRequests()) + : NioChannelOption.of(ExtendedSocketOptions.TCP_KEEPCOUNT), config.keepAliveCount()) + .maxKeepAliveRequests(config.maxKeepAliveRequests()) .doOnChannelInit((observer, channel, remoteAddress) -> { final ChannelHandler idle = new IdleStateHandler(0, 0, currentConfiguration.getClientsIdleTimeoutSeconds()); channel.pipeline().addFirst("idleStateHandler", idle); - if (config.isSsl()) { + if (config.ssl()) { final ChannelHandler sni = new ListenersSniHandler(currentConfiguration, parent, listeningChannel); channel.pipeline().addFirst(sni); } @@ -216,7 +216,7 @@ private void bootListener(final NetworkListenerConfiguration config) throws Inte .doOnConnection(conn -> { CURRENT_CONNECTED_CLIENTS_GAUGE.inc(); conn.channel().closeFuture().addListener(e -> CURRENT_CONNECTED_CLIENTS_GAUGE.dec()); - config.getGroup().add(conn.channel()); + config.group().add(conn.channel()); }) .childObserve((connection, state) -> { if (state == CONNECTED) { diff --git a/carapace-server/src/main/java/org/carapaceproxy/core/ListeningChannel.java b/carapace-server/src/main/java/org/carapaceproxy/core/ListeningChannel.java index d62f3b6d7..c0811d0a7 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/core/ListeningChannel.java +++ b/carapace-server/src/main/java/org/carapaceproxy/core/ListeningChannel.java @@ -70,7 +70,7 @@ public int getLocalPort() { public void disposeChannel() { this.channel.disposeNow(Duration.ofSeconds(10)); - FutureMono.from(this.config.getGroup().close()).block(Duration.ofSeconds(10)); + FutureMono.from(this.config.group().close()).block(Duration.ofSeconds(10)); } public void incRequests() { @@ -84,7 +84,7 @@ public void clear() { @Override public Future map(String sniHostname, Promise promise) { try { - var key = config.getHost() + ":" + hostPort.port() + "+" + sniHostname; + final var key = config.host() + ":" + hostPort.port() + "+" + sniHostname; if (LOG.isDebugEnabled()) { LOG.debug("resolve SNI mapping {}, key: {}", sniHostname, key); } @@ -99,7 +99,7 @@ public Future map(String sniHostname, Promise promise) { var chosen = chooseCertificate(sniHostname); if (chosen == null) { throw new ConfigurationNotValidException("cannot find a certificate for snihostname " + sniHostname - + ", with default cert for listener as '" + config.getDefaultCertificate() + + ", with default cert for listener as '" + config.defaultCertificate() + "', available " + currentConfiguration.getCertificates().keySet()); } return bootSslContext(config, chosen); @@ -124,12 +124,12 @@ public Future map(String sniHostname, Promise promise) { } private SSLCertificateConfiguration chooseCertificate(final String sniHostname) { - return CertificatesUtils.chooseCertificate(currentConfiguration, sniHostname, config.getDefaultCertificate()); + return CertificatesUtils.chooseCertificate(currentConfiguration, sniHostname, config.defaultCertificate()); } private SslContext bootSslContext(NetworkListenerConfiguration listener, SSLCertificateConfiguration certificate) throws ConfigurationNotValidException { - var port = listener.getPort() + parent.getListenersOffsetPort(); - var sslCiphers = listener.getSslCiphers(); + final var hostPort = new EndpointKey(listener.host(), listener.port()).offsetPort(parent.getListenersOffsetPort()); + var sslCiphers = listener.sslCiphers(); try { // Try to find certificate data on db @@ -137,15 +137,15 @@ private SslContext bootSslContext(NetworkListenerConfiguration listener, SSLCert final KeyStore keystore; if (keystoreContent == null) { if (certificate.isDynamic()) { // fallback to default certificate - certificate = currentConfiguration.getCertificates().get(listener.getDefaultCertificate()); + certificate = currentConfiguration.getCertificates().get(listener.defaultCertificate()); if (certificate == null) { - throw new ConfigurationNotValidException("Unable to boot SSL context for listener " + listener.getHost() + ": no default certificate setup."); + throw new ConfigurationNotValidException("Unable to boot SSL context for listener " + hostPort.host() + ": no default certificate setup."); } } - LOG.debug("start SSL with certificate id {}, on listener {}:{} file={}", certificate.getId(), listener.getHost(), port, certificate.getFile()); + LOG.debug("start SSL with certificate id {}, on listener {}:{} file={}", certificate.getId(), hostPort.host(), hostPort.port(), certificate.getFile()); keystore = loadKeyStoreFromFile(certificate.getFile(), certificate.getPassword(), basePath); } else { - LOG.debug("start SSL with dynamic certificate id {}, on listener {}:{}", certificate.getId(), listener.getHost(), port); + LOG.debug("start SSL with dynamic certificate id {}, on listener {}:{}", certificate.getId(), hostPort.host(), hostPort.port()); keystore = loadKeyStoreData(keystoreContent, certificate.getPassword()); } KeyManagerFactory keyFactory = new OpenSslCachingX509KeyManagerFactory(KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())); @@ -161,7 +161,7 @@ private SslContext bootSslContext(NetworkListenerConfiguration listener, SSLCert .enableOcsp(currentConfiguration.isOcspEnabled() && OpenSsl.isOcspSupported()) .trustManager(parent.getTrustStoreManager().getTrustManagerFactory()) .sslProvider(SslProvider.OPENSSL) - .protocols(listener.getSslProtocols()) + .protocols(listener.sslProtocols()) .ciphers(ciphers).build(); var chain = readChainFromKeystore(keystore); diff --git a/carapace-server/src/main/java/org/carapaceproxy/core/RuntimeServerConfiguration.java b/carapace-server/src/main/java/org/carapaceproxy/core/RuntimeServerConfiguration.java index 6e7b607f6..b0e1de625 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/core/RuntimeServerConfiguration.java +++ b/carapace-server/src/main/java/org/carapaceproxy/core/RuntimeServerConfiguration.java @@ -36,6 +36,8 @@ import static org.carapaceproxy.server.config.NetworkListenerConfiguration.DEFAULT_SSL_PROTOCOLS; import static org.carapaceproxy.server.config.NetworkListenerConfiguration.getDefaultHttpProtocols; import static org.carapaceproxy.server.filters.RequestFilterFactory.buildRequestFilter; +import io.netty.channel.group.DefaultChannelGroup; +import io.netty.util.concurrent.DefaultEventExecutor; import java.io.File; import java.security.NoSuchAlgorithmException; import java.sql.Timestamp; @@ -47,6 +49,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import javax.net.ssl.SSLContext; import lombok.Data; import org.carapaceproxy.configstore.ConfigurationStore; @@ -59,6 +62,7 @@ import org.carapaceproxy.server.mapper.StandardEndpointMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import reactor.netty.http.HttpProtocol; /** * Implementation of a configuration for the whole server. @@ -342,6 +346,7 @@ private void configureListeners(ConfigurationStore properties) throws Configurat final var port = properties.getInt(prefix + "port", 0); if (port > 0) { final var ssl = properties.getBoolean(prefix + "ssl", false); + final var protocols = properties.getValues(prefix + "protocol", Set.of()); this.addListener(new NetworkListenerConfiguration( properties.getString(prefix + "host", "0.0.0.0"), port, @@ -357,8 +362,11 @@ private void configureListeners(ConfigurationStore properties) throws Configurat properties.getInt(prefix + "maxkeepaliverequests", maxKeepAliveRequests), properties.getString(prefix + "forwarded", DEFAULT_FORWARDED_STRATEGY), properties.getValues(prefix + "trustedips", Set.of()), - properties.getValues(prefix + "protocol", getDefaultHttpProtocols(ssl)) - )); + protocols.isEmpty() ? getDefaultHttpProtocols(ssl) : protocols.stream() + .map(String::toUpperCase) + .map(HttpProtocol::valueOf) + .collect(Collectors.toUnmodifiableSet()), + new DefaultChannelGroup(new DefaultEventExecutor()))); } } } @@ -450,22 +458,21 @@ private void configureConnectionPools(ConfigurationStore properties) throws Conf } public void addListener(NetworkListenerConfiguration listener) throws ConfigurationNotValidException { - if (listener.isSsl() && !certificates.containsKey(listener.getDefaultCertificate())) { + if (listener.ssl() && !certificates.containsKey(listener.defaultCertificate())) { throw new ConfigurationNotValidException( - "Listener " + listener.getHost() + ":" + listener.getPort() + ", " - + "ssl=true, " - + "default certificate " + listener.getDefaultCertificate() + " not configured." + "Listener " + listener.host() + ":" + listener.port() + ", ssl=true, " + + "default certificate " + listener.defaultCertificate() + " not configured." ); } - if (listener.isSsl()) { + if (listener.ssl()) { try { if (supportedSSLProtocols == null) { supportedSSLProtocols = Set.of(SSLContext.getDefault().getSupportedSSLParameters().getProtocols()); } - if (!supportedSSLProtocols.containsAll(listener.getSslProtocols())) { + if (!supportedSSLProtocols.containsAll(listener.sslProtocols())) { throw new ConfigurationNotValidException( - "Unsupported SSL Protocols " + listener.getSslProtocols() - + " for listener " + listener.getHost() + ":" + listener.getPort() + "Unsupported SSL Protocols " + listener.sslProtocols() + + " for listener " + listener.host() + ":" + listener.port() ); } } catch (NoSuchAlgorithmException ex) { diff --git a/carapace-server/src/main/java/org/carapaceproxy/server/config/NetworkListenerConfiguration.java b/carapace-server/src/main/java/org/carapaceproxy/server/config/NetworkListenerConfiguration.java index 22f05551b..0dbb248b2 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/server/config/NetworkListenerConfiguration.java +++ b/carapace-server/src/main/java/org/carapaceproxy/server/config/NetworkListenerConfiguration.java @@ -26,8 +26,6 @@ import io.netty.channel.group.DefaultChannelGroup; import io.netty.util.concurrent.DefaultEventExecutor; import java.util.Set; -import java.util.stream.Collectors; -import lombok.Getter; import org.carapaceproxy.core.EndpointKey; import org.carapaceproxy.core.ForwardedStrategies; import reactor.netty.http.HttpProtocol; @@ -35,8 +33,24 @@ /** * Listens for connections on the network */ -@Getter -public class NetworkListenerConfiguration { +public record NetworkListenerConfiguration( + String host, + int port, + boolean ssl, + String sslCiphers, + String defaultCertificate, + Set sslProtocols, + int soBacklog, + boolean keepAlive, + int keepAliveIdle, + int keepAliveInterval, + int keepAliveCount, + int maxKeepAliveRequests, + String forwardedStrategy, + Set trustedIps, + Set protocols, + ChannelGroup group +) { public static final Set DEFAULT_SSL_PROTOCOLS = Set.of("TLSv1.2", "TLSv1.3"); public static final int DEFAULT_SO_BACKLOG = 128; @@ -47,25 +61,17 @@ public class NetworkListenerConfiguration { public static final int DEFAULT_MAX_KEEP_ALIVE_REQUESTS = 1000; public static final String DEFAULT_FORWARDED_STRATEGY = ForwardedStrategies.preserve().name(); - private final String host; - private final int port; - private final boolean ssl; - private final String sslCiphers; - private final String defaultCertificate; - private final String forwardedStrategy; - private final Set trustedIps; - private final Set sslProtocols; - private final int soBacklog; - private final boolean keepAlive; - private final int keepAliveIdle; - private final int keepAliveInterval; - private final int keepAliveCount; - private final int maxKeepAliveRequests; - private final ChannelGroup group; - private final Set protocols; + public NetworkListenerConfiguration { + if (protocols == null || protocols.isEmpty()) { + throw new IllegalArgumentException("At least one HTTP protocol is required!!!"); + } + if (!ssl && protocols.contains(H2)) { + throw new IllegalArgumentException("H2 requires SSL support"); + } + } - public NetworkListenerConfiguration(final String host, final int port) { - this( + public static NetworkListenerConfiguration withDefault(final String host, final int port) { + return withDefault( host, port, false, @@ -80,7 +86,7 @@ public NetworkListenerConfiguration(final String host, final int port) { ); } - public NetworkListenerConfiguration( + public static NetworkListenerConfiguration withDefault( final String host, final int port, final boolean ssl, @@ -92,7 +98,7 @@ public NetworkListenerConfiguration( final int keepAliveInterval, final int keepAliveCount, final int maxKeepAliveRequests) { - this( + return new NetworkListenerConfiguration( host, port, ssl, @@ -107,59 +113,15 @@ public NetworkListenerConfiguration( maxKeepAliveRequests, DEFAULT_FORWARDED_STRATEGY, Set.of(), - getDefaultHttpProtocols(ssl) - ); + getDefaultHttpProtocols(ssl), + new DefaultChannelGroup(new DefaultEventExecutor())); } - public NetworkListenerConfiguration( - final String host, - final int port, - final boolean ssl, - final String sslCiphers, - final String defaultCertificate, - final Set sslProtocols, - final int soBacklog, - final boolean keepAlive, - final int keepAliveIdle, - final int keepAliveInterval, - final int keepAliveCount, - final int maxKeepAliveRequests, - final String forwardedStrategy, - final Set trustedIps, - final Set protocols) { - this.host = host; - this.port = port; - this.ssl = ssl; - this.sslCiphers = sslCiphers; - this.defaultCertificate = defaultCertificate; - this.forwardedStrategy = forwardedStrategy; - this.trustedIps = trustedIps; - this.sslProtocols = ssl && sslProtocols != null ? Set.copyOf(sslProtocols) : Set.of(); - this.soBacklog = soBacklog; - this.keepAlive = keepAlive; - this.keepAliveIdle = keepAliveIdle; - this.keepAliveInterval = keepAliveInterval; - this.keepAliveCount = keepAliveCount; - this.maxKeepAliveRequests = maxKeepAliveRequests; - this.group = new DefaultChannelGroup(new DefaultEventExecutor()); - if (protocols == null || protocols.isEmpty()) { - throw new IllegalArgumentException("At least one HTTP protocol is required!!!"); - } - final var httpProtocols = protocols.stream() - .map(String::toUpperCase) - .map(HttpProtocol::valueOf) - .collect(Collectors.toUnmodifiableSet()); - if (!ssl && httpProtocols.contains(H2)) { - throw new IllegalArgumentException("H2 requires SSL support"); - } - this.protocols = httpProtocols; + public static Set getDefaultHttpProtocols(final boolean ssl) { + return Set.of(HTTP11, ssl ? H2 : H2C); } public EndpointKey getKey() { return new EndpointKey(host, port); } - - public static Set getDefaultHttpProtocols(final boolean ssl) { - return Set.of(HTTP11.name(), (ssl ? H2 : H2C).name()); - } } diff --git a/carapace-server/src/test/java/org/carapaceproxy/RawClientTest.java b/carapace-server/src/test/java/org/carapaceproxy/RawClientTest.java index 1cb1cc80d..4efa49a41 100644 --- a/carapace-server/src/test/java/org/carapaceproxy/RawClientTest.java +++ b/carapace-server/src/test/java/org/carapaceproxy/RawClientTest.java @@ -54,6 +54,7 @@ import io.netty.channel.epoll.Epoll; import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.epoll.EpollServerSocketChannel; +import io.netty.channel.group.DefaultChannelGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; @@ -67,6 +68,7 @@ import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.LastHttpContent; +import io.netty.util.concurrent.DefaultEventExecutor; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; @@ -734,7 +736,7 @@ public void testClosedProxy(String scheme) throws Exception { TestEndpointMapper mapper = new TestEndpointMapper("localhost", wireMockRule.port(), true); try (HttpProxyServer server = new HttpProxyServer(mapper, tmpDir.getRoot())) { server.addCertificate(new SSLCertificateConfiguration("localhost", null, "localhost.p12", "testproxy", STATIC)); - server.addListener(new NetworkListenerConfiguration("localhost", 0, scheme.equals("https"), null, "localhost", DEFAULT_SSL_PROTOCOLS, 128, true, 300, 60, 8, 100, DEFAULT_FORWARDED_STRATEGY, Set.of(), Set.of(HTTP11.name()))); + server.addListener(new NetworkListenerConfiguration("localhost", 0, scheme.equals("https"), null, "localhost", DEFAULT_SSL_PROTOCOLS, 128, true, 300, 60, 8, 100, DEFAULT_FORWARDED_STRATEGY, Set.of(), Set.of(HTTP11), new DefaultChannelGroup(new DefaultEventExecutor()))); server.start(); int port = server.getLocalPort(); diff --git a/carapace-server/src/test/java/org/carapaceproxy/SimpleHTTPProxyTest.java b/carapace-server/src/test/java/org/carapaceproxy/SimpleHTTPProxyTest.java index cdc25ed04..8cec9d5ba 100644 --- a/carapace-server/src/test/java/org/carapaceproxy/SimpleHTTPProxyTest.java +++ b/carapace-server/src/test/java/org/carapaceproxy/SimpleHTTPProxyTest.java @@ -31,6 +31,8 @@ import static org.junit.Assert.fail; import static reactor.netty.http.HttpProtocol.HTTP11; import com.github.tomakehurst.wiremock.junit.WireMockRule; +import io.netty.channel.group.DefaultChannelGroup; +import io.netty.util.concurrent.DefaultEventExecutor; import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.net.URI; @@ -113,7 +115,7 @@ public void testSsl() throws Exception { try (HttpProxyServer server = new HttpProxyServer(mapper, tmpDir.getRoot());) { server.addCertificate(new SSLCertificateConfiguration("localhost", null, certificate, "changeit", STATIC)); - server.addListener(new NetworkListenerConfiguration("localhost", 0, true, null, "localhost", DEFAULT_SSL_PROTOCOLS, 128, true, 300, 60, 8, 1000, DEFAULT_FORWARDED_STRATEGY, Set.of(), Set.of(HTTP11.name()))); + server.addListener(new NetworkListenerConfiguration("localhost", 0, true, null, "localhost", DEFAULT_SSL_PROTOCOLS, 128, true, 300, 60, 8, 1000, DEFAULT_FORWARDED_STRATEGY, Set.of(), Set.of(HTTP11), new DefaultChannelGroup(new DefaultEventExecutor()))); server.start(); int port = server.getLocalPort(); diff --git a/carapace-server/src/test/java/org/carapaceproxy/backends/StuckRequestsTest.java b/carapace-server/src/test/java/org/carapaceproxy/backends/StuckRequestsTest.java index ad29c3245..17c6769b7 100644 --- a/carapace-server/src/test/java/org/carapaceproxy/backends/StuckRequestsTest.java +++ b/carapace-server/src/test/java/org/carapaceproxy/backends/StuckRequestsTest.java @@ -104,7 +104,7 @@ public void testBackendUnreachableOnStuckRequest(boolean backendsUnreachableOnSt properties.put("connectionsmanager.backendsunreachableonstuckrequests", backendsUnreachableOnStuckRequests + ""); // configure resets all listeners configurations server.configureAtBoot(new PropertiesConfigurationStore(properties)); - server.addListener(new NetworkListenerConfiguration("localhost", 0)); + server.addListener(NetworkListenerConfiguration.withDefault("localhost", 0)); assertEquals(100, server.getCurrentConfiguration().getStuckRequestTimeout()); assertEquals(backendsUnreachableOnStuckRequests, server.getCurrentConfiguration().isBackendsUnreachableOnStuckRequests()); server.start(); diff --git a/carapace-server/src/test/java/org/carapaceproxy/backends/UnreachableBackendTest.java b/carapace-server/src/test/java/org/carapaceproxy/backends/UnreachableBackendTest.java index 87b915e57..5c1b8b0ea 100644 --- a/carapace-server/src/test/java/org/carapaceproxy/backends/UnreachableBackendTest.java +++ b/carapace-server/src/test/java/org/carapaceproxy/backends/UnreachableBackendTest.java @@ -138,7 +138,7 @@ public void testEmptyResponse() throws Exception { properties.put("healthmanager.tolerant", "true"); // configure resets all listeners configurations server.configureAtBoot(new PropertiesConfigurationStore(properties)); - server.addListener(new NetworkListenerConfiguration("localhost", 0)); + server.addListener(NetworkListenerConfiguration.withDefault("localhost", 0)); server.start(); int port = server.getLocalPort(); assertTrue(port > 0); @@ -223,7 +223,7 @@ public void testNonHttpResponseThenClose() throws Exception { properties.put("route.100.action", properties.getProperty("action.1.id")); properties.put("healthmanager.tolerant", "true"); server.configureAtBoot(new PropertiesConfigurationStore(properties)); - server.addListener(new NetworkListenerConfiguration("localhost", 0)); + server.addListener(NetworkListenerConfiguration.withDefault("localhost", 0)); server.start(); int port = server.getLocalPort(); diff --git a/carapace-server/src/test/java/org/carapaceproxy/core/ForwardedStrategyTest.java b/carapace-server/src/test/java/org/carapaceproxy/core/ForwardedStrategyTest.java index a428d9c6e..64e654282 100644 --- a/carapace-server/src/test/java/org/carapaceproxy/core/ForwardedStrategyTest.java +++ b/carapace-server/src/test/java/org/carapaceproxy/core/ForwardedStrategyTest.java @@ -17,6 +17,8 @@ import static org.junit.Assert.assertTrue; import static reactor.netty.http.HttpProtocol.HTTP11; import com.github.tomakehurst.wiremock.junit.WireMockRule; +import io.netty.channel.group.DefaultChannelGroup; +import io.netty.util.concurrent.DefaultEventExecutor; import java.io.IOException; import java.util.List; import java.util.Set; @@ -189,8 +191,8 @@ private static NetworkListenerConfiguration getConfiguration(final ForwardedStra DEFAULT_MAX_KEEP_ALIVE_REQUESTS, strategy.name(), trustedIps, - Set.of(HTTP11.name()) - ); + Set.of(HTTP11), + new DefaultChannelGroup(new DefaultEventExecutor())); } private static String requestWithHeader(final RawHttpClient client) throws IOException { diff --git a/carapace-server/src/test/java/org/carapaceproxy/core/Http2HeadersTest.java b/carapace-server/src/test/java/org/carapaceproxy/core/Http2HeadersTest.java index ce95a8ba6..7f53c647c 100644 --- a/carapace-server/src/test/java/org/carapaceproxy/core/Http2HeadersTest.java +++ b/carapace-server/src/test/java/org/carapaceproxy/core/Http2HeadersTest.java @@ -18,9 +18,11 @@ import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; import com.github.tomakehurst.wiremock.junit.WireMockRule; +import io.netty.channel.group.DefaultChannelGroup; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; +import io.netty.util.concurrent.DefaultEventExecutor; import java.io.IOException; import java.util.Set; import org.carapaceproxy.server.config.ConfigurationNotValidException; @@ -68,8 +70,9 @@ public void test() throws IOException, ConfigurationNotValidException, Interrupt DEFAULT_MAX_KEEP_ALIVE_REQUESTS, DEFAULT_FORWARDED_STRATEGY, Set.of(), - Set.of(HttpProtocol.H2C.name(), HttpProtocol.HTTP11.name()) - )); + Set.of(HttpProtocol.H2C, HttpProtocol.HTTP11), + new DefaultChannelGroup(new DefaultEventExecutor())) + ); server.start(); final var port = server.getLocalPort(); diff --git a/carapace-server/src/test/java/org/carapaceproxy/core/Http2Test.java b/carapace-server/src/test/java/org/carapaceproxy/core/Http2Test.java index 1649e2340..b3760c53f 100644 --- a/carapace-server/src/test/java/org/carapaceproxy/core/Http2Test.java +++ b/carapace-server/src/test/java/org/carapaceproxy/core/Http2Test.java @@ -6,7 +6,6 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static com.github.tomakehurst.wiremock.core.Options.DYNAMIC_PORT; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; -import static java.util.stream.Collectors.toUnmodifiableSet; import static org.carapaceproxy.server.config.NetworkListenerConfiguration.DEFAULT_FORWARDED_STRATEGY; import static org.carapaceproxy.server.config.NetworkListenerConfiguration.DEFAULT_KEEP_ALIVE; import static org.carapaceproxy.server.config.NetworkListenerConfiguration.DEFAULT_KEEP_ALIVE_COUNT; @@ -18,7 +17,9 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import com.github.tomakehurst.wiremock.junit.WireMockRule; +import io.netty.channel.group.DefaultChannelGroup; import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.util.concurrent.DefaultEventExecutor; import java.io.IOException; import java.util.Collection; import java.util.List; @@ -46,12 +47,12 @@ public class Http2Test { public TemporaryFolder tmpDir = new TemporaryFolder(); private final HttpProtocol protocol; - private final Set carapaceProtocols; + private final Set carapaceProtocols; private final boolean withCache; public Http2Test(final HttpProtocol protocol, final Set carapaceProtocols, final boolean withCache) { this.protocol = protocol; - this.carapaceProtocols = carapaceProtocols.stream().map(HttpProtocol::name).collect(toUnmodifiableSet()); + this.carapaceProtocols = carapaceProtocols; this.withCache = withCache; } @@ -93,8 +94,8 @@ public void test() throws IOException, ConfigurationNotValidException, Interrupt DEFAULT_MAX_KEEP_ALIVE_REQUESTS, DEFAULT_FORWARDED_STRATEGY, Set.of(), - carapaceProtocols - )); + carapaceProtocols, + new DefaultChannelGroup(new DefaultEventExecutor()))); server.start(); final var port = server.getLocalPort(); diff --git a/carapace-server/src/test/java/org/carapaceproxy/listeners/ListenerConfigurationTest.java b/carapace-server/src/test/java/org/carapaceproxy/listeners/ListenerConfigurationTest.java index 1b2335efb..5c7713638 100644 --- a/carapace-server/src/test/java/org/carapaceproxy/listeners/ListenerConfigurationTest.java +++ b/carapace-server/src/test/java/org/carapaceproxy/listeners/ListenerConfigurationTest.java @@ -40,12 +40,12 @@ public void testListenerKeepAliveConfiguration() throws Exception { Map listeners = server.getListeners().getListeningChannels(); //check default configuration - assertTrue(listeners.get(listenerKey).getConfig().isKeepAlive()); - assertEquals(128, listeners.get(listenerKey).getConfig().getSoBacklog()); - assertEquals(300, listeners.get(listenerKey).getConfig().getKeepAliveIdle()); - assertEquals(60, listeners.get(listenerKey).getConfig().getKeepAliveInterval()); - assertEquals(8, listeners.get(listenerKey).getConfig().getKeepAliveCount()); - assertEquals(1000, listeners.get(listenerKey).getConfig().getMaxKeepAliveRequests()); + assertTrue(listeners.get(listenerKey).getConfig().keepAlive()); + assertEquals(128, listeners.get(listenerKey).getConfig().soBacklog()); + assertEquals(300, listeners.get(listenerKey).getConfig().keepAliveIdle()); + assertEquals(60, listeners.get(listenerKey).getConfig().keepAliveInterval()); + assertEquals(8, listeners.get(listenerKey).getConfig().keepAliveCount()); + assertEquals(1000, listeners.get(listenerKey).getConfig().maxKeepAliveRequests()); } //disable keepAlive { @@ -60,7 +60,7 @@ public void testListenerKeepAliveConfiguration() throws Exception { Map listeners = server.getListeners().getListeningChannels(); assertEquals(1, listeners.size()); - assertFalse(listeners.get(listenerKey).getConfig().isKeepAlive()); + assertFalse(listeners.get(listenerKey).getConfig().keepAlive()); } //customize keepAlive options @@ -80,12 +80,12 @@ public void testListenerKeepAliveConfiguration() throws Exception { Map listeners = server.getListeners().getListeningChannels(); - assertTrue(listeners.get(listenerKey).getConfig().isKeepAlive()); - assertEquals(10, listeners.get(listenerKey).getConfig().getSoBacklog()); - assertEquals(10, listeners.get(listenerKey).getConfig().getKeepAliveIdle()); - assertEquals(5, listeners.get(listenerKey).getConfig().getKeepAliveInterval()); - assertEquals(2, listeners.get(listenerKey).getConfig().getKeepAliveCount()); - assertEquals(2, listeners.get(listenerKey).getConfig().getMaxKeepAliveRequests()); + assertTrue(listeners.get(listenerKey).getConfig().keepAlive()); + assertEquals(10, listeners.get(listenerKey).getConfig().soBacklog()); + assertEquals(10, listeners.get(listenerKey).getConfig().keepAliveIdle()); + assertEquals(5, listeners.get(listenerKey).getConfig().keepAliveInterval()); + assertEquals(2, listeners.get(listenerKey).getConfig().keepAliveCount()); + assertEquals(2, listeners.get(listenerKey).getConfig().maxKeepAliveRequests()); } //negative maxkeepAliverequests diff --git a/carapace-server/src/test/java/org/carapaceproxy/listeners/MultiListeningEndpointTest.java b/carapace-server/src/test/java/org/carapaceproxy/listeners/MultiListeningEndpointTest.java index 539cd000c..d97964d99 100644 --- a/carapace-server/src/test/java/org/carapaceproxy/listeners/MultiListeningEndpointTest.java +++ b/carapace-server/src/test/java/org/carapaceproxy/listeners/MultiListeningEndpointTest.java @@ -62,7 +62,7 @@ public void test() throws Exception { TestEndpointMapper mapper = new TestEndpointMapper("localhost", wireMockRule.port()); try (HttpProxyServer server = HttpProxyServer.buildForTests("localhost", port, mapper, tmpDir.newFolder());) { - server.addListener(new NetworkListenerConfiguration("localhost", port2)); + server.addListener(NetworkListenerConfiguration.withDefault("localhost", port2)); server.start(); // proxy diff --git a/carapace-server/src/test/java/org/carapaceproxy/listeners/SSLSNITest.java b/carapace-server/src/test/java/org/carapaceproxy/listeners/SSLSNITest.java index 5cd092a52..b70fb3831 100644 --- a/carapace-server/src/test/java/org/carapaceproxy/listeners/SSLSNITest.java +++ b/carapace-server/src/test/java/org/carapaceproxy/listeners/SSLSNITest.java @@ -31,6 +31,8 @@ import static org.junit.Assert.assertTrue; import static reactor.netty.http.HttpProtocol.HTTP11; import com.github.tomakehurst.wiremock.junit.WireMockRule; +import io.netty.channel.group.DefaultChannelGroup; +import io.netty.util.concurrent.DefaultEventExecutor; import java.net.InetAddress; import java.security.cert.X509Certificate; import java.util.Set; @@ -73,7 +75,7 @@ public void testSelectCertWithoutSNI() throws Exception { try (HttpProxyServer server = new HttpProxyServer(mapper, tmpDir.getRoot())) { server.addCertificate(new SSLCertificateConfiguration(nonLocalhost, null, certificate, "testproxy", STATIC)); - server.addListener(new NetworkListenerConfiguration(nonLocalhost, 0, true, null, nonLocalhost /* default */, DEFAULT_SSL_PROTOCOLS, 128, true, 300, 60, 8, 1000, DEFAULT_FORWARDED_STRATEGY, Set.of(), Set.of(HTTP11.name()))); + server.addListener(new NetworkListenerConfiguration(nonLocalhost, 0, true, null, nonLocalhost /* default */, DEFAULT_SSL_PROTOCOLS, 128, true, 300, 60, 8, 1000, DEFAULT_FORWARDED_STRATEGY, Set.of(), Set.of(HTTP11), new DefaultChannelGroup(new DefaultEventExecutor()))); server.start(); int port = server.getLocalPort(); @@ -164,7 +166,7 @@ public void testTLSVersion() throws Exception { try (HttpProxyServer server = new HttpProxyServer(mapper, tmpDir.getRoot())) { server.addCertificate(new SSLCertificateConfiguration(nonLocalhost, null, certificate, "testproxy", STATIC)); server.addListener(new NetworkListenerConfiguration(nonLocalhost, 0, true, null, nonLocalhost, Set.of("TLSv1.3"), - 128, true, 300, 60, 8, 1000, DEFAULT_FORWARDED_STRATEGY, Set.of(), Set.of(HTTP11.name()))); + 128, true, 300, 60, 8, 1000, DEFAULT_FORWARDED_STRATEGY, Set.of(), Set.of(HTTP11), new DefaultChannelGroup(new DefaultEventExecutor()))); server.start(); int port = server.getLocalPort(); try (RawHttpClient client = new RawHttpClient(nonLocalhost, port, true, nonLocalhost)) { @@ -180,7 +182,7 @@ public void testTLSVersion() throws Exception { try (HttpProxyServer server = new HttpProxyServer(mapper, tmpDir.getRoot())) { server.addCertificate(new SSLCertificateConfiguration(nonLocalhost, null, certificate, "testproxy", STATIC)); server.addListener(new NetworkListenerConfiguration(nonLocalhost, 0, true, null, nonLocalhost, Set.of(proto), - 128, true, 300, 60, 8, 1000, DEFAULT_FORWARDED_STRATEGY, Set.of(), Set.of(HTTP11.name()))); + 128, true, 300, 60, 8, 1000, DEFAULT_FORWARDED_STRATEGY, Set.of(), Set.of(HTTP11), new DefaultChannelGroup(new DefaultEventExecutor()))); server.start(); int port = server.getLocalPort(); try (RawHttpClient client = new RawHttpClient(nonLocalhost, port, true, nonLocalhost)) { @@ -195,7 +197,7 @@ public void testTLSVersion() throws Exception { server.addCertificate(new SSLCertificateConfiguration(nonLocalhost, null, certificate, "testproxy", STATIC)); server.addListener(new NetworkListenerConfiguration(nonLocalhost, 0, true, null, nonLocalhost, DEFAULT_SSL_PROTOCOLS, - 128, true, 300, 60, 8, 1000, DEFAULT_FORWARDED_STRATEGY, Set.of(), Set.of(HTTP11.name()))); + 128, true, 300, 60, 8, 1000, DEFAULT_FORWARDED_STRATEGY, Set.of(), Set.of(HTTP11), new DefaultChannelGroup(new DefaultEventExecutor()))); server.start(); int port = server.getLocalPort(); try (RawHttpClient client = new RawHttpClient(nonLocalhost, port, true, nonLocalhost)) { @@ -211,7 +213,7 @@ public void testTLSVersion() throws Exception { try (HttpProxyServer server = new HttpProxyServer(mapper, tmpDir.getRoot())) { server.addCertificate(new SSLCertificateConfiguration(nonLocalhost, null, certificate, "testproxy", STATIC)); server.addListener(new NetworkListenerConfiguration(nonLocalhost, 0, true, null, nonLocalhost, Set.of("TLSvWRONG"), - 128, true, 300, 60, 8, 1000, DEFAULT_FORWARDED_STRATEGY, Set.of(), Set.of(HTTP11.name()))); + 128, true, 300, 60, 8, 1000, DEFAULT_FORWARDED_STRATEGY, Set.of(), Set.of(HTTP11), new DefaultChannelGroup(new DefaultEventExecutor()))); } }); } diff --git a/carapace-server/src/test/java/org/carapaceproxy/server/cache/CacheTest.java b/carapace-server/src/test/java/org/carapaceproxy/server/cache/CacheTest.java index 1164a5d3b..71a5896a5 100644 --- a/carapace-server/src/test/java/org/carapaceproxy/server/cache/CacheTest.java +++ b/carapace-server/src/test/java/org/carapaceproxy/server/cache/CacheTest.java @@ -36,7 +36,9 @@ import static org.junit.Assert.assertTrue; import static reactor.netty.http.HttpProtocol.HTTP11; import com.github.tomakehurst.wiremock.junit.WireMockRule; +import io.netty.channel.group.DefaultChannelGroup; import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.util.concurrent.DefaultEventExecutor; import java.util.List; import java.util.Map; import java.util.Set; @@ -206,7 +208,7 @@ public void testBootSslRelativeCertificatePath() throws Exception { server.addCertificate(new SSLCertificateConfiguration("localhost", null, "localhost.p12", "testproxy", STATIC)); server.addListener(new NetworkListenerConfiguration("localhost", 0, true, null, "localhost", DEFAULT_SSL_PROTOCOLS, - 128, true, 300, 60, 8, 1000, DEFAULT_FORWARDED_STRATEGY, Set.of(), Set.of(HTTP11.name()))); + 128, true, 300, 60, 8, 1000, DEFAULT_FORWARDED_STRATEGY, Set.of(), Set.of(HTTP11), new DefaultChannelGroup(new DefaultEventExecutor()))); server.start(); } } @@ -229,7 +231,7 @@ public void testServeFromCacheSsl(boolean cacheDisabledForSecureRequestsWithoutP try (HttpProxyServer server = new HttpProxyServer(mapper, tmpDir.getRoot());) { server.addCertificate(new SSLCertificateConfiguration("localhost", null, "localhost.p12", "testproxy", STATIC)); server.addListener(new NetworkListenerConfiguration("localhost", 0, true, null, "localhost", - DEFAULT_SSL_PROTOCOLS, 128, true, 300, 60, 8, 1000, DEFAULT_FORWARDED_STRATEGY, Set.of(), Set.of(HTTP11.name()))); + DEFAULT_SSL_PROTOCOLS, 128, true, 300, 60, 8, 1000, DEFAULT_FORWARDED_STRATEGY, Set.of(), Set.of(HTTP11), new DefaultChannelGroup(new DefaultEventExecutor()))); RuntimeServerConfiguration currentConfiguration = server.getCurrentConfiguration(); currentConfiguration.setCacheDisabledForSecureRequestsWithoutPublic(cacheDisabledForSecureRequestsWithoutPublic); @@ -360,8 +362,8 @@ public void testServeFromCacheWithRequestProtocol() throws Exception { try (HttpProxyServer server = new HttpProxyServer(mapper, tmpDir.getRoot());) { server.addCertificate(new SSLCertificateConfiguration("localhost", null, "localhost.p12", "testproxy", STATIC)); server.addListener(new NetworkListenerConfiguration("localhost", httpsPort, true, null, "localhost", - DEFAULT_SSL_PROTOCOLS, 128, true, 300, 60, 8, 1000, DEFAULT_FORWARDED_STRATEGY, Set.of(), Set.of(HTTP11.name()))); - server.addListener(new NetworkListenerConfiguration("localhost", httpPort)); + DEFAULT_SSL_PROTOCOLS, 128, true, 300, 60, 8, 1000, DEFAULT_FORWARDED_STRATEGY, Set.of(), Set.of(HTTP11), new DefaultChannelGroup(new DefaultEventExecutor()))); + server.addListener(NetworkListenerConfiguration.withDefault("localhost", httpPort)); server.start(); server.getCache().getStats().resetCacheMetrics(); diff --git a/carapace-server/src/test/java/org/carapaceproxy/server/filters/XTlsCipherFilterTest.java b/carapace-server/src/test/java/org/carapaceproxy/server/filters/XTlsCipherFilterTest.java index e8bcd2281..e8c1578c3 100644 --- a/carapace-server/src/test/java/org/carapaceproxy/server/filters/XTlsCipherFilterTest.java +++ b/carapace-server/src/test/java/org/carapaceproxy/server/filters/XTlsCipherFilterTest.java @@ -12,6 +12,8 @@ import static org.junit.Assert.assertTrue; import static reactor.netty.http.HttpProtocol.HTTP11; import com.github.tomakehurst.wiremock.junit.WireMockRule; +import io.netty.channel.group.DefaultChannelGroup; +import io.netty.util.concurrent.DefaultEventExecutor; import java.util.Collections; import java.util.Set; import org.carapaceproxy.core.EndpointKey; @@ -59,7 +61,7 @@ public void TestXTlsProtocol() throws Exception { server.addRequestFilter(new RequestFilterConfiguration(XTlsCipherRequestFilter.TYPE, Collections.emptyMap())); server.addRequestFilter(new RequestFilterConfiguration(XTlsProtocolRequestFilter.TYPE, Collections.emptyMap())); server.addListener(new NetworkListenerConfiguration("0.0.0.0", 0, true, null, "*", Set.of("TLSv1.2"), - 128, true, 300, 60, 8, 1000, DEFAULT_FORWARDED_STRATEGY, Set.of(), Set.of(HTTP11.name()))); + 128, true, 300, 60, 8, 1000, DEFAULT_FORWARDED_STRATEGY, Set.of(), Set.of(HTTP11), new DefaultChannelGroup(new DefaultEventExecutor()))); server.start(); int port = server.getLocalPort(); @@ -74,7 +76,7 @@ public void TestXTlsProtocol() throws Exception { server.addCertificate(new SSLCertificateConfiguration("*", null, certificate, "testproxy", STATIC)); server.addRequestFilter(new RequestFilterConfiguration(XTlsProtocolRequestFilter.TYPE, Collections.emptyMap())); server.addListener(new NetworkListenerConfiguration("0.0.0.0", 0, true, null, "*", Set.of("TLSv1.2"), - 128, true, 300, 60, 8, 1000, DEFAULT_FORWARDED_STRATEGY, Set.of(), Set.of(HTTP11.name()))); + 128, true, 300, 60, 8, 1000, DEFAULT_FORWARDED_STRATEGY, Set.of(), Set.of(HTTP11), new DefaultChannelGroup(new DefaultEventExecutor()))); server.start(); int port = server.getLocalPort(); diff --git a/carapace-server/src/test/java/org/carapaceproxy/server/filters/XTlsProtocolFilterTest.java b/carapace-server/src/test/java/org/carapaceproxy/server/filters/XTlsProtocolFilterTest.java index 82921f79e..7e083deec 100644 --- a/carapace-server/src/test/java/org/carapaceproxy/server/filters/XTlsProtocolFilterTest.java +++ b/carapace-server/src/test/java/org/carapaceproxy/server/filters/XTlsProtocolFilterTest.java @@ -11,6 +11,8 @@ import static org.junit.Assert.assertTrue; import static reactor.netty.http.HttpProtocol.HTTP11; import com.github.tomakehurst.wiremock.junit.WireMockRule; +import io.netty.channel.group.DefaultChannelGroup; +import io.netty.util.concurrent.DefaultEventExecutor; import java.util.Collections; import java.util.Set; import org.carapaceproxy.core.HttpProxyServer; @@ -56,7 +58,7 @@ public void TestXTlsProtocol() throws Exception { server.addCertificate(new SSLCertificateConfiguration("*", null, certificate, "testproxy", STATIC)); server.addRequestFilter(new RequestFilterConfiguration(XTlsProtocolRequestFilter.TYPE, Collections.emptyMap())); server.addListener(new NetworkListenerConfiguration("0.0.0.0", 0, true, null, "*", Set.of("TLSv1.2"), - 128, true, 300, 60, 8, 1000, DEFAULT_FORWARDED_STRATEGY, Set.of(), Set.of(HTTP11.name()))); + 128, true, 300, 60, 8, 1000, DEFAULT_FORWARDED_STRATEGY, Set.of(), Set.of(HTTP11), new DefaultChannelGroup(new DefaultEventExecutor()))); server.start(); int port = server.getLocalPort(); @@ -70,7 +72,7 @@ public void TestXTlsProtocol() throws Exception { try (HttpProxyServer server = HttpProxyServer.buildForTests("localhost", 0, mapper, tmpDir.newFolder())) { server.addCertificate(new SSLCertificateConfiguration("*", null, certificate, "testproxy", STATIC)); server.addListener(new NetworkListenerConfiguration("0.0.0.0", 0, true, null, "*", Set.of("TLSv1.2"), - 128, true, 300, 60, 8, 1000, DEFAULT_FORWARDED_STRATEGY, Set.of(), Set.of(HTTP11.name()))); + 128, true, 300, 60, 8, 1000, DEFAULT_FORWARDED_STRATEGY, Set.of(), Set.of(HTTP11), new DefaultChannelGroup(new DefaultEventExecutor()))); server.start(); int port = server.getLocalPort(); From 45bfb02b8bd965eaa8d6187ba882a6b81f8964bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccol=C3=B2=20Maltoni?= Date: Mon, 7 Oct 2024 15:29:31 +0200 Subject: [PATCH 5/8] refactor: delombok ListeningChannel --- .../carapaceproxy/api/ListenersResource.java | 2 +- .../org/carapaceproxy/core/Listeners.java | 6 ++-- .../carapaceproxy/core/ListeningChannel.java | 33 ++++++++++++------- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/carapace-server/src/main/java/org/carapaceproxy/api/ListenersResource.java b/carapace-server/src/main/java/org/carapaceproxy/api/ListenersResource.java index f8a0c39aa..8ebbf7c1b 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/api/ListenersResource.java +++ b/carapace-server/src/main/java/org/carapaceproxy/api/ListenersResource.java @@ -70,7 +70,7 @@ public Map getAllListeners() { channel.getConfig().sslCiphers(), channel.getConfig().sslProtocols(), channel.getConfig().defaultCertificate(), - (int) channel.getTotalRequests().get() + channel.getTotalRequests() ))); } diff --git a/carapace-server/src/main/java/org/carapaceproxy/core/Listeners.java b/carapace-server/src/main/java/org/carapaceproxy/core/Listeners.java index c6ffa8aab..a418d1226 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/core/Listeners.java +++ b/carapace-server/src/main/java/org/carapaceproxy/core/Listeners.java @@ -85,7 +85,7 @@ public RuntimeServerConfiguration getCurrentConfiguration() { public int getLocalPort() { for (final ListeningChannel listeningChannel : listeningChannels.values()) { - return listeningChannel.getLocalPort(); + return listeningChannel.getHostPort().port(); } return -1; } @@ -176,8 +176,8 @@ private void stopListener(EndpointKey hostPort) throws InterruptedException { } private void bootListener(final NetworkListenerConfiguration config) throws InterruptedException { - final EndpointKey hostPort = new EndpointKey(config.host(), config.port()).offsetPort(parent.getListenersOffsetPort()); - final ListeningChannel listeningChannel = new ListeningChannel(basePath, currentConfiguration, parent, sslContexts, hostPort, config); + final ListeningChannel listeningChannel = new ListeningChannel(basePath, currentConfiguration, parent, sslContexts, config); + final EndpointKey hostPort = listeningChannel.getHostPort(); LOG.info("Starting listener at {}:{} ssl:{}", hostPort.host(), hostPort.port(), config.ssl()); // Listener setup diff --git a/carapace-server/src/main/java/org/carapaceproxy/core/ListeningChannel.java b/carapace-server/src/main/java/org/carapaceproxy/core/ListeningChannel.java index c0811d0a7..5c2f76fe4 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/core/ListeningChannel.java +++ b/carapace-server/src/main/java/org/carapaceproxy/core/ListeningChannel.java @@ -14,7 +14,6 @@ import io.prometheus.client.Counter; import java.io.File; import java.io.IOException; -import java.net.InetSocketAddress; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.time.Duration; @@ -24,7 +23,6 @@ import java.util.Map; import java.util.concurrent.ConcurrentMap; import javax.net.ssl.KeyManagerFactory; -import lombok.Data; import org.carapaceproxy.server.config.ConfigurationNotValidException; import org.carapaceproxy.server.config.NetworkListenerConfiguration; import org.carapaceproxy.server.config.SSLCertificateConfiguration; @@ -35,7 +33,6 @@ import reactor.netty.DisposableServer; import reactor.netty.FutureMono; -@Data public class ListeningChannel implements io.netty.util.AsyncMapping { private static final Logger LOG = LoggerFactory.getLogger(ListeningChannel.class); @@ -44,7 +41,7 @@ public class ListeningChannel implements io.netty.util.AsyncMapping listenerSslContexts = new HashMap<>(); @@ -54,20 +51,16 @@ public class ListeningChannel implements io.netty.util.AsyncMapping sslContexts; private DisposableServer channel; - public ListeningChannel(final File basePath, final RuntimeServerConfiguration currentConfiguration, final HttpProxyServer parent, final ConcurrentMap sslContexts, EndpointKey hostPort, NetworkListenerConfiguration config) { - this.hostPort = hostPort; + public ListeningChannel(final File basePath, final RuntimeServerConfiguration currentConfiguration, final HttpProxyServer parent, final ConcurrentMap sslContexts, NetworkListenerConfiguration config) { + this.localPort = config.port() + parent.getListenersOffsetPort(); this.config = config; - this.totalRequests = TOTAL_REQUESTS_PER_LISTENER_COUNTER.labels(hostPort.host() + "_" + hostPort.port()); + this.totalRequests = TOTAL_REQUESTS_PER_LISTENER_COUNTER.labels(config.host() + "_" + this.localPort); this.basePath = basePath; this.currentConfiguration = currentConfiguration; this.parent = parent; this.sslContexts = sslContexts; } - public int getLocalPort() { - return ((InetSocketAddress) this.channel.address()).getPort(); - } - public void disposeChannel() { this.channel.disposeNow(Duration.ofSeconds(10)); FutureMono.from(this.config.group().close()).block(Duration.ofSeconds(10)); @@ -84,7 +77,7 @@ public void clear() { @Override public Future map(String sniHostname, Promise promise) { try { - final var key = config.host() + ":" + hostPort.port() + "+" + sniHostname; + final var key = config.host() + ":" + this.localPort + "+" + sniHostname; if (LOG.isDebugEnabled()) { LOG.debug("resolve SNI mapping {}, key: {}", sniHostname, key); } @@ -123,6 +116,22 @@ public Future map(String sniHostname, Promise promise) { } } + public NetworkListenerConfiguration getConfig() { + return this.config; + } + + public int getTotalRequests() { + return (int) this.totalRequests.get(); + } + + public void setChannel(DisposableServer channel) { + this.channel = channel; + } + + public EndpointKey getHostPort() { + return new EndpointKey(this.config.host(), this.localPort); + } + private SSLCertificateConfiguration chooseCertificate(final String sniHostname) { return CertificatesUtils.chooseCertificate(currentConfiguration, sniHostname, config.defaultCertificate()); } From 9053ee0d00f99287eb00bc9465be9a4df2903160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccol=C3=B2=20Maltoni?= Date: Mon, 7 Oct 2024 17:41:44 +0200 Subject: [PATCH 6/8] refactor: make it work (at least without OCSP) --- .../org/carapaceproxy/core/Listeners.java | 27 ++-- .../carapaceproxy/core/ListeningChannel.java | 117 +++++++++++------- .../utils/CertificatesUtils.java | 20 --- 3 files changed, 91 insertions(+), 73 deletions(-) diff --git a/carapace-server/src/main/java/org/carapaceproxy/core/Listeners.java b/carapace-server/src/main/java/org/carapaceproxy/core/Listeners.java index a418d1226..cd7f2c1ea 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/core/Listeners.java +++ b/carapace-server/src/main/java/org/carapaceproxy/core/Listeners.java @@ -85,7 +85,10 @@ public RuntimeServerConfiguration getCurrentConfiguration() { public int getLocalPort() { for (final ListeningChannel listeningChannel : listeningChannels.values()) { - return listeningChannel.getHostPort().port(); + final int port = listeningChannel.getChannelPort(); + if (port > 0) { + return port; + } } return -1; } @@ -184,12 +187,17 @@ private void bootListener(final NetworkListenerConfiguration config) throws Inte HttpServer httpServer = HttpServer.create() .host(hostPort.host()) .port(hostPort.port()) - .protocol(config.protocols().toArray(HttpProtocol[]::new)) - /* - // .secure() - todo: to enable H2, see config.isSsl() & snimappings - see https://projectreactor.io/docs/netty/release/reference/index.html#_server_name_indication_3 - */ + .protocol(config.protocols().toArray(HttpProtocol[]::new)); + if (config.ssl()) { + httpServer = httpServer.secure(sslContextSpec -> { + final var sslContextBuilder = sslContextSpec.sslContext(listeningChannel.defaultSslContext()); + for (final var certificate : this.currentConfiguration.getCertificates().values()) { + final var id = certificate.getId(); + listeningChannel.applySslContext(id, sslContextBuilder); + } + }); + } + httpServer = httpServer .metrics(true, Function.identity()) .forwarded(ForwardedStrategy.of(config.forwardedStrategy(), config.trustedIps())) .option(ChannelOption.SO_BACKLOG, config.soBacklog()) @@ -208,10 +216,11 @@ private void bootListener(final NetworkListenerConfiguration config) throws Inte .doOnChannelInit((observer, channel, remoteAddress) -> { final ChannelHandler idle = new IdleStateHandler(0, 0, currentConfiguration.getClientsIdleTimeoutSeconds()); channel.pipeline().addFirst("idleStateHandler", idle); - if (config.ssl()) { + // todo re-enable OCSP + /* if (config.ssl()) { final ChannelHandler sni = new ListenersSniHandler(currentConfiguration, parent, listeningChannel); channel.pipeline().addFirst(sni); - } + } */ }) .doOnConnection(conn -> { CURRENT_CONNECTED_CLIENTS_GAUGE.inc(); diff --git a/carapace-server/src/main/java/org/carapaceproxy/core/ListeningChannel.java b/carapace-server/src/main/java/org/carapaceproxy/core/ListeningChannel.java index 5c2f76fe4..a09f9562a 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/core/ListeningChannel.java +++ b/carapace-server/src/main/java/org/carapaceproxy/core/ListeningChannel.java @@ -14,6 +14,7 @@ import io.prometheus.client.Counter; import java.io.File; import java.io.IOException; +import java.net.InetSocketAddress; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.time.Duration; @@ -75,63 +76,43 @@ public void clear() { } @Override - public Future map(String sniHostname, Promise promise) { + public Future map(final String sniHostname, final Promise promise) { + try { + return promise.setSuccess(map(sniHostname)); + } catch (final Exception err) { + LOG.error(err.getMessage(), err); + return promise.setFailure(err); + } + } + + public SslContext map(final String sniHostname) throws ConfigurationNotValidException { try { final var key = config.host() + ":" + this.localPort + "+" + sniHostname; if (LOG.isDebugEnabled()) { LOG.debug("resolve SNI mapping {}, key: {}", sniHostname, key); } - try { - var sslContext = listenerSslContexts.get(key); - if (sslContext != null) { - return promise.setSuccess(sslContext); + if (listenerSslContexts.containsKey(key)) { + return listenerSslContexts.get(key); + } + if (!sslContexts.containsKey(key)) { + var chosen = chooseCertificate(sniHostname); + if (chosen == null) { + throw new ConfigurationNotValidException( + "Cannot find a certificate for snihostname " + sniHostname + + ", with default cert for listener as '" + config.defaultCertificate() + + "', available " + currentConfiguration.getCertificates().keySet()); } - - sslContext = sslContexts.computeIfAbsent(key, (k) -> { - try { - var chosen = chooseCertificate(sniHostname); - if (chosen == null) { - throw new ConfigurationNotValidException("cannot find a certificate for snihostname " + sniHostname - + ", with default cert for listener as '" + config.defaultCertificate() - + "', available " + currentConfiguration.getCertificates().keySet()); - } - return bootSslContext(config, chosen); - } catch (ConfigurationNotValidException ex) { - throw new RuntimeException(ex); - } - }); + final var sslContext = bootSslContext(config, chosen); + sslContexts.put(key, sslContext); listenerSslContexts.put(key, sslContext); - - return promise.setSuccess(sslContext); - } catch (RuntimeException err) { - if (err.getCause() instanceof ConfigurationNotValidException) { - throw (ConfigurationNotValidException) err.getCause(); - } else { - throw new ConfigurationNotValidException(err); - } } + return sslContexts.get(key); } catch (ConfigurationNotValidException err) { LOG.error("Error booting certificate for SNI hostname {}, on listener {}", sniHostname, config, err); - return promise.setFailure(err); + throw err; } } - public NetworkListenerConfiguration getConfig() { - return this.config; - } - - public int getTotalRequests() { - return (int) this.totalRequests.get(); - } - - public void setChannel(DisposableServer channel) { - this.channel = channel; - } - - public EndpointKey getHostPort() { - return new EndpointKey(this.config.host(), this.localPort); - } - private SSLCertificateConfiguration chooseCertificate(final String sniHostname) { return CertificatesUtils.chooseCertificate(currentConfiguration, sniHostname, config.defaultCertificate()); } @@ -186,4 +167,52 @@ private SslContext bootSslContext(NetworkListenerConfiguration listener, SSLCert throw new ConfigurationNotValidException(err); } } + + public NetworkListenerConfiguration getConfig() { + return this.config; + } + + public int getTotalRequests() { + return (int) this.totalRequests.get(); + } + + public void setChannel(DisposableServer channel) { + this.channel = channel; + } + + public EndpointKey getHostPort() { + return new EndpointKey(this.config.host(), this.localPort); + } + + public int getChannelPort() { + if (this.channel != null) { + if (this.channel.address() instanceof InetSocketAddress address) { + return address.getPort(); + } + LOG.warn("Unexpected channel address {}", this.channel.address()); + } + return -1; + } + + public SslContext defaultSslContext() { + try { + return map(config.defaultCertificate()); + } catch (final ConfigurationNotValidException e) { + throw new RuntimeException("Failed to load default SSL context", e); + } + } + + public void applySslContext(final String sniHostname, final reactor.netty.tcp.SslProvider.Builder sslContextBuilder) { + if (sniHostname.charAt(0) == '*' && (sniHostname.length() < 3 || sniHostname.charAt(1) != '.')) { + // skip, ReactorNetty won't accept it! + return; + } + try { + // #map should cache the certificate after the first search of the different SANs of the same certificate + final SslContext sslContext = map(sniHostname); + sslContextBuilder.addSniMapping(sniHostname, spec -> spec.sslContext(sslContext)); + } catch (final ConfigurationNotValidException e) { + throw new RuntimeException(e); + } + } } diff --git a/carapace-server/src/main/java/org/carapaceproxy/utils/CertificatesUtils.java b/carapace-server/src/main/java/org/carapaceproxy/utils/CertificatesUtils.java index 5929e07ac..22698b00b 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/utils/CertificatesUtils.java +++ b/carapace-server/src/main/java/org/carapaceproxy/utils/CertificatesUtils.java @@ -19,7 +19,6 @@ */ package org.carapaceproxy.utils; -import io.netty.handler.ssl.SslContext; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; @@ -43,7 +42,6 @@ import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; -import org.carapaceproxy.core.EndpointKey; import org.carapaceproxy.core.RuntimeServerConfiguration; import org.carapaceproxy.server.config.SSLCertificateConfiguration; @@ -251,18 +249,6 @@ public static String addWildcard(String name) { return WILDCARD_PREFIX + Objects.requireNonNull(name); } - /** - * {@link SslContext}s are cached at listener level. - * This method computes the key from hostname, port, and SNI. - * - * @param hostPort the host and port tuple - * @param sniHostname the Server Name Indication (SNI) indication - * @return the cache key - */ - public static String computeKey(final EndpointKey hostPort, final String sniHostname) { - return hostPort.host() + ":" + hostPort.port() + "+" + sniHostname; - } - public static boolean certificateMatches( final String hostname, final SSLCertificateConfiguration certificate, final boolean exact) { if (certificate.getSubjectAltNames() == null || certificate.getSubjectAltNames().isEmpty()) { @@ -308,12 +294,6 @@ public static SSLCertificateConfiguration chooseCertificate(final RuntimeServerC if (chosen == null) { chosen = certificates.get(defaultCertificate); } - /* LOG.info("Resolving SNI for hostname: {}", sniHostname); - if (chosen == null) { - LOG.error("No certificate found for SNI hostname: {}", sniHostname); - } else { - LOG.info("Using certificate: {}", chosen.getId()); - } */ return chosen; } } From 3c76956752a913bf9180b4eec1d51ba5f9d01c78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccol=C3=B2=20Maltoni?= Date: Wed, 9 Oct 2024 18:17:23 +0200 Subject: [PATCH 7/8] fix: restore OCSP support --- .../org/carapaceproxy/core/Listeners.java | 11 ++-- .../core/ListenersSniHandler.java | 53 ------------------- .../carapaceproxy/core/ListeningChannel.java | 8 +++ .../carapaceproxy/core/OcspSslHandler.java | 40 ++++++++++++++ .../carapaceproxy/listeners/SSLSNITest.java | 51 +++++++++++------- 5 files changed, 84 insertions(+), 79 deletions(-) delete mode 100644 carapace-server/src/main/java/org/carapaceproxy/core/ListenersSniHandler.java create mode 100644 carapace-server/src/main/java/org/carapaceproxy/core/OcspSslHandler.java diff --git a/carapace-server/src/main/java/org/carapaceproxy/core/Listeners.java b/carapace-server/src/main/java/org/carapaceproxy/core/Listeners.java index cd7f2c1ea..11a9a5d8b 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/core/Listeners.java +++ b/carapace-server/src/main/java/org/carapaceproxy/core/Listeners.java @@ -190,7 +190,11 @@ private void bootListener(final NetworkListenerConfiguration config) throws Inte .protocol(config.protocols().toArray(HttpProtocol[]::new)); if (config.ssl()) { httpServer = httpServer.secure(sslContextSpec -> { - final var sslContextBuilder = sslContextSpec.sslContext(listeningChannel.defaultSslContext()); + final var sslCtx = listeningChannel.defaultSslContext(); + final var sslContextBuilder = sslContextSpec.sslContext(sslCtx); + if (listeningChannel.isOcspEnabled()) { + sslContextBuilder.handlerConfigurator(new OcspSslHandler(sslCtx, parent.getOcspStaplingManager())); + } for (final var certificate : this.currentConfiguration.getCertificates().values()) { final var id = certificate.getId(); listeningChannel.applySslContext(id, sslContextBuilder); @@ -216,11 +220,6 @@ private void bootListener(final NetworkListenerConfiguration config) throws Inte .doOnChannelInit((observer, channel, remoteAddress) -> { final ChannelHandler idle = new IdleStateHandler(0, 0, currentConfiguration.getClientsIdleTimeoutSeconds()); channel.pipeline().addFirst("idleStateHandler", idle); - // todo re-enable OCSP - /* if (config.ssl()) { - final ChannelHandler sni = new ListenersSniHandler(currentConfiguration, parent, listeningChannel); - channel.pipeline().addFirst(sni); - } */ }) .doOnConnection(conn -> { CURRENT_CONNECTED_CLIENTS_GAUGE.inc(); diff --git a/carapace-server/src/main/java/org/carapaceproxy/core/ListenersSniHandler.java b/carapace-server/src/main/java/org/carapaceproxy/core/ListenersSniHandler.java deleted file mode 100644 index 6ff6a49f7..000000000 --- a/carapace-server/src/main/java/org/carapaceproxy/core/ListenersSniHandler.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.carapaceproxy.core; - -import io.netty.buffer.ByteBufAllocator; -import io.netty.handler.ssl.OpenSsl; -import io.netty.handler.ssl.ReferenceCountedOpenSslEngine; -import io.netty.handler.ssl.SniHandler; -import io.netty.handler.ssl.SslContext; -import io.netty.handler.ssl.SslHandler; -import io.netty.util.AttributeKey; -import java.io.IOException; -import java.security.cert.Certificate; -import org.carapaceproxy.server.certificates.ocsp.OcspStaplingManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -class ListenersSniHandler extends SniHandler { - private static final Logger LOG = LoggerFactory.getLogger(ListenersSniHandler.class); - - private final RuntimeServerConfiguration currentConfiguration; - private final HttpProxyServer parent; - - public ListenersSniHandler(final RuntimeServerConfiguration currentConfiguration, final HttpProxyServer parent, final ListeningChannel listeningChannel) { - super(listeningChannel); - this.currentConfiguration = currentConfiguration; - this.parent = parent; - } - - @Override - protected SslHandler newSslHandler(final SslContext context, final ByteBufAllocator allocator) { - final SslHandler handler = super.newSslHandler(context, allocator); - if (currentConfiguration.isOcspEnabled() && OpenSsl.isOcspSupported()) { - final Certificate cert = (Certificate) context.attributes() - .attr(AttributeKey.valueOf(Listeners.OCSP_CERTIFICATE_CHAIN)) - .get(); - if (cert == null) { - LOG.error("Cannot set OCSP response without the certificate"); - return handler; - } - if (!(handler.engine() instanceof ReferenceCountedOpenSslEngine engine)) { - LOG.error("Unexpected OpenSSL Engine used; cannot set OCSP response."); - return handler; - } - try { - final OcspStaplingManager ocspStaplingManager = parent.getOcspStaplingManager(); - final byte[] response = ocspStaplingManager.getOcspResponseForCertificate(cert); - engine.setOcspResponse(response); - } catch (final IOException ex) { - LOG.error("Error setting OCSP response.", ex); - } - } - return handler; - } -} diff --git a/carapace-server/src/main/java/org/carapaceproxy/core/ListeningChannel.java b/carapace-server/src/main/java/org/carapaceproxy/core/ListeningChannel.java index a09f9562a..2bd7308a5 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/core/ListeningChannel.java +++ b/carapace-server/src/main/java/org/carapaceproxy/core/ListeningChannel.java @@ -210,9 +210,17 @@ public void applySslContext(final String sniHostname, final reactor.netty.tcp.Ss try { // #map should cache the certificate after the first search of the different SANs of the same certificate final SslContext sslContext = map(sniHostname); + if (isOcspEnabled()) { + sslContextBuilder.handlerConfigurator(new OcspSslHandler(sslContext, parent.getOcspStaplingManager())); + } sslContextBuilder.addSniMapping(sniHostname, spec -> spec.sslContext(sslContext)); } catch (final ConfigurationNotValidException e) { throw new RuntimeException(e); } } + + public boolean isOcspEnabled() { + return currentConfiguration.isOcspEnabled() && OpenSsl.isOcspSupported(); + } + } diff --git a/carapace-server/src/main/java/org/carapaceproxy/core/OcspSslHandler.java b/carapace-server/src/main/java/org/carapaceproxy/core/OcspSslHandler.java new file mode 100644 index 000000000..97c4358c4 --- /dev/null +++ b/carapace-server/src/main/java/org/carapaceproxy/core/OcspSslHandler.java @@ -0,0 +1,40 @@ +package org.carapaceproxy.core; + +import io.netty.handler.ssl.ReferenceCountedOpenSslEngine; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslHandler; +import io.netty.util.AttributeKey; +import java.io.IOException; +import java.security.cert.Certificate; +import java.util.function.Consumer; +import org.carapaceproxy.server.certificates.ocsp.OcspStaplingManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class OcspSslHandler implements Consumer { + private static final Logger LOG = LoggerFactory.getLogger(OcspSslHandler.class); + private static final AttributeKey ATTRIBUTE = AttributeKey.valueOf(Listeners.OCSP_CERTIFICATE_CHAIN); + + private final SslContext sslContext; + private final OcspStaplingManager ocspStaplingManager; + + public OcspSslHandler(final SslContext sslContext, final OcspStaplingManager ocspStaplingManager1) { + this.sslContext = sslContext; + this.ocspStaplingManager = ocspStaplingManager1; + } + + @Override + public void accept(final SslHandler sslHandler) { + final var cert = sslContext.attributes().attr(ATTRIBUTE).get(); + if (cert == null) { + LOG.error("Cannot set OCSP response without the certificate"); + return; + } + try { + final var engine = (ReferenceCountedOpenSslEngine) sslHandler.engine(); + engine.setOcspResponse(ocspStaplingManager.getOcspResponseForCertificate(cert)); + } catch (IOException ex) { + LOG.error("Error setting OCSP response.", ex); + } + } +} diff --git a/carapace-server/src/test/java/org/carapaceproxy/listeners/SSLSNITest.java b/carapace-server/src/test/java/org/carapaceproxy/listeners/SSLSNITest.java index b70fb3831..731858f3c 100644 --- a/carapace-server/src/test/java/org/carapaceproxy/listeners/SSLSNITest.java +++ b/carapace-server/src/test/java/org/carapaceproxy/listeners/SSLSNITest.java @@ -27,6 +27,7 @@ import static org.carapaceproxy.server.config.NetworkListenerConfiguration.DEFAULT_SSL_PROTOCOLS; import static org.carapaceproxy.server.config.SSLCertificateConfiguration.CertificateMode.STATIC; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static reactor.netty.http.HttpProtocol.HTTP11; @@ -102,38 +103,38 @@ public void testChooseCertificate() throws Exception { // client requests bad SNI, bad default in listener - assertNull(CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "no", "no-default")); + assertNull(chooseCert(server, "no", "no-default")); - assertEquals("*.qatest.pexample.it", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "test2.qatest.pexample.it", "no-default").getId()); + assertEquals("*.qatest.pexample.it", chooseCertId(server, "test2.qatest.pexample.it", "no-default")); // client requests SNI, bad default in listener - assertEquals("other", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "other", "no-default").getId()); + assertEquals("other", chooseCertId(server, "other", "no-default")); - assertEquals("www.example.com", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "unkn-other", "www.example.com").getId()); + assertEquals("www.example.com", chooseCertId(server, "unkn-other", "www.example.com")); // client without SNI - assertEquals("www.example.com", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), null, "www.example.com").getId()); + assertEquals("www.example.com", chooseCertId(server, null, "www.example.com")); // exact match - assertEquals("www.example.com", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "www.example.com", "no-default").getId()); + assertEquals("www.example.com", chooseCertId(server, "www.example.com", "no-default")); // wildcard - assertEquals("*.example.com", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "test.example.com", "no-default").getId()); + assertEquals("*.example.com", chooseCertId(server, "test.example.com", "no-default")); // san - assertEquals("*.example.com", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "example.com", "no-default").getId()); - assertEquals("*.example.com", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "test.example2.com", "no-default").getId()); + assertEquals("*.example.com", chooseCertId(server, "example.com", "no-default")); + assertEquals("*.example.com", chooseCertId(server, "test.example2.com", "no-default")); // full wildcard server.addCertificate(new SSLCertificateConfiguration("*", null, "cert", "pwd", STATIC)); // full wildcard has not to hide more specific wildcard one - assertEquals("*.example.com", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "test.example.com", "no-default").getId()); + assertEquals("*.example.com", chooseCertId(server, "test.example.com", "no-default")); // san - assertEquals("*.example.com", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "example.com", "no-default").getId()); - assertEquals("*.example.com", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "test.example2.com", "no-default").getId()); + assertEquals("*.example.com", chooseCertId(server, "example.com", "no-default")); + assertEquals("*.example.com", chooseCertId(server, "test.example2.com", "no-default")); // more specific wildcard server.addCertificate(new SSLCertificateConfiguration("*.test.example.com", null, "cert", "pwd", STATIC)); // more specific wildcard has to hide less specific one (*.example.com) - assertEquals("*.test.example.com", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "pippo.test.example.com", "no-default").getId()); + assertEquals("*.test.example.com", chooseCertId(server, "pippo.test.example.com", "no-default")); // san - assertEquals("*.example.com", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "example.com", "no-default").getId()); - assertEquals("*.example.com", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "test.example2.com", "no-default").getId()); + assertEquals("*.example.com", chooseCertId(server, "example.com", "no-default")); + assertEquals("*.example.com", chooseCertId(server, "test.example2.com", "no-default")); } try (HttpProxyServer server = new HttpProxyServer(mapper, tmpDir.getRoot())) { @@ -141,14 +142,24 @@ public void testChooseCertificate() throws Exception { // full wildcard server.addCertificate(new SSLCertificateConfiguration("*", null, "cert", "pwd", STATIC)); - assertEquals("*", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), null, "www.example.com").getId()); - assertEquals("*", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "www.example.com", null).getId()); - assertEquals("*", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), null, null).getId()); - assertEquals("*", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), "", null).getId()); - assertEquals("*", CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), null, "").getId()); + assertEquals("*", chooseCertId(server, null, "www.example.com")); + assertEquals("*", chooseCertId(server, "www.example.com", null)); + assertEquals("*", chooseCertId(server, null, null)); + assertEquals("*", chooseCertId(server, "", null)); + assertEquals("*", chooseCertId(server, null, "")); } } + private static SSLCertificateConfiguration chooseCert(final HttpProxyServer server, final String sniHostname, final String defaultCertificate) { + return CertificatesUtils.chooseCertificate(server.getListeners().getCurrentConfiguration(), sniHostname, defaultCertificate); + } + + private static String chooseCertId(final HttpProxyServer server, final String sniHostname, final String defaultCertificate) { + final var certificate = chooseCert(server, sniHostname, defaultCertificate); + assertNotNull(certificate); + return certificate.getId(); + } + @Test public void testTLSVersion() throws Exception { String nonLocalhost = InetAddress.getLocalHost().getCanonicalHostName(); From 2f96899e6b39447a74fc4be718ed024395b34836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccol=C3=B2=20Maltoni?= Date: Tue, 10 Dec 2024 17:24:07 +0100 Subject: [PATCH 8/8] test: fix SSLSNITest --- .../carapaceproxy/utils/CertificatesUtils.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/carapace-server/src/main/java/org/carapaceproxy/utils/CertificatesUtils.java b/carapace-server/src/main/java/org/carapaceproxy/utils/CertificatesUtils.java index 22698b00b..fc2bc68a0 100644 --- a/carapace-server/src/main/java/org/carapaceproxy/utils/CertificatesUtils.java +++ b/carapace-server/src/main/java/org/carapaceproxy/utils/CertificatesUtils.java @@ -254,16 +254,16 @@ public static boolean certificateMatches( if (certificate.getSubjectAltNames() == null || certificate.getSubjectAltNames().isEmpty()) { if (exact) { return !certificate.isWildcard() && hostname.equals(certificate.getHostname()); - } else { - return certificate.isWildcard() && hostname.endsWith(certificate.getHostname()); } + return certificate.isWildcard() && hostname.endsWith(certificate.getHostname()); } - for (final var name : certificate.getNames()) { - final var wildcard = isWildcard(name); - if (exact) { - return !wildcard && hostname.equals(name); - } else { - return wildcard && hostname.endsWith(removeWildcard(name)); + for (final String name : certificate.getNames()) { + final boolean wildcard = isWildcard(name); + if (exact && !wildcard && hostname.equals(name)) { + return true; + } + if (!exact && wildcard && hostname.endsWith(CertificatesUtils.removeWildcard(name))) { + return true; } } return false;