Skip to content

Commit 8429c4b

Browse files
committed
Revised TransportHandlingSockJsService for defensive transport checking and consistent logging
Issue: SPR-13545 (cherry picked from commit 966f95b)
1 parent be90f7d commit 8429c4b

File tree

8 files changed

+112
-85
lines changed

8 files changed

+112
-85
lines changed

spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java

Lines changed: 54 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -272,17 +272,16 @@ public boolean isWebSocketEnabled() {
272272
}
273273

274274
/**
275-
* Configure allowed {@code Origin} header values. This check is mostly designed for
276-
* browser clients. There is nothing preventing other types of client to modify the
277-
* {@code Origin} header value.
278-
*
279-
* <p>When SockJS is enabled and origins are restricted, transport types that do not
280-
* allow to check request origin (JSONP and Iframe based transports) are disabled.
281-
* As a consequence, IE 6 to 9 are not supported when origins are restricted.
282-
*
283-
* <p>Each provided allowed origin must start by "http://", "https://" or be "*"
284-
* (means that all origins are allowed).
285-
*
275+
* Configure allowed {@code Origin} header values. This check is mostly
276+
* designed for browsers. There is nothing preventing other types of client
277+
* to modify the {@code Origin} header value.
278+
* <p>When SockJS is enabled and origins are restricted, transport types
279+
* that do not allow to check request origin (JSONP and Iframe based
280+
* transports) are disabled. As a consequence, IE 6 to 9 are not supported
281+
* when origins are restricted.
282+
* <p>Each provided allowed origin must have a scheme, and optionally a port
283+
* (e.g. "http://example.org", "http://example.org:9090"). An allowed origin
284+
* string may also be "*" in which case all origins are allowed.
286285
* @since 4.1.2
287286
* @see <a href="https://tools.ietf.org/html/rfc6454">RFC 6454: The Web Origin Concept</a>
288287
* @see <a href="https://github.com/sockjs/sockjs-client#supported-transports-by-browser-html-served-from-http-or-https">SockJS supported transports by browser</a>
@@ -319,6 +318,7 @@ public boolean shouldSuppressCors() {
319318
return this.suppressCors;
320319
}
321320

321+
322322
/**
323323
* This method determines the SockJS path and handles SockJS static URLs.
324324
* Session URLs and raw WebSocket requests are delegated to abstract methods.
@@ -342,68 +342,89 @@ public final void handleRequest(ServerHttpRequest request, ServerHttpResponse re
342342
// As per SockJS protocol content-type can be ignored (it's always json)
343343
}
344344

345-
String requestInfo = logger.isDebugEnabled() ? request.getMethod() + " " + request.getURI() : "";
345+
String requestInfo = (logger.isDebugEnabled() ? request.getMethod() + " " + request.getURI() : null);
346+
346347
try {
347348
if (sockJsPath.equals("") || sockJsPath.equals("/")) {
348-
logger.debug(requestInfo);
349+
if (requestInfo != null) {
350+
logger.debug("Processing transport request: " + requestInfo);
351+
}
349352
response.getHeaders().setContentType(new MediaType("text", "plain", UTF8_CHARSET));
350353
response.getBody().write("Welcome to SockJS!\n".getBytes(UTF8_CHARSET));
351354
}
355+
352356
else if (sockJsPath.equals("/info")) {
353-
logger.debug(requestInfo);
357+
if (requestInfo != null) {
358+
logger.debug("Processing transport request: " + requestInfo);
359+
}
354360
this.infoHandler.handle(request, response);
355361
}
362+
356363
else if (sockJsPath.matches("/iframe[0-9-.a-z_]*.html")) {
357364
if (!this.allowedOrigins.isEmpty() && !this.allowedOrigins.contains("*")) {
358-
if (logger.isDebugEnabled()) {
359-
logger.debug("Iframe support is disabled when an origin check is required, ignoring " +
360-
requestInfo);
365+
if (requestInfo != null) {
366+
logger.debug("Iframe support is disabled when an origin check is required. " +
367+
"Ignoring transport request: " + requestInfo);
361368
}
362369
response.setStatusCode(HttpStatus.NOT_FOUND);
363370
return;
364371
}
365372
if (this.allowedOrigins.isEmpty()) {
366373
response.getHeaders().add(XFRAME_OPTIONS_HEADER, "SAMEORIGIN");
367374
}
368-
logger.debug(requestInfo);
375+
if (requestInfo != null) {
376+
logger.debug("Processing transport request: " + requestInfo);
377+
}
369378
this.iframeHandler.handle(request, response);
370379
}
380+
371381
else if (sockJsPath.equals("/websocket")) {
372382
if (isWebSocketEnabled()) {
373-
logger.debug(requestInfo);
383+
if (requestInfo != null) {
384+
logger.debug("Processing transport request: " + requestInfo);
385+
}
374386
handleRawWebSocketRequest(request, response, wsHandler);
375387
}
376-
else if (logger.isDebugEnabled()) {
377-
logger.debug("WebSocket disabled, ignoring " + requestInfo);
388+
else if (requestInfo != null) {
389+
logger.debug("WebSocket disabled. Ignoring transport request: " + requestInfo);
378390
}
379391
}
392+
380393
else {
381394
String[] pathSegments = StringUtils.tokenizeToStringArray(sockJsPath.substring(1), "/");
382395
if (pathSegments.length != 3) {
383396
if (logger.isWarnEnabled()) {
384-
logger.warn("Ignoring invalid transport request " + requestInfo);
397+
logger.warn("Invalid SockJS path '" + sockJsPath + "' - required to have 3 path segments");
398+
}
399+
if (requestInfo != null) {
400+
logger.debug("Ignoring transport request: " + requestInfo);
385401
}
386402
response.setStatusCode(HttpStatus.NOT_FOUND);
387403
return;
388404
}
405+
389406
String serverId = pathSegments[0];
390407
String sessionId = pathSegments[1];
391408
String transport = pathSegments[2];
392409

393410
if (!isWebSocketEnabled() && transport.equals("websocket")) {
394-
if (logger.isDebugEnabled()) {
395-
logger.debug("WebSocket transport is disabled, ignoring " + requestInfo);
411+
if (requestInfo != null) {
412+
logger.debug("WebSocket disabled. Ignoring transport request: " + requestInfo);
396413
}
397414
response.setStatusCode(HttpStatus.NOT_FOUND);
398415
return;
399416
}
400417
else if (!validateRequest(serverId, sessionId, transport)) {
401-
if (logger.isWarnEnabled()) {
402-
logger.warn("Ignoring transport request " + requestInfo);
418+
if (requestInfo != null) {
419+
logger.debug("Ignoring transport request: " + requestInfo);
403420
}
404421
response.setStatusCode(HttpStatus.NOT_FOUND);
405422
return;
406423
}
424+
425+
if (requestInfo != null) {
426+
logger.debug("Processing transport request: " + requestInfo);
427+
}
407428
handleTransportRequest(request, response, wsHandler, sessionId, transport);
408429
}
409430
response.close();
@@ -415,14 +436,16 @@ else if (!validateRequest(serverId, sessionId, transport)) {
415436

416437
protected boolean validateRequest(String serverId, String sessionId, String transport) {
417438
if (!StringUtils.hasText(serverId) || !StringUtils.hasText(sessionId) || !StringUtils.hasText(transport)) {
418-
logger.warn("No server, session, or transport path segment");
439+
logger.warn("No server, session, or transport path segment in SockJS request.");
419440
return false;
420441
}
442+
421443
// Server and session id's must not contain "."
422444
if (serverId.contains(".") || sessionId.contains(".")) {
423445
logger.warn("Either server or session contains a \".\" which is not allowed by SockJS protocol.");
424446
return false;
425447
}
448+
426449
return true;
427450
}
428451

@@ -455,7 +478,9 @@ protected boolean checkAndAddCorsHeaders(ServerHttpRequest request, ServerHttpRe
455478
}
456479

457480
if (!WebUtils.isValidOrigin(request, this.allowedOrigins)) {
458-
logger.debug("Request rejected, Origin header value " + origin + " not allowed");
481+
if (logger.isWarnEnabled()) {
482+
logger.warn("Origin header value '" + origin + "' not allowed.");
483+
}
459484
response.setStatusCode(HttpStatus.FORBIDDEN);
460485
return false;
461486
}
@@ -535,8 +560,7 @@ public void handle(ServerHttpRequest request, ServerHttpResponse response) throw
535560
}
536561
}
537562
else if (HttpMethod.OPTIONS.equals(request.getMethod())) {
538-
if (checkAndAddCorsHeaders(request, response, HttpMethod.OPTIONS,
539-
HttpMethod.GET)) {
563+
if (checkAndAddCorsHeaders(request, response, HttpMethod.OPTIONS, HttpMethod.GET)) {
540564
addCacheHeaders(response);
541565
response.setStatusCode(HttpStatus.NO_CONTENT);
542566
}

spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/SockJsHttpRequestHandler.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2015 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -56,8 +56,8 @@ public class SockJsHttpRequestHandler implements HttpRequestHandler {
5656
* @param webSocketHandler the websocket handler
5757
*/
5858
public SockJsHttpRequestHandler(SockJsService sockJsService, WebSocketHandler webSocketHandler) {
59-
Assert.notNull(sockJsService, "sockJsService must not be null");
60-
Assert.notNull(webSocketHandler, "webSocketHandler must not be null");
59+
Assert.notNull(sockJsService, "SockJsService must not be null");
60+
Assert.notNull(webSocketHandler, "WebSocketHandler must not be null");
6161
this.sockJsService = sockJsService;
6262
this.webSocketHandler =
6363
new ExceptionWebSocketHandlerDecorator(new LoggingWebSocketHandlerDecorator(webSocketHandler));
@@ -97,7 +97,7 @@ public void handleRequest(HttpServletRequest servletRequest, HttpServletResponse
9797
private String getSockJsPath(HttpServletRequest servletRequest) {
9898
String attribute = HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE;
9999
String path = (String) servletRequest.getAttribute(attribute);
100-
return ((path.length() > 0) && (path.charAt(0) != '/')) ? "/" + path : path;
100+
return (path.length() > 0 && path.charAt(0) != '/' ? "/" + path : path);
101101
}
102102

103103
}

spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/TransportHandlingSockJsService.java

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -288,13 +288,21 @@ else if (transportType.supportsCors()) {
288288

289289
@Override
290290
protected boolean validateRequest(String serverId, String sessionId, String transport) {
291-
if (!getAllowedOrigins().contains("*") && !TransportType.fromValue(transport).supportsOrigin()) {
292-
if (logger.isWarnEnabled()) {
293-
logger.warn("Origin check has been enabled, but transport " + transport + " does not support it");
294-
}
291+
if (!super.validateRequest(serverId, sessionId, transport)) {
295292
return false;
296293
}
297-
return super.validateRequest(serverId, sessionId, transport);
294+
295+
if (!getAllowedOrigins().contains("*")) {
296+
TransportType transportType = TransportType.fromValue(transport);
297+
if (transportType == null || !transportType.supportsOrigin()) {
298+
if (logger.isWarnEnabled()) {
299+
logger.warn("Origin check enabled but transport '" + transport + "' does not support it.");
300+
}
301+
return false;
302+
}
303+
}
304+
305+
return true;
298306
}
299307

300308
private SockJsSession createSockJsSession(String sessionId, SockJsSessionFactory sessionFactory,

spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/TransportType.java

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,8 @@ public enum TransportType {
5050
HTML_FILE("htmlfile", HttpMethod.GET, "cors", "jsessionid", "no_cache");
5151

5252

53-
private final String value;
54-
55-
private final HttpMethod httpMethod;
56-
57-
private final List<String> headerHints;
58-
5953
private static final Map<String, TransportType> TRANSPORT_TYPES;
54+
6055
static {
6156
Map<String, TransportType> transportTypes = new HashMap<String, TransportType>();
6257
for (TransportType type : values()) {
@@ -65,8 +60,19 @@ public enum TransportType {
6560
TRANSPORT_TYPES = Collections.unmodifiableMap(transportTypes);
6661
}
6762

63+
public static TransportType fromValue(String value) {
64+
return TRANSPORT_TYPES.get(value);
65+
}
66+
67+
68+
private final String value;
69+
70+
private final HttpMethod httpMethod;
6871

69-
private TransportType(String value, HttpMethod httpMethod, String... headerHints) {
72+
private final List<String> headerHints;
73+
74+
75+
TransportType(String value, HttpMethod httpMethod, String... headerHints) {
7076
this.value = value;
7177
this.httpMethod = httpMethod;
7278
this.headerHints = Arrays.asList(headerHints);
@@ -77,9 +83,6 @@ public String value() {
7783
return this.value;
7884
}
7985

80-
/**
81-
* The HTTP method for this transport.
82-
*/
8386
public HttpMethod getHttpMethod() {
8487
return this.httpMethod;
8588
}
@@ -88,6 +91,10 @@ public boolean sendsNoCacheInstruction() {
8891
return this.headerHints.contains("no_cache");
8992
}
9093

94+
public boolean sendsSessionCookie() {
95+
return this.headerHints.contains("jsessionid");
96+
}
97+
9198
public boolean supportsCors() {
9299
return this.headerHints.contains("cors");
93100
}
@@ -96,13 +103,6 @@ public boolean supportsOrigin() {
96103
return this.headerHints.contains("cors") || this.headerHints.contains("origin");
97104
}
98105

99-
public boolean sendsSessionCookie() {
100-
return this.headerHints.contains("jsessionid");
101-
}
102-
103-
public static TransportType fromValue(String value) {
104-
return TRANSPORT_TYPES.get(value);
105-
}
106106

107107
@Override
108108
public String toString() {

spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/handler/DefaultSockJsServiceTests.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,6 @@
1616

1717
package org.springframework.web.socket.sockjs.transport.handler;
1818

19-
import static org.junit.Assert.*;
20-
import static org.mockito.BDDMockito.*;
21-
2219
import java.util.Arrays;
2320
import java.util.Collections;
2421
import java.util.Map;
@@ -28,6 +25,7 @@
2825
import org.mockito.Mock;
2926
import org.mockito.MockitoAnnotations;
3027

28+
import org.springframework.http.HttpHeaders;
3129
import org.springframework.scheduling.TaskScheduler;
3230
import org.springframework.web.socket.AbstractHttpRequestTests;
3331
import org.springframework.web.socket.WebSocketHandler;
@@ -41,11 +39,15 @@
4139
import org.springframework.web.socket.sockjs.transport.session.StubSockJsServiceConfig;
4240
import org.springframework.web.socket.sockjs.transport.session.TestSockJsSession;
4341

42+
import static org.junit.Assert.*;
43+
import static org.mockito.BDDMockito.*;
44+
4445
/**
4546
* Test fixture for {@link org.springframework.web.socket.sockjs.transport.handler.DefaultSockJsService}.
4647
*
4748
* @author Rossen Stoyanchev
4849
* @author Sebastien Deleuze
50+
* @author Ben Kiefer
4951
*/
5052
public class DefaultSockJsServiceTests extends AbstractHttpRequestTests {
5153

@@ -176,6 +178,18 @@ public void handleTransportRequestXhrAllowedOriginsNoMatch() throws Exception {
176178
assertNull(this.response.getHeaders().getFirst("Access-Control-Allow-Credentials"));
177179
}
178180

181+
@Test // SPR-13545
182+
public void handleInvalidTransportType() throws Exception {
183+
String sockJsPath = sessionUrlPrefix + "invalid";
184+
setRequest("POST", sockJsPrefix + sockJsPath);
185+
this.service.setAllowedOrigins(Arrays.asList("http://mydomain1.com"));
186+
this.servletRequest.addHeader(HttpHeaders.ORIGIN, "http://mydomain2.com");
187+
this.servletRequest.setServerName("mydomain2.com");
188+
this.service.handleRequest(this.request, this.response, sockJsPath, this.wsHandler);
189+
190+
assertEquals(404, this.servletResponse.getStatus());
191+
}
192+
179193
@Test
180194
public void handleTransportRequestXhrOptions() throws Exception {
181195
String sockJsPath = sessionUrlPrefix + "xhr";

0 commit comments

Comments
 (0)