Skip to content

Commit

Permalink
JGRP-2813 GossipRouter Support Certificate Reloading
Browse files Browse the repository at this point in the history
  • Loading branch information
belaban committed Jul 12, 2024
1 parent c072bc9 commit 068c305
Show file tree
Hide file tree
Showing 8 changed files with 543 additions and 58 deletions.
4 changes: 3 additions & 1 deletion src/org/jgroups/protocols/SSL_KEY_EXCHANGE.java
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,9 @@ public void init() throws Exception {
.trustStoreFileName(truststore_name)
.trustStorePassword(truststore_password.toCharArray())
.sslProtocol(ssl_protocol)
.provider(ssl_provider).getContext();
.provider(ssl_provider)
.build()
.sslContext();
if (client_ssl_ctx == null) {
client_ssl_ctx = sslContext;
client_ssl_ctx_reloader.setContext(client_ssl_ctx).setFactory(sslContextFactory);
Expand Down
1 change: 1 addition & 0 deletions src/org/jgroups/stack/GossipRouter.java
Original file line number Diff line number Diff line change
Expand Up @@ -954,6 +954,7 @@ public static void main(String[] args) throws Exception {
String type="";
if(tls.enabled()) {
tls.init();
tls.setWatcher(new FileWatcher());
SSLContext context=tls.createContext();
SocketFactory socket_factory=tls.createSocketFactory(context);
router.socketFactory(socket_factory);
Expand Down
105 changes: 105 additions & 0 deletions src/org/jgroups/util/FileWatcher.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package org.jgroups.util;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;

import org.jgroups.logging.Log;
import org.jgroups.logging.LogFactory;

public class FileWatcher implements Runnable, AutoCloseable {

static final Log LOG = LogFactory.getLog(FileWatcher.class);

public static final int SLEEP = 2_000;
private final Thread thread;
private final ConcurrentHashMap<Path, Watched> watched;
private boolean running = true;

public FileWatcher() {
watched = new ConcurrentHashMap<>();
thread = new Thread(this, "FileWatcher");
Runtime.getRuntime().addShutdownHook(new Thread(this::stop));
thread.start();
}

public void unwatch(Path path) {
watched.remove(path);
LOG.debug("Unwatched %s", path);
}

public void watch(Path path, Consumer<Path> callback) {
watched.compute(path, (k, w) -> {
if (w == null) {
w = new Watched();
try {
w.lastModified = Files.getLastModifiedTime(path).toMillis();
} catch (FileNotFoundException | NoSuchFileException e) {
w.lastModified = -1;
LOG.debug("File not found %s", path);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
w.watchers.add(callback);
return w;
});
LOG.debug("Watching %s", path);
}

@Override
public void run() {
while (running) {
try {
Thread.sleep(SLEEP);
} catch (InterruptedException e) {
return;
}
if (!running) {
return;
}
for (Map.Entry<Path, Watched> e : watched.entrySet()) {
Watched w = e.getValue();
try {
long lastModified = Files.getLastModifiedTime(e.getKey()).toMillis();
if (w.lastModified < lastModified) {
w.lastModified = lastModified;
for (Consumer<Path> c : w.watchers) {
c.accept(e.getKey());
}
}
} catch (FileNotFoundException | NoSuchFileException ex) {
w.lastModified = -1;
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}
}

public void stop() {
running = false;
try {
thread.join();
} catch (InterruptedException e) {
// Ignore
}
}

@Override
public void close() {
stop();
}

static class Watched {
long lastModified;
List<Consumer<Path>> watchers = new ArrayList<>(2);
}
}
101 changes: 101 additions & 0 deletions src/org/jgroups/util/ReloadingX509KeyManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package org.jgroups.util;

import java.io.Closeable;
import java.io.IOException;
import java.net.Socket;
import java.nio.file.Path;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;

import javax.net.ssl.SSLEngine;
import javax.net.ssl.X509ExtendedKeyManager;

import org.jgroups.logging.Log;
import org.jgroups.logging.LogFactory;

/**
* A {@link X509ExtendedKeyManager} which uses a @{@link FileWatcher} to check for changes.
*/
public final class ReloadingX509KeyManager extends X509ExtendedKeyManager implements Closeable {

static final Log LOG = LogFactory.getLog(ReloadingX509KeyManager.class);

private final AtomicReference<X509ExtendedKeyManager> manager;
private final Path path;
private final Function<Path, X509ExtendedKeyManager> action;
private final FileWatcher watcher;
private Instant lastLoaded;

public ReloadingX509KeyManager(FileWatcher watcher, Path path, Function<Path, X509ExtendedKeyManager> action) {
Objects.requireNonNull(watcher, "watcher must be non-null");
Objects.requireNonNull(path, "path must be non-null");
Objects.requireNonNull(action, "action must be non-null");

this.manager = new AtomicReference<>();
this.watcher = watcher;
this.path = path;
this.action = action;
reload(this.path);
watcher.watch(path, this::reload);
}

private void reload(Path path) {
manager.set(action.apply(path));
lastLoaded = Instant.now();
LOG.debug("Loaded '%s'", path);
}

@Override
public String[] getClientAliases(String keyType, Principal[] issuers) {
return manager.get().getClientAliases(keyType, issuers);
}

@Override
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
return manager.get().chooseClientAlias(keyType, issuers, socket);
}

@Override
public String[] getServerAliases(String keyType, Principal[] issuers) {
return manager.get().getServerAliases(keyType, issuers);
}

@Override
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
return manager.get().chooseServerAlias(keyType, issuers, socket);
}

@Override
public X509Certificate[] getCertificateChain(String alias) {
return manager.get().getCertificateChain(alias);
}

@Override
public PrivateKey getPrivateKey(String alias) {
return manager.get().getPrivateKey(alias);
}

@Override
public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) {
return manager.get().chooseEngineClientAlias(keyType, issuers, engine);
}

@Override
public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) {
return manager.get().chooseEngineServerAlias(keyType, issuers, engine);
}

public Instant lastLoaded() {
return lastLoaded;
}

@Override
public void close() throws IOException {
watcher.unwatch(path);
}
}
94 changes: 94 additions & 0 deletions src/org/jgroups/util/ReloadingX509TrustManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package org.jgroups.util;

import java.io.Closeable;
import java.io.IOException;
import java.net.Socket;
import java.nio.file.Path;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;

import javax.net.ssl.SSLEngine;
import javax.net.ssl.X509ExtendedTrustManager;

import org.jgroups.logging.Log;
import org.jgroups.logging.LogFactory;

/**
* A {@link X509ExtendedTrustManager} which uses a @{@link FileWatcher} to check for changes.
*/
public class ReloadingX509TrustManager extends X509ExtendedTrustManager implements Closeable {

static final Log LOG = LogFactory.getLog(ReloadingX509TrustManager.class);

private final AtomicReference<X509ExtendedTrustManager> manager;
private final Path path;
private final Function<Path, X509ExtendedTrustManager> action;
private final FileWatcher watcher;
private Instant lastLoaded;

public ReloadingX509TrustManager(FileWatcher watcher, Path path, Function<Path, X509ExtendedTrustManager> action) {
Objects.requireNonNull(watcher, "watcher must be non-null");
Objects.requireNonNull(path, "path must be non-null");
Objects.requireNonNull(action, "action must be non-null");
this.manager = new AtomicReference<>();
this.path = path;
this.action = action;
this.watcher = watcher;
reload(this.path);
watcher.watch(this.path, this::reload);
}

private void reload(Path path) {
manager.set(action.apply(path));
lastLoaded = Instant.now();
LOG.debug("Loaded '%s'", path);
}

@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
manager.get().checkClientTrusted(chain, authType);
}

@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
manager.get().checkServerTrusted(chain, authType);
}

@Override
public X509Certificate[] getAcceptedIssuers() {
return manager.get().getAcceptedIssuers();
}

@Override
public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException {
manager.get().checkClientTrusted(chain, authType, socket);
}

@Override
public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException {
manager.get().checkServerTrusted(chain, authType, socket);
}

@Override
public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException {
manager.get().checkClientTrusted(chain, authType, engine);
}

@Override
public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException {
manager.get().checkServerTrusted(chain, authType, engine);
}

public Instant lastLoaded() {
return lastLoaded;
}

@Override
public void close() throws IOException {
watcher.unwatch(path);
}
}
Loading

0 comments on commit 068c305

Please sign in to comment.