Commit 2f5191f0 authored by Guus der Kinderen's avatar Guus der Kinderen

OF-1373: Allow certificate changes to be applied without a restart.

This commit grew a little larger than anticipated - my apologies for the reviewer. The original intend
for this was to allow changes in the java Keystore to be applied, without requiring a restart of Openfire.
This is what allows outside processes to update the Keystores - think Let's Encrypt - but could also be
useful for other purposes.

In the process of writing this code, I've found that a restart of the Connection Acceptor would still cause
all existing connections to be terminated. This is why a 'reload configuration' method was added.

I've found that the old Certificate Event Listener methods were unuseful. They were pretty detailled, but
all implementations used them in the same way: "reload everything". I've replaced those with one
'something changed' event. Also, the event listeners are no longer triggered in various places in the code.
Instead, the event listeners will now be triggered by the filesystem-based change of the keystore - the same
event that's used to reload configuration when Let's Encrypt updates pop up.

I've removed various bits of lengthy, unused code in the old CertificateManager (primarily code that
interacts directly with KeyStores, as that caused timing issues during the reload).

One functional change (that was marked as 'unsure why we do this' in code) is that after this commit, the
content of the identity store is no longer merged with the content of the trust store, while determining if
a particular end-entity certificate is to be trusted.
parent 539cbf6a
......@@ -44,6 +44,7 @@ import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.webapp.WebAppContext;
import org.jivesoftware.openfire.JMXManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.keystore.CertificateStore;
import org.jivesoftware.openfire.keystore.IdentityStore;
import org.jivesoftware.openfire.spi.ConnectionConfiguration;
import org.jivesoftware.openfire.spi.ConnectionManagerImpl;
......@@ -413,25 +414,9 @@ public class AdminConsolePlugin implements Plugin {
private class CertificateListener implements CertificateEventListener {
@Override
public void certificateCreated(KeyStore keyStore, String alias, X509Certificate cert) {
// If new certificate is RSA then (re)start the HTTPS service
if ("RSA".equals(cert.getPublicKey().getAlgorithm())) {
restartNeeded = true;
}
}
@Override
public void certificateDeleted(KeyStore keyStore, String alias) {
public void storeContentChanged( CertificateStore store )
{
restartNeeded = true;
}
@Override
public void certificateSigned(KeyStore keyStore, String alias,
List<X509Certificate> certificates) {
// If new certificate is RSA then (re)start the HTTPS service
if ("RSA".equals(certificates.get(0).getPublicKey().getAlgorithm())) {
restartNeeded = true;
}
}
}
}
......@@ -35,6 +35,7 @@ import org.eclipse.jetty.webapp.WebAppContext;
import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.JMXManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.keystore.CertificateStore;
import org.jivesoftware.openfire.keystore.IdentityStore;
import org.jivesoftware.openfire.spi.ConnectionConfiguration;
import org.jivesoftware.openfire.spi.ConnectionManagerImpl;
......@@ -866,24 +867,8 @@ public final class HttpBindManager implements CertificateEventListener, Property
}
@Override
public void certificateCreated(KeyStore keyStore, String alias, X509Certificate cert) {
// If new certificate is RSA then (re)start the HTTPS service
if ("RSA".equals(cert.getPublicKey().getAlgorithm())) {
restartServer();
}
}
@Override
public void certificateDeleted(KeyStore keyStore, String alias) {
public void storeContentChanged( CertificateStore store )
{
restartServer();
}
@Override
public void certificateSigned(KeyStore keyStore, String alias,
List<X509Certificate> certificates) {
// If new certificate is RSA then (re)start the HTTPS service
if ("RSA".equals(certificates.get(0).getPublicKey().getAlgorithm())) {
restartServer();
}
}
}
package org.jivesoftware.openfire.keystore;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.jivesoftware.util.CertificateManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -89,6 +90,7 @@ public abstract class CertificateStore
try ( final FileInputStream is = new FileInputStream( configuration.getFile() ) )
{
store.load( is, configuration.getPassword() );
CertificateManager.fireCertificateStoreChanged( this );
}
catch ( IOException | NoSuchAlgorithmException | CertificateException ex )
{
......@@ -143,7 +145,6 @@ public abstract class CertificateStore
* When the store does not contain an entry that matches the provided alias, this method does nothing.
*
* @param alias The alias for which to delete an entry (cannot be null or empty).
* @throws CertificateStoreConfigException
*/
public void delete( String alias ) throws CertificateStoreConfigException
{
......@@ -170,8 +171,6 @@ public abstract class CertificateStore
throw new CertificateStoreConfigException( "Unable to install a certificate into an identity store.", e );
}
// TODO: Notify listeners that a new certificate has been removed.
}
public KeyStore getStore()
......
......@@ -17,9 +17,9 @@ public class CertificateStoreConfiguration
protected final char[] password;
/**
* Creates a new instance.
* Creates a new instance.
*
* @param type The store type (jks, jceks, pkcs12, etc). Cannot be null or an empty string.
* @param type The store type (jks, jceks, pkcs12, etc). Cannot be null or an empty string.
* @param file The file-system based representation of the store (cannot be null).
* @param password the password used to check the integrity of the store, the password used to unlock the store, or null.
*/
......
......@@ -11,6 +11,7 @@ import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
......@@ -29,6 +30,8 @@ public class CertificateStoreManager extends BasicModule
private final ConcurrentMap<CertificateStoreConfiguration, IdentityStore> identityStores = new ConcurrentHashMap<>();
private final ConcurrentMap<CertificateStoreConfiguration, TrustStore> trustStores = new ConcurrentHashMap<>();
private CertificateStoreWatcher storeWatcher;
public CertificateStoreManager( )
{
super( "Certificate Store Manager" );
......@@ -39,7 +42,9 @@ public class CertificateStoreManager extends BasicModule
{
super.initialize( server );
for ( ConnectionType type : ConnectionType.values() )
storeWatcher = new CertificateStoreWatcher();
for ( final ConnectionType type : ConnectionType.values() )
{
try
{
......@@ -49,6 +54,7 @@ public class CertificateStoreManager extends BasicModule
{
final IdentityStore store = new IdentityStore( identityStoreConfiguration, false );
identityStores.put( identityStoreConfiguration, store );
storeWatcher.watch( store );
}
typeToIdentityStore.put( type, identityStoreConfiguration );
}
......@@ -65,6 +71,7 @@ public class CertificateStoreManager extends BasicModule
{
final TrustStore store = new TrustStore( trustStoreConfiguration, false );
trustStores.put( trustStoreConfiguration, store );
storeWatcher.watch( store );
}
typeToTrustStore.put( type, trustStoreConfiguration );
}
......@@ -78,6 +85,7 @@ public class CertificateStoreManager extends BasicModule
@Override
public synchronized void destroy()
{
storeWatcher.destroy();
typeToIdentityStore.clear();
typeToTrustStore.clear();
identityStores.clear();
......@@ -124,6 +132,7 @@ public class CertificateStoreManager extends BasicModule
// This constructor can throw an exception. If it does, the state of the manager should not have already changed.
final IdentityStore store = new IdentityStore( configuration, createIfAbsent );
identityStores.put( configuration, store );
storeWatcher.watch( store );
}
typeToIdentityStore.put( type, configuration );
......@@ -132,7 +141,11 @@ public class CertificateStoreManager extends BasicModule
// If the old store is not used by any other type, it can be shut down.
if ( oldConfig != null && !typeToIdentityStore.containsValue( oldConfig ) )
{
identityStores.remove( oldConfig );
final IdentityStore store = identityStores.remove( oldConfig );
if ( store != null )
{
storeWatcher.unwatch( store );
}
}
// Update all connection listeners that were using the old configuration.
......@@ -172,6 +185,7 @@ public class CertificateStoreManager extends BasicModule
// This constructor can throw an exception. If it does, the state of the manager should not have already changed.
final TrustStore store = new TrustStore( configuration, createIfAbsent );
trustStores.put( configuration, store );
storeWatcher.watch( store );
}
typeToTrustStore.put( type, configuration );
......@@ -180,7 +194,11 @@ public class CertificateStoreManager extends BasicModule
// If the old store is not used by any other type, it can be shut down.
if ( oldConfig != null && !typeToTrustStore.containsValue( oldConfig ) )
{
trustStores.remove( oldConfig );
final TrustStore store = trustStores.remove( oldConfig );
if ( store != null )
{
storeWatcher.unwatch( store );
}
}
// Update all connection listeners that were using the old configuration.
......
package org.jivesoftware.openfire.keystore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.*;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* Detects file-system based changes to (Java) keystores that back Openfire Certificate Stores, reloading them when
* needed.
*
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
*/
public class CertificateStoreWatcher
{
private static final Logger Log = LoggerFactory.getLogger( CertificateStoreWatcher.class );
private final Map<CertificateStore, Path> watchedStores = new HashMap<>();
private final Map<Path, WatchKey> watchedPaths = new HashMap<>();
private WatchService storeWatcher;
private ExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
public CertificateStoreWatcher()
{
try
{
storeWatcher = FileSystems.getDefault().newWatchService();
executorService.submit( new Runnable()
{
@Override
public void run()
{
while ( !executorService.isShutdown() )
{
final WatchKey key;
try
{
key = storeWatcher.poll( 5, TimeUnit.SECONDS );
}
catch ( InterruptedException e )
{
// Interrupted. Stop waiting
continue;
}
if ( key == null )
{
continue;
}
for ( final WatchEvent<?> event : key.pollEvents() )
{
final WatchEvent.Kind<?> kind = event.kind();
// An OVERFLOW event can occur regardless of what kind of events the watcher was configured for.
if ( kind == StandardWatchEventKinds.OVERFLOW )
{
continue;
}
synchronized ( watchedStores )
{
// The filename is the context of the event.
final WatchEvent<Path> ev = (WatchEvent<Path>) event;
final Path changedFile = ((Path) key.watchable()).resolve( ev.context() );
// Can't use the value from the 'watchedStores' map, as that's the parent dir, not the keystore file!
for ( final CertificateStore store : watchedStores.keySet() )
{
final Path storeFile = store.getConfiguration().getFile().toPath().normalize();
if ( storeFile.equals( changedFile ) )
{
Log.info( "A file system change was detected. A(nother) certificate store that is backed by file '{}' will be reloaded.", storeFile );
try
{
store.reload();
}
catch ( CertificateStoreConfigException e )
{
Log.warn( "An unexpected exception occurred while trying to reload a certificate store that is backed by file '{}'!", storeFile, e );
}
}
}
}
}
// Reset the key to receive further events.
key.reset();
}
}
});
}
catch ( UnsupportedOperationException e )
{
storeWatcher = null;
Log.info( "This file system does not support watching file system objects for changes and events. Changes to Openfire certificate stores made outside of Openfire might not be detected. A restart of Openfire might be required for these to be applied." );
}
catch ( IOException e )
{
storeWatcher = null;
Log.warn( "An exception occured while trying to create a service that monitors the Openfire certificate stores for changes. Changes to Openfire certificate stores made outside of Openfire might not be detected. A restart of Openfire might be required for these to be applied.", e );
}
}
/**
* Shuts down this watcher, releasing all resources.
*/
public void destroy()
{
if ( executorService != null )
{
executorService.shutdown();
}
synchronized ( watchedStores )
{
if ( storeWatcher != null )
{
try
{
storeWatcher.close();
}
catch ( IOException e )
{
Log.warn( "Unable to close the watcherservice that is watching for file system changes to certificate stores.", e );
}
}
}
}
/**
* Start watching the file that backs a Certificate Store for changes, reloading the Certificate Store when
* appropriate.
*
* This method does nothing when the file watching functionality is not supported by the file system.
*
* @param store The certificate store (cannot be null).
*/
public void watch( CertificateStore store )
{
if ( store == null )
{
throw new IllegalArgumentException( "Argument 'store' cannot be null." );
}
if ( storeWatcher == null )
{
return;
}
final Path dir = store.getConfiguration().getFile().toPath().normalize().getParent();
synchronized ( watchedStores )
{
watchedStores.put( store, dir );
// Watch the directory that contains the keystore, if we're not already watching it.
if ( !watchedPaths.containsKey( dir ) )
{
try
{
// Ignoring deletion events, as those changes should be applied via property value changes.
final WatchKey watchKey = dir.register( storeWatcher, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_CREATE );
watchedPaths.put( dir, watchKey );
}
catch ( Throwable t )
{
Log.warn( "Unable to add a watcher for a path that contains files that provides the backend storage for certificate stores. Changes to those files are unlikely to be picked up automatically. Path: {}", dir, t );
watchedStores.remove( store );
}
}
}
}
/**
* Stop watching the file that backs a Certificate Store for changes
*
* @param store The certificate store (cannot be null).
*/
public synchronized void unwatch( CertificateStore store )
{
if ( store == null )
{
throw new IllegalArgumentException( "Argument 'store' cannot be null." );
}
synchronized ( watchedStores )
{
watchedStores.remove( store );
final Path dir = store.getConfiguration().getFile().toPath().normalize().getParent();
// Check if there are any other stores being watched in the same directory.
if ( watchedStores.containsValue( dir ) )
{
return;
}
final WatchKey key = watchedPaths.remove( dir );
if ( key != null )
{
key.cancel();
}
}
}
}
......@@ -82,7 +82,140 @@ public class TrustStore extends CertificateStore
{
reload(); // re-initialize store.
}
}
// TODO Notify listeners that a new certificate has been added.
/**
* Decide whether or not to trust the given supplied certificate chain. For certain failures, we SHOULD generate
* an exception - revocations and the like, but we currently do not.
*
* @param chain an array of X509Certificate where the first one is the endEntityCertificate.
* @return true if the content of this trust store allows the chain to be trusted, otherwise false.
*/
public boolean isTrusted( Certificate chain[] )
{
return getEndEntityCertificate( chain ) != null;
}
/**
* Decide whether or not to trust the given supplied certificate chain, returning the
* End Entity Certificate in this case where it can, and null otherwise.
* A self-signed certificate will, for example, return null.
* For certain failures, we SHOULD generate an exception - revocations and the like,
* but we currently do not.
*
* @param chain an array of X509Certificate where the first one is the endEntityCertificate.
* @return trusted end-entity certificate, or null.
*/
public X509Certificate getEndEntityCertificate( Certificate chain[] )
{
if ( chain == null || chain.length == 0 )
{
return null;
}
final X509Certificate first = (X509Certificate) chain[ 0 ];
try
{
first.checkValidity();
}
catch ( CertificateException e )
{
Log.warn( "EE Certificate not valid: " + e.getMessage() );
return null;
}
if ( chain.length == 1 && first.getSubjectX500Principal().equals( first.getIssuerX500Principal() ) )
{
// Chain is single cert, and self-signed.
try
{
if ( store.getCertificateAlias( first ) != null )
{
// Interesting case: trusted self-signed cert.
return first;
}
}
catch ( KeyStoreException e )
{
Log.warn( "Keystore error while looking for self-signed cert; assuming untrusted." );
}
return null;
}
final List<Certificate> allCerts = new ArrayList<>();
try
{
// Add the trusted certs.
for ( Enumeration<String> aliases = store.aliases(); aliases.hasMoreElements(); )
{
String alias = aliases.nextElement();
if ( store.isCertificateEntry( alias ) )
{
X509Certificate cert = (X509Certificate) store.getCertificate( alias );
allCerts.add( cert );
}
}
// Finally, add all the certs in the chain:
allCerts.addAll( Arrays.asList( chain ) );
final CertStore cs = CertStore.getInstance( "Collection", new CollectionCertStoreParameters( allCerts ) );
final X509CertSelector selector = new X509CertSelector();
selector.setCertificate( first );
// / selector.setSubject(first.getSubjectX500Principal());
final PKIXBuilderParameters params = new PKIXBuilderParameters( store, selector );
params.addCertStore( cs );
params.setDate( new Date() );
params.setRevocationEnabled( false );
/* Code here is the right way to do things. */
final CertPathBuilder pathBuilder = CertPathBuilder.getInstance( CertPathBuilder.getDefaultType() );
final CertPath cp = pathBuilder.build( params ).getCertPath();
/*
* This section is an alternative to using CertPathBuilder which is not as complete (or safe), but will emit
* much better errors. If things break, swap around the code.
*
**** COMMENTED OUT. ****
*
final List<X509Certificate> ls = new ArrayList<>();
for ( final Certificate cert : chain )
{
ls.add( (X509Certificate) cert );
}
for ( X509Certificate last = ls.get( ls.size() - 1 );
!last.getIssuerX500Principal().equals( last.getSubjectX500Principal() );
last = ls.get( ls.size() - 1 ) )
{
final X509CertSelector sel = new X509CertSelector();
sel.setSubject( last.getIssuerX500Principal() );
ls.add( (X509Certificate) cs.getCertificates( sel ).toArray()[ 0 ] );
}
final CertPath cp = CertificateFactory.getInstance( "X.509" ).generateCertPath( ls );
****** END ALTERNATIVE. ****
*/
// Not entirely sure if I need to do this with CertPathBuilder. Can't hurt.
final CertPathValidator pathValidator = CertPathValidator.getInstance( "PKIX" );
pathValidator.validate( cp, params );
return (X509Certificate) cp.getCertificates().get( 0 );
}
catch ( CertPathBuilderException e )
{
Log.warn( "Path builder exception while validating certificate chain:", e );
}
catch ( CertPathValidatorException e )
{
Log.warn( "Path exception while validating certificate chain:", e );
}
catch ( Exception e )
{
Log.warn( "Unkown exception while validating certificate chain:" + e.getMessage() );
}
return null;
}
}
......@@ -26,6 +26,7 @@ import org.jivesoftware.openfire.XMPPServerInfo;
import org.jivesoftware.openfire.auth.AuthFactory;
import org.jivesoftware.openfire.auth.AuthToken;
import org.jivesoftware.openfire.keystore.CertificateStoreManager;
import org.jivesoftware.openfire.keystore.TrustStore;
import org.jivesoftware.openfire.lockout.LockOutManager;
import org.jivesoftware.openfire.sasl.Failure;
import org.jivesoftware.openfire.sasl.JiveSharedSecretSaslServer;
......@@ -40,7 +41,6 @@ import javax.security.sasl.Sasl;
import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer;
import javax.security.sasl.SaslServerFactory;
import java.security.KeyStore;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
......@@ -194,16 +194,11 @@ public class SASLAuthentication {
if (mech.equals("EXTERNAL")) {
boolean trustedCert = false;
if (session.isSecure()) {
final LocalClientSession localClientSession = (LocalClientSession)session;
final Connection connection = localClientSession.getConnection();
final KeyStore keyStore = connection.getConfiguration().getIdentityStore().getStore();
final KeyStore trustStore = connection.getConfiguration().getTrustStore().getStore();
final X509Certificate trusted = CertificateManager.getEndEntityCertificate(connection.getPeerCertificates(), keyStore, trustStore);
if (trusted != null) {
trustedCert = true;
}
final Connection connection = ( (LocalClientSession) session ).getConnection();
final TrustStore trustStore = connection.getConfiguration().getTrustStore();
trustedCert = trustStore.isTrusted( connection.getPeerCertificates() );
}
if (trustedCert == false) {
if ( !trustedCert ) {
continue; // Do not offer EXTERNAL.
}
}
......@@ -218,9 +213,8 @@ public class SASLAuthentication {
final Element result = DocumentHelper.createElement( new QName( "mechanisms", new Namespace( "", SASL_NAMESPACE ) ) );
if (session.isSecure()) {
final Connection connection = session.getConnection();
final KeyStore keyStore = connection.getConfiguration().getIdentityStore().getStore();
final KeyStore trustStore = session.getConnection().getConfiguration().getTrustStore().getStore();
final X509Certificate trusted = CertificateManager.getEndEntityCertificate( session.getConnection().getPeerCertificates(), keyStore, trustStore );
final TrustStore trustStore = connection.getConfiguration().getTrustStore();
final X509Certificate trusted = trustStore.getEndEntityCertificate( session.getConnection().getPeerCertificates() );
boolean haveTrustedCertificate = trusted != null;
if (trusted != null && session.getDefaultIdentity() != null) {
......@@ -409,9 +403,8 @@ public class SASLAuthentication {
public static boolean verifyCertificates(Certificate[] chain, String hostname, boolean isS2S) {
final CertificateStoreManager certificateStoreManager = XMPPServer.getInstance().getCertificateStoreManager();
final ConnectionType connectionType = isS2S ? ConnectionType.SOCKET_S2S : ConnectionType.SOCKET_C2S;
final KeyStore keyStore = certificateStoreManager.getIdentityStore( connectionType ).getStore();
final KeyStore trustStore = certificateStoreManager.getTrustStore( connectionType ).getStore();
final X509Certificate trusted = CertificateManager.getEndEntityCertificate( chain, keyStore, trustStore );
final TrustStore trustStore = certificateStoreManager.getTrustStore( connectionType );
final X509Certificate trusted = trustStore.getEndEntityCertificate( chain );
if (trusted != null) {
return verifyCertificate(trusted, hostname);
}
......
......@@ -2,6 +2,7 @@ package org.jivesoftware.openfire.sasl;
import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.auth.AuthorizationManager;
import org.jivesoftware.openfire.keystore.TrustStore;
import org.jivesoftware.openfire.session.LocalClientSession;
import org.jivesoftware.util.CertificateManager;
import org.slf4j.Logger;
......@@ -12,7 +13,6 @@ import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer;
import java.nio.charset.StandardCharsets;
import java.security.cert.Certificate;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
......@@ -64,9 +64,8 @@ public class ExternalClientSaslServer implements SaslServer
throw new SaslException( "No peer certificates." );
}
final KeyStore keyStore = connection.getConfiguration().getIdentityStore().getStore();
final KeyStore trustStore = connection.getConfiguration().getTrustStore().getStore();
final X509Certificate trusted = CertificateManager.getEndEntityCertificate( peerCertificates, keyStore, trustStore );
final TrustStore trustStore = connection.getConfiguration().getTrustStore();
final X509Certificate trusted = trustStore.getEndEntityCertificate( peerCertificates );
if ( trusted == null )
{
throw new SaslException( "Certificate chain of peer is not trusted." );
......
......@@ -367,12 +367,7 @@ public class LocalIncomingServerSession extends LocalServerSession implements In
if (chain == null || chain.length == 0) {
usingSelfSigned = true;
} else {
try {
usingSelfSigned = CertificateManager.isSelfSignedCertificate((X509Certificate) chain[0]);
} catch (KeyStoreException ex) {
Log.warn("Exception occurred while trying to determine whether local certificate is self-signed. Proceeding as if it is.", ex);
usingSelfSigned = true;
}
usingSelfSigned = CertificateManager.isSelfSignedCertificate((X509Certificate) chain[0]);
}
if (usingSelfSigned && ServerDialback.isEnabledForSelfSigned() && validatedDomains.isEmpty()) {
......
......@@ -10,7 +10,7 @@ package org.jivesoftware.openfire.spi;
*/
public abstract class ConnectionAcceptor
{
protected final ConnectionConfiguration configuration;
protected ConnectionConfiguration configuration;
/**
* Constructs a new instance which will accept new connections based on the provided configuration.
......@@ -37,7 +37,7 @@ public abstract class ConnectionAcceptor
* An invocation of this method on an instance that is already started should have no effect (to the extend that the
* instance should continue to accept connections without interruption or configuration changes).
*/
abstract void start();
abstract public void start();
/**
* Halts connection acceptation and gracefully releases resources.
......@@ -47,12 +47,25 @@ public abstract class ConnectionAcceptor
* Instances of this class do not support configuration changes (see class documentation). As a result, there is no
* requirement that an instance that is stopped after it was running can successfully be restarted.
*/
abstract void stop();
abstract public void stop();
/**
* Determines if this instance is currently in a state where it is actively serving connections.
*
* @return false when this instance is started and is currently being used to serve connections (otherwise true)
*/
abstract boolean isIdle();
abstract public boolean isIdle();
/**
* Reloads the acceptor configuration, without causing a disconnect of already established connections.
*
* A best-effort reload will be attempted. Configuration changes that require a restart of connections,
* such as the bind-address and port, will not be applied.
*
* The configuration that's provided will replace the existing configuration. A restart of the connection
* acceptor after this method was invoked will apply all configuration changes.
*
* @param configuration The configuration for connections to be accepted (cannot be null).
*/
abstract public void reconfigure( ConnectionConfiguration configuration );
}
......@@ -314,7 +314,23 @@ public class ConnectionListener
{
start(); // won't actually start anything if not enabled.
}
Log.debug( "Done restarting..." );
Log.info( "Done restarting..." );
}
/**
* Reconfigures the acceptor without breaking existing connections. Note that not all configuration changes
* can be applied. These changes will be applied after a restart.
*/
public synchronized void reloadConfiguration()
{
if ( connectionAcceptor == null )
{
return; // There's no point in reloading config of a stopped instance. Config will be reloaded when started.
}
Log.debug( "Reconfiguring..." );
connectionAcceptor.reconfigure( generateConnectionConfiguration() );
Log.info( "Reconfigured." );
}
/**
......
......@@ -27,6 +27,7 @@ import org.jivesoftware.openfire.container.BasicModule;
import org.jivesoftware.openfire.container.PluginManager;
import org.jivesoftware.openfire.container.PluginManagerListener;
import org.jivesoftware.openfire.http.HttpBindManager;
import org.jivesoftware.openfire.keystore.CertificateStore;
import org.jivesoftware.openfire.keystore.CertificateStoreManager;
import org.jivesoftware.openfire.net.SocketSendingTracker;
import org.jivesoftware.openfire.session.ConnectionSettings;
......@@ -40,8 +41,6 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.*;
public class ConnectionManagerImpl extends BasicModule implements ConnectionManager, CertificateEventListener, PropertyEventListener
......@@ -576,50 +575,22 @@ public class ConnectionManagerImpl extends BasicModule implements ConnectionMana
// Certificates events
// #####################################################################
public void certificateCreated(KeyStore keyStore, String alias, X509Certificate cert) {
// Note that all non-SSL listeners can be using TLS - these also need to be restarted.
for ( final ConnectionListener listener : getListeners() )
{
// TODO determine by purpose exactly what needs and what need not be restarted.
try
{
listener.restart();
}
catch ( RuntimeException ex )
{
Log.error( "An exception occurred while restarting listener " + listener + ". The reason for restart was a certificate store change.", ex );
}
}
}
public void certificateDeleted(KeyStore keyStore, String alias) {
// Note that all non-SSL listeners can be using TLS - these also need to be restarted.
for ( final ConnectionListener listener : getListeners() )
{
// TODO determine by purpose exactly what needs and what need not be restarted.
try
{
listener.restart();
}
catch ( RuntimeException ex )
{
Log.error( "An exception occurred while restarting listener " + listener + ". The reason for restart was a certificate store change.", ex );
}
}
}
public void certificateSigned(KeyStore keyStore, String alias, List<X509Certificate> certificates) {
@Override
public void storeContentChanged( CertificateStore store )
{
// Note that all non-SSL listeners can be using TLS - these also need to be restarted.
for ( final ConnectionListener listener : getListeners() )
{
// TODO determine by purpose exactly what needs and what need not be restarted.
try
{
listener.restart();
}
catch ( RuntimeException ex )
if ( listener.getIdentityStoreConfiguration().equals( store.getConfiguration() ) || listener.getTrustStoreConfiguration().equals( store.getConfiguration() ) )
{
Log.error( "An exception occurred while restarting listener " + listener + ". The reason for restart was a certificate store change.", ex );
try
{
listener.reloadConfiguration();
}
catch ( RuntimeException ex )
{
Log.error( "An exception occurred while reloading listener " + listener + ". The reason for the reload was a certificate store change.", ex );
}
}
}
}
......
......@@ -87,8 +87,16 @@ public class LegacyConnectionAcceptor extends ConnectionAcceptor
}
@Override
boolean isIdle()
public synchronized boolean isIdle()
{
return socketAcceptThread != null; // We're not tracking actual sessions. This is a best effort response.
}
@Override
public synchronized void reconfigure( ConnectionConfiguration configuration )
{
this.configuration = configuration;
// nothing can be reloaded in this implementation.
}
}
......@@ -27,6 +27,10 @@ import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import java.lang.management.ManagementFactory;
import java.net.InetSocketAddress;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
......@@ -169,11 +173,66 @@ class MINAConnectionAcceptor extends ConnectionAcceptor
* @return false when this instance is started and is currently being used to serve connections (otherwise true)
*/
@Override
public boolean isIdle()
public synchronized boolean isIdle()
{
return this.socketAcceptor != null && this.socketAcceptor.getManagedSessionCount() == 0;
}
@Override
public synchronized void reconfigure( ConnectionConfiguration configuration )
{
this.configuration = configuration;
if ( socketAcceptor == null )
{
return; // reconfig will occur when acceptor is started.
}
final DefaultIoFilterChainBuilder filterChain = socketAcceptor.getFilterChain();
if ( filterChain.contains( ConnectionManagerImpl.EXECUTOR_FILTER_NAME ) )
{
final ExecutorFilter executorFilter = (ExecutorFilter) filterChain.get( ConnectionManagerImpl.EXECUTOR_FILTER_NAME );
( (ThreadPoolExecutor) executorFilter.getExecutor()).setCorePoolSize( ( configuration.getMaxThreadPoolSize() / 4 ) + 1 );
( (ThreadPoolExecutor) executorFilter.getExecutor()).setMaximumPoolSize( ( configuration.getMaxThreadPoolSize() ) );
}
if ( configuration.getTlsPolicy() == Connection.TLSPolicy.legacyMode )
{
// add or replace TLS filter (that's used only for 'direct-TLS')
try
{
final SslFilter sslFilter = encryptionArtifactFactory.createServerModeSslFilter();
if ( filterChain.contains( ConnectionManagerImpl.TLS_FILTER_NAME ) )
{
filterChain.replace( ConnectionManagerImpl.TLS_FILTER_NAME, sslFilter );
}
else
{
filterChain.addAfter( ConnectionManagerImpl.EXECUTOR_FILTER_NAME, ConnectionManagerImpl.TLS_FILTER_NAME, sslFilter );
}
}
catch ( KeyManagementException | NoSuchAlgorithmException | UnrecoverableKeyException | KeyStoreException e )
{
Log.error( "An exception occurred while reloading the TLS configuration.", e );
}
}
else
{
// The acceptor is in 'startTLS' mode. Remove TLS filter (that's used only for 'direct-TLS')
if ( filterChain.contains( ConnectionManagerImpl.TLS_FILTER_NAME ) )
{
filterChain.remove( ConnectionManagerImpl.TLS_FILTER_NAME );
}
}
if ( configuration.getMaxBufferSize() > 0 )
{
socketAcceptor.getSessionConfig().setMaxReadBufferSize( configuration.getMaxBufferSize() );
Log.debug( "Throttling read buffer for connections to max={} bytes", configuration.getMaxBufferSize() );
}
}
public synchronized int getPort()
{
return configuration.getPort();
......
/*
* Copyright (C) 2004-2009 Jive Software. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.util;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.List;
/**
* Interface to listen for certificate events. Use
* the {@link CertificateManager#addListener(CertificateEventListener)} method to register for events.
*
* @author Gaston Dombiak
*/
public interface CertificateEventListener {
/**
* Event triggered when a new certificate is created.
*
* @param keyStore key store where the certificate has been added.
* @param alias the alias of the certificate in the keystore.
* @param cert the new certificate created.
*/
void certificateCreated(KeyStore keyStore, String alias, X509Certificate cert);
/**
* Event triggered when a certificate is being deleted from the keystore.
*
* @param keyStore key store where the certificate is being deleted.
* @param alias the alias of the certificate in the keystore.
*/
void certificateDeleted(KeyStore keyStore, String alias);
/**
* Event triggered when a certificate has been signed by a Certificate Authority.
*
* @param keyStore key store where the certificate is stored.
* @param alias the alias of the certificate in the keystore.
* @param certificates chain of certificates. First certificate in the list is the certificate
* being signed and last certificate in the list is the root certificate.
*/
void certificateSigned(KeyStore keyStore, String alias, List<X509Certificate> certificates);
}
/*
* Copyright (C) 2004-2009 Jive Software. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.util;
import org.jivesoftware.openfire.keystore.CertificateStore;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.List;
/**
* Interface to listen for certificate events. Use
* the {@link CertificateManager#addListener(CertificateEventListener)} method to register for events.
*
* @author Gaston Dombiak
*/
public interface CertificateEventListener {
/**
* Event triggered when the content of a certificate store was changed.
*
* @param store The store for which the content was changed.
*/
void storeContentChanged( CertificateStore store );
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment