Security context
Medium· 6.5GHSA-rcpf-vj53-7h2m CVE-2018-1257Published Oct 17, 2018

Denial of Service in org.springframework:spring-core

Research this vulnerability

Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.

Affected versions

5.0.0 → fixed in 5.0.60 → fixed in 4.3.17

Details

Spring Framework, versions 5.0.x prior to 5.0.6, versions 4.3.x prior to 4.3.17, and older unsupported versions allows applications to expose STOMP over WebSocket endpoints with a simple, in-memory STOMP broker through the spring-messaging module. A malicious user (or attacker) can craft a message to the broker that can lead to a regular expression, denial of service attack.

The fix

Selector header name is exposed for configuration

Rossen Stoyanchev· Apr 17, 2018, 04:20 AM+14434246a6db1ca
spring-messaging/src/main/java/org/springframework/messaging/simp/broker/DefaultSubscriptionRegistry.java+32 24
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2017 the original author or authors.
+ * Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -43,6 +43,7 @@
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.PathMatcher;
+import org.springframework.util.StringUtils;
/**
* Implementation of {@link SubscriptionRegistry} that stores subscriptions
@@ -113,24 +114,25 @@ public int getCacheLimit() {
}
/**
- * Configure the name of a selector header that a subscription message can
- * have in order to filter messages based on their headers. The value of the
- * header can use Spring EL expressions against message headers.
- * <p>For example the following expression expects a header called "foo" to
- * have the value "bar":
+ * Configure the name of a header that a subscription message can have for
+ * the purpose of filtering messages matched to the subscription. The header
+ * value is expected to be a Spring EL boolean expression to be applied to
+ * the headers of messages matched to the subscription.
+ * <p>For example:
* <pre>
* headers.foo == 'bar'
* </pre>
- * <p>By default this is set to "selector".
+ * <p>By default this is set to "selector". You can set it to a different
+ * name, or to {@code null} to turn off support for a selector header.
+ * @param selectorHeaderName the name to use for a selector header
* @since 4.2
*/
public void setSelectorHeaderName(String selectorHeaderName) {
- Assert.notNull(selectorHeaderName, "'selectorHeaderName' must not be null");
- this.selectorHeaderName = selectorHeaderName;
+ this.selectorHeaderName = StringUtils.hasText(selectorHeaderName) ? selectorHeaderName : null;
}
/**
- * Return the name for the selector header.
+ * Return the name for the selector header name.
* @since 4.2
*/
public String getSelectorHeaderName() {
@@ -142,25 +144,31 @@ public String getSelectorHeaderName() {
protected void addSubscriptionInternal(
String sessionId, String subsId, String destination, Message<?> message) {
+ Expression expression = getSelectorExpression(message.getHeaders());
+ this.subscriptionRegistry.addSubscription(sessionId, subsId, destination, expression);
+ this.destinationCache.updateAfterNewSubscription(destination, sessionId, subsId);
+ }
+
+ private Expression getSelectorExpression(MessageHeaders headers) {
Expression expression = null;
- MessageHeaders headers = message.getHeaders();
- String selector = SimpMessageHeaderAccessor.getFirstNativeHeader(getSelectorHeaderName(), headers);
- if (selector != null) {
- try {
- expression = this.expressionParser.parseExpression(selector);
- this.selectorHeaderInUse = true;
- if (logger.isTraceEnabled()) {
- logger.trace("Subscription selector: [" + selector + "]");
+ if (getSelectorHeaderName() != null) {
+ String selector = SimpMessageHeaderAccessor.getFirstNativeHeader(getSelectorHeaderName(), headers);
+ if (selector != null) {
+ try {
+ expression = this.expressionParser.parseExpression(selector);
+ this.selectorHeaderInUse = true;
+ if (logger.isTraceEnabled()) {
+ logger.trace("Subscription selector: [" + selector + "]");
+ }
}
- }
- catch (Throwable ex) {
- if (logger.isDebugEnabled()) {
- logger.debug("Failed to parse selector: " + selector, ex);
+ catch (Throwable ex) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Failed to parse selector: " + selector, ex);
+ }
}
}
}
- this.subscriptionRegistry.addSubscription(sessionId, subsId, destination, expression);
- this.destinationCache.updateAfterNewSubscription(destination, sessionId, subsId);
+ return expression;
}
@Override
spring-messaging/src/main/java/org/springframework/messaging/simp/broker/SimpleBrokerMessageHandler.java+38 6
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2017 the original author or authors.
+ * Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -51,23 +51,27 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler {
private static final byte[] EMPTY_PAYLOAD = new byte[0];
- private final Map<String, SessionInfo> sessions = new ConcurrentHashMap<String, SessionInfo>();
-
- private SubscriptionRegistry subscriptionRegistry;
private PathMatcher pathMatcher;
private Integer cacheLimit;
+ private String selectorHeaderName = "selector";
+
private TaskScheduler taskScheduler;
private long[] heartbeatValue;
- private ScheduledFuture<?> heartbeatFuture;
-
private MessageHeaderInitializer headerInitializer;
+ private SubscriptionRegistry subscriptionRegistry;
+
+ private final Map<String, SessionInfo> sessions = new ConcurrentHashMap<String, SessionInfo>();
+
+ private ScheduledFuture<?> heartbeatFuture;
+
+
/**
* Create a SimpleBrokerMessageHandler instance with the given message channels
* and destination prefixes.
@@ -96,12 +100,40 @@ public void setSubscriptionRegistry(SubscriptionRegistry subscriptionRegistry) {
this.subscriptionRegistry = subscriptionRegistry;
initPathMatcherToUse();
initCacheLimitToUse();
+ initSelectorHeaderNameToUse();
}
public SubscriptionRegistry getSubscriptionRegistry() {
return this.subscriptionRegistry;
}
+ /**
+ * Configure the name of a header that a subscription message can have for
+ * the purpose of filtering messages matched to the subscription. The header
+ * value is expected to be a Spring EL boolean expression to be applied to
+ * the headers of messages matched to the subscription.
+ * <p>For example:
+ * <pre>
+ * headers.foo == 'bar'
+ * </pre>
+ * <p>By default this is set to "selector". You can set it to a different
+ * name, or to {@code null} to turn off support for a selector header.
+ * @param selectorHeaderName the name to use for a selector header
+ * @since 4.3.17
+ * @see #setSubscriptionRegistry
+ * @see DefaultSubscriptionRegistry#setSelectorHeaderName(String)
+ */
+ public void setSelectorHeaderName(String selectorHeaderName) {
+ this.selectorHeaderName = selectorHeaderName;
+ initSelectorHeaderNameToUse();
+ }
+
+ private void initSelectorHeaderNameToUse() {
+ if (this.subscriptionRegistry instanceof DefaultSubscriptionRegistry) {
+ ((DefaultSubscriptionRegistry) this.subscriptionRegistry).setSelectorHeaderName(this.selectorHeaderName);
+ }
+ }
+
/**
* When configured, the given PathMatcher is passed down to the underlying
* SubscriptionRegistry to use for matching destination to subscriptions.
spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java+22 1
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -33,6 +33,8 @@ public class SimpleBrokerRegistration extends AbstractBrokerRegistration {
private long[] heartbeat;
+ private String selectorHeaderName = "selector";
+
public SimpleBrokerRegistration(SubscribableChannel inChannel, MessageChannel outChannel, String[] prefixes) {
super(inChannel, outChannel, prefixes);
@@ -65,6 +67,24 @@ public SimpleBrokerRegistration setHeartbeatValue(long[] heartbeat) {
return this;
}
+ /**
+ * Configure the name of a header that a subscription message can have for
+ * the purpose of filtering messages matched to the subscription. The header
+ * value is expected to be a Spring EL boolean expression to be applied to
+ * the headers of messages matched to the subscription.
+ * <p>For example:
+ * <pre>
+ * headers.foo == 'bar'
+ * </pre>
+ * <p>By default this is set to "selector". You can set it to a different
+ * name, or to {@code null} to turn off support for a selector header.
+ * @param selectorHeaderName the name to use for a selector header
+ * @since 4.3.17
+ */
+ public void setSelectorHeaderName(String selectorHeaderName) {
+ this.selectorHeaderName = selectorHeaderName;
+ }
+
@Override
protected SimpleBrokerMessageHandler getMessageHandler(SubscribableChannel brokerChannel) {
@@ -76,6 +96,7 @@ protected SimpleBrokerMessageHandler getMessageHandler(SubscribableChannel broke
if (this.heartbeat != null) {
handler.setHeartbeatValue(this.heartbeat);
}
+ handler.setSelectorHeaderName(this.selectorHeaderName);
return handler;
}
spring-messaging/src/test/java/org/springframework/messaging/simp/broker/DefaultSubscriptionRegistryTests.java+25 0
@@ -264,6 +264,8 @@ public void registerSubscriptionWithSelector() throws Exception {
String destination = "/foo";
String selector = "headers.foo == 'bar'";
+ // First, try with selector header
+
this.registry.registerSubscription(subscribeMessage(sessionId, subscriptionId, destination, selector));
SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create();
@@ -276,11 +278,34 @@ public void registerSubscriptionWithSelector() throws Exception {
assertEquals(1, actual.size());
assertEquals(Collections.singletonList(subscriptionId), actual.get(sessionId));
+ // Then without
+
actual = this.registry.findSubscriptions(createMessage(destination));
assertNotNull(actual);
assertEquals(0, actual.size());
}
+ @Test
+ public void registerSubscriptionWithSelectorNotSupported() {
+ String sessionId = "sess01";
+ String subscriptionId = "subs01";
+ String destination = "/foo";
+ String selector = "headers.foo == 'bar'";
+
+ this.registry.setSelectorHeaderName(null);
+ this.registry.registerSubscription(subscribeMessage(sessionId, subscriptionId, destination, selector));
+
+ SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create();
+ accessor.setDestination(destination);
+ accessor.setNativeHeader("foo", "bazz");
+ Message<?> message = MessageBuilder.createMessage("", accessor.getMessageHeaders());
+
+ MultiValueMap<String, String> actual = this.registry.findSubscriptions(message);
+ assertNotNull(actual);
+ assertEquals(1, actual.size());
+ assertEquals(Collections.singletonList(subscriptionId), actual.get(sessionId));
+ }
+
@Test // SPR-11931
public void registerSubscriptionTwiceAndUnregister() {
this.registry.registerSubscription(subscribeMessage("sess01", "subs01", "/foo"));
spring-websocket/src/main/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParser.java+5 1
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2017 the original author or authors.
+ * Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -381,6 +381,10 @@ private RootBeanDefinition registerMessageBroker(Element brokerElement,
String heartbeatValue = simpleBrokerElem.getAttribute("heartbeat");
brokerDef.getPropertyValues().add("heartbeatValue", heartbeatValue);
}
+ if (simpleBrokerElem.hasAttribute("selector-header")) {
+ String headerName = simpleBrokerElem.getAttribute("selector-header");
+ brokerDef.getPropertyValues().add("selectorHeaderName", headerName);
+ }
}
else if (brokerRelayElem != null) {
String prefix = brokerRelayElem.getAttribute("prefix");
spring-websocket/src/main/resources/org/springframework/web/socket/config/spring-websocket-4.3.xsd+16 0
@@ -384,6 +384,22 @@
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
+ <xsd:attribute name="selector-header" type="xsd:string">
+ <xsd:annotation>
+ <xsd:documentation source="java:org.springframework.messaging.simp.stomp.SimpleBrokerMessageHandler"><![CDATA[
+ Configure the name of a header that a subscription message can have for
+ the purpose of filtering messages matched to the subscription. The header
+ value is expected to be a Spring EL boolean expression to be applied to
+ the headers of messages matched to the subscription.
+
+ For example:
+ headers.foo == 'bar'
+
+ By default this is set to "selector". You can set it to a different
+ name, or to "" to turn off support for a selector header.
+ ]]></xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
</xsd:complexType>
<xsd:complexType name="channel">
spring-websocket/src/test/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParserTests.java+4 1
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2017 the original author or authors.
+ * Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -45,6 +45,7 @@
import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler;
+import org.springframework.messaging.simp.broker.DefaultSubscriptionRegistry;
import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler;
import org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler;
import org.springframework.messaging.simp.user.DefaultUserDestinationResolver;
@@ -199,6 +200,8 @@ public void simpleBroker() throws Exception {
assertNotNull(brokerMessageHandler);
Collection<String> prefixes = brokerMessageHandler.getDestinationPrefixes();
assertEquals(Arrays.asList("/topic", "/queue"), new ArrayList<String>(prefixes));
+ DefaultSubscriptionRegistry registry = (DefaultSubscriptionRegistry) brokerMessageHandler.getSubscriptionRegistry();
+ assertEquals("my-selector", registry.getSelectorHeaderName());
assertNotNull(brokerMessageHandler.getTaskScheduler());
assertArrayEquals(new long[] {15000, 15000}, brokerMessageHandler.getHeartbeatValue());
spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-broker-simple.xml+2 1
@@ -35,7 +35,8 @@
<websocket:stomp-error-handler ref="errorHandler" />
- <websocket:simple-broker prefix="/topic, /queue" heartbeat="15000,15000" scheduler="scheduler" />
+ <websocket:simple-broker prefix="/topic, /queue" selector-header="my-selector"
+ heartbeat="15000,15000" scheduler="scheduler" />
</websocket:message-broker>

Selector header name is exposed for configuration

Rossen Stoyanchev· Apr 17, 2018, 03:56 AM+15034ff2228fdaf
spring-messaging/src/main/java/org/springframework/messaging/simp/broker/DefaultSubscriptionRegistry.java+35 24
@@ -44,6 +44,7 @@
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.PathMatcher;
+import org.springframework.util.StringUtils;
/**
* Implementation of {@link SubscriptionRegistry} that stores subscriptions
@@ -73,6 +74,7 @@ public class DefaultSubscriptionRegistry extends AbstractSubscriptionRegistry {
private volatile int cacheLimit = DEFAULT_CACHE_LIMIT;
+ @Nullable
private String selectorHeaderName = "selector";
private volatile boolean selectorHeaderInUse = false;
@@ -114,26 +116,28 @@ public int getCacheLimit() {
}
/**
- * Configure the name of a selector header that a subscription message can
- * have in order to filter messages based on their headers. The value of the
- * header can use Spring EL expressions against message headers.
- * <p>For example the following expression expects a header called "foo" to
- * have the value "bar":
+ * Configure the name of a header that a subscription message can have for
+ * the purpose of filtering messages matched to the subscription. The header
+ * value is expected to be a Spring EL boolean expression to be applied to
+ * the headers of messages matched to the subscription.
+ * <p>For example:
* <pre>
* headers.foo == 'bar'
* </pre>
- * <p>By default this is set to "selector".
+ * <p>By default this is set to "selector". You can set it to a different
+ * name, or to {@code null} to turn off support for a selector header.
+ * @param selectorHeaderName the name to use for a selector header
* @since 4.2
*/
- public void setSelectorHeaderName(String selectorHeaderName) {
- Assert.notNull(selectorHeaderName, "'selectorHeaderName' must not be null");
- this.selectorHeaderName = selectorHeaderName;
+ public void setSelectorHeaderName(@Nullable String selectorHeaderName) {
+ this.selectorHeaderName = StringUtils.hasText(selectorHeaderName) ? selectorHeaderName : null;
}
/**
- * Return the name for the selector header.
+ * Return the name for the selector header name.
* @since 4.2
*/
+ @Nullable
public String getSelectorHeaderName() {
return this.selectorHeaderName;
}
@@ -143,25 +147,32 @@ public String getSelectorHeaderName() {
protected void addSubscriptionInternal(
String sessionId, String subsId, String destination, Message<?> message) {
+ Expression expression = getSelectorExpression(message.getHeaders());
+ this.subscriptionRegistry.addSubscription(sessionId, subsId, destination, expression);
+ this.destinationCache.updateAfterNewSubscription(destination, sessionId, subsId);
+ }
+
+ @Nullable
+ private Expression getSelectorExpression(MessageHeaders headers) {
Expression expression = null;
- MessageHeaders headers = message.getHeaders();
- String selector = SimpMessageHeaderAccessor.getFirstNativeHeader(getSelectorHeaderName(), headers);
- if (selector != null) {
- try {
- expression = this.expressionParser.parseExpression(selector);
- this.selectorHeaderInUse = true;
- if (logger.isTraceEnabled()) {
- logger.trace("Subscription selector: [" + selector + "]");
+ if (getSelectorHeaderName() != null) {
+ String selector = SimpMessageHeaderAccessor.getFirstNativeHeader(getSelectorHeaderName(), headers);
+ if (selector != null) {
+ try {
+ expression = this.expressionParser.parseExpression(selector);
+ this.selectorHeaderInUse = true;
+ if (logger.isTraceEnabled()) {
+ logger.trace("Subscription selector: [" + selector + "]");
+ }
}
- }
- catch (Throwable ex) {
- if (logger.isDebugEnabled()) {
- logger.debug("Failed to parse selector: " + selector, ex);
+ catch (Throwable ex) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Failed to parse selector: " + selector, ex);
+ }
}
}
}
- this.subscriptionRegistry.addSubscription(sessionId, subsId, destination, expression);
- this.destinationCache.updateAfterNewSubscription(destination, sessionId, subsId);
+ return expression;
}
@Override
spring-messaging/src/main/java/org/springframework/messaging/simp/broker/SimpleBrokerMessageHandler.java+38 5
@@ -51,9 +51,6 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler {
private static final byte[] EMPTY_PAYLOAD = new byte[0];
- private final Map<String, SessionInfo> sessions = new ConcurrentHashMap<>();
-
- private SubscriptionRegistry subscriptionRegistry;
@Nullable
private PathMatcher pathMatcher;
@@ -61,6 +58,9 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler {
@Nullable
private Integer cacheLimit;
+ @Nullable
+ private String selectorHeaderName = "selector";
+
@Nullable
private TaskScheduler taskScheduler;
@@ -68,10 +68,15 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler {
private long[] heartbeatValue;
@Nullable
- private ScheduledFuture<?> heartbeatFuture;
+ private MessageHeaderInitializer headerInitializer;
+
+
+ private SubscriptionRegistry subscriptionRegistry;
+
+ private final Map<String, SessionInfo> sessions = new ConcurrentHashMap<>();
@Nullable
- private MessageHeaderInitializer headerInitializer;
+ private ScheduledFuture<?> heartbeatFuture;
/**
@@ -102,6 +107,7 @@ public void setSubscriptionRegistry(SubscriptionRegistry subscriptionRegistry) {
this.subscriptionRegistry = subscriptionRegistry;
initPathMatcherToUse();
initCacheLimitToUse();
+ initSelectorHeaderNameToUse();
}
public SubscriptionRegistry getSubscriptionRegistry() {
@@ -149,6 +155,33 @@ private void initCacheLimitToUse() {
}
}
+ /**
+ * Configure the name of a header that a subscription message can have for
+ * the purpose of filtering messages matched to the subscription. The header
+ * value is expected to be a Spring EL boolean expression to be applied to
+ * the headers of messages matched to the subscription.
+ * <p>For example:
+ * <pre>
+ * headers.foo == 'bar'
+ * </pre>
+ * <p>By default this is set to "selector". You can set it to a different
+ * name, or to {@code null} to turn off support for a selector header.
+ * @param selectorHeaderName the name to use for a selector header
+ * @since 4.3.17
+ * @see #setSubscriptionRegistry
+ * @see DefaultSubscriptionRegistry#setSelectorHeaderName(String)
+ */
+ public void setSelectorHeaderName(@Nullable String selectorHeaderName) {
+ this.selectorHeaderName = selectorHeaderName;
+ initSelectorHeaderNameToUse();
+ }
+
+ private void initSelectorHeaderNameToUse() {
+ if (this.subscriptionRegistry instanceof DefaultSubscriptionRegistry) {
+ ((DefaultSubscriptionRegistry) this.subscriptionRegistry).setSelectorHeaderName(this.selectorHeaderName);
+ }
+ }
+
/**
* Configure the {@link org.springframework.scheduling.TaskScheduler} to
* use for providing heartbeat support. Setting this property also sets the
spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java+23 1
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2017 the original author or authors.
+ * Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -36,6 +36,9 @@ public class SimpleBrokerRegistration extends AbstractBrokerRegistration {
@Nullable
private long[] heartbeat;
+ @Nullable
+ private String selectorHeaderName = "selector";
+
public SimpleBrokerRegistration(SubscribableChannel inChannel, MessageChannel outChannel, String[] prefixes) {
super(inChannel, outChannel, prefixes);
@@ -68,6 +71,24 @@ public SimpleBrokerRegistration setHeartbeatValue(long[] heartbeat) {
return this;
}
+ /**
+ * Configure the name of a header that a subscription message can have for
+ * the purpose of filtering messages matched to the subscription. The header
+ * value is expected to be a Spring EL boolean expression to be applied to
+ * the headers of messages matched to the subscription.
+ * <p>For example:
+ * <pre>
+ * headers.foo == 'bar'
+ * </pre>
+ * <p>By default this is set to "selector". You can set it to a different
+ * name, or to {@code null} to turn off support for a selector header.
+ * @param selectorHeaderName the name to use for a selector header
+ * @since 4.3.17
+ */
+ public void setSelectorHeaderName(@Nullable String selectorHeaderName) {
+ this.selectorHeaderName = selectorHeaderName;
+ }
+
@Override
protected SimpleBrokerMessageHandler getMessageHandler(SubscribableChannel brokerChannel) {
@@ -79,6 +100,7 @@ protected SimpleBrokerMessageHandler getMessageHandler(SubscribableChannel broke
if (this.heartbeat != null) {
handler.setHeartbeatValue(this.heartbeat);
}
+ handler.setSelectorHeaderName(this.selectorHeaderName);
return handler;
}
spring-messaging/src/test/java/org/springframework/messaging/simp/broker/DefaultSubscriptionRegistryTests.java+26 1
@@ -258,7 +258,7 @@ public void registerSubscriptionWithDestinationPatternRegex() {
}
@Test
- public void registerSubscriptionWithSelector() throws Exception {
+ public void registerSubscriptionWithSelector() {
String sessionId = "sess01";
String subscriptionId = "subs01";
String destination = "/foo";
@@ -266,6 +266,8 @@ public void registerSubscriptionWithSelector() throws Exception {
this.registry.registerSubscription(subscribeMessage(sessionId, subscriptionId, destination, selector));
+ // First, try with selector header
+
SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create();
accessor.setDestination(destination);
accessor.setNativeHeader("foo", "bar");
@@ -276,11 +278,34 @@ public void registerSubscriptionWithSelector() throws Exception {
assertEquals(1, actual.size());
assertEquals(Collections.singletonList(subscriptionId), actual.get(sessionId));
+ // Then without
+
actual = this.registry.findSubscriptions(createMessage(destination));
assertNotNull(actual);
assertEquals(0, actual.size());
}
+ @Test
+ public void registerSubscriptionWithSelectorNotSupported() {
+ String sessionId = "sess01";
+ String subscriptionId = "subs01";
+ String destination = "/foo";
+ String selector = "headers.foo == 'bar'";
+
+ this.registry.setSelectorHeaderName(null);
+ this.registry.registerSubscription(subscribeMessage(sessionId, subscriptionId, destination, selector));
+
+ SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create();
+ accessor.setDestination(destination);
+ accessor.setNativeHeader("foo", "bazz");
+ Message<?> message = MessageBuilder.createMessage("", accessor.getMessageHeaders());
+
+ MultiValueMap<String, String> actual = this.registry.findSubscriptions(message);
+ assertNotNull(actual);
+ assertEquals(1, actual.size());
+ assertEquals(Collections.singletonList(subscriptionId), actual.get(sessionId));
+ }
+
@Test // SPR-11931
public void registerSubscriptionTwiceAndUnregister() {
this.registry.registerSubscription(subscribeMessage("sess01", "subs01", "/foo"));
spring-websocket/src/main/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParser.java+4 0
@@ -383,6 +383,10 @@ private RootBeanDefinition registerMessageBroker(Element brokerElement,
String heartbeatValue = simpleBrokerElem.getAttribute("heartbeat");
brokerDef.getPropertyValues().add("heartbeatValue", heartbeatValue);
}
+ if (simpleBrokerElem.hasAttribute("selector-header")) {
+ String headerName = simpleBrokerElem.getAttribute("selector-header");
+ brokerDef.getPropertyValues().add("selectorHeaderName", headerName);
+ }
}
else if (brokerRelayElem != null) {
String prefix = brokerRelayElem.getAttribute("prefix");
spring-websocket/src/main/resources/org/springframework/web/socket/config/spring-websocket.xsd+17 0
@@ -384,6 +384,23 @@
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
+ <xsd:attribute name="selector-header" type="xsd:string">
+ <xsd:annotation>
+ <xsd:documentation source="java:org.springframework.messaging.simp.stomp.SimpleBrokerMessageHandler"><![CDATA[
+ Configure the name of a header that a subscription message can have for
+ the purpose of filtering messages matched to the subscription. The header
+ value is expected to be a Spring EL boolean expression to be applied to
+ the headers of messages matched to the subscription.
+
+ For example:
+ headers.foo == 'bar'
+
+ By default this is set to "selector". You can set it to a different
+ name, or to "" to turn off support for a selector header.
+ ]]></xsd:documentation>
+ </xsd:annotation>
+ </xsd:attribute>
+
</xsd:complexType>
<xsd:complexType name="channel">
spring-websocket/src/test/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParserTests.java+5 2
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2017 the original author or authors.
+ * Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -47,6 +47,7 @@
import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler;
+import org.springframework.messaging.simp.broker.DefaultSubscriptionRegistry;
import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler;
import org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler;
import org.springframework.messaging.simp.user.DefaultUserDestinationResolver;
@@ -202,6 +203,8 @@ public void simpleBroker() throws Exception {
assertNotNull(brokerMessageHandler);
Collection<String> prefixes = brokerMessageHandler.getDestinationPrefixes();
assertEquals(Arrays.asList("/topic", "/queue"), new ArrayList<>(prefixes));
+ DefaultSubscriptionRegistry registry = (DefaultSubscriptionRegistry) brokerMessageHandler.getSubscriptionRegistry();
+ assertEquals("my-selector", registry.getSelectorHeaderName());
assertNotNull(brokerMessageHandler.getTaskScheduler());
assertArrayEquals(new long[] {15000, 15000}, brokerMessageHandler.getHeartbeatValue());
@@ -228,7 +231,7 @@ public void simpleBroker() throws Exception {
assertNotNull(this.appContext.getBean("webSocketScopeConfigurer", CustomScopeConfigurer.class));
- DirectFieldAccessor accessor = new DirectFieldAccessor(brokerMessageHandler.getSubscriptionRegistry());
+ DirectFieldAccessor accessor = new DirectFieldAccessor(registry);
Object pathMatcher = accessor.getPropertyValue("pathMatcher");
String pathSeparator = (String) new DirectFieldAccessor(pathMatcher).getPropertyValue("pathSeparator");
assertEquals(".", pathSeparator);
spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-broker-simple.xml+2 1
@@ -35,7 +35,8 @@
<websocket:stomp-error-handler ref="errorHandler" />
- <websocket:simple-broker prefix="/topic, /queue" heartbeat="15000,15000" scheduler="scheduler" />
+ <websocket:simple-broker prefix="/topic, /queue" selector-header="my-selector"
+ heartbeat="15000,15000" scheduler="scheduler" />
</websocket:message-broker>

References