This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/openwebbeans-meecrowave.git
commit 5a7973c51b72c9df9c44721cc7c58e5763004ba0 Author: Mark Struberg <[email protected]> AuthorDate: Thu May 7 21:51:44 2026 +0200 MEECROWAVE-347 fix sslConfig It seems that the sslhostconfig does not work anymore with more recent Tomcat Versions. I did not investigate when it broke, but somewhen between tc9 and 11 the certificate related attributes got moved from {SSLHostConfig} to an own class {SSLHostConfigCertificate} and is now 1:n on the hostConfig. --- .../tests/ssl/TlsVirtualHostPropertiesTest.java | 103 ++++++------ .../java/org/apache/meecrowave/Meecrowave.java | 44 +----- .../configuration/SslHostConfiguration.java | 172 +++++++++++++++++++++ pom.xml | 10 +- 4 files changed, 236 insertions(+), 93 deletions(-) diff --git a/integration-tests/ssl/src/test/java/org/apache/meecrowave/tests/ssl/TlsVirtualHostPropertiesTest.java b/integration-tests/ssl/src/test/java/org/apache/meecrowave/tests/ssl/TlsVirtualHostPropertiesTest.java index 3f5946b..d73f571 100644 --- a/integration-tests/ssl/src/test/java/org/apache/meecrowave/tests/ssl/TlsVirtualHostPropertiesTest.java +++ b/integration-tests/ssl/src/test/java/org/apache/meecrowave/tests/ssl/TlsVirtualHostPropertiesTest.java @@ -30,6 +30,7 @@ import java.util.Properties; import org.apache.meecrowave.Meecrowave; import org.apache.meecrowave.Meecrowave.Builder; import org.apache.tomcat.util.net.SSLHostConfig; +import org.apache.tomcat.util.net.SSLHostConfigCertificate; import org.junit.Test; /* @@ -37,8 +38,8 @@ import org.junit.Test; * <Connector port="8443" protocol="HTTP/1.1" maxThreads="10" SSLEnabled="true" scheme="https" secure="true" sslDefaultHost="*meecrowave-localhost"> <SSLHostConfig honorCipherOrder="false" hostName="localhost"> - <Certificate certificateKeystoreFile="meecrowave.jks" - certificateKeystorePassword="meecrowave" + <Certificate certificateKeystoreFile="meecrowave.jks" + certificateKeystorePassword="meecrowave" certificateKeyAlias="meecrowave" truststoreFile = "meecrowave.jks" truststorePassword = "meecrowave" /> @@ -59,15 +60,15 @@ import org.junit.Test; public class TlsVirtualHostPropertiesTest { private static final String keyStorePath1 = "meecrowave_first_host.jks"; private static final String keyStorePath2 = "meecrowave_second_host.jks"; - + static { - System.setProperty("javax.net.ssl.trustStore", Paths.get("").toAbsolutePath() + "/target/classes/meecrowave_trust.jks"); + System.setProperty("javax.net.ssl.trustStore", Paths.get("").toAbsolutePath() + "/target/classes/meecrowave_trust.jks"); System.setProperty("javax.net.ssl.trustStorePassword", "meecrowave"); } - + public static final Properties p = new Properties() {{ setProperty("connector.attributes.maxThreads", "10"); - + setProperty("connector.sslhostconfig.certificateKeystoreFile", keyStorePath1); setProperty("connector.sslhostconfig.certificateKeystoreType", "JKS"); setProperty("connector.sslhostconfig.certificateKeystorePassword", "meecrowave"); @@ -75,62 +76,70 @@ public class TlsVirtualHostPropertiesTest { setProperty("connector.sslhostconfig.hostName", "localhost"); setProperty("connector.sslhostconfig.truststoreFile", "meecrowave_trust.jks"); setProperty("connector.sslhostconfig.truststorePassword", "meecrowave"); - + setProperty("connector.sslhostconfig.1.certificateKeystoreFile", keyStorePath2); setProperty("connector.sslhostconfig.1.certificateKeystoreType", "JKS"); setProperty("connector.sslhostconfig.1.certificateKeystorePassword", "meecrowave"); setProperty("connector.sslhostconfig.1.certificateKeyAlias", "meecrowave"); - setProperty("connector.sslhostconfig.1.protocols", "TLSv1.1,TLSv1.2"); + setProperty("connector.sslhostconfig.1.protocols", "TLSv1.2,TLSv1.3"); setProperty("connector.sslhostconfig.1.hostName", "meecrowave-localhost"); - + setProperty("connector.sslhostconfig.2.hostName", "meecrowave.apache.org"); setProperty("connector.sslhostconfig.2.certificateKeyFile", "meecrowave.key.pem"); setProperty("connector.sslhostconfig.2.certificateFile", "meecrowave.cert.pem"); setProperty("connector.sslhostconfig.2.certificateChainFile", "ca-chain.cert.pem"); - setProperty("connector.sslhostconfig.2.protocols", "TLSv1.2"); - + setProperty("connector.sslhostconfig.2.protocols", "TLSv1.3"); + }}; - + @Test public void run() throws IOException { try (final Meecrowave CONTAINER = new Meecrowave(new Builder() {{ - randomHttpsPort(); - setSkipHttp(true); - includePackages("org.apache.meecrowave.tests.ssl"); - setSsl(true); - setDefaultSSLHostConfigName("localhost"); - setTomcatNoJmx(false); - setProperties(p); - }}).bake()) { - final String confPath = CONTAINER.getBase().getCanonicalPath() + "/conf/"; - SSLHostConfig[] sslHostConfigs = CONTAINER.getTomcat().getService().findConnectors()[0].findSslHostConfigs(); - assertEquals(3, sslHostConfigs.length); - assertTrue(isFilesSame(confPath + keyStorePath1, sslHostConfigs[0].getCertificateKeystoreFile())); - assertEquals("JKS", sslHostConfigs[0].getCertificateKeystoreType()); - assertEquals("meecrowave", sslHostConfigs[0].getCertificateKeystorePassword()); - assertEquals("meecrowave", sslHostConfigs[0].getCertificateKeyAlias()); - assertEquals("localhost", sslHostConfigs[0].getHostName()); - assertTrue(isFilesSame(confPath + "meecrowave_trust.jks", sslHostConfigs[0].getTruststoreFile())); - assertEquals("meecrowave", sslHostConfigs[0].getTruststorePassword()); - - assertTrue(isFilesSame(confPath + keyStorePath2, sslHostConfigs[1].getCertificateKeystoreFile())); - assertEquals("JKS", sslHostConfigs[1].getCertificateKeystoreType()); - assertEquals("meecrowave", sslHostConfigs[1].getCertificateKeystorePassword()); - assertEquals("meecrowave", sslHostConfigs[1].getCertificateKeyAlias()); - assertEquals("meecrowave-localhost", sslHostConfigs[1].getHostName()); - assertEquals(2, sslHostConfigs[1].getProtocols().size()); - - assertEquals("meecrowave.apache.org", sslHostConfigs[2].getHostName()); - assertTrue(isFilesSame(confPath + "meecrowave.key.pem", sslHostConfigs[2].getCertificateKeyFile())); - assertTrue(isFilesSame(confPath + "meecrowave.cert.pem", sslHostConfigs[2].getCertificateFile())); - assertTrue(isFilesSame(confPath + "ca-chain.cert.pem", sslHostConfigs[2].getCertificateChainFile())); - assertEquals("TLSv1.2", sslHostConfigs[2].getProtocols().toArray()[0]); - - assertEquals("Hello", TestSetup.callJaxrsService(CONTAINER.getConfiguration().getHttpsPort())); + randomHttpsPort(); + setSkipHttp(true); + includePackages("org.apache.meecrowave.tests.ssl"); + setSsl(true); + setDefaultSSLHostConfigName("localhost"); + setTomcatNoJmx(false); + setProperties(p); + }}).bake()) { + + final String confPath = CONTAINER.getBase().getCanonicalPath() + "/conf/"; + SSLHostConfig[] sslHostConfigs = CONTAINER.getTomcat().getService().findConnectors()[0].findSslHostConfigs(); + + assertEquals(3, sslHostConfigs.length); + + // In Tomcat 11 sind Zertifikatsinformationen strikter in SSLHostConfigCertificate gekapselt + // Wir greifen auf das jeweils erste Zertifikat des Hosts zu + + // Validierung Host 0 + SSLHostConfigCertificate cert0 = sslHostConfigs[0].getCertificates().iterator().next(); + assertEquals("localhost", sslHostConfigs[0].getHostName()); + assertTrue(isFilesSame(confPath + keyStorePath1, cert0.getCertificateKeystoreFile())); + assertEquals("JKS", cert0.getCertificateKeystoreType()); + assertEquals("meecrowave", cert0.getCertificateKeystorePassword()); + assertTrue(isFilesSame(confPath + "meecrowave_trust.jks", sslHostConfigs[0].getTruststoreFile())); + + // Validierung Host 1 + SSLHostConfigCertificate cert1 = sslHostConfigs[1].getCertificates().iterator().next(); + assertEquals("meecrowave-localhost", sslHostConfigs[1].getHostName()); + assertTrue(isFilesSame(confPath + keyStorePath2, cert1.getCertificateKeystoreFile())); + // Tomcat 11 entfernt veraltete Protokolle aus dem Set, daher prüfen wir auf die neuen Standards + assertTrue(sslHostConfigs[1].getProtocols().contains("TLSv1.3")); + + // Validierung Host 2 + SSLHostConfigCertificate cert2 = sslHostConfigs[2].getCertificates().iterator().next(); + assertEquals("meecrowave.apache.org", sslHostConfigs[2].getHostName()); + assertTrue(isFilesSame(confPath + "meecrowave.key.pem", cert2.getCertificateKeyFile())); + assertTrue(isFilesSame(confPath + "meecrowave.cert.pem", cert2.getCertificateFile())); + assertTrue(sslHostConfigs[2].getProtocols().contains("TLSv1.3")); + + assertEquals("Hello", TestSetup.callJaxrsService(CONTAINER.getConfiguration().getHttpsPort())); } } - + boolean isFilesSame(final String input, final String output) throws IOException { + if (input == null || output == null) return false; return Files.isSameFile(Paths.get(input), Paths.get(output)); } -} +} \ No newline at end of file diff --git a/meecrowave-core/src/main/java/org/apache/meecrowave/Meecrowave.java b/meecrowave-core/src/main/java/org/apache/meecrowave/Meecrowave.java index e70a737..6bdcae6 100644 --- a/meecrowave-core/src/main/java/org/apache/meecrowave/Meecrowave.java +++ b/meecrowave-core/src/main/java/org/apache/meecrowave/Meecrowave.java @@ -24,7 +24,6 @@ import static java.util.Comparator.comparing; import static java.util.Locale.ROOT; import static java.util.Optional.ofNullable; import static java.util.stream.Collectors.toList; -import static java.util.stream.Collectors.toSet; import java.io.File; import java.io.FileInputStream; @@ -98,6 +97,7 @@ import org.apache.coyote.http2.Http2Protocol; import org.apache.meecrowave.api.StartListening; import org.apache.meecrowave.api.StopListening; import org.apache.meecrowave.configuration.Configuration; +import org.apache.meecrowave.configuration.SslHostConfiguration; import org.apache.meecrowave.cxf.ConfigurableBus; import org.apache.meecrowave.cxf.CxfCdiAutoSetup; import org.apache.meecrowave.cxf.Cxfs; @@ -655,7 +655,7 @@ public class Meecrowave implements AutoCloseable { if (configuration.isHttp2()) { httpsConnector.addUpgradeProtocol(new Http2Protocol()); } - final List<SSLHostConfig> buildSslHostConfig = buildSslHostConfig(); + final List<SSLHostConfig> buildSslHostConfig = SslHostConfiguration.buildSslHostConfig(configuration); if (!buildSslHostConfig.isEmpty()) { createDirectory(base, "conf"); } @@ -796,7 +796,7 @@ public class Meecrowave implements AutoCloseable { final Path dstFile = Paths.get(base.getAbsolutePath() + "/conf/" + certificate); resourceAsStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(certificate); if (resourceAsStream == null) { - resourceAsStream = new FileInputStream(new File((this.getClass().getResource("/").toString().replaceAll("file:", "") + "/" + certificate))); + resourceAsStream = new FileInputStream((this.getClass().getResource("/").toString().replaceAll("file:", "") + "/" + certificate)); } Files.copy(resourceAsStream, dstFile, StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { @@ -916,44 +916,6 @@ public class Meecrowave implements AutoCloseable { return valves; } - private List<SSLHostConfig> buildSslHostConfig() { - final List<SSLHostConfig> sslHostConfigs = new ArrayList<>(); - // Configures default SSLHostConfig - final ObjectRecipe defaultSslHostConfig = newRecipe(SSLHostConfig.class.getName()); - for (final String key : configuration.getProperties().stringPropertyNames()) { - if (key.startsWith("connector.sslhostconfig.") && key.split("\\.").length == 3) { - final String substring = key.substring("connector.sslhostconfig.".length()); - defaultSslHostConfig.setProperty(substring, configuration.getProperties().getProperty(key)); - } - } - if (!defaultSslHostConfig.getProperties().isEmpty()) { - sslHostConfigs.add(SSLHostConfig.class.cast(defaultSslHostConfig.create())); - } - // Allows to add N Multiple SSLHostConfig elements not including the default one. - final Collection<Integer> itemNumbers = configuration.getProperties().stringPropertyNames() - .stream() - .filter(key -> (key.startsWith("connector.sslhostconfig.") && key.split("\\.").length == 4)) - .map(key -> Integer.parseInt(key.split("\\.")[2])) - .collect(toSet()); - itemNumbers.stream().sorted().forEach(itemNumber -> { - final ObjectRecipe recipe = newRecipe(SSLHostConfig.class.getName()); - final String prefix = "connector.sslhostconfig." + itemNumber + '.'; - configuration.getProperties().stringPropertyNames().stream() - .filter(k -> k.startsWith(prefix)) - .forEach(key -> { - final String keyName = key.split("\\.")[3]; - recipe.setProperty(keyName, configuration.getProperties().getProperty(key)); - }); - if (!recipe.getProperties().isEmpty()) { - final SSLHostConfig sslHostConfig = SSLHostConfig.class.cast(recipe.create()); - sslHostConfigs.add(sslHostConfig); - new LogFacade(Meecrowave.class.getName()) - .info("Created SSLHostConfig #" + itemNumber + " (" + sslHostConfig.getHostName() + ")"); - } - }); - return sslHostConfigs; - } - protected void beforeStart() { // no-op } diff --git a/meecrowave-core/src/main/java/org/apache/meecrowave/configuration/SslHostConfiguration.java b/meecrowave-core/src/main/java/org/apache/meecrowave/configuration/SslHostConfiguration.java new file mode 100644 index 0000000..09a2b99 --- /dev/null +++ b/meecrowave-core/src/main/java/org/apache/meecrowave/configuration/SslHostConfiguration.java @@ -0,0 +1,172 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.meecrowave.configuration; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.meecrowave.logging.tomcat.LogFacade; +import org.apache.tomcat.util.net.SSLHostConfig; +import org.apache.tomcat.util.net.SSLHostConfigCertificate; +import org.apache.xbean.recipe.ObjectRecipe; +import org.apache.xbean.recipe.Option; + +/** + * Parses the attributes for the SSLHostConfig. + * + * This is a bit more complicated as the config of the certificates changed in Tomcat 10, + * and we liked to keep backward compatibility. + */ +public class SslHostConfiguration { + + private static final Set<String> KNOWN_FLAT_CERTIFICATE_ATTRIBUTES; + static { + // fill all the setter which take a String as parameter + KNOWN_FLAT_CERTIFICATE_ATTRIBUTES = new HashSet<>(); + final List<String> list = Arrays.stream(SSLHostConfigCertificate.class.getMethods()) + .filter(SslHostConfiguration::isStringSetter) + .map(SslHostConfiguration::getSetterAttribute) + .toList(); + KNOWN_FLAT_CERTIFICATE_ATTRIBUTES.addAll(list); + } + + private static String getSetterAttribute(Method method) { + final String name = method.getName(); + return Character.toLowerCase(name.charAt(3)) + name.substring(4); + } + + private static boolean isStringSetter(Method m) { + final String name = m.getName(); + return name.length() >= 4 && name.startsWith("set") && Character.isUpperCase(name.charAt(3)) + && m.getParameterCount() == 1 && m.getParameterTypes()[0] == String.class; + } + + public static final String DEFAULT_HOST_KEY = "__default__"; + + private SslHostConfiguration() { + // utility class ct + } + + public static List<SSLHostConfig> buildSslHostConfig(Configuration configuration) { + final List<SSLHostConfig> sslHostConfigs = new ArrayList<>(); + + final Map<String, Map<String, String>> sslConfigProps = groupPropertiesBySslHost(configuration); + + if (!sslConfigProps.isEmpty()) { + // first we register the default host + if (sslConfigProps.containsKey(DEFAULT_HOST_KEY)) { + sslHostConfigs.add(createSslHostConfig(sslConfigProps.get(DEFAULT_HOST_KEY))); + } + + // now sort the rest and add it. + sslConfigProps.keySet().stream() + .filter(h -> !DEFAULT_HOST_KEY.endsWith(h)) + .map(Integer::parseInt) + .sorted() + .forEach(hostNr -> { + final SSLHostConfig sslHostConfig = createSslHostConfig(sslConfigProps.get(hostNr.toString())); + sslHostConfigs.add(sslHostConfig); + new LogFacade(SslHostConfiguration.class.getName()) + .info("Created SSLHostConfig #" + hostNr + " (" + sslHostConfig.getHostName() + ")"); + }); + } + + return sslHostConfigs; + } + + /** + * Group the connector.sslhostconfig properties by sslHost. + * + * @return key is the ssl host. entry is configKey=configValue map. the default host has '__default__' as key. + */ + private static Map<String, Map<String, String>> groupPropertiesBySslHost(Configuration configuration) { + Map<String, Map<String, String>> sslConfigProps = new HashMap<>(); + + // First we sort all the configuration properties and group them by sslHost + for (String key : configuration.getProperties().stringPropertyNames()) { + if (key.startsWith("connector.sslhostconfig.")) { + final String[] keyParts = key.split("\\."); + if (keyParts.length == 3) { + // default + sslConfigProps.computeIfAbsent(DEFAULT_HOST_KEY, k -> new HashMap<>()) + .put(keyParts[2], configuration.getProperties().getProperty(key)); + } else if (keyParts.length == 4) { + // numerated other sslhosts + // In this case the keyParts[2] is the 'number' and all config is keyParts[3] + String number = keyParts[2]; + sslConfigProps.computeIfAbsent(number, k -> new HashMap<>()) + .put(keyParts[3], configuration.getProperties().getProperty(key)); + } + } + } + return sslConfigProps; + } + + public static SSLHostConfig createSslHostConfig(Map<String, String> sslHostConfigProperties) { + // step1: expand well known previously 'flat' stored certificate attributes + // this is necessary for backward compatibility reasons + final ObjectRecipe sslHostConfigReceipe = newRecipe(SSLHostConfig.class.getName()); + + List<String> certificateConfig = new ArrayList<>(); + for (String key : sslHostConfigProperties.keySet()) { + if (KNOWN_FLAT_CERTIFICATE_ATTRIBUTES.contains(key)) { + certificateConfig.add(key); + } else { + sslHostConfigReceipe.setProperty(key, sslHostConfigProperties.get(key)); + } + } + + final SSLHostConfig sslHostConfig = (SSLHostConfig) sslHostConfigReceipe.create(); + + if (!certificateConfig.isEmpty()) { + final SSLHostConfigCertificate sslConfig = sslHostConfig.getCertificates(true).iterator().next(); + + for (String key : certificateConfig) { + invokeSetter(sslConfig, key, sslHostConfigProperties.get(key)); + } + } + + return sslHostConfig; + } + + private static void invokeSetter(SSLHostConfigCertificate sslConfig, String key, String value) { + String setterName = "set" + Character.toUpperCase(key.charAt(0)) + key.substring(1); + + try { + final Method m = SSLHostConfigCertificate.class.getMethod(setterName, String.class); + m.invoke(sslConfig, value); + } + catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + private static ObjectRecipe newRecipe(final String clazz) { + final ObjectRecipe recipe = new ObjectRecipe(clazz); + recipe.allow(Option.FIELD_INJECTION); + recipe.allow(Option.PRIVATE_PROPERTIES); + return recipe; + } + +} diff --git a/pom.xml b/pom.xml index b6f9840..e6c0860 100644 --- a/pom.xml +++ b/pom.xml @@ -109,10 +109,7 @@ <module>meecrowave-jpa</module> <module>meecrowave-doc</module> <module>meecrowave-jta</module> - -<!--X TODO re-enable <module>integration-tests</module> ---> <module>meecrowave-oauth2-minimal</module> <module>meecrowave-oauth2</module> <module>meecrowave-letsencrypt</module> @@ -127,8 +124,6 @@ <artifactId>jakarta.annotation-api</artifactId> <version>${jakarta.annotation-api.version}</version> </dependency> - - <dependency> <groupId>jakarta.enterprise</groupId> <artifactId>jakarta.enterprise.cdi-api</artifactId> @@ -169,6 +164,11 @@ <artifactId>jakarta.ws.rs-api</artifactId> <version>${jakarta.ws.rs-api.version}</version> </dependency> + <dependency> + <groupId>jakarta.validation</groupId> + <artifactId>jakarta.validation-api</artifactId> + <version>${jakarta.validation-api.version}</version> + </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-servlet-api</artifactId>
