QPID-8306: [Broker-J] Add operation to update port TLS support
This closes #31
diff --git a/broker-core/src/main/java/org/apache/qpid/server/model/Port.java b/broker-core/src/main/java/org/apache/qpid/server/model/Port.java
index 3850afd..510d4d5 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/model/Port.java
+++ b/broker-core/src/main/java/org/apache/qpid/server/model/Port.java
@@ -24,6 +24,8 @@
import java.util.List;
import java.util.Set;
+import javax.net.ssl.SSLContext;
+
import com.google.common.util.concurrent.ListenableFuture;
import org.apache.qpid.server.configuration.CommonProperties;
@@ -105,6 +107,8 @@
+ "hostname. If null or * then bind to all interfaces.")
String getBindingAddress();
+ SSLContext getSSLContext();
+
@ManagedAttribute
boolean getNeedClientAuth();
@@ -133,5 +137,14 @@
SubjectCreator getSubjectCreator(final boolean secure, String host);
+ @DerivedAttribute(description = "Indicates whether TLS transport support is created.")
+ boolean isTlsSupported();
+ @ManagedOperation(description =
+ "Updates port TLS support without affecting existing connections."
+ + " The TLS changes are applied to new connections only."
+ + " Returns true if TLS support is successfully updated.",
+ nonModifying = true,
+ changesConfiguredObjectState = false)
+ boolean updateTLS();
}
diff --git a/broker-core/src/main/java/org/apache/qpid/server/model/port/AbstractPort.java b/broker-core/src/main/java/org/apache/qpid/server/model/port/AbstractPort.java
index 45efb41..a5fb3d2 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/model/port/AbstractPort.java
+++ b/broker-core/src/main/java/org/apache/qpid/server/model/port/AbstractPort.java
@@ -504,4 +504,22 @@
return getCategoryClass().getSimpleName() + "[id=" + getId() + ", name=" + getName() + ", type=" + getType() + ", port=" + getPort() + "]";
}
+ @Override
+ public boolean isTlsSupported()
+ {
+ return getSSLContext() != null;
+ }
+
+ @Override
+ public boolean updateTLS()
+ {
+ if (isTlsSupported())
+ {
+ return updateSSLContext();
+ }
+ return false;
+ }
+
+ protected abstract boolean updateSSLContext();
+
}
diff --git a/broker-core/src/main/java/org/apache/qpid/server/model/port/AmqpPort.java b/broker-core/src/main/java/org/apache/qpid/server/model/port/AmqpPort.java
index c1144d1..f88b99a 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/model/port/AmqpPort.java
+++ b/broker-core/src/main/java/org/apache/qpid/server/model/port/AmqpPort.java
@@ -24,8 +24,6 @@
import java.util.List;
import java.util.Set;
-import javax.net.ssl.SSLContext;
-
import org.apache.qpid.server.model.DerivedAttribute;
import org.apache.qpid.server.model.ManagedAttribute;
import org.apache.qpid.server.model.ManagedContextDefault;
@@ -132,8 +130,6 @@
description = "The connection property enrichers to apply to connections created on this port.")
String DEFAULT_CONNECTION_PROTOCOL_ENRICHERS = "[ \"STANDARD\" ] ";
- SSLContext getSSLContext();
-
@ManagedAttribute( defaultValue = AmqpPort.DEFAULT_AMQP_TCP_NO_DELAY )
boolean isTcpNoDelay();
diff --git a/broker-core/src/main/java/org/apache/qpid/server/model/port/AmqpPortImpl.java b/broker-core/src/main/java/org/apache/qpid/server/model/port/AmqpPortImpl.java
index 4e4fc64..88fc19c 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/model/port/AmqpPortImpl.java
+++ b/broker-core/src/main/java/org/apache/qpid/server/model/port/AmqpPortImpl.java
@@ -98,8 +98,8 @@
private final Container<?> _container;
private final AtomicBoolean _closingOrDeleting = new AtomicBoolean();
- private AcceptingTransport _transport;
- private SSLContext _sslContext;
+ private volatile AcceptingTransport _transport;
+ private volatile SSLContext _sslContext;
private volatile int _connectionWarnCount;
private volatile long _protocolHandshakeTimeout;
private volatile int _boundPort = -1;
@@ -275,6 +275,18 @@
}
@Override
+ protected boolean updateSSLContext()
+ {
+ final Set<Transport> transports = getTransports();
+ if (transports.contains(Transport.SSL) || transports.contains(Transport.WSS))
+ {
+ _sslContext = createSslContext();
+ return _transport.updatesSSLContext();
+ }
+ return false;
+ }
+
+ @Override
protected ListenableFuture<Void> beforeClose()
{
_closingOrDeleting.set(true);
diff --git a/broker-core/src/main/java/org/apache/qpid/server/model/port/HttpPortImpl.java b/broker-core/src/main/java/org/apache/qpid/server/model/port/HttpPortImpl.java
index 21b4c26..a3d714d 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/model/port/HttpPortImpl.java
+++ b/broker-core/src/main/java/org/apache/qpid/server/model/port/HttpPortImpl.java
@@ -23,12 +23,15 @@
import java.util.Map;
import java.util.Set;
+import javax.net.ssl.SSLContext;
+
import org.apache.qpid.server.configuration.IllegalConfigurationException;
import org.apache.qpid.server.model.ConfiguredObject;
import org.apache.qpid.server.model.Container;
import org.apache.qpid.server.model.ManagedAttributeField;
import org.apache.qpid.server.model.ManagedObjectFactoryConstructor;
import org.apache.qpid.server.model.State;
+import org.apache.qpid.server.model.Transport;
public class HttpPortImpl extends AbstractPort<HttpPortImpl> implements HttpPort<HttpPortImpl>
{
@@ -66,7 +69,8 @@
@Override
public int getBoundPort()
{
- return _portManager == null ? -1 : _portManager.getBoundPort(this);
+ final PortManager portManager = getPortManager();
+ return portManager == null ? -1 : portManager.getBoundPort(this);
}
@Override
@@ -108,13 +112,15 @@
@Override
public int getNumberOfAcceptors()
{
- return _portManager == null ? 0 : _portManager.getNumberOfAcceptors(this) ;
+ final PortManager portManager = getPortManager();
+ return portManager == null ? 0 : portManager.getNumberOfAcceptors(this) ;
}
@Override
public int getNumberOfSelectors()
{
- return _portManager == null ? 0 : _portManager.getNumberOfSelectors(this) ;
+ final PortManager portManager = getPortManager();
+ return portManager == null ? 0 : portManager.getNumberOfSelectors(this) ;
}
@Override
@@ -145,7 +151,7 @@
@Override
protected State onActivate()
{
- if(_portManager != null)
+ if(getPortManager() != null)
{
return super.onActivate();
}
@@ -156,6 +162,29 @@
}
@Override
+ public SSLContext getSSLContext()
+ {
+ final PortManager portManager = getPortManager();
+ return portManager == null ? null : portManager.getSSLContext(this);
+ }
+
+ @Override
+ protected boolean updateSSLContext()
+ {
+ if (getTransports().contains(Transport.SSL))
+ {
+ final PortManager portManager = getPortManager();
+ return portManager != null && portManager.updateSSLContext(this);
+ }
+ return false;
+ }
+
+ private PortManager getPortManager()
+ {
+ return _portManager;
+ }
+
+ @Override
public void onValidate()
{
super.onValidate();
diff --git a/broker-core/src/main/java/org/apache/qpid/server/model/port/PortManager.java b/broker-core/src/main/java/org/apache/qpid/server/model/port/PortManager.java
index b535a8b..3a06462 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/model/port/PortManager.java
+++ b/broker-core/src/main/java/org/apache/qpid/server/model/port/PortManager.java
@@ -20,6 +20,8 @@
*/
package org.apache.qpid.server.model.port;
+import javax.net.ssl.SSLContext;
+
public interface PortManager
{
int getBoundPort(HttpPort httpPort);
@@ -27,4 +29,8 @@
int getNumberOfAcceptors(HttpPort httpPort);
int getNumberOfSelectors(HttpPort httpPort);
+
+ SSLContext getSSLContext(HttpPort httpPort);
+
+ boolean updateSSLContext(HttpPort httpPort);
}
diff --git a/broker-core/src/main/java/org/apache/qpid/server/transport/AcceptingTransport.java b/broker-core/src/main/java/org/apache/qpid/server/transport/AcceptingTransport.java
index e8b7fda..bfefac1 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/transport/AcceptingTransport.java
+++ b/broker-core/src/main/java/org/apache/qpid/server/transport/AcceptingTransport.java
@@ -27,4 +27,6 @@
void close();
int getAcceptingPort();
+
+ boolean updatesSSLContext();
}
diff --git a/broker-core/src/main/java/org/apache/qpid/server/transport/NonBlockingNetworkTransport.java b/broker-core/src/main/java/org/apache/qpid/server/transport/NonBlockingNetworkTransport.java
index 02767f4..76f5977 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/transport/NonBlockingNetworkTransport.java
+++ b/broker-core/src/main/java/org/apache/qpid/server/transport/NonBlockingNetworkTransport.java
@@ -42,7 +42,7 @@
private static final Logger LOGGER = LoggerFactory.getLogger(NonBlockingNetworkTransport.class);
- private final Set<TransportEncryption> _encryptionSet;
+ private volatile Set<TransportEncryption> _encryptionSet;
private final MultiVersionProtocolEngineFactory _factory;
private final ServerSocketChannel _serverSocket;
private final NetworkConnectionScheduler _scheduler;
@@ -205,4 +205,9 @@
}
}
}
+
+ void setEncryptionSet(final Set<TransportEncryption> encryptionSet)
+ {
+ _encryptionSet = encryptionSet;
+ }
}
diff --git a/broker-core/src/main/java/org/apache/qpid/server/transport/TCPandSSLTransport.java b/broker-core/src/main/java/org/apache/qpid/server/transport/TCPandSSLTransport.java
index f52d50d..477d784 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/transport/TCPandSSLTransport.java
+++ b/broker-core/src/main/java/org/apache/qpid/server/transport/TCPandSSLTransport.java
@@ -32,7 +32,7 @@
class TCPandSSLTransport implements AcceptingTransport
{
private NonBlockingNetworkTransport _networkTransport;
- private Set<Transport> _transports;
+ private volatile Set<Transport> _transports;
private AmqpPort<?> _port;
private Set<Protocol> _supported;
private Protocol _defaultSupportedProtocolReply;
@@ -60,15 +60,7 @@
_port,
_transports.contains(Transport.TCP) ? Transport.TCP : Transport.SSL);
- EnumSet<TransportEncryption> encryptionSet = EnumSet.noneOf(TransportEncryption.class);
- if(_transports.contains(Transport.TCP))
- {
- encryptionSet.add(TransportEncryption.NONE);
- }
- if(_transports.contains(Transport.SSL))
- {
- encryptionSet.add(TransportEncryption.TLS);
- }
+ EnumSet<TransportEncryption> encryptionSet = buildEncryptionSet(_transports);
long threadPoolKeepAliveTimeout = _port.getContextValue(Long.class, AmqpPort.PORT_AMQP_THREAD_POOL_KEEP_ALIVE_TIMEOUT);
@@ -80,6 +72,20 @@
_networkTransport.start();
}
+ private EnumSet<TransportEncryption> buildEncryptionSet(final Set<Transport> transports)
+ {
+ EnumSet<TransportEncryption> encryptionSet = EnumSet.noneOf(TransportEncryption.class);
+ if(transports.contains(Transport.TCP))
+ {
+ encryptionSet.add(TransportEncryption.NONE);
+ }
+ if(transports.contains(Transport.SSL))
+ {
+ encryptionSet.add(TransportEncryption.TLS);
+ }
+ return encryptionSet;
+ }
+
@Override
public int getAcceptingPort()
{
@@ -88,6 +94,15 @@
}
@Override
+ public boolean updatesSSLContext()
+ {
+ Set<Transport> transports = _port.getTransports();
+ _transports = transports;
+ _networkTransport.setEncryptionSet(buildEncryptionSet(transports));
+ return true;
+ }
+
+ @Override
public void close()
{
if (_networkTransport != null)
diff --git a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/HttpManagement.java b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/HttpManagement.java
index b76ab9d..4fa87ed 100644
--- a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/HttpManagement.java
+++ b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/HttpManagement.java
@@ -181,6 +181,7 @@
private boolean _compressResponses;
private final Map<HttpPort<?>, ServerConnector> _portConnectorMap = new ConcurrentHashMap<>();
+ private final Map<HttpPort<?>, SslContextFactory> _sslContextFactoryMap = new ConcurrentHashMap<>();
private final BrokerChangeListener _brokerChangeListener = new BrokerChangeListener();
private volatile boolean _serveUncompressedDojo;
@@ -459,6 +460,46 @@
}
}
+ @Override
+ public SSLContext getSSLContext(final HttpPort httpPort)
+ {
+ final SslContextFactory sslContextFactory = getSslContextFactory(httpPort);
+ if ( sslContextFactory != null)
+ {
+ return sslContextFactory.getSslContext();
+ }
+ return null;
+ }
+
+ @Override
+ public boolean updateSSLContext(final HttpPort httpPort)
+ {
+ final SslContextFactory sslContextFactory = getSslContextFactory(httpPort);
+ if ( sslContextFactory != null)
+ {
+ try
+ {
+ final SSLContext sslContext = createSslContext(httpPort);
+ sslContextFactory.reload(f -> {
+ f.setSslContext(sslContext);
+ f.setNeedClientAuth(httpPort.getNeedClientAuth());
+ f.setWantClientAuth(httpPort.getWantClientAuth());
+ });
+ return true;
+ }
+ catch (Exception e)
+ {
+ throw new IllegalConfigurationException("Unexpected exception on reload of ssl context factory", e);
+ }
+ }
+ return false;
+ }
+
+ private SslContextFactory getSslContextFactory(final HttpPort httpPort)
+ {
+ return _sslContextFactoryMap.get(httpPort);
+ }
+
private ServerConnector createConnector(final HttpPort<?> port, final Server server)
{
port.setPortManager(this);
@@ -481,13 +522,14 @@
ConnectionFactory[] connectionFactories;
Collection<Transport> transports = port.getTransports();
+ SslContextFactory sslContextFactory = null;
if (!transports.contains(Transport.SSL))
{
connectionFactories = new ConnectionFactory[]{httpConnectionFactory};
}
else if (transports.contains(Transport.SSL))
{
- SslContextFactory sslContextFactory = getSslContextFactory(port);
+ sslContextFactory = createSslContextFactory(port);
ConnectionFactory sslConnectionFactory;
if (port.getTransports().contains(Transport.TCP))
{
@@ -523,6 +565,7 @@
}
catch (BindException e)
{
+ _sslContextFactoryMap.remove(port);
InetSocketAddress addr = getHost() == null ? new InetSocketAddress(getPort())
: new InetSocketAddress(getHost(), getPort());
throw new PortBindFailureException(addr);
@@ -589,40 +632,16 @@
acceptors,
selectors));
}
+ if (sslContextFactory != null)
+ {
+ _sslContextFactoryMap.put(port, sslContextFactory);
+ }
return connector;
}
- private SslContextFactory getSslContextFactory(final HttpPort<?> port)
+ private SslContextFactory createSslContextFactory(final HttpPort<?> port)
{
- KeyStore keyStore = port.getKeyStore();
- if (keyStore == null)
- {
- throw new IllegalConfigurationException(
- "Key store is not configured. Cannot start management on HTTPS port without keystore");
- }
-
- boolean needClientCert = port.getNeedClientAuth() || port.getWantClientAuth();
- Collection<TrustStore> trustStores = port.getTrustStores();
-
- if (needClientCert && trustStores.isEmpty())
- {
- throw new IllegalConfigurationException(String.format(
- "Client certificate authentication is enabled on HTTPS port '%s' but no trust store defined",
- this.getName()));
- }
-
- SSLContext sslContext = SSLUtil.createSslContext(keyStore, trustStores, port.getName());
- SSLSessionContext serverSessionContext = sslContext.getServerSessionContext();
- if (port.getTLSSessionCacheSize() > 0)
- {
- serverSessionContext.setSessionCacheSize(port.getTLSSessionCacheSize());
- }
- if (port.getTLSSessionTimeout() > 0)
- {
- serverSessionContext.setSessionTimeout(port.getTLSSessionTimeout());
- }
-
SslContextFactory factory = new SslContextFactory()
{
@Override
@@ -644,7 +663,7 @@
port.getTlsProtocolBlackList());
}
};
- factory.setSslContext(sslContext);
+ factory.setSslContext(createSslContext(port));
if (port.getNeedClientAuth())
{
factory.setNeedClientAuth(true);
@@ -656,6 +675,38 @@
return factory;
}
+ private SSLContext createSslContext(final HttpPort<?> port)
+ {
+ KeyStore keyStore = port.getKeyStore();
+ if (keyStore == null)
+ {
+ throw new IllegalConfigurationException(
+ "Key store is not configured. Cannot start management on HTTPS port without keystore");
+ }
+
+ final boolean needClientCert = port.getNeedClientAuth() || port.getWantClientAuth();
+ final Collection<TrustStore> trustStores = port.getTrustStores();
+
+ if (needClientCert && trustStores.isEmpty())
+ {
+ throw new IllegalConfigurationException(String.format(
+ "Client certificate authentication is enabled on HTTPS port '%s' but no trust store defined",
+ this.getName()));
+ }
+
+ final SSLContext sslContext = SSLUtil.createSslContext(port.getKeyStore(), trustStores, port.getName());
+ final SSLSessionContext serverSessionContext = sslContext.getServerSessionContext();
+ if (port.getTLSSessionCacheSize() > 0)
+ {
+ serverSessionContext.setSessionCacheSize(port.getTLSSessionCacheSize());
+ }
+ if (port.getTLSSessionTimeout() > 0)
+ {
+ serverSessionContext.setSessionTimeout(port.getTLSSessionTimeout());
+ }
+ return sslContext;
+ }
+
private void addRestServlet(final ServletContextHandler root)
{
final Map<String, ManagementControllerFactory> factories = ManagementControllerFactory.loadFactories();
@@ -934,6 +985,7 @@
Server server = _server;
if (server != null)
{
+ _sslContextFactoryMap.remove(port);
final ServerConnector connector = _portConnectorMap.remove(port);
if (connector != null)
{
diff --git a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/Port.js b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/Port.js
index 153c3f7..44c6675 100644
--- a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/Port.js
+++ b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/Port.js
@@ -72,6 +72,11 @@
that.showEditDialog();
});
+ that.updateTLSButton = registry.byNode(query(".updateTLSButton", contentPane.containerNode)[0]);
+ that.updateTLSButton.on("click", function (evt) {
+ that.updateTLS();
+ });
+
that.portUpdater.update(function ()
{
updater.add(that.portUpdater);
@@ -116,6 +121,28 @@
}, util.xhrErrorHandler);
};
+ Port.prototype.updateTLS = function ()
+ {
+ if (confirm("Are you sure you want to update TLS?"))
+ {
+ this.updateTLSButton.set("disabled", true);
+ var that = this;
+ this.management.update({parent: this.modelObj, type: this.modelObj.type, name: "updateTLS"}, {})
+ .then(function (data)
+ {
+ that.updateTLSButton.set("disabled", false);
+ if (data)
+ {
+ alert("TLS was successfully updated.");
+ }
+ else
+ {
+ alert("TLS was not updated.");
+ }
+ });
+ }
+ };
+
function PortUpdater(portTab)
{
var that = this;
@@ -197,6 +224,7 @@
: "";
this.protocolsValue.innerHTML = printArray("protocols", this.portData);
this.transportsValue.innerHTML = printArray("transports", this.portData);
+ this.tabObject.updateTLSButton.set("disabled", !this.portData.tlsSupported);
this.bindingAddressValue.innerHTML =
this.portData["bindingAddress"] ? entities.encode(String(this.portData["bindingAddress"])) : "";
this.maxOpenConnectionsValue.innerHTML =
diff --git a/broker-plugins/management-http/src/main/java/resources/showPort.html b/broker-plugins/management-http/src/main/java/resources/showPort.html
index 883f3f9..6543080 100644
--- a/broker-plugins/management-http/src/main/java/resources/showPort.html
+++ b/broker-plugins/management-http/src/main/java/resources/showPort.html
@@ -113,6 +113,7 @@
<div class="clear"></div>
</div>
<div class="dijitDialogPaneActionBar">
+ <button data-dojo-type="dijit.form.Button" class="updateTLSButton" type="button">Update TLS</button>
<button data-dojo-type="dijit.form.Button" class="editPortButton" type="button">Edit</button>
<button data-dojo-type="dijit.form.Button" class="deletePortButton" type="button">Delete</button>
</div>
diff --git a/broker-plugins/websocket/src/main/java/org/apache/qpid/server/transport/websocket/WebSocketProvider.java b/broker-plugins/websocket/src/main/java/org/apache/qpid/server/transport/websocket/WebSocketProvider.java
index 8394ca8..2d4e494 100644
--- a/broker-plugins/websocket/src/main/java/org/apache/qpid/server/transport/websocket/WebSocketProvider.java
+++ b/broker-plugins/websocket/src/main/java/org/apache/qpid/server/transport/websocket/WebSocketProvider.java
@@ -65,6 +65,7 @@
import org.slf4j.LoggerFactory;
import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.configuration.IllegalConfigurationException;
import org.apache.qpid.server.model.Broker;
import org.apache.qpid.server.model.Protocol;
import org.apache.qpid.server.model.Transport;
@@ -87,7 +88,7 @@
private static final String AMQP_WEBSOCKET_SUBPROTOCOL = "amqp";
private final Transport _transport;
- private final SSLContext _sslContext;
+ private final SslContextFactory _sslContextFactory;
private final AmqpPort<?> _port;
private final Broker<?> _broker;
private final Set<Protocol> _supported;
@@ -108,7 +109,7 @@
final Protocol defaultSupportedProtocolReply)
{
_transport = transport;
- _sslContext = sslContext;
+ _sslContextFactory = transport == Transport.WSS ? createSslContextFactory(port) : null;
_port = port;
_broker = ((Broker<?>) port.getParent());
_supported = supported;
@@ -142,29 +143,7 @@
}
else if (_transport == Transport.WSS)
{
- SslContextFactory sslContextFactory = new SslContextFactory()
- {
- @Override
- public void customize(final SSLEngine sslEngine)
- {
- super.customize(sslEngine);
- SSLUtil.updateEnabledCipherSuites(sslEngine, _port.getTlsCipherSuiteWhiteList(), _port.getTlsCipherSuiteBlackList());
- SSLUtil.updateEnabledTlsProtocols(sslEngine, _port.getTlsProtocolWhiteList(), _port.getTlsProtocolBlackList());
-
- if(_port.getTlsCipherSuiteWhiteList() != null
- && !_port.getTlsCipherSuiteWhiteList().isEmpty())
- {
- SSLParameters sslParameters = sslEngine.getSSLParameters();
- sslParameters.setUseCipherSuitesOrder(true);
- sslEngine.setSSLParameters(sslParameters);
- }
- }
- };
- sslContextFactory.setSslContext(_sslContext);
-
- sslContextFactory.setNeedClientAuth(_port.getNeedClientAuth());
- sslContextFactory.setWantClientAuth(_port.getWantClientAuth());
- connector = new ServerConnector(_server, sslContextFactory, httpConnectionFactory);
+ connector = new ServerConnector(_server, _sslContextFactory, httpConnectionFactory);
connector.addBean(new SslHandshakeListener()
{
@Override
@@ -270,6 +249,36 @@
}
+ private SslContextFactory createSslContextFactory(final AmqpPort<?> port)
+ {
+ SslContextFactory sslContextFactory = new SslContextFactory()
+ {
+ @Override
+ public void customize(final SSLEngine sslEngine)
+ {
+ super.customize(sslEngine);
+ SSLUtil.updateEnabledCipherSuites(sslEngine,
+ port.getTlsCipherSuiteWhiteList(),
+ port.getTlsCipherSuiteBlackList());
+ SSLUtil.updateEnabledTlsProtocols(sslEngine,
+ port.getTlsProtocolWhiteList(),
+ port.getTlsProtocolBlackList());
+
+ if (port.getTlsCipherSuiteWhiteList() != null
+ && !port.getTlsCipherSuiteWhiteList().isEmpty())
+ {
+ SSLParameters sslParameters = sslEngine.getSSLParameters();
+ sslParameters.setUseCipherSuitesOrder(true);
+ sslEngine.setSSLParameters(sslParameters);
+ }
+ }
+ };
+ sslContextFactory.setSslContext(port.getSSLContext());
+ sslContextFactory.setNeedClientAuth(port.getNeedClientAuth());
+ sslContextFactory.setWantClientAuth(port.getWantClientAuth());
+ return sslContextFactory;
+ }
+
@Override
public void close()
{
@@ -295,6 +304,28 @@
((ServerConnector) server.getConnectors()[0]).getLocalPort();
}
+ @Override
+ public boolean updatesSSLContext()
+ {
+ if (_sslContextFactory != null)
+ {
+ try
+ {
+ _sslContextFactory.reload(f -> {
+ f.setSslContext(_port.getSSLContext());
+ f.setNeedClientAuth(_port.getNeedClientAuth());
+ f.setWantClientAuth(_port.getWantClientAuth());
+ });
+ return true;
+ }
+ catch (Exception e)
+ {
+ throw new IllegalConfigurationException("Unexpected exception on reload of ssl context factory", e);
+ }
+ }
+ return false;
+ }
+
private static class QBBTrackingThreadPool extends QueuedThreadPool
{
private final ThreadFactory _threadFactory = QpidByteBuffer.createQpidByteBufferTrackingThreadFactory(r -> QBBTrackingThreadPool.super.newThread(r));
diff --git a/systests/qpid-systests-http-management/src/test/java/org/apache/qpid/tests/http/endtoend/port/PortTest.java b/systests/qpid-systests-http-management/src/test/java/org/apache/qpid/tests/http/endtoend/port/PortTest.java
new file mode 100644
index 0000000..320a73a
--- /dev/null
+++ b/systests/qpid-systests-http-management/src/test/java/org/apache/qpid/tests/http/endtoend/port/PortTest.java
@@ -0,0 +1,400 @@
+package org.apache.qpid.tests.http.endtoend.port;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static javax.servlet.http.HttpServletResponse.SC_CREATED;
+import static javax.servlet.http.HttpServletResponse.SC_OK;
+import static org.apache.qpid.test.utils.TestSSLConstants.JAVA_KEYSTORE_TYPE;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeThat;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import javax.jms.Connection;
+import javax.jms.JMSException;
+import javax.jms.Message;
+import javax.jms.MessageConsumer;
+import javax.jms.MessageProducer;
+import javax.jms.Session;
+import javax.jms.TextMessage;
+import javax.naming.NamingException;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.qpid.server.model.ConfiguredObject;
+import org.apache.qpid.server.model.Port;
+import org.apache.qpid.server.model.Protocol;
+import org.apache.qpid.server.model.Transport;
+import org.apache.qpid.server.security.NonJavaKeyStore;
+import org.apache.qpid.server.security.auth.manager.AnonymousAuthenticationManager;
+import org.apache.qpid.server.transport.network.security.ssl.SSLUtil;
+import org.apache.qpid.server.util.DataUrlUtils;
+import org.apache.qpid.systests.ConnectionBuilder;
+import org.apache.qpid.tests.http.HttpTestBase;
+import org.apache.qpid.tests.http.HttpTestHelper;
+
+public class PortTest extends HttpTestBase
+{
+ private static final String PASS = "changeit";
+ private static final String QUEUE_NAME = "testQueue";
+ private static final TypeReference<Boolean> BOOLEAN = new TypeReference<Boolean>()
+ {
+ };
+ private String _portName;
+ private String _authenticationProvider;
+ private String _keyStoreName;
+ private Set<File> _storeFiles;
+ private File _storeFile;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ assumeThat(SSLUtil.canGenerateCerts(), is(true));
+
+ _portName = getTestName();
+ _authenticationProvider = _portName + "AuthenticationProvider";
+ _keyStoreName = _portName + "KeyStore";
+ createAnonymousAuthenticationProvider();
+ final SSLUtil.KeyCertPair keyCertPair = createKeyStore(_keyStoreName);
+ final X509Certificate certificate = keyCertPair.getCertificate();
+
+ _storeFiles = new HashSet<>();
+ _storeFile = createTrustStore(certificate);
+
+ getBrokerAdmin().createQueue(QUEUE_NAME);
+ }
+
+
+ @After
+ public void tearDown()
+ {
+ _storeFiles.forEach(f -> assertTrue(f.delete()));
+ }
+
+ @Test
+ public void testSwapKeyStoreAndUpdateTlsOnAmqpPort() throws Exception
+ {
+ final int port = createPort(Transport.SSL);
+ final Connection connection = createConnection(port, _storeFile.getAbsolutePath());
+ try
+ {
+ final Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
+ final MessageProducer producer = session.createProducer(session.createQueue(QUEUE_NAME));
+ producer.send(session.createTextMessage("A"));
+
+ final SSLUtil.KeyCertPair keyCertPair = createKeyStoreAndUpdatePortTLS();
+ final File storeFile = createTrustStore(keyCertPair.getCertificate());
+ final Connection connection2 = createConnection(port, storeFile.getAbsolutePath());
+ try
+ {
+ producer.send(session.createTextMessage("B"));
+
+ final Session session2 = connection2.createSession(false, Session.AUTO_ACKNOWLEDGE);
+ final MessageConsumer consumer = session2.createConsumer(session2.createQueue(QUEUE_NAME));
+ connection2.start();
+
+ assertMessage(consumer.receive(getReceiveTimeout()), "A");
+ assertMessage(consumer.receive(getReceiveTimeout()), "B");
+ }
+ finally
+ {
+ connection2.close();
+ }
+ }
+ finally
+ {
+ connection.close();
+ }
+ }
+
+ @Test
+ public void testUpdateKeyStoreAndUpdateTlsOnAmqpPort() throws Exception
+ {
+ final int port = createPort(Transport.SSL);
+ final Connection connection = createConnection(port, _storeFile.getAbsolutePath());
+ try
+ {
+ final Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
+ final MessageProducer producer = session.createProducer(session.createQueue(QUEUE_NAME));
+ producer.send(session.createTextMessage("A"));
+
+ final SSLUtil.KeyCertPair keyCertPair = updateKeyStoreAndUpdatePortTLS();
+ final File storeFile = createTrustStore(keyCertPair.getCertificate());
+ final Connection connection2 = createConnection(port, storeFile.getAbsolutePath());
+ try
+ {
+ producer.send(session.createTextMessage("B"));
+
+ final Session session2 = connection2.createSession(false, Session.AUTO_ACKNOWLEDGE);
+ final MessageConsumer consumer = session2.createConsumer(session2.createQueue(QUEUE_NAME));
+ connection2.start();
+
+ assertMessage(consumer.receive(getReceiveTimeout()), "A");
+ assertMessage(consumer.receive(getReceiveTimeout()), "B");
+ }
+ finally
+ {
+ connection2.close();
+ }
+ }
+ finally
+ {
+ connection.close();
+ }
+ }
+
+ @Test
+ public void testSwapKeyStoreAndUpdateTlsOnWssPort() throws Exception
+ {
+ assumeThat(getProtocol(), is(equalTo(Protocol.AMQP_1_0)));
+ final int port = createPort(Transport.WSS);
+ final Connection connection = createConnectionBuilder(port, _storeFile.getAbsolutePath())
+ .setTransport("amqpws").build();
+ try
+ {
+ final Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
+ final MessageProducer producer = session.createProducer(session.createQueue(QUEUE_NAME));
+ producer.send(session.createTextMessage("A"));
+
+ final SSLUtil.KeyCertPair keyCertPair = createKeyStoreAndUpdatePortTLS();
+ final File storeFile = createTrustStore(keyCertPair.getCertificate());
+ final Connection connection2 = createConnectionBuilder(port, storeFile.getAbsolutePath())
+ .setTransport("amqpws").build();
+ try
+ {
+ producer.send(session.createTextMessage("B"));
+
+ final Session session2 = connection2.createSession(false, Session.AUTO_ACKNOWLEDGE);
+ final MessageConsumer consumer = session2.createConsumer(session2.createQueue(QUEUE_NAME));
+ connection2.start();
+
+ assertMessage(consumer.receive(getReceiveTimeout()), "A");
+ assertMessage(consumer.receive(getReceiveTimeout()), "B");
+ }
+ finally
+ {
+ connection2.close();
+ }
+ }
+ finally
+ {
+ connection.close();
+ }
+ }
+
+ @Test
+ public void testSwapKeyStoreAndUpdateTlsOnHttpPort() throws Exception
+ {
+ final int port = createHttpPort();
+
+ HttpTestHelper helper = new HttpTestHelper(getBrokerAdmin(), null, port);
+ helper.setTls(true);
+ helper.setKeyStore(_storeFile.getAbsolutePath(), PASS);
+
+ final Map<String, Object> attributes = getHelper().getJsonAsMap("port/" + _portName);
+ final Map<String, Object> ownAttributes = helper.getJsonAsMap("port/" + _portName);
+ assertEquals(attributes, ownAttributes);
+
+ final SSLUtil.KeyCertPair keyCertPair = createKeyStoreAndUpdatePortTLS();
+ final File storeFile = createTrustStore(keyCertPair.getCertificate());
+ helper.setKeyStore(storeFile.getAbsolutePath(), PASS);
+
+ final Map<String, Object> attributes2 = getHelper().getJsonAsMap("port/" + _portName);
+ final Map<String, Object> ownAttributes2 = helper.getJsonAsMap("port/" + _portName);
+ assertEquals(attributes2, ownAttributes2);
+ }
+
+ private void createAnonymousAuthenticationProvider() throws IOException
+ {
+ final Map<String, Object> data = Collections.singletonMap(ConfiguredObject.TYPE,
+ AnonymousAuthenticationManager.PROVIDER_TYPE);
+ getHelper().submitRequest("authenticationprovider/" + _authenticationProvider, "PUT", data, SC_CREATED);
+ }
+
+ private SSLUtil.KeyCertPair createKeyStore(final String keyStoreName) throws Exception
+ {
+ return submitKeyStoreAttributes(keyStoreName, SC_CREATED);
+ }
+
+ private SSLUtil.KeyCertPair submitKeyStoreAttributes(final String keyStoreName, final int status) throws Exception
+ {
+ final SSLUtil.KeyCertPair keyCertPair = generateSelfSignedCertificate();
+
+ final Map<String, Object> attributes = new HashMap<>();
+ attributes.put(NonJavaKeyStore.NAME, keyStoreName);
+ attributes.put(NonJavaKeyStore.PRIVATE_KEY_URL,
+ DataUrlUtils.getDataUrlForBytes(toPEM(keyCertPair.getPrivateKey()).getBytes(UTF_8)));
+ attributes.put(NonJavaKeyStore.CERTIFICATE_URL,
+ DataUrlUtils.getDataUrlForBytes(toPEM(keyCertPair.getCertificate()).getBytes(UTF_8)));
+ attributes.put(NonJavaKeyStore.TYPE, "NonJavaKeyStore");
+
+ getHelper().submitRequest("keystore/" + keyStoreName, "PUT", attributes, status);
+ return keyCertPair;
+ }
+
+ private ConnectionBuilder createConnectionBuilder(final int port, final String absolutePath)
+ {
+ return getConnectionBuilder().setPort(port)
+ .setTls(true)
+ .setVerifyHostName(false)
+ .setTrustStoreLocation(absolutePath)
+ .setTrustStorePassword(PASS);
+ }
+
+ private Connection createConnection(final int port, final String absolutePath)
+ throws NamingException, JMSException
+ {
+ return createConnectionBuilder(port, absolutePath).build();
+ }
+
+ private int createPort(final Transport transport) throws IOException
+ {
+ return createPort("AMQP", transport);
+ }
+
+ private int createHttpPort() throws IOException
+ {
+ return createPort("HTTP", Transport.SSL);
+ }
+
+ private int createPort(final String type, final Transport transport) throws IOException
+ {
+ final Map<String, Object> port = new HashMap<>();
+ port.put(Port.NAME, _portName);
+ port.put(Port.AUTHENTICATION_PROVIDER, _authenticationProvider);
+ port.put(Port.TYPE, type);
+ port.put(Port.PORT, 0);
+ port.put(Port.KEY_STORE, _keyStoreName);
+ port.put(Port.TRANSPORTS, Collections.singleton(transport));
+
+ getHelper().submitRequest("port/" + _portName, "PUT", port, SC_CREATED);
+
+ return getBoundPort();
+ }
+
+ private int getBoundPort() throws IOException
+ {
+ final Map<String, Object> attributes = getHelper().getJsonAsMap("port/" + _portName);
+ assertTrue(attributes.containsKey("boundPort"));
+ assertTrue(attributes.get("boundPort") instanceof Number);
+
+ return ((Number) attributes.get("boundPort")).intValue();
+ }
+
+ private File createTrustStore(final X509Certificate certificate)
+ throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException
+ {
+ final java.security.KeyStore ks = java.security.KeyStore.getInstance(JAVA_KEYSTORE_TYPE);
+ ks.load(null);
+ ks.setCertificateEntry("certificate", certificate);
+ final File storeFile = File.createTempFile(getTestName(), ".pkcs12");
+ try (FileOutputStream fos = new FileOutputStream(storeFile))
+ {
+ ks.store(fos, PASS.toCharArray());
+ }
+ finally
+ {
+ _storeFiles.add(storeFile);
+ }
+ return storeFile;
+ }
+
+ private SSLUtil.KeyCertPair generateSelfSignedCertificate() throws Exception
+ {
+ return SSLUtil.generateSelfSignedCertificate("RSA",
+ "SHA256WithRSA",
+ 2048,
+ Instant.now()
+ .minus(1, ChronoUnit.DAYS)
+ .toEpochMilli(),
+ Duration.of(365, ChronoUnit.DAYS)
+ .getSeconds(),
+ "CN=foo",
+ Collections.emptySet(),
+ Collections.emptySet());
+ }
+
+ private String toPEM(final Certificate pub) throws CertificateEncodingException
+ {
+ return toPEM(pub.getEncoded(), "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----");
+ }
+
+ private String toPEM(final PrivateKey key)
+ {
+ return toPEM(key.getEncoded(), "-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----");
+ }
+
+ private String toPEM(final byte[] bytes, final String header, final String footer)
+ {
+ StringBuilder pem = new StringBuilder();
+ pem.append(header).append("\n");
+ String base64encoded = Base64.getEncoder().encodeToString(bytes);
+ while (base64encoded.length() > 76)
+ {
+ pem.append(base64encoded, 0, 76).append("\n");
+ base64encoded = base64encoded.substring(76);
+ }
+ pem.append(base64encoded).append("\n");
+ pem.append(footer).append("\n");
+ return pem.toString();
+ }
+
+ private void assertMessage(final Message messageA, final String a) throws JMSException
+ {
+ assertThat(messageA, is(notNullValue()));
+ assertThat(messageA, is(instanceOf(TextMessage.class)));
+ assertThat(((TextMessage) messageA).getText(), is(equalTo(a)));
+ }
+
+ private SSLUtil.KeyCertPair createKeyStoreAndUpdatePortTLS() throws Exception
+ {
+ final SSLUtil.KeyCertPair keyCertPair = createKeyStore(_keyStoreName + "_2");
+ final Map<String, Object> data = Collections.singletonMap(Port.KEY_STORE, _keyStoreName + "_2");
+ getHelper().submitRequest("port/" + _portName, "POST", data, SC_OK);
+ final boolean response = getHelper().postJson("port/" + _portName + "/updateTLS",
+ Collections.emptyMap(),
+ BOOLEAN,
+ SC_OK);
+ assertTrue(response);
+
+ return keyCertPair;
+ }
+
+ private SSLUtil.KeyCertPair updateKeyStoreAndUpdatePortTLS() throws Exception
+ {
+ final SSLUtil.KeyCertPair keyCertPair = submitKeyStoreAttributes(_keyStoreName, SC_OK);
+ final boolean response = getHelper().postJson("port/" + _portName + "/updateTLS",
+ Collections.emptyMap(),
+ BOOLEAN,
+ SC_OK);
+ assertTrue(response);
+
+ return keyCertPair;
+ }
+}
diff --git a/systests/qpid-systests-jms-core/src/main/java/org/apache/qpid/systests/ConnectionBuilder.java b/systests/qpid-systests-jms-core/src/main/java/org/apache/qpid/systests/ConnectionBuilder.java
index 4f113b4..fe64610 100644
--- a/systests/qpid-systests-jms-core/src/main/java/org/apache/qpid/systests/ConnectionBuilder.java
+++ b/systests/qpid-systests-jms-core/src/main/java/org/apache/qpid/systests/ConnectionBuilder.java
@@ -68,4 +68,6 @@
Connection build() throws NamingException, JMSException;
ConnectionFactory buildConnectionFactory() throws NamingException;
String buildConnectionURL();
+
+ ConnectionBuilder setTransport(String transport);
}
diff --git a/systests/qpid-systests-jms-core/src/main/java/org/apache/qpid/systests/QpidJmsClient0xConnectionBuilder.java b/systests/qpid-systests-jms-core/src/main/java/org/apache/qpid/systests/QpidJmsClient0xConnectionBuilder.java
index 914cbe8..7935cb3 100644
--- a/systests/qpid-systests-jms-core/src/main/java/org/apache/qpid/systests/QpidJmsClient0xConnectionBuilder.java
+++ b/systests/qpid-systests-jms-core/src/main/java/org/apache/qpid/systests/QpidJmsClient0xConnectionBuilder.java
@@ -72,7 +72,7 @@
public ConnectionBuilder setPort(final int port)
{
_port = port;
- return this;
+ return setSslPort(port);
}
@Override
@@ -361,6 +361,12 @@
return cUrlBuilder.toString();
}
+ @Override
+ public ConnectionBuilder setTransport(final String transport)
+ {
+ throw new UnsupportedOperationException("Cannot modify transport");
+ }
+
private String buildTransportQuery()
{
final StringBuilder builder = new StringBuilder();
diff --git a/systests/qpid-systests-jms-core/src/main/java/org/apache/qpid/systests/QpidJmsClientConnectionBuilder.java b/systests/qpid-systests-jms-core/src/main/java/org/apache/qpid/systests/QpidJmsClientConnectionBuilder.java
index 6da37ca..c47e81e 100644
--- a/systests/qpid-systests-jms-core/src/main/java/org/apache/qpid/systests/QpidJmsClientConnectionBuilder.java
+++ b/systests/qpid-systests-jms-core/src/main/java/org/apache/qpid/systests/QpidJmsClientConnectionBuilder.java
@@ -51,6 +51,7 @@
private boolean _enableTls;
private boolean _enableFailover;
private final List<Integer> _failoverPorts = new ArrayList<>();
+ private String _transport = "amqp";
QpidJmsClientConnectionBuilder()
{
@@ -72,7 +73,7 @@
public ConnectionBuilder setPort(final int port)
{
_port = port;
- return this;
+ return setSslPort(port);
}
@Override
@@ -351,18 +352,25 @@
}
else if (!_enableTls)
{
- connectionUrlBuilder.append("amqp://").append(_host).append(":").append(_port);
+ connectionUrlBuilder.append(_transport).append("://").append(_host).append(":").append(_port);
appendOptions(options, connectionUrlBuilder);
}
else
{
- connectionUrlBuilder.append("amqps://").append(_host).append(":").append(_sslPort);
+ connectionUrlBuilder.append(_transport).append("s").append("://").append(_host).append(":").append(_sslPort);
appendOptions(options, connectionUrlBuilder);
}
return connectionUrlBuilder.toString();
}
+ @Override
+ public ConnectionBuilder setTransport(final String transport)
+ {
+ _transport = transport;
+ return this;
+ }
+
private void appendOptions(final Map<String, Object> actualOptions, final StringBuilder stem)
{
boolean first = true;