Skip to content

Commit

Permalink
Add flag to enable subscription name validation
Browse files Browse the repository at this point in the history
JMS 3.1 Spec "A.3.16. Subscription name characters and length"
https://jakarta.ee/specifications/messaging/3.1/jakarta-messaging-spec-3.1.html#subscription-name-characters-and-length-2

The validation can be enabled with a flag, it's disabled by default,
to avoid breaking existing code.

The flag is RMQConnectionFactory#validateSubscriptionNames.

References #180

Fixes #203
  • Loading branch information
acogoluegnes committed Sep 27, 2022
1 parent 197fddd commit a073da5
Show file tree
Hide file tree
Showing 10 changed files with 155 additions and 1 deletion.
21 changes: 20 additions & 1 deletion src/main/java/com/rabbitmq/jms/admin/RMQConnectionFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,14 @@ public class RMQConnectionFactory implements ConnectionFactory, Referenceable, S
*/
private boolean declareReplyToDestination = true;

/**
* Whether to validate subscription names or not, according to JMS 2.0. Default is false (no
* validation).
*
* @since 2.7.0
*/
private boolean validateSubscriptionNames = false;

/**
* {@inheritDoc}
*/
Expand Down Expand Up @@ -332,6 +340,7 @@ protected Connection createConnection(String username, String password, Connecti
.setTrustedPackages(this.trustedPackages)
.setRequeueOnTimeout(this.requeueOnTimeout)
.setKeepTextMessageType(this.keepTextMessageType)
.setValidateSubscriptionNames(this.validateSubscriptionNames)
);
logger.debug("Connection {} created.", conn);
return conn;
Expand Down Expand Up @@ -1080,7 +1089,17 @@ public void setKeepTextMessageType(boolean keepTextMessageType) {
this.keepTextMessageType = keepTextMessageType;
}

@FunctionalInterface
/**
* Whether to validate subscription names or not, according to JMS 2.0. Default is false (no
* validation).
*
* @since 2.7.0
*/
public void setValidateSubscriptionNames(boolean validateSubscriptionNames) {
this.validateSubscriptionNames = validateSubscriptionNames;
}

