diff --git a/src/main/java/com/rabbitmq/jms/admin/RMQConnectionFactory.java b/src/main/java/com/rabbitmq/jms/admin/RMQConnectionFactory.java index f19d977b..0af72a84 100644 --- a/src/main/java/com/rabbitmq/jms/admin/RMQConnectionFactory.java +++ b/src/main/java/com/rabbitmq/jms/admin/RMQConnectionFactory.java @@ -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} */ @@ -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; @@ -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; } diff --git a/src/main/java/com/rabbitmq/jms/client/ConnectionParams.java b/src/main/java/com/rabbitmq/jms/client/ConnectionParams.java index f1f7dbbe..37fdb8cf 100644 --- a/src/main/java/com/rabbitmq/jms/client/ConnectionParams.java +++ b/src/main/java/com/rabbitmq/jms/client/ConnectionParams.java @@ -117,6 +117,8 @@ public class ConnectionParams { private List trustedPackages = WhiteListObjectInputStream.DEFAULT_TRUSTED_PACKAGES; + private boolean validateSubscriptionNames = false; + public Connection getRabbitConnection() { return rabbitConnection; } @@ -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; + } } diff --git a/src/main/java/com/rabbitmq/jms/client/RMQConnection.java b/src/main/java/com/rabbitmq/jms/client/RMQConnection.java index 876ecc95..13a1d41d 100644 --- a/src/main/java/com/rabbitmq/jms/client/RMQConnection.java +++ b/src/main/java/com/rabbitmq/jms/client/RMQConnection.java @@ -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 @@ -187,6 +189,7 @@ public RMQConnection(ConnectionParams connectionParams) { this.trustedPackages = connectionParams.getTrustedPackages(); this.requeueOnTimeout = connectionParams.willRequeueOnTimeout(); this.keepTextMessageType = connectionParams.isKeepTextMessageType(); + this.validateSubscriptionNames = connectionParams.isValidateSubscriptionNames(); } /** @@ -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; diff --git a/src/main/java/com/rabbitmq/jms/client/RMQSession.java b/src/main/java/com/rabbitmq/jms/client/RMQSession.java index 222b528e..7b9a5fe2 100644 --- a/src/main/java/com/rabbitmq/jms/client/RMQSession.java +++ b/src/main/java/com/rabbitmq/jms/client/RMQSession.java @@ -225,6 +225,8 @@ private static Map generateJMSTypeIdents() { private final boolean keepTextMessageType; + private final boolean validateSubscriptionNames; + /** * Creates a session object associated with a connection * @param sessionParams parameters for this session @@ -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; @@ -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) { diff --git a/src/main/java/com/rabbitmq/jms/client/SessionParams.java b/src/main/java/com/rabbitmq/jms/client/SessionParams.java index 94419e84..3c15c15e 100644 --- a/src/main/java/com/rabbitmq/jms/client/SessionParams.java +++ b/src/main/java/com/rabbitmq/jms/client/SessionParams.java @@ -111,6 +111,8 @@ public class SessionParams { private boolean keepTextMessageType = false; + private boolean validateSubscriptionNames = false; + private List trustedPackages = WhiteListObjectInputStream.DEFAULT_TRUSTED_PACKAGES; public RMQConnection getConnection() { @@ -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; + } } diff --git a/src/main/java/com/rabbitmq/jms/client/Utils.java b/src/main/java/com/rabbitmq/jms/client/Utils.java new file mode 100644 index 00000000..c093343b --- /dev/null +++ b/src/main/java/com/rabbitmq/jms/client/Utils.java @@ -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 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; + }; + +} diff --git a/src/test/java/com/rabbitmq/Assertions.java b/src/test/java/com/rabbitmq/Assertions.java index d5a9ff92..8a7ef519 100644 --- a/src/test/java/com/rabbitmq/Assertions.java +++ b/src/test/java/com/rabbitmq/Assertions.java @@ -13,6 +13,10 @@ public abstract class Assertions { + private Assertions() { + + } + public static JmsMessageAssert assertThat(Message message) { return new JmsMessageAssert(message); } diff --git a/src/test/java/com/rabbitmq/integration/tests/AbstractITTopic.java b/src/test/java/com/rabbitmq/integration/tests/AbstractITTopic.java index d4c77d77..2590e203 100644 --- a/src/test/java/com/rabbitmq/integration/tests/AbstractITTopic.java +++ b/src/test/java/com/rabbitmq/integration/tests/AbstractITTopic.java @@ -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; @@ -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) diff --git a/src/test/java/com/rabbitmq/integration/tests/SimpleDurableTopicMessageIT.java b/src/test/java/com/rabbitmq/integration/tests/SimpleDurableTopicMessageIT.java index d9ba7672..30699f2f 100644 --- a/src/test/java/com/rabbitmq/integration/tests/SimpleDurableTopicMessageIT.java +++ b/src/test/java/com/rabbitmq/integration/tests/SimpleDurableTopicMessageIT.java @@ -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; @@ -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(); @@ -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"); + } } diff --git a/src/test/java/com/rabbitmq/jms/client/UtilsTest.java b/src/test/java/com/rabbitmq/jms/client/UtilsTest.java new file mode 100644 index 00000000..30293754 --- /dev/null +++ b/src/test/java/com/rabbitmq/jms/client/UtilsTest.java @@ -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); + } +}