Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

James 3502, add ssl support #307

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fc942b8
JAMES-3502, add rabbitmq tls support
Feb 23, 2021
d2e1abf
JAMES-3502, add rabbitmq tls support in connection factory
Feb 23, 2021
a1fe208
JAMES-3502, update equals and hashCode methods on RabbitMQConfigurati…
Feb 24, 2021
686105f
JAMES-3502, update equals and hashCode methods on RabbitMQConfigurati…
Feb 24, 2021
90d8482
JAMES-3502, set keystore Optional.empty() as default (client cert aut…
Feb 24, 2021
193c938
JAMES-3502, move exception catching to top level, use key store passw…
Feb 24, 2021
5707b46
JAMES-3502, add ssl tests for RabbitMQConfiguration and RabbitMqConne…
Feb 24, 2021
16389c9
JAMES-3502, add configuration key "useSslManagement"
Feb 25, 2021
469d21e
JAMES-3502, refactor, add more tests
Feb 25, 2021
cabee16
JAMES-3502, add tls support in RabbitMQManagementAPI.java
Feb 25, 2021
2bdeedd
JAMES-3502, update documentation for rabbitmq configuration keys.
Feb 25, 2021
a30d665
JAMES-3502, rabbitmqmanagementapi refactor
Feb 25, 2021
0a5c078
JAMES-3502, update RabbitMQConfiguration documentation
Feb 25, 2021
3ee2612
JAMES-3502, fix checkstyle
Feb 25, 2021
45f98a4
JAMES-3502, add documentation in config-rabbitmq.xml
Feb 25, 2021
62bd3ee
JAMES-3502, add functional interface annotations
Feb 25, 2021
02606ab
JAMES-3502, update ssl.keystore documentation
Feb 25, 2021
3c0c552
JAMES-3502, update ssl.hostname.verifier and ssl.validation.strategy …
Feb 25, 2021
4c2ecd2
JAMES-3502, remove duplicate line, refactor method name
Mar 1, 2021
50b6372
JAMES-3502, use the version in root pom
Mar 1, 2021
c0461e2
JAMES-3502, use the version in root pom for httpcore library
Mar 2, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions backends-common/rabbitmq/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
</dependency>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you quickly explain why we need these dependencies? At first glance it seems unrelated to AMQP...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

httpcore, has useful class SSLContextBuilder
httpclient, is needed for configuring mangement api client for org.apache.http.conn.ssl.DefaultHostnameVerifier

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,26 @@
****************************************************************/
package org.apache.james.backends.rabbitmq;

import static org.apache.james.backends.rabbitmq.RabbitMQConfiguration.SSLConfiguration.HostNameVerifier;
import static org.apache.james.backends.rabbitmq.RabbitMQConfiguration.SSLConfiguration.SSLKeyStore;
import static org.apache.james.backends.rabbitmq.RabbitMQConfiguration.SSLConfiguration.SSLTrustStore;
import static org.apache.james.backends.rabbitmq.RabbitMQConfiguration.SSLConfiguration.SSLValidationStrategy;

import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.time.Duration;
import java.util.Optional;

import javax.inject.Inject;
import javax.net.ssl.SSLContext;

import org.apache.commons.lang3.NotImplementedException;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.TrustStrategy;

import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
Expand All @@ -31,14 +48,16 @@

public class RabbitMQConnectionFactory {

private static final TrustStrategy TRUST_ALL = (x509Certificates, authType) -> true;

private final ConnectionFactory connectionFactory;

private final RabbitMQConfiguration configuration;

@Inject
public RabbitMQConnectionFactory(RabbitMQConfiguration rabbitMQConfiguration) {
this.connectionFactory = from(rabbitMQConfiguration);
this.configuration = rabbitMQConfiguration;
this.connectionFactory = from(rabbitMQConfiguration);
}

private ConnectionFactory from(RabbitMQConfiguration rabbitMQConfiguration) {
Expand All @@ -47,25 +66,115 @@ private ConnectionFactory from(RabbitMQConfiguration rabbitMQConfiguration) {
connectionFactory.setUri(rabbitMQConfiguration.getUri());
connectionFactory.setHandshakeTimeout(rabbitMQConfiguration.getHandshakeTimeoutInMs());
connectionFactory.setShutdownTimeout(rabbitMQConfiguration.getShutdownTimeoutInMs());
connectionFactory.setChannelRpcTimeout(rabbitMQConfiguration.getChannelRpcTimeoutInMs());
connectionFactory.setConnectionTimeout(rabbitMQConfiguration.getConnectionTimeoutInMs());
connectionFactory.setNetworkRecoveryInterval(rabbitMQConfiguration.getNetworkRecoveryIntervalInMs());
connectionFactory
.setChannelRpcTimeout(rabbitMQConfiguration.getChannelRpcTimeoutInMs());
connectionFactory
.setConnectionTimeout(rabbitMQConfiguration.getConnectionTimeoutInMs());
connectionFactory
.setNetworkRecoveryInterval(rabbitMQConfiguration.getNetworkRecoveryIntervalInMs());

connectionFactory
.setUsername(rabbitMQConfiguration.getManagementCredentials().getUser());
connectionFactory.setPassword(
String.valueOf(rabbitMQConfiguration.getManagementCredentials().getPassword()));

connectionFactory.setUsername(rabbitMQConfiguration.getManagementCredentials().getUser());
connectionFactory.setPassword(String.valueOf(rabbitMQConfiguration.getManagementCredentials().getPassword()));
if (configuration.useSsl()) {
setupSslConfiguration(connectionFactory);
}

return connectionFactory;
} catch (Exception e) {
throw new RuntimeException(e);
}
}

private void setupSslConfiguration(ConnectionFactory connectionFactory)
throws CertificateException, UnrecoverableKeyException, NoSuchAlgorithmException,
KeyManagementException, KeyStoreException, IOException {
connectionFactory.useSslProtocol(sslContext(configuration));
setupHostNameVerification(connectionFactory);
}

private SSLContext sslContext(RabbitMQConfiguration configuration)
throws KeyManagementException, NoSuchAlgorithmException, CertificateException,
KeyStoreException, IOException, UnrecoverableKeyException {
SSLContextBuilder sslContextBuilder = new SSLContextBuilder();

RabbitMQConfiguration.SSLConfiguration sslConfiguration =
configuration.getSslConfiguration();

setupSslValidationStrategy(sslContextBuilder, sslConfiguration);

setupClientCertificateAuthentication(sslContextBuilder, sslConfiguration);

return sslContextBuilder.build();

}

private void setupClientCertificateAuthentication(SSLContextBuilder sslContextBuilder,
RabbitMQConfiguration.SSLConfiguration sslConfiguration)
throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException,
CertificateException, IOException {
Optional<SSLKeyStore> keyStore = sslConfiguration.getKeyStore();

if (keyStore.isPresent()) {
SSLKeyStore sslKeyStore = keyStore.get();

sslContextBuilder.loadKeyMaterial(sslKeyStore.getFile(), sslKeyStore.getPassword(),
sslKeyStore.getPassword());
}
}

private void setupSslValidationStrategy(SSLContextBuilder sslContextBuilder,
RabbitMQConfiguration.SSLConfiguration sslConfiguration)
throws NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException {
SSLValidationStrategy strategy = sslConfiguration
.getStrategy();

switch (strategy) {
case DEFAULT:
break;
case IGNORE:
sslContextBuilder.loadTrustMaterial(TRUST_ALL);
break;
case OVERRIDE:
applyTrustStore(sslContextBuilder);
break;
default:
throw new NotImplementedException(
String.format("unrecognized strategy '%s'", strategy.name()));
}
}

private SSLContextBuilder applyTrustStore(SSLContextBuilder sslContextBuilder)
throws CertificateException, NoSuchAlgorithmException,
KeyStoreException, IOException {

SSLTrustStore trustStore = configuration.getSslConfiguration()
.getTrustStore()
.orElseThrow(() -> new IllegalStateException("SSLTrustStore cannot to be empty"));

return sslContextBuilder
.loadTrustMaterial(trustStore.getFile(), trustStore.getPassword());
}

private void setupHostNameVerification(ConnectionFactory connectionFactory) {
HostNameVerifier hostNameVerifier = configuration.getSslConfiguration()
.getHostNameVerifier();

if (hostNameVerifier == HostNameVerifier.DEFAULT) {
connectionFactory.enableHostnameVerification();
}
}

Connection create() {
return connectionMono().block();
}

Mono<Connection> connectionMono() {
return Mono.fromCallable(connectionFactory::newConnection)
.retryWhen(Retry.backoff(configuration.getMaxRetries(), Duration.ofMillis(configuration.getMinDelayInMs())).scheduler(Schedulers.elastic()));
.retryWhen(Retry.backoff(configuration.getMaxRetries(),
Duration.ofMillis(configuration.getMinDelayInMs()))
.scheduler(Schedulers.elastic()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,32 @@

package org.apache.james.backends.rabbitmq;

import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;

import org.apache.commons.lang3.NotImplementedException;
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.TrustStrategy;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonValue;
import com.google.common.base.MoreObjects;

import feign.Client;
import feign.Feign;
import feign.Logger;
import feign.Param;
Expand Down Expand Up @@ -299,26 +315,119 @@ public final boolean equals(Object o) {

@Override
public final int hashCode() {
return Objects.hash(source, vhost, destination, destinationType, routingKey, arguments, propertiesKey);
return Objects.hash(source, vhost, destination, destinationType, routingKey, arguments,
propertiesKey);
}
}

static RabbitMQManagementAPI from(RabbitMQConfiguration configuration) {
RabbitMQConfiguration.ManagementCredentials credentials = configuration.getManagementCredentials();
return Feign.builder()
.requestInterceptor(new BasicAuthRequestInterceptor(credentials.getUser(), new String(credentials.getPassword())))
.logger(new Slf4jLogger(RabbitMQManagementAPI.class))
.logLevel(Logger.Level.FULL)
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.retryer(new Retryer.Default())
.errorDecoder(RETRY_500)
.target(RabbitMQManagementAPI.class, configuration.getManagementUri().toString());
try {
RabbitMQConfiguration.ManagementCredentials credentials =
configuration.getManagementCredentials();
RabbitMQManagementAPI rabbitMQManagementAPI = Feign.builder()
.client(getClient(configuration))
.requestInterceptor(new BasicAuthRequestInterceptor(credentials.getUser(),
new String(credentials.getPassword())))
.logger(new Slf4jLogger(RabbitMQManagementAPI.class))
.logLevel(Logger.Level.FULL)
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.retryer(new Retryer.Default())
.errorDecoder(RETRY_500)
.target(RabbitMQManagementAPI.class, configuration.getManagementUri().toString());

return rabbitMQManagementAPI;
} catch (KeyManagementException | NoSuchAlgorithmException | CertificateException | KeyStoreException | IOException | UnrecoverableKeyException e) {
throw new RuntimeException(e);
}
}

private static Client getClient(RabbitMQConfiguration configuration)
throws KeyManagementException, NoSuchAlgorithmException, CertificateException,
KeyStoreException, IOException, UnrecoverableKeyException {
if (configuration.useSslManagement()) {
SSLContextBuilder sslContextBuilder = new SSLContextBuilder();

setupSslValidationStrategy(sslContextBuilder, configuration);

setupClientCertificateAuthentication(sslContextBuilder, configuration);

SSLContext sslContext = sslContextBuilder.build();

return new Client.Default(sslContext.getSocketFactory(),
getHostNameVerifier(configuration));
} else {
return new Client.Default(null, null);
}

}

private static HostnameVerifier getHostNameVerifier(RabbitMQConfiguration configuration) {
switch (configuration.getSslConfiguration().getHostNameVerifier()) {
case ACCEPT_ANY_HOSTNAME:
return ((hostname, session) -> true);
default:
return new DefaultHostnameVerifier();
}
}

private static void setupClientCertificateAuthentication(SSLContextBuilder sslContextBuilder,
RabbitMQConfiguration configuration)
throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException,
CertificateException, IOException {
Optional<RabbitMQConfiguration.SSLConfiguration.SSLKeyStore> keyStore =
configuration.getSslConfiguration().getKeyStore();

if (keyStore.isPresent()) {
RabbitMQConfiguration.SSLConfiguration.SSLKeyStore sslKeyStore = keyStore.get();

sslContextBuilder.loadKeyMaterial(sslKeyStore.getFile(), sslKeyStore.getPassword(),
sslKeyStore.getPassword());
}
}

private static void setupSslValidationStrategy(SSLContextBuilder sslContextBuilder,
RabbitMQConfiguration configuration)
throws NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException {
RabbitMQConfiguration.SSLConfiguration.SSLValidationStrategy strategy = configuration
.getSslConfiguration()
.getStrategy();

final TrustStrategy TRUST_ALL = (x509Certificates, authType) -> true;

switch (strategy) {
case DEFAULT:
break;
case IGNORE:
sslContextBuilder.loadTrustMaterial(TRUST_ALL);
break;
case OVERRIDE:
applyTrustStore(sslContextBuilder, configuration);
break;
default:
throw new NotImplementedException(
String.format("unrecognized strategy '%s'", strategy.name()));
}
}

private static SSLContextBuilder applyTrustStore(SSLContextBuilder sslContextBuilder,
RabbitMQConfiguration configuration)
throws CertificateException, NoSuchAlgorithmException,
KeyStoreException, IOException {

RabbitMQConfiguration.SSLConfiguration.SSLTrustStore trustStore =
configuration.getSslConfiguration()
.getTrustStore()
.orElseThrow(() -> new IllegalStateException("SSLTrustStore cannot to be empty"));

return sslContextBuilder
.loadTrustMaterial(trustStore.getFile(), trustStore.getPassword());
}

ErrorDecoder RETRY_500 = (methodKey, response) -> {
if (response.status() == 500) {
throw new RetryableException(response.status(), "Error encountered, scheduling retry", response.request().httpMethod(), new Date());
throw new RetryableException(response.status(), "Error encountered, scheduling retry",
response.request().httpMethod(), new Date());
}
throw new RuntimeException("Non recoverable exception status: " + response.status());
};
Expand Down
Loading