INT-4039: Add Allowed Origins to WebSockets

JIRA: https://jira.spring.io/browse/INT-4039

Also add `suppressCors` for the `SockJsServiceOptions`

Polishing according PR comments
This commit is contained in:
Artem Bilan
2016-05-25 18:55:59 -04:00
committed by Gary Russell
parent d414383eac
commit f5488efcaa
8 changed files with 168 additions and 83 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2015 the original author or authors.
* Copyright 2014-2016 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.
@@ -58,6 +58,8 @@ public class ServerWebSocketContainer extends IntegrationWebSocketContainer impl
private SockJsServiceOptions sockJsServiceOptions;
private String[] origins;
public ServerWebSocketContainer(String... paths) {
this.paths = paths;
}
@@ -91,6 +93,18 @@ public class ServerWebSocketContainer extends IntegrationWebSocketContainer impl
return this;
}
/**
* Configure allowed {@code Origin} header values.
* @param origins the origins to allow.
* @return the current ServerWebSocketContainer
* @since 4.3
* @see WebSocketHandlerRegistration#setAllowedOrigins(String...)
*/
public ServerWebSocketContainer setAllowedOrigins(String... origins) {
this.origins = origins; //NOSONAR - fully delegated
return this;
}
public ServerWebSocketContainer withSockJs(SockJsServiceOptions... sockJsServiceOptions) {
if (ObjectUtils.isEmpty(sockJsServiceOptions)) {
this.sockJsServiceOptions = new SockJsServiceOptions();
@@ -118,7 +132,8 @@ public class ServerWebSocketContainer extends IntegrationWebSocketContainer impl
WebSocketHandlerRegistration registration = registry.addHandler(webSocketHandler, this.paths)
.setHandshakeHandler(this.handshakeHandler)
.addInterceptors(this.interceptors);
.addInterceptors(this.interceptors)
.setAllowedOrigins(this.origins);
if (this.sockJsServiceOptions != null) {
SockJsServiceRegistration sockJsServiceRegistration = registration.withSockJS();
@@ -155,6 +170,10 @@ public class ServerWebSocketContainer extends IntegrationWebSocketContainer impl
if (this.sockJsServiceOptions.messageCodec != null) {
sockJsServiceRegistration.setMessageCodec(this.sockJsServiceOptions.messageCodec);
}
if (this.sockJsServiceOptions.suppressCors != null) {
sockJsServiceRegistration.setSupressCors(this.sockJsServiceOptions.suppressCors);
}
}
}
@@ -184,6 +203,8 @@ public class ServerWebSocketContainer extends IntegrationWebSocketContainer impl
private SockJsMessageCodec messageCodec;
private Boolean suppressCors;
public SockJsServiceOptions setTaskScheduler(TaskScheduler taskScheduler) {
this.taskScheduler = taskScheduler;
return this;
@@ -234,6 +255,11 @@ public class ServerWebSocketContainer extends IntegrationWebSocketContainer impl
return this;
}
public SockJsServiceOptions setSuppressCors(boolean suppressCors) {
this.suppressCors = suppressCors;
return this;
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2015 the original author or authors.
* Copyright 2014-2016 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.
@@ -87,6 +87,7 @@ public class ServerWebSocketContainerParser extends AbstractSingleBeanDefinition
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(sockjsBuilder, sockjs, "scheduler",
"taskScheduler");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(sockjsBuilder, sockjs, "message-codec");
IntegrationNamespaceUtils.setValueIfAttributeDefined(sockjsBuilder, sockjs, "suppress-cors");
String transportHandlers = sockjs.getAttribute("transport-handlers");
if (StringUtils.hasText(transportHandlers)) {
@@ -103,6 +104,7 @@ public class ServerWebSocketContainerParser extends AbstractSingleBeanDefinition
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "handshake-handler");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "send-buffer-size-limit");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "send-time-limit");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "allowed-origins");
}
}

View File

