CVE-2018-1257
Description
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.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Spring Framework STOMP over WebSocket broker vulnerable to ReDoS via crafted selector header, causing denial of service.
Vulnerability
Spring Framework versions 5.0.x prior to 5.0.6, 4.3.x prior to 4.3.17, and older unsupported versions expose a STOMP over WebSocket endpoint using a simple in-memory broker via the spring-messaging module. The DefaultSubscriptionRegistry in the broker processes selector headers using a regular expression that can be exploited to cause catastrophic backtracking, leading to a denial of service condition [1][2].
Exploitation
An attacker must be able to send STOMP messages to the WebSocket endpoint. If the endpoint is unauthenticated, the attacker can directly send a subscription frame with a specially crafted selector header containing a malicious payload designed to trigger exponential backtracking in the regex engine [1].
Impact
Successful exploitation results in a regular expression denial of service (ReDoS) attack, consuming excessive CPU resources and potentially making the application unresponsive. This can lead to a denial of service for legitimate users [1].
Mitigation
Upgrade to Spring Framework 5.0.6, 4.3.17, or later. As a workaround, the selector header support can be disabled by setting the selectorHeaderName property to null via configuration. The fix is included in commits that allow the header name to be null [3][4]. No CISA KEV listing or EOL status has been published for this CVE.
AI Insight generated on May 22, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.springframework:spring-coreMaven | >= 5.0.0, < 5.0.6 | 5.0.6 |
org.springframework:spring-coreMaven | < 4.3.17 | 4.3.17 |
Affected products
2- Pivotal/Spring Frameworkv5Range: 5.0.x prior to 5.0.6; 4.3.x prior to 4.3.17
Patches
4246a6db1cad2Selector header name is exposed for configuration
8 files changed · +144 −34
spring-messaging/src/main/java/org/springframework/messaging/simp/broker/DefaultSubscriptionRegistry.java+32 −24 modified@@ -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 modified@@ -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 modified@@ -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 modified@@ -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 modified@@ -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 modified@@ -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 modified@@ -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 modified@@ -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>
246a6db1cad2Selector header name is exposed for configuration
8 files changed · +144 −34
spring-messaging/src/main/java/org/springframework/messaging/simp/broker/DefaultSubscriptionRegistry.java+32 −24 modified@@ -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 modified@@ -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 modified@@ -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 modified@@ -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 modified@@ -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 modified@@ -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 modified@@ -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 modified@@ -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>
ff2228fdaf13Selector header name is exposed for configuration
8 files changed · +150 −34
spring-messaging/src/main/java/org/springframework/messaging/simp/broker/DefaultSubscriptionRegistry.java+35 −24 modified@@ -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 modified@@ -51,27 +51,32 @@ 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; @Nullable private Integer cacheLimit; + @Nullable + private String selectorHeaderName = "selector"; + @Nullable private TaskScheduler taskScheduler; @Nullable 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 modified@@ -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 modified@@ -258,14 +258,16 @@ public void registerSubscriptionWithDestinationPatternRegex() { } @Test - public void registerSubscriptionWithSelector() throws Exception { + public void registerSubscriptionWithSelector() { String sessionId = "sess01"; String subscriptionId = "subs01"; String destination = "/foo"; String selector = "headers.foo == 'bar'"; 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 modified@@ -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 modified@@ -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 modified@@ -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 modified@@ -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>
ff2228fdaf13Selector header name is exposed for configuration
8 files changed · +150 −34
spring-messaging/src/main/java/org/springframework/messaging/simp/broker/DefaultSubscriptionRegistry.java+35 −24 modified@@ -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 modified@@ -51,27 +51,32 @@ 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; @Nullable private Integer cacheLimit; + @Nullable + private String selectorHeaderName = "selector"; + @Nullable private TaskScheduler taskScheduler; @Nullable 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 modified@@ -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 modified@@ -258,14 +258,16 @@ public void registerSubscriptionWithDestinationPatternRegex() { } @Test - public void registerSubscriptionWithSelector() throws Exception { + public void registerSubscriptionWithSelector() { String sessionId = "sess01"; String subscriptionId = "subs01"; String destination = "/foo"; String selector = "headers.foo == 'bar'"; 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 modified@@ -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 modified@@ -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 modified@@ -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 modified@@ -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>
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
15- access.redhat.com/errata/RHSA-2018:1809ghsavendor-advisoryx_refsource_REDHATWEB
- access.redhat.com/errata/RHSA-2018:3768ghsavendor-advisoryx_refsource_REDHATWEB
- github.com/advisories/GHSA-rcpf-vj53-7h2mghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2018-1257ghsaADVISORY
- www.oracle.com/technetwork/security-advisory/cpuoct2018-4428296.htmlghsax_refsource_CONFIRMWEB
- www.securityfocus.com/bid/104260ghsavdb-entryx_refsource_BIDWEB
- github.com/spring-projects/spring-framework/commit/246a6db1cad205ca9b6fca00c544ab7443ba202ghsaWEB
- github.com/spring-projects/spring-framework/commit/ff2228fdaf131d57b5c8c5918ee8d07c6dd9bbaghsaWEB
- pivotal.io/security/cve-2018-1257ghsax_refsource_CONFIRMWEB
- www.oracle.com/security-alerts/cpujan2020.htmlghsax_refsource_MISCWEB
- www.oracle.com/security-alerts/cpujul2020.htmlghsax_refsource_MISCWEB
- www.oracle.com/security-alerts/cpuoct2021.htmlghsax_refsource_MISCWEB
- www.oracle.com/technetwork/security-advisory/cpuapr2019-5072813.htmlghsax_refsource_MISCWEB
- www.oracle.com/technetwork/security-advisory/cpujan2019-5072801.htmlghsax_refsource_CONFIRMWEB
- www.oracle.com/technetwork/security-advisory/cpujul2019-5072835.htmlghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.