diff --git a/jmx_prometheus_common/pom.xml b/jmx_prometheus_common/pom.xml index 142df15c..4cbe8713 100644 --- a/jmx_prometheus_common/pom.xml +++ b/jmx_prometheus_common/pom.xml @@ -52,6 +52,11 @@ collector ${project.version} + + io.github.hakky54 + ayza + 10.0.1 + org.junit.jupiter junit-jupiter-api diff --git a/jmx_prometheus_common/src/main/java/io/prometheus/jmx/common/HTTPServerFactory.java b/jmx_prometheus_common/src/main/java/io/prometheus/jmx/common/HTTPServerFactory.java index b7d2760c..bb35200f 100644 --- a/jmx_prometheus_common/src/main/java/io/prometheus/jmx/common/HTTPServerFactory.java +++ b/jmx_prometheus_common/src/main/java/io/prometheus/jmx/common/HTTPServerFactory.java @@ -25,7 +25,6 @@ import io.prometheus.jmx.common.authenticator.PBKDF2Authenticator; import io.prometheus.jmx.common.authenticator.PlaintextAuthenticator; import io.prometheus.jmx.common.util.MapAccessor; -import io.prometheus.jmx.common.util.SSLContextFactory; import io.prometheus.jmx.common.util.YamlSupport; import io.prometheus.jmx.common.util.functions.IntegerInRange; import io.prometheus.jmx.common.util.functions.StringIsNotBlank; @@ -39,8 +38,13 @@ import java.io.File; import java.io.IOException; import java.net.InetAddress; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; import java.security.GeneralSecurityException; import java.security.KeyStore; +import java.time.Instant; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -48,12 +52,21 @@ import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import javax.net.ssl.SSLParameters; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509ExtendedTrustManager; +import nl.altindag.ssl.SSLFactory; +import nl.altindag.ssl.exception.GenericException; +import nl.altindag.ssl.util.KeyManagerUtils; +import nl.altindag.ssl.util.KeyStoreUtils; +import nl.altindag.ssl.util.SSLSessionUtils; +import nl.altindag.ssl.util.TrustManagerUtils; /** * Class to create the HTTPServer used by both the Java agent exporter and the Standalone exporter @@ -83,6 +96,11 @@ public class HTTPServerFactory { private static final Set PBKDF2_ALGORITHMS; private static final Map PBKDF2_ALGORITHM_ITERATIONS; private static final int PBKDF2_KEY_LENGTH_BITS = 128; + private static final ScheduledExecutorService EXECUTOR_SERVICE = + Executors.newSingleThreadScheduledExecutor(); + + private static KeyStoreProperties keyStoreProperties; + private static KeyStoreProperties trustStoreProperties; static { // Get the keystore type system property @@ -119,6 +137,8 @@ public class HTTPServerFactory { PBKDF2_ALGORITHM_ITERATIONS.put("PBKDF2WithHmacSHA1", 1300000); PBKDF2_ALGORITHM_ITERATIONS.put("PBKDF2WithHmacSHA256", 600000); PBKDF2_ALGORITHM_ITERATIONS.put("PBKDF2WithHmacSHA512", 210000); + + Runtime.getRuntime().addShutdownHook(new Thread(EXECUTOR_SERVICE::shutdownNow)); } /** Constructor */ @@ -663,173 +683,15 @@ public static void configureSSL( MapAccessor rootMapAccessor, HTTPServer.Builder httpServerBuilder) { if (rootMapAccessor.containsPath("/httpServer/ssl")) { try { - String keyStoreFilename = - rootMapAccessor - .get("/httpServer/ssl/keyStore/filename") - .map( - new ToString( - ConfigurationException.supplier( - "Invalid configuration for" - + " /httpServer/ssl/keyStore/filename" - + " must be a string"))) - .map( - new StringIsNotBlank( - ConfigurationException.supplier( - "Invalid configuration for" - + " /httpServer/ssl/keyStore/filename" - + " must not be blank"))) - .orElse(System.getProperty(JAVAX_NET_SSL_KEY_STORE)); - - String keyStoreType = - rootMapAccessor - .get("/httpServer/ssl/keyStore/type") - .map( - new ToString( - ConfigurationException.supplier( - "Invalid configuration for" - + " /httpServer/ssl/keyStore/type" - + " must be a string"))) - .map( - new StringIsNotBlank( - ConfigurationException.supplier( - "Invalid configuration for" - + " /httpServer/ssl/keyStore/type" - + " must not be blank"))) - .orElse(DEFAULT_KEYSTORE_TYPE); - - String keyStorePassword = - rootMapAccessor - .get("/httpServer/ssl/keyStore/password") - .map( - new ToString( - ConfigurationException.supplier( - "Invalid configuration for" - + " /httpServer/ssl/keyStore/password" - + " must be a string"))) - .map( - new StringIsNotBlank( - ConfigurationException.supplier( - "Invalid configuration for" - + " /httpServer/ssl/keyStore/password" - + " must not be blank"))) - .orElse(System.getProperty(JAVAX_NET_SSL_KEY_STORE_PASSWORD)); - - // Resolve the password - keyStorePassword = VariableResolver.resolveVariable(keyStorePassword); - - String certificateAlias = - rootMapAccessor - .get("/httpServer/ssl/certificate/alias") - .map( - new ToString( - ConfigurationException.supplier( - "Invalid configuration for" - + " /httpServer/ssl/certificate/alias" - + " must be a string"))) - .map( - new StringIsNotBlank( - ConfigurationException.supplier( - "Invalid configuration for" - + " /httpServer/ssl/certificate/alias" - + " must not be blank"))) - .orElseThrow( - ConfigurationException.supplier( - "/httpServer/ssl/certificate/alias is a required" - + " string")); - - String trustStoreFilename = null; - String trustStoreType = null; - String trustStorePassword = null; - - final boolean mutualTLS = - rootMapAccessor - .get("/httpServer/ssl/mutualTLS") - .map( - new ToString( - ConfigurationException.supplier( - "Invalid configuration for" - + " /httpServer/ssl/mutualTLS" - + " must be a boolean"))) - .map( - new StringIsNotBlank( - ConfigurationException.supplier( - "Invalid configuration for" - + " /httpServer/ssl/mutualTLS" - + " must not be blank"))) - .map( - new ToBoolean( - ConfigurationException.supplier( - "Invalid configuration for" - + " /httpServer/ssl/mutualTLS" - + " must be a boolean"))) - .orElse(false); - - if (mutualTLS) { - trustStoreFilename = - rootMapAccessor - .get("/httpServer/ssl/trustStore/filename") - .map( - new ToString( - ConfigurationException.supplier( - "Invalid configuration for" - + " /httpServer/ssl/trustStore/filename" - + " must be a string"))) - .map( - new StringIsNotBlank( - ConfigurationException.supplier( - "Invalid configuration for" - + " /httpServer/ssl/trustStore/filename" - + " must not be blank"))) - .orElse(System.getProperty(JAVAX_NET_SSL_TRUST_STORE)); - - trustStoreType = - rootMapAccessor - .get("/httpServer/ssl/trustStore/type") - .map( - new ToString( - ConfigurationException.supplier( - "Invalid configuration for" - + " /httpServer/ssl/trustStore/type" - + " must be a string"))) - .map( - new StringIsNotBlank( - ConfigurationException.supplier( - "Invalid configuration for" - + " /httpServer/ssl/trustStore/type" - + " must not be blank"))) - .orElse(DEFAULT_TRUST_STORE_TYPE); - - trustStorePassword = - rootMapAccessor - .get("/httpServer/ssl/trustStore/password") - .map( - new ToString( - ConfigurationException.supplier( - "Invalid configuration for" - + " /httpServer/ssl/trustStore/password" - + " must be a string"))) - .map( - new StringIsNotBlank( - ConfigurationException.supplier( - "Invalid configuration for" - + " /httpServer/ssl/trustStore/password" - + " must not be blank"))) - .orElse(System.getProperty(JAVAX_NET_SSL_TRUST_STORE_PASSWORD)); - - // Resolve the password - trustStorePassword = VariableResolver.resolveVariable(trustStorePassword); - } + boolean mutualTLS = isMutualTls(rootMapAccessor); + SSLFactory sslFactory = createSslFactory(rootMapAccessor); + Runnable sslUpdater = () -> reloadSsl(sslFactory, rootMapAccessor); + // check every hour for file changes and if it has been modified update the ssl + // configuration + EXECUTOR_SERVICE.scheduleAtFixedRate(sslUpdater, 1, 1, TimeUnit.HOURS); httpServerBuilder.httpsConfigurator( - new HttpsConfigurator( - SSLContextFactory.createSSLContext( - keyStoreType, - keyStoreFilename, - keyStorePassword, - certificateAlias, - trustStoreType, - trustStoreFilename, - trustStorePassword)) { + new HttpsConfigurator(sslFactory.getSslContext()) { @Override public void configure(HttpsParameters params) { SSLParameters sslParameters = @@ -838,7 +700,7 @@ public void configure(HttpsParameters params) { params.setSSLParameters(sslParameters); } }); - } catch (GeneralSecurityException | IOException e) { + } catch (GenericException e) { String message = e.getMessage(); if (message != null && !message.trim().isEmpty()) { message = ", " + message.trim(); @@ -852,6 +714,266 @@ public void configure(HttpsParameters params) { } } + private static SSLFactory createSslFactory(MapAccessor rootMapAccessor) { + keyStoreProperties = getKeyStoreProperties(rootMapAccessor); + Optional trustProps = getTrustStoreProperties(rootMapAccessor); + + SSLFactory.Builder sslFactoryBuilder = + SSLFactory.builder() + .withSwappableIdentityMaterial() + .withIdentityMaterial( + keyStoreProperties.getFilename(), + keyStoreProperties.getPassword(), + keyStoreProperties.getPassword(), + keyStoreProperties.getType()); + + if (trustProps.isPresent()) { + trustStoreProperties = trustProps.get(); + sslFactoryBuilder + .withSwappableTrustMaterial() + .withTrustMaterial( + trustStoreProperties.getFilename(), + trustStoreProperties.getPassword(), + trustStoreProperties.getType()); + } + + return sslFactoryBuilder.build(); + } + + @SuppressWarnings("OptionalGetWithoutIsPresent") + private static void reloadSsl(SSLFactory sslFactory, MapAccessor rootMapAccessor) { + KeyStoreProperties keyProps = getKeyStoreProperties(rootMapAccessor); + Optional trustProps = getTrustStoreProperties(rootMapAccessor); + + boolean sslUpdated = false; + if (keyStoreProperties.getLastModifiedTime().isBefore(keyProps.lastModifiedTime)) { + KeyStore keyStore = + KeyStoreUtils.loadKeyStore( + keyProps.getFilename(), keyProps.getPassword(), keyProps.getType()); + X509ExtendedKeyManager keyManager = + KeyManagerUtils.createKeyManager(keyStore, keyProps.getPassword()); + KeyManagerUtils.swapKeyManager(sslFactory.getKeyManager().get(), keyManager); + keyStoreProperties = keyProps; + sslUpdated = true; + } + + if (trustProps.isPresent() + && getTrustStoreProperties().isPresent() + && getTrustStoreProperties() + .get() + .getLastModifiedTime() + .isBefore(trustProps.get().lastModifiedTime)) { + KeyStore keyStore = + KeyStoreUtils.loadKeyStore( + trustProps.get().getFilename(), + trustProps.get().getPassword(), + trustProps.get().getType()); + X509ExtendedTrustManager trustManager = TrustManagerUtils.createTrustManager(keyStore); + TrustManagerUtils.swapTrustManager(sslFactory.getTrustManager().get(), trustManager); + trustStoreProperties = trustProps.get(); + sslUpdated = true; + } + + if (sslUpdated) { + SSLSessionUtils.invalidateCaches(sslFactory); + } + } + + private static KeyStoreProperties getKeyStoreProperties(MapAccessor rootMapAccessor) { + String keyStoreFilename = + rootMapAccessor + .get("/httpServer/ssl/keyStore/filename") + .map( + new ToString( + ConfigurationException.supplier( + "Invalid configuration for" + + " /httpServer/ssl/keyStore/filename" + + " must be a string"))) + .map( + new StringIsNotBlank( + ConfigurationException.supplier( + "Invalid configuration for" + + " /httpServer/ssl/keyStore/filename" + + " must not be blank"))) + .orElse(System.getProperty(JAVAX_NET_SSL_KEY_STORE)); + + Instant lastModifiedTime = getLastModifiedTime(keyStoreFilename); + + String keyStoreType = + rootMapAccessor + .get("/httpServer/ssl/keyStore/type") + .map( + new ToString( + ConfigurationException.supplier( + "Invalid configuration for" + + " /httpServer/ssl/keyStore/type" + + " must be a string"))) + .map( + new StringIsNotBlank( + ConfigurationException.supplier( + "Invalid configuration for" + + " /httpServer/ssl/keyStore/type" + + " must not be blank"))) + .orElse(DEFAULT_KEYSTORE_TYPE); + + String keyStorePassword = + rootMapAccessor + .get("/httpServer/ssl/keyStore/password") + .map( + new ToString( + ConfigurationException.supplier( + "Invalid configuration for" + + " /httpServer/ssl/keyStore/password" + + " must be a string"))) + .map( + new StringIsNotBlank( + ConfigurationException.supplier( + "Invalid configuration for" + + " /httpServer/ssl/keyStore/password" + + " must not be blank"))) + .orElse(System.getProperty(JAVAX_NET_SSL_KEY_STORE_PASSWORD)); + + // Resolve the password + keyStorePassword = VariableResolver.resolveVariable(keyStorePassword); + + String certificateAlias = + rootMapAccessor + .get("/httpServer/ssl/certificate/alias") + .map( + new ToString( + ConfigurationException.supplier( + "Invalid configuration for" + + " /httpServer/ssl/certificate/alias" + + " must be a string"))) + .map( + new StringIsNotBlank( + ConfigurationException.supplier( + "Invalid configuration for" + + " /httpServer/ssl/certificate/alias" + + " must not be blank"))) + .orElseThrow( + ConfigurationException.supplier( + "/httpServer/ssl/certificate/alias is a required" + + " string")); + + return new KeyStoreProperties( + keyStoreFilename, + lastModifiedTime, + keyStorePassword.toCharArray(), + keyStoreType, + certificateAlias); + } + + private static Instant getLastModifiedTime(String filename) { + try { + return Files.readAttributes(Paths.get(filename), BasicFileAttributes.class) + .lastModifiedTime() + .toInstant(); + } catch (IOException e) { + return Instant.EPOCH; + } + } + + private static Optional getTrustStoreProperties( + MapAccessor rootMapAccessor) { + final boolean mutualTLS = isMutualTls(rootMapAccessor); + if (!mutualTLS) { + return Optional.empty(); + } + + String trustStoreFilename = + rootMapAccessor + .get("/httpServer/ssl/trustStore/filename") + .map( + new ToString( + ConfigurationException.supplier( + "Invalid configuration for" + + " /httpServer/ssl/trustStore/filename" + + " must be a string"))) + .map( + new StringIsNotBlank( + ConfigurationException.supplier( + "Invalid configuration for" + + " /httpServer/ssl/trustStore/filename" + + " must not be blank"))) + .orElse(System.getProperty(JAVAX_NET_SSL_TRUST_STORE)); + + Instant lastModifiedTime = getLastModifiedTime(trustStoreFilename); + + String trustStoreType = + rootMapAccessor + .get("/httpServer/ssl/trustStore/type") + .map( + new ToString( + ConfigurationException.supplier( + "Invalid configuration for" + + " /httpServer/ssl/trustStore/type" + + " must be a string"))) + .map( + new StringIsNotBlank( + ConfigurationException.supplier( + "Invalid configuration for" + + " /httpServer/ssl/trustStore/type" + + " must not be blank"))) + .orElse(DEFAULT_TRUST_STORE_TYPE); + + String trustStorePassword = + rootMapAccessor + .get("/httpServer/ssl/trustStore/password") + .map( + new ToString( + ConfigurationException.supplier( + "Invalid configuration for" + + " /httpServer/ssl/trustStore/password" + + " must be a string"))) + .map( + new StringIsNotBlank( + ConfigurationException.supplier( + "Invalid configuration for" + + " /httpServer/ssl/trustStore/password" + + " must not be blank"))) + .orElse(System.getProperty(JAVAX_NET_SSL_TRUST_STORE_PASSWORD)); + + // Resolve the password + trustStorePassword = VariableResolver.resolveVariable(trustStorePassword); + + return Optional.of( + new KeyStoreProperties( + trustStoreFilename, + lastModifiedTime, + trustStorePassword.toCharArray(), + trustStoreType, + null)); + } + + private static boolean isMutualTls(MapAccessor rootMapAccessor) { + return rootMapAccessor + .get("/httpServer/ssl/mutualTLS") + .map( + new ToString( + ConfigurationException.supplier( + "Invalid configuration for" + + " /httpServer/ssl/mutualTLS" + + " must be a boolean"))) + .map( + new StringIsNotBlank( + ConfigurationException.supplier( + "Invalid configuration for" + + " /httpServer/ssl/mutualTLS" + + " must not be blank"))) + .map( + new ToBoolean( + ConfigurationException.supplier( + "Invalid configuration for" + + " /httpServer/ssl/mutualTLS" + + " must be a boolean"))) + .orElse(false); + } + + private static Optional getTrustStoreProperties() { + return Optional.ofNullable(trustStoreProperties); + } + /** * Class to implement a named thread factory * @@ -898,4 +1020,47 @@ public void rejectedExecution(Runnable runnable, ThreadPoolExecutor threadPoolEx } } } + + private static final class KeyStoreProperties { + + private final Path filename; + private final Instant lastModifiedTime; + private final char[] password; + private final String type; + private final String certificateAlias; + + private KeyStoreProperties( + String filename, + Instant lastModifiedTime, + char[] password, + String type, + String certificateAlias) { + + this.filename = Paths.get(filename); + this.lastModifiedTime = lastModifiedTime; + this.password = password; + this.type = type; + this.certificateAlias = certificateAlias; + } + + public Path getFilename() { + return filename; + } + + public Instant getLastModifiedTime() { + return lastModifiedTime; + } + + public char[] getPassword() { + return password; + } + + public String getType() { + return type; + } + + public Optional getCertificateAlias() { + return Optional.ofNullable(certificateAlias); + } + } }