@@ -248,6 +248,15 @@
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="suppress-cors" type="xsd:string" default="false">
<xsd:annotation>
<xsd:documentation>
This option can be used to disable automatic addition
of CORS headers for SockJS requests.
The default value is 'false'.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
</xsd:element>
</xsd:sequence>
@@ -311,6 +320,24 @@
<xsd:union memberTypes="xsd:int xsd:string"/>
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="allowed-origins" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Configure allowed Origin header values. Multiple origins may be specified
as a comma-separated list.
This check is mostly designed for browser clients. There is noting preventing other
types of client to modify the Origin header value.
When SockJS is enabled and allowed origins are restricted, transport types that do not
use Origin headers for cross origin requests (jsonp-polling, iframe-xhr-polling,
iframe-eventsource and iframe-htmlfile) are disabled. As a consequence, IE6/IE7 are not
supported and IE8/IE9 will only be supported without cookies.
By default, all origins are allowed.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
</xsd:element>

View File

@@ -19,7 +19,8 @@
send-time-limit="100"
handshake-handler="handshakeHandler"
handshake-interceptors="handshakeInterceptor"
decorator-factories="decoratorFactory">
decorator-factories="decoratorFactory"
allowed-origins="http://foo.com">
<int-websocket:sockjs client-library-url="https://foo.sock.js"
disconnect-delay="4000"
heartbeat-time="30000"
@@ -28,34 +29,35 @@
stream-bytes-limit="2000"
websocket-enabled="false"
scheduler="taskScheduler"
message-codec="sockJsMessageCodec"/>
message-codec="sockJsMessageCodec"
suppress-cors="true" />
</int-websocket:server-container>
<bean id="handshakeHandler" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="org.springframework.web.socket.server.HandshakeHandler"/>
<constructor-arg value="org.springframework.web.socket.server.HandshakeHandler" />
</bean>
<bean id="handshakeInterceptor" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="org.springframework.web.socket.server.HandshakeInterceptor"/>
<constructor-arg value="org.springframework.web.socket.server.HandshakeInterceptor" />
</bean>
<bean id="sockJsMessageCodec" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="org.springframework.web.socket.sockjs.frame.SockJsMessageCodec"/>
<constructor-arg value="org.springframework.web.socket.sockjs.frame.SockJsMessageCodec" />
</bean>
<bean id="decoratorFactory"
class="org.springframework.integration.websocket.config.WebSocketParserTests.TestWebSocketHandlerDecoratorFactory" />
<websocket:message-broker application-destination-prefix="/app">
<websocket:stomp-endpoint path="/foo"/>
<websocket:simple-broker prefix="/topic"/>
<websocket:stomp-endpoint path="/foo" />
<websocket:simple-broker prefix="/topic" />
</websocket:message-broker>
<int-websocket:inbound-channel-adapter id="defaultInboundAdapter" container="serverWebSocketContainer"
use-broker="true"/>
use-broker="true" />
<bean id="webSocketClient" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="org.springframework.web.socket.client.WebSocketClient"/>
<constructor-arg value="org.springframework.web.socket.client.WebSocketClient" />
</bean>
<int-websocket:client-container id="clientWebSocketContainer"
@@ -67,18 +69,19 @@
origin="FOO"
phase="100">
<int-websocket:http-headers>
<entry key="FOO" value="BAR,baz"/>
<entry key="FOO" value="BAR,baz" />
</int-websocket:http-headers>
</int-websocket:client-container>
<bean id="stompSubProtocolHandler" class="org.springframework.web.socket.messaging.StompSubProtocolHandler"/>
<bean id="stompSubProtocolHandler" class="org.springframework.web.socket.messaging.StompSubProtocolHandler" />
<bean id="passThruSubProtocolHandler"
class="org.springframework.integration.websocket.support.PassThruSubProtocolHandler"/>
class="org.springframework.integration.websocket.support.PassThruSubProtocolHandler" />
<bean id="simpleMessageConverter" class="org.springframework.integration.support.converter.SimpleMessageConverter"/>
<bean id="simpleMessageConverter"
class="org.springframework.integration.support.converter.SimpleMessageConverter" />
<bean id="mapMessageConverter" class="org.springframework.integration.support.converter.MapMessageConverter"/>
<bean id="mapMessageConverter" class="org.springframework.integration.support.converter.MapMessageConverter" />
<int-websocket:inbound-channel-adapter id="customInboundAdapter" container="clientWebSocketContainer"
auto-startup="false"
@@ -90,23 +93,23 @@
channel="clientInboundChannel"
error-channel="errorChannel"
send-timeout="2000"
phase="200"/>
phase="200" />
<int:channel id="clientInboundChannel"/>
<int:channel id="clientInboundChannel" />
<int-websocket:outbound-channel-adapter id="defaultOutboundAdapter" container="serverWebSocketContainer"/>
<int-websocket:outbound-channel-adapter id="defaultOutboundAdapter" container="serverWebSocketContainer" />
<int-websocket:outbound-channel-adapter id="customOutboundAdapter" container="clientWebSocketContainer"
default-protocol-handler="stompSubProtocolHandler"
protocol-handlers="passThruSubProtocolHandler"
message-converters="simpleMessageConverter,mapMessageConverter"
merge-with-default-converters="true"
channel="clientOutboundChannel"/>
channel="clientOutboundChannel" />
<int:channel id="clientOutboundChannel"/>
<int:channel id="clientOutboundChannel" />
<int-websocket:client-container id="simpleClientWebSocketContainer"
client="webSocketClient"
uri="ws://foo.bar"/>
uri="ws://foo.bar" />
</beans>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2015 the original author or authors.
* Copyright 2014-2016 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.
@@ -17,6 +17,7 @@
package org.springframework.integration.websocket.config;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -164,6 +165,8 @@ public class WebSocketParserTests {
assertSame(this.handshakeInterceptor, interceptors[0]);
assertEquals(100, TestUtils.getPropertyValue(this.serverWebSocketContainer, "sendTimeLimit"));
assertEquals(100000, TestUtils.getPropertyValue(this.serverWebSocketContainer, "sendBufferSizeLimit"));
assertArrayEquals(new String[] {"http://foo.com"},
TestUtils.getPropertyValue(this.serverWebSocketContainer, "origins", String[].class));
WebSocketHandlerDecoratorFactory[] decoratorFactories =
TestUtils.getPropertyValue(this.serverWebSocketContainer, "decoratorFactories",
@@ -189,6 +192,7 @@ public class WebSocketParserTests {
assertEquals("https://foo.sock.js", sockJsService.getSockJsClientLibraryUrl());
assertFalse(sockJsService.isSessionCookieNeeded());
assertFalse(sockJsService.isWebSocketEnabled());
assertTrue(sockJsService.shouldSuppressCors());
assertSame(this.serverWebSocketContainer,
TestUtils.getPropertyValue(this.defaultInboundAdapter, "webSocketContainer"));

View File

@@ -188,7 +188,10 @@ public class WebSocketServerTests {
@Bean
public IntegrationWebSocketContainer clientWebSocketContainer() {
return new ClientWebSocketContainer(webSocketClient(), server().getWsBaseUrl() + "/ws");
ClientWebSocketContainer clientWebSocketContainer =
new ClientWebSocketContainer(webSocketClient(), server().getWsBaseUrl() + "/ws");
clientWebSocketContainer.setOrigin("http://foo.com");
return clientWebSocketContainer;
}
@Bean
@@ -251,6 +254,7 @@ public class WebSocketServerTests {
public ServerWebSocketContainer serverWebSocketContainer() {
return new ServerWebSocketContainer("/ws")
.setDecoratorFactories(testWebSocketHandlerDecoratorFactory())
.setAllowedOrigins("http://foo.com")
.withSockJs();
}