@FunctionalInterface
private interface ConnectionCreator {
com.rabbitmq.client.Connection create(com.rabbitmq.client.ConnectionFactory cf) throws Exception;
}
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/com/rabbitmq/jms/client/ConnectionParams.java
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ public class ConnectionParams {

private List<String> trustedPackages = WhiteListObjectInputStream.DEFAULT_TRUSTED_PACKAGES;

private boolean validateSubscriptionNames = false;

public Connection getRabbitConnection() {
return rabbitConnection;
}
Expand Down Expand Up @@ -260,4 +262,13 @@ public ConnectionParams setRequeueOnTimeout(boolean requeueOnTimeout) {
public boolean willRequeueOnTimeout() {
return requeueOnTimeout;
}

public ConnectionParams setValidateSubscriptionNames(boolean validateSubscriptionNames) {
this.validateSubscriptionNames = validateSubscriptionNames;
return this;
}

public boolean isValidateSubscriptionNames() {
return validateSubscriptionNames;
}
}
4 changes: 4 additions & 0 deletions src/main/java/com/rabbitmq/jms/client/RMQConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ public class RMQConnection implements Connection, QueueConnection, TopicConnecti

private final boolean keepTextMessageType;

private final boolean validateSubscriptionNames;

/**
* Creates an RMQConnection object.
* @param connectionParams parameters for this connection
Expand Down Expand Up @@ -187,6 +189,7 @@ public RMQConnection(ConnectionParams connectionParams) {
this.trustedPackages = connectionParams.getTrustedPackages();
this.requeueOnTimeout = connectionParams.willRequeueOnTimeout();
this.keepTextMessageType = connectionParams.isKeepTextMessageType();
this.validateSubscriptionNames = connectionParams.isValidateSubscriptionNames();
}

/**
Expand Down Expand Up @@ -243,6 +246,7 @@ public Session createSession(boolean transacted, int acknowledgeMode) throws JMS
.setTrustedPackages(this.trustedPackages)
.setRequeueOnTimeout(this.requeueOnTimeout)
.setKeepTextMessageType(this.keepTextMessageType)
.setValidateSubscriptionNames(this.validateSubscriptionNames)
);
this.sessions.add(session);
return session;
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/com/rabbitmq/jms/client/RMQSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ private static Map<String, SqlExpressionType> generateJMSTypeIdents() {

private final boolean keepTextMessageType;

private final boolean validateSubscriptionNames;

/**
* Creates a session object associated with a connection
* @param sessionParams parameters for this session
Expand Down Expand Up @@ -253,6 +255,7 @@ public RMQSession(SessionParams sessionParams) throws JMSException {
this.trustedPackages = sessionParams.getTrustedPackages();
this.requeueOnTimeout = sessionParams.willRequeueOnTimeout();
this.keepTextMessageType = sessionParams.isKeepTextMessageType();
this.validateSubscriptionNames = sessionParams.isValidateSubscriptionNames();

if (transacted) {
this.acknowledgeMode = Session.SESSION_TRANSACTED;
Expand Down Expand Up @@ -1015,6 +1018,17 @@ public MessageConsumer createDurableConsumer(Topic topic, String name, String me
boolean noLocal) throws JMSException {
illegalStateExceptionIfClosed();

if (this.validateSubscriptionNames) {
boolean subscriptionIsValid = Utils.SUBSCRIPTION_NAME_PREDICATE.test(name);
if (!subscriptionIsValid) {
// the specification is not clear on which exception to throw when the
// subscription name is not valid, so throwing a JMSException.
throw new JMSException("This subscription name is not valid: " + name + ". "
+ "It must not be more than 128 characters and should contain only "
+ "Java letters, digits, '_', '.', and '-'.");
}
}

RMQDestination topicDest = (RMQDestination) topic;
RMQMessageConsumer previousConsumer = this.subscriptions.get(name);
if (previousConsumer!=null) {
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/com/rabbitmq/jms/client/SessionParams.java
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ public class SessionParams {

private boolean keepTextMessageType = false;

private boolean validateSubscriptionNames = false;

private List<String> trustedPackages = WhiteListObjectInputStream.DEFAULT_TRUSTED_PACKAGES;

public RMQConnection getConnection() {
Expand Down Expand Up @@ -256,4 +258,13 @@ public SessionParams setRequeueOnTimeout(boolean requeueOnTimeout) {
public boolean willRequeueOnTimeout() {
return requeueOnTimeout;
}

public SessionParams setValidateSubscriptionNames(boolean validateSubscriptionNames) {
this.validateSubscriptionNames = validateSubscriptionNames;
return this;
}

public boolean isValidateSubscriptionNames() {
return validateSubscriptionNames;
}
}
33 changes: 33 additions & 0 deletions src/main/java/com/rabbitmq/jms/client/Utils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 VMware, Inc. or its affiliates. All rights reserved.

package com.rabbitmq.jms.client;

import java.util.function.Predicate;

abstract class Utils {

private Utils() {

}

static final Predicate<String> SUBSCRIPTION_NAME_PREDICATE = name -> {
if (name == null) {
return true;
}
if (name.length() > 128) {
return false;
}
for (int i = 0; i < name.length(); i++) {
char c = name.charAt(i);
if (c != '_' && c != '.' && c != '-' && !Character.isLetter(c) && !Character.isDigit(c)) {
return false;
}
}
return true;
};

}
4 changes: 4 additions & 0 deletions src/test/java/com/rabbitmq/Assertions.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@

public abstract class Assertions {

private Assertions() {

}

public static JmsMessageAssert assertThat(Message message) {
return new JmsMessageAssert(message);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// Copyright (c) 2013-2020 VMware, Inc. or its affiliates. All rights reserved.
package com.rabbitmq.integration.tests;

import com.rabbitmq.jms.admin.RMQConnectionFactory;
import javax.jms.TopicConnection;
import javax.jms.TopicConnectionFactory;

Expand All @@ -20,9 +21,14 @@ public void beforeTests() throws Exception {
this.connFactory =
(TopicConnectionFactory) AbstractTestConnectionFactory.getTestConnectionFactory()
.getConnectionFactory();
customise((RMQConnectionFactory) connFactory);
this.topicConn = connFactory.createTopicConnection();
}

protected void customise(RMQConnectionFactory connectionFactory) {

}

@AfterEach
public void afterTests() throws Exception {
if (this.topicConn != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
// Copyright (c) 2013-2022 VMware, Inc. or its affiliates. All rights reserved.
package com.rabbitmq.integration.tests;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import com.rabbitmq.jms.admin.RMQConnectionFactory;
import javax.jms.DeliveryMode;
import javax.jms.JMSException;
import javax.jms.MessageConsumer;
import javax.jms.Session;
import javax.jms.TextMessage;
Expand All @@ -32,6 +35,12 @@ public class SimpleDurableTopicMessageIT extends AbstractITTopic {

private static final String MESSAGE_TEXT_1 = "Hello " + SimpleDurableTopicMessageIT.class.getName();

@Override
protected void customise(RMQConnectionFactory connectionFactory) {
super.customise(connectionFactory);
connectionFactory.setValidateSubscriptionNames(true);
}

@Test
public void testSendAndReceiveTextMessage() throws Exception {
topicConn.start();
Expand Down Expand Up @@ -83,4 +92,13 @@ public void testSendAndReceiveAfterReconnect() throws Exception {
}
}

@Test
public void subscriptionShouldFailWhenSubscriptionNameIsNotValid() throws Exception {
topicConn.start();
TopicSession topicSession = topicConn.createTopicSession(false, Session.DUPS_OK_ACKNOWLEDGE);
Topic topic = topicSession.createTopic(TOPIC_NAME);
assertThatThrownBy(() -> topicSession.createDurableConsumer(topic, "invalid?subscription?name"))
.isInstanceOf(JMSException.class)
.hasMessageContainingAll("This subscription name is not valid");
}
}
34 changes: 34 additions & 0 deletions src/test/java/com/rabbitmq/jms/client/UtilsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 VMware, Inc. or its affiliates. All rights reserved.

package com.rabbitmq.jms.client;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

public class UtilsTest {

@CsvSource({
"valid-subscription-name,true",
"valid_subscription_name,true",
"valid.subscription.name,true",
"yet-another_valid.subscription.name,true",
"invalid?subscription!name,false",
"invalid(subscription)name,false",
"invalid(subscription)name,false",
"very.loooooooooooooooooooooooooooooooooooooooooooooooooooooooo"
+ "ooooooooooooooooooooooooooooooooooooooooooooong.subscription.name,true",
"too.loooooooooooooooooooooooooooooooooooooooooooooooooooooooo"
+ "oooooooooooooooooooooooooooooooooooooooooooooooooooong.subscription.name,false",
})
@ParameterizedTest
void subscriptionNameValidation(String subscriptionName, boolean valid) {
// https://jakarta.ee/specifications/messaging/3.1/jakarta-messaging-spec-3.1.html#subscription-name-characters-and-length
assertThat(Utils.SUBSCRIPTION_NAME_PREDICATE.test(subscriptionName)).isEqualTo(valid);
}
}

0 comments on commit a073da5

Please sign in to comment.