mirror of
https://github.com/apache/activemq-artemis.git
synced 2025-02-22 18:30:43 +00:00
ARTEMIS-4586 Auto reload web binding SSL stores on change
This commit is contained in:
parent
20840cfdf1
commit
29781bd5da
@ -81,6 +81,9 @@ public class BindingDTO {
|
||||
@XmlAttribute
|
||||
private Boolean sniRequired;
|
||||
|
||||
@XmlAttribute
|
||||
private Boolean sslAutoReload;
|
||||
|
||||
public String getKeyStorePassword() throws Exception {
|
||||
return getPassword(this.keyStorePassword);
|
||||
}
|
||||
@ -225,6 +228,14 @@ public class BindingDTO {
|
||||
this.sniRequired = sniRequired;
|
||||
}
|
||||
|
||||
public Boolean getSslAutoReload() {
|
||||
return sslAutoReload;
|
||||
}
|
||||
|
||||
public void setSslAutoReload(Boolean sslAutoReload) {
|
||||
this.sslAutoReload = sslAutoReload;
|
||||
}
|
||||
|
||||
public BindingDTO() {
|
||||
apps = new ArrayList<>();
|
||||
}
|
||||
|
@ -104,6 +104,9 @@ public class WebServerDTO extends ComponentDTO {
|
||||
@XmlAttribute
|
||||
public Integer idleThreadTimeout = 60000;
|
||||
|
||||
@XmlAttribute
|
||||
public Integer scanPeriod;
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
@ -168,6 +171,14 @@ public class WebServerDTO extends ComponentDTO {
|
||||
this.idleThreadTimeout = idleThreadTimeout;
|
||||
}
|
||||
|
||||
public Integer getScanPeriod() {
|
||||
return scanPeriod;
|
||||
}
|
||||
|
||||
public void setScanPeriod(Integer scanPeriod) {
|
||||
this.scanPeriod = scanPeriod;
|
||||
}
|
||||
|
||||
public List<BindingDTO> getBindings() {
|
||||
return bindings;
|
||||
}
|
||||
|
@ -49,6 +49,7 @@ public class WebServerDTOTest {
|
||||
Assert.assertNull(defaultBinding.getTrustStorePassword());
|
||||
Assert.assertNull(defaultBinding.getSniHostCheck());
|
||||
Assert.assertNull(defaultBinding.getSniRequired());
|
||||
Assert.assertNull(defaultBinding.getSslAutoReload());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -189,6 +189,7 @@
|
||||
<include>server-cert.pem</include>
|
||||
<include>server-key.pem</include>
|
||||
<include>server-pem-props-config.txt</include>
|
||||
<include>other-server-keystore.p12</include>
|
||||
</includes>
|
||||
</resource>
|
||||
</resources>
|
||||
|
@ -25,7 +25,9 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.activemq.artemis.ActiveMQWebLogger;
|
||||
@ -51,8 +53,10 @@ import org.eclipse.jetty.server.handler.ContextHandler;
|
||||
import org.eclipse.jetty.server.handler.HandlerList;
|
||||
import org.eclipse.jetty.server.handler.ResourceHandler;
|
||||
import org.eclipse.jetty.servlet.FilterHolder;
|
||||
import org.eclipse.jetty.util.Scanner;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
import org.eclipse.jetty.util.thread.QueuedThreadPool;
|
||||
import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
|
||||
import org.eclipse.jetty.webapp.WebAppContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -71,6 +75,10 @@ public class WebServerComponent implements ExternalComponent, WebServerComponent
|
||||
|
||||
public static final boolean DEFAULT_SNI_REQUIRED_VALUE = false;
|
||||
|
||||
public static final boolean DEFAULT_SSL_AUTO_RELOAD_VALUE = false;
|
||||
|
||||
public static final int DEFAULT_SCAN_PERIOD_VALUE = 5;
|
||||
|
||||
private Server server;
|
||||
private HandlerList handlers;
|
||||
private WebServerDTO webServerConfig;
|
||||
@ -83,12 +91,23 @@ public class WebServerComponent implements ExternalComponent, WebServerComponent
|
||||
private String artemisInstance;
|
||||
private String artemisHome;
|
||||
|
||||
private int scanPeriod;
|
||||
private Scanner scanner;
|
||||
private ScheduledExecutorScheduler scannerScheduler;
|
||||
private Map<String, List<Runnable>> scannerTasks = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public void configure(ComponentDTO config, String artemisInstance, String artemisHome) throws Exception {
|
||||
this.webServerConfig = (WebServerDTO) config;
|
||||
this.artemisInstance = artemisInstance;
|
||||
this.artemisHome = artemisHome;
|
||||
|
||||
if (webServerConfig.getScanPeriod() != null) {
|
||||
scanPeriod = webServerConfig.getScanPeriod();
|
||||
} else {
|
||||
scanPeriod = DEFAULT_SCAN_PERIOD_VALUE;
|
||||
}
|
||||
|
||||
temporaryWarDir = Paths.get(artemisInstance != null ? artemisInstance : ".").resolve("tmp").resolve("webapps").toAbsolutePath();
|
||||
if (!Files.exists(temporaryWarDir)) {
|
||||
Files.createDirectories(temporaryWarDir);
|
||||
@ -258,6 +277,10 @@ public class WebServerComponent implements ExternalComponent, WebServerComponent
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Boolean.TRUE.equals(binding.getSslAutoReload())) {
|
||||
addStoreResourceScannerTask(binding.getKeyStorePath(), sslFactory);
|
||||
addStoreResourceScannerTask(binding.getTrustStorePath(), sslFactory);
|
||||
}
|
||||
|
||||
SslConnectionFactory sslConnectionFactory = new SslConnectionFactory(sslFactory, "HTTP/1.1");
|
||||
|
||||
@ -269,7 +292,6 @@ public class WebServerComponent implements ExternalComponent, WebServerComponent
|
||||
HttpConnectionFactory httpFactory = new HttpConnectionFactory(httpConfiguration);
|
||||
|
||||
connector = new ServerConnector(server, sslConnectionFactory, httpFactory);
|
||||
|
||||
} else {
|
||||
httpConfiguration.setSendServerVersion(false);
|
||||
ConnectionFactory connectionFactory = new HttpConnectionFactory(httpConfiguration);
|
||||
@ -281,6 +303,75 @@ public class WebServerComponent implements ExternalComponent, WebServerComponent
|
||||
return connector;
|
||||
}
|
||||
|
||||
private File getStoreFile(String storeFilename) {
|
||||
File storeFile = new File(storeFilename);
|
||||
if (!storeFile.exists())
|
||||
throw new IllegalArgumentException("Store file does not exist: " + storeFilename);
|
||||
if (storeFile.isDirectory())
|
||||
throw new IllegalArgumentException("Expected store file not directory: " + storeFilename);
|
||||
|
||||
return storeFile;
|
||||
}
|
||||
|
||||
private File getParentStoreFile(File storeFile) {
|
||||
File parentFile = storeFile.getParentFile();
|
||||
if (!parentFile.exists() || !parentFile.isDirectory())
|
||||
throw new IllegalArgumentException("Error obtaining store dir for " + storeFile);
|
||||
|
||||
return parentFile;
|
||||
}
|
||||
|
||||
private Scanner getScanner() {
|
||||
if (scannerScheduler == null) {
|
||||
scannerScheduler = new ScheduledExecutorScheduler("WebScannerScheduler", true, 1);
|
||||
server.addBean(scannerScheduler);
|
||||
}
|
||||
|
||||
if (scanner == null) {
|
||||
scanner = new Scanner(scannerScheduler);
|
||||
scanner.setScanInterval(scanPeriod);
|
||||
scanner.setReportDirs(false);
|
||||
scanner.setReportExistingFilesOnStartup(false);
|
||||
scanner.setScanDepth(1);
|
||||
scanner.addListener((Scanner.BulkListener) filenames -> {
|
||||
for (String filename: filenames) {
|
||||
List<Runnable> tasks = scannerTasks.get(filename);
|
||||
if (tasks != null) {
|
||||
tasks.forEach(t -> t.run());
|
||||
}
|
||||
}
|
||||
});
|
||||
server.addBean(scanner);
|
||||
}
|
||||
|
||||
return scanner;
|
||||
}
|
||||
|
||||
private void addScannerTask(File file, Runnable task) {
|
||||
File parentFile = getParentStoreFile(file);
|
||||
String storeFilename = file.toPath().toString();
|
||||
List<Runnable> tasks = scannerTasks.get(storeFilename);
|
||||
if (tasks == null) {
|
||||
tasks = new ArrayList<>();
|
||||
scannerTasks.put(storeFilename, tasks);
|
||||
}
|
||||
tasks.add(task);
|
||||
getScanner().addDirectory(parentFile.toPath());
|
||||
}
|
||||
|
||||
private void addStoreResourceScannerTask(String storeFilename, SslContextFactory.Server sslFactory) {
|
||||
if (storeFilename != null) {
|
||||
File storeFile = getStoreFile(storeFilename);
|
||||
addScannerTask(storeFile, () -> {
|
||||
try {
|
||||
sslFactory.reload(f -> { });
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to reload the ssl factory related to {}", storeFile, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private RequestLog getRequestLog() {
|
||||
RequestLogWriter requestLogWriter = new RequestLogWriter();
|
||||
CustomRequestLog requestLog;
|
||||
@ -413,6 +504,9 @@ public class WebServerComponent implements ExternalComponent, WebServerComponent
|
||||
ActiveMQWebLogger.LOGGER.stoppingEmbeddedWebServer();
|
||||
server.stop();
|
||||
server = null;
|
||||
scanner = null;
|
||||
scannerScheduler = null;
|
||||
scannerTasks.clear();
|
||||
cleanupWebTemporaryFiles(webContextData);
|
||||
webContextData.clear();
|
||||
jolokiaUrls.clear();
|
||||
|
@ -16,10 +16,12 @@
|
||||
*/
|
||||
package org.apache.activemq.cli.test;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.SNIHostName;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLEngine;
|
||||
import javax.net.ssl.SSLParameters;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
@ -30,7 +32,9 @@ import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@ -38,6 +42,7 @@ import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarOutputStream;
|
||||
@ -75,6 +80,7 @@ import org.apache.activemq.artemis.dto.BindingDTO;
|
||||
import org.apache.activemq.artemis.dto.BrokerDTO;
|
||||
import org.apache.activemq.artemis.dto.WebServerDTO;
|
||||
import org.apache.activemq.artemis.utils.ThreadLeakCheckRule;
|
||||
import org.apache.activemq.artemis.utils.Wait;
|
||||
import org.apache.http.HttpException;
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
@ -97,12 +103,17 @@ import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
public class WebServerComponentTest extends Assert {
|
||||
|
||||
@Rule
|
||||
public ThreadLeakCheckRule leakCheckRule = new ThreadLeakCheckRule();
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder tempFolder = new TemporaryFolder();
|
||||
|
||||
|
||||
static final String URL = System.getProperty("url", "http://localhost:8161/WebServerComponentTest.txt");
|
||||
static final String SECURE_URL = System.getProperty("url", "https://localhost:8448/WebServerComponentTest.txt");
|
||||
|
||||
@ -289,6 +300,7 @@ public class WebServerComponentTest extends Assert {
|
||||
webServerDTO.setBindings(Collections.singletonList(bindingDTO));
|
||||
webServerDTO.path = "webapps";
|
||||
webServerDTO.webContentEnabled = true;
|
||||
webServerDTO.setScanPeriod(1);
|
||||
|
||||
WebServerComponent webServerComponent = new WebServerComponent();
|
||||
Assert.assertFalse(webServerComponent.isStarted());
|
||||
@ -416,12 +428,67 @@ public class WebServerComponentTest extends Assert {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSSLAutoReload() throws Exception {
|
||||
File keyStoreFile = tempFolder.newFile();
|
||||
|
||||
Files.copy(WebServerComponentTest.class.getClassLoader().getResourceAsStream("server-keystore.p12"),
|
||||
keyStoreFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
BindingDTO bindingDTO = new BindingDTO();
|
||||
bindingDTO.setSslAutoReload(true);
|
||||
bindingDTO.setKeyStorePath(keyStoreFile.getAbsolutePath());
|
||||
bindingDTO.setKeyStorePassword(KEY_STORE_PASSWORD);
|
||||
WebServerComponent webServerComponent = startSimpleSecureServer(bindingDTO);
|
||||
|
||||
try {
|
||||
int port = webServerComponent.getPort(0);
|
||||
AtomicReference<SSLSession> sslSessionReference = new AtomicReference<>();
|
||||
HostnameVerifier hostnameVerifier = (s, sslSession) -> {
|
||||
sslSessionReference.set(sslSession);
|
||||
return true;
|
||||
};
|
||||
|
||||
// check server certificate contains ActiveMQ Artemis Server
|
||||
Assert.assertTrue(testSimpleSecureServer("localhost", port, "localhost", null, hostnameVerifier) == 200 &&
|
||||
sslSessionReference.get().getPeerCertificates()[0].toString().contains("CN=ActiveMQ Artemis Server,"));
|
||||
|
||||
// check server certificate doesn't contain ActiveMQ Artemis Server
|
||||
Assert.assertFalse(testSimpleSecureServer("localhost", port, "localhost", null, hostnameVerifier) == 200 &&
|
||||
sslSessionReference.get().getPeerCertificates()[0].toString().contains("CN=ActiveMQ Artemis Other Server,"));
|
||||
|
||||
// update server keystore
|
||||
Files.copy(WebServerComponentTest.class.getClassLoader().getResourceAsStream("other-server-keystore.p12"),
|
||||
keyStoreFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
// check server certificate contains ActiveMQ Artemis Other Server
|
||||
Wait.assertTrue(() -> testSimpleSecureServer("localhost", port, "localhost", null, hostnameVerifier) == 200 &&
|
||||
sslSessionReference.get().getPeerCertificates()[0].toString().contains("CN=ActiveMQ Artemis Other Server,"));
|
||||
|
||||
// check server certificate doesn't contain ActiveMQ Artemis Server
|
||||
Assert.assertFalse(testSimpleSecureServer("localhost", port, "localhost", null, hostnameVerifier) == 200 &&
|
||||
sslSessionReference.get().getPeerCertificates()[0].toString().contains("CN=ActiveMQ Artemis Server,"));
|
||||
} finally {
|
||||
webServerComponent.stop(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private int testSimpleSecureServer(String webServerHostname, int webServerPort, String requestHostname, String sniHostname) throws Exception {
|
||||
return testSimpleSecureServer(webServerHostname, webServerPort, requestHostname, sniHostname, null);
|
||||
}
|
||||
|
||||
private int testSimpleSecureServer(String webServerHostname, int webServerPort, String requestHostname, String sniHostname, HostnameVerifier hostnameVerifier) throws Exception {
|
||||
HttpGet request = new HttpGet("https://" + (requestHostname != null ? requestHostname : webServerHostname) + ":" + webServerPort + "/WebServerComponentTest.txt");
|
||||
|
||||
HttpHost webServerHost = HttpHost.create("https://" + webServerHostname + ":" + webServerPort);
|
||||
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, (certificate, authType) -> true).build();
|
||||
SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, new NoopHostnameVerifier()) {
|
||||
|
||||
if (hostnameVerifier == null) {
|
||||
hostnameVerifier = new NoopHostnameVerifier();
|
||||
}
|
||||
|
||||
SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier) {
|
||||
@Override
|
||||
protected void prepareSocket(SSLSocket socket) throws IOException {
|
||||
super.prepareSocket(socket);
|
||||
|
@ -43,6 +43,8 @@ The minimum number of threads the embedded web server will hold to service HTTP
|
||||
Default is `8` or the value of `maxThreads` if it is lower.
|
||||
idleThreadTimeout::
|
||||
The time to wait before terminating an idle thread from the embedded web server. Measured in milliseconds. Default is `60000`.
|
||||
scanPeriod::
|
||||
How often to scan for changes of the key and trust store files related to a binding when the `sslAutoReload` attribute value of the `binding` element is `true`, for further details see <<Binding>>. Measured in seconds. Default is `5`.
|
||||
|
||||
=== Binding
|
||||
|
||||
@ -106,6 +108,11 @@ Whether or not the client request must include an SNI Host name.
|
||||
Default is `false`.
|
||||
Only applicable when using `https`.
|
||||
|
||||
sslAutoReload::
|
||||
Whether or not the key and trust store files must be watched for changes and automatically reloaded.
|
||||
The watch period is controlled by the `scanPeriod` attribute of the `web` element, for further details see <<Web>>.
|
||||
Default is `false`.
|
||||
|
||||
=== App
|
||||
|
||||
Each web application should be defined in an `app` element inside an `binding` element.
|
||||
|
@ -46,6 +46,7 @@ public class WebServerDTOConfigTest {
|
||||
properties.put(ActiveMQDefaultConfiguration.getDefaultSystemWebPropertyPrefix() + "customizer", "customizerTest");
|
||||
properties.put(ActiveMQDefaultConfiguration.getDefaultSystemWebPropertyPrefix() + "rootRedirectLocation", "locationTest");
|
||||
properties.put(ActiveMQDefaultConfiguration.getDefaultSystemWebPropertyPrefix() + "webContentEnabled", "true");
|
||||
properties.put(ActiveMQDefaultConfiguration.getDefaultSystemWebPropertyPrefix() + "scanPeriod", "1234");
|
||||
properties.put(ActiveMQDefaultConfiguration.getDefaultSystemWebPropertyPrefix() + INVALID_ATTRIBUTE_NAME, "true");
|
||||
Configuration configuration = new ConfigurationImpl();
|
||||
String systemWebPropertyPrefix = ActiveMQDefaultConfiguration.getDefaultSystemWebPropertyPrefix();
|
||||
@ -54,6 +55,7 @@ public class WebServerDTOConfigTest {
|
||||
Assert.assertEquals("customizerTest", webServer.getCustomizer());
|
||||
Assert.assertEquals("locationTest", webServer.getRootRedirectLocation());
|
||||
Assert.assertEquals(true, webServer.getWebContentEnabled());
|
||||
Assert.assertEquals(Integer.valueOf(1234), webServer.getScanPeriod());
|
||||
|
||||
testStatus(configuration.getStatus(), "system-" + systemWebPropertyPrefix, "");
|
||||
}
|
||||
@ -102,6 +104,7 @@ public class WebServerDTOConfigTest {
|
||||
properties.put(ActiveMQDefaultConfiguration.getDefaultSystemWebPropertyPrefix() + "bindings." + bindingName + ".trustStorePassword", "test-trustStorePassword");
|
||||
properties.put(ActiveMQDefaultConfiguration.getDefaultSystemWebPropertyPrefix() + "bindings." + bindingName + ".sniHostCheck", !WebServerComponent.DEFAULT_SNI_HOST_CHECK_VALUE);
|
||||
properties.put(ActiveMQDefaultConfiguration.getDefaultSystemWebPropertyPrefix() + "bindings." + bindingName + ".sniRequired", !WebServerComponent.DEFAULT_SNI_REQUIRED_VALUE);
|
||||
properties.put(ActiveMQDefaultConfiguration.getDefaultSystemWebPropertyPrefix() + "bindings." + bindingName + ".sslAutoReload", !WebServerComponent.DEFAULT_SNI_REQUIRED_VALUE);
|
||||
properties.put(ActiveMQDefaultConfiguration.getDefaultSystemWebPropertyPrefix() + "bindings." + bindingName + "." + INVALID_ATTRIBUTE_NAME, "true");
|
||||
Configuration configuration = new ConfigurationImpl();
|
||||
String systemWebPropertyPrefix = ActiveMQDefaultConfiguration.getDefaultSystemWebPropertyPrefix();
|
||||
@ -123,6 +126,7 @@ public class WebServerDTOConfigTest {
|
||||
Assert.assertEquals("test-trustStorePassword", testBinding.getTrustStorePassword());
|
||||
Assert.assertEquals(!WebServerComponent.DEFAULT_SNI_HOST_CHECK_VALUE, testBinding.getSniHostCheck());
|
||||
Assert.assertEquals(!WebServerComponent.DEFAULT_SNI_REQUIRED_VALUE, testBinding.getSniRequired());
|
||||
Assert.assertEquals(!WebServerComponent.DEFAULT_SSL_AUTO_RELOAD_VALUE, testBinding.getSslAutoReload());
|
||||
|
||||
testStatus(configuration.getStatus(), "system-" + systemWebPropertyPrefix, "bindings." + bindingName + ".");
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user