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);
+ }
+ }
}