Skip to content

Commit

Permalink
ARTEMIS-4586 Auto reload web binding SSL stores on change
Browse files Browse the repository at this point in the history
  • Loading branch information
brusdev committed Feb 8, 2024
1 parent b08602b commit 6081666
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ public class BindingDTO {
@XmlAttribute
private Boolean sniRequired;

@XmlAttribute
private Boolean sslAutoReload;

public String getKeyStorePassword() throws Exception {
return getPassword(this.keyStorePassword);
}
Expand Down Expand Up @@ -225,6 +228,14 @@ public void setSniRequired(Boolean sniRequired) {
this.sniRequired = sniRequired;
}

public Boolean getSslAutoReload() {
return sslAutoReload;
}

public void setSslAutoReload(Boolean sslAutoReload) {
this.sslAutoReload = sslAutoReload;
}

public BindingDTO() {
apps = new ArrayList<>();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ public class WebServerDTO extends ComponentDTO {
@XmlAttribute
public Integer idleThreadTimeout = 60000;

@XmlAttribute
public Integer scanPeriod;

public String getPath() {
return path;
}
Expand Down Expand Up @@ -168,6 +171,14 @@ public void setIdleThreadTimeout(Integer idleThreadTimeout) {
this.idleThreadTimeout = idleThreadTimeout;
}

public Integer getScanPeriod() {
return scanPeriod;
}

public void setScanPeriod(Integer scanPeriod) {
this.scanPeriod = scanPeriod;
}

public List<BindingDTO> getBindings() {
return bindings;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public void testDefault() throws Exception {
Assert.assertNull(defaultBinding.getTrustStorePassword());
Assert.assertNull(defaultBinding.getSniHostCheck());
Assert.assertNull(defaultBinding.getSniRequired());
Assert.assertNull(defaultBinding.getSslAutoReload());
}

@Test
Expand Down
1 change: 1 addition & 0 deletions artemis-web/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
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;
Expand All @@ -51,8 +53,10 @@
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;
Expand All @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -258,6 +277,10 @@ private ServerConnector createServerConnector(HttpConfiguration httpConfiguratio
}
}
}
if (Boolean.TRUE.equals(binding.getSslAutoReload())) {
addStoreResourceScannerTask(binding.getKeyStorePath(), sslFactory);
addStoreResourceScannerTask(binding.getTrustStorePath(), sslFactory);
}

SslConnectionFactory sslConnectionFactory = new SslConnectionFactory(sslFactory, "HTTP/1.1");

Expand All @@ -269,7 +292,6 @@ private ServerConnector createServerConnector(HttpConfiguration httpConfiguratio
HttpConnectionFactory httpFactory = new HttpConnectionFactory(httpConfiguration);

connector = new ServerConnector(server, sslConnectionFactory, httpFactory);

} else {
httpConfiguration.setSendServerVersion(false);
ConnectionFactory connectionFactory = new HttpConnectionFactory(httpConfiguration);
Expand All @@ -281,6 +303,75 @@ private ServerConnector createServerConnector(HttpConfiguration httpConfiguratio
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;
Expand Down Expand Up @@ -413,6 +504,9 @@ public synchronized void stop(boolean isShutdown) throws Exception {
ActiveMQWebLogger.LOGGER.stoppingEmbeddedWebServer();
server.stop();
server = null;
scanner = null;
scannerScheduler = null;
scannerTasks.clear();
cleanupWebTemporaryFiles(webContextData);
webContextData.clear();
jolokiaUrls.clear();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,14 +32,17 @@
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;
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;
Expand Down Expand Up @@ -75,6 +80,7 @@
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;
Expand All @@ -97,12 +103,17 @@
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");

Expand Down Expand Up @@ -289,6 +300,7 @@ private WebServerComponent startSimpleSecureServer(BindingDTO bindingDTO) throws
webServerDTO.setBindings(Collections.singletonList(bindingDTO));
webServerDTO.path = "webapps";
webServerDTO.webContentEnabled = true;
webServerDTO.setScanPeriod(1);

WebServerComponent webServerComponent = new WebServerComponent();
Assert.assertFalse(webServerComponent.isStarted());
Expand Down Expand Up @@ -416,12 +428,67 @@ private void testSimpleSecureServerWithSniRequired(Boolean enabled) throws Excep
}
}

@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);
Expand Down
7 changes: 7 additions & 0 deletions docs/user-manual/web-server.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
Loading

0 comments on commit 6081666

Please sign in to comment.