View Javadoc
1   /*
2    * Copyright 2002-2015 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.web.socket.sockjs.support;
18  
19  import java.io.IOException;
20  import java.util.Arrays;
21  import javax.servlet.ServletOutputStream;
22  import javax.servlet.http.HttpServletResponse;
23  
24  import static org.junit.Assert.assertEquals;
25  import org.junit.Before;
26  import org.junit.Test;
27  
28  import org.springframework.http.HttpStatus;
29  import org.springframework.http.server.ServerHttpRequest;
30  import org.springframework.http.server.ServerHttpResponse;
31  import org.springframework.http.server.ServletServerHttpResponse;
32  import org.springframework.scheduling.TaskScheduler;
33  import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
34  import org.springframework.web.socket.AbstractHttpRequestTests;
35  import org.springframework.web.socket.WebSocketHandler;
36  import org.springframework.web.socket.sockjs.SockJsException;
37  
38  import static org.junit.Assert.*;
39  import static org.mockito.BDDMockito.*;
40  
41  /**
42   * Test fixture for {@link AbstractSockJsService}.
43   *
44   * @author Rossen Stoyanchev
45   * @author Sebastien Deleuze
46   */
47  public class SockJsServiceTests extends AbstractHttpRequestTests {
48  
49  	private TestSockJsService service;
50  
51  	private WebSocketHandler handler;
52  
53  
54  	@Override
55  	@Before
56  	public void setUp() {
57  		super.setUp();
58  		this.service = new TestSockJsService(new ThreadPoolTaskScheduler());
59  	}
60  
61  
62  	@Test
63  	public void validateRequest() throws Exception {
64  
65  		this.service.setWebSocketEnabled(false);
66  		resetResponseAndHandleRequest("GET", "/echo/server/session/websocket", HttpStatus.NOT_FOUND);
67  
68  		this.service.setWebSocketEnabled(true);
69  		resetResponseAndHandleRequest("GET", "/echo/server/session/websocket", HttpStatus.OK);
70  
71  		resetResponseAndHandleRequest("GET", "/echo//", HttpStatus.NOT_FOUND);
72  		resetResponseAndHandleRequest("GET", "/echo///", HttpStatus.NOT_FOUND);
73  		resetResponseAndHandleRequest("GET", "/echo/other", HttpStatus.NOT_FOUND);
74  		resetResponseAndHandleRequest("GET", "/echo//service/websocket", HttpStatus.NOT_FOUND);
75  		resetResponseAndHandleRequest("GET", "/echo/server//websocket", HttpStatus.NOT_FOUND);
76  		resetResponseAndHandleRequest("GET", "/echo/server/session/", HttpStatus.NOT_FOUND);
77  		resetResponseAndHandleRequest("GET", "/echo/s.erver/session/websocket", HttpStatus.NOT_FOUND);
78  		resetResponseAndHandleRequest("GET", "/echo/server/s.ession/websocket", HttpStatus.NOT_FOUND);
79  	}
80  
81  	@Test
82  	public void handleInfoGet() throws Exception {
83  		resetResponseAndHandleRequest("GET", "/echo/info", HttpStatus.OK);
84  
85  		assertEquals("application/json;charset=UTF-8", this.servletResponse.getContentType());
86  		assertEquals("no-store, no-cache, must-revalidate, max-age=0", this.servletResponse.getHeader("Cache-Control"));
87  		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Origin"));
88  		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Credentials"));
89  		assertNull(this.servletResponse.getHeader("Vary"));
90  
91  		String body = this.servletResponse.getContentAsString();
92  		assertEquals("{\"entropy\"", body.substring(0, body.indexOf(':')));
93  		assertEquals(",\"origins\":[\"*:*\"],\"cookie_needed\":true,\"websocket\":true}",
94  				body.substring(body.indexOf(',')));
95  
96  		this.service.setSessionCookieNeeded(false);
97  		this.service.setWebSocketEnabled(false);
98  		resetResponseAndHandleRequest("GET", "/echo/info", HttpStatus.OK);
99  
100 		body = this.servletResponse.getContentAsString();
101 		assertEquals(",\"origins\":[\"*:*\"],\"cookie_needed\":false,\"websocket\":false}",
102 				body.substring(body.indexOf(',')));
103 
104 		this.service.setAllowedOrigins(Arrays.asList("http://mydomain1.com"));
105 		resetResponseAndHandleRequest("GET", "/echo/info", HttpStatus.OK);
106 		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Origin"));
107 		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Credentials"));
108 		assertNull(this.servletResponse.getHeader("Vary"));
109 	}
110 
111 	@Test  // SPR-12226 and SPR-12660
112 	public void handleInfoGetWithOrigin() throws Exception {
113 		this.servletRequest.setServerName("mydomain2.com");
114 		setOrigin("http://mydomain2.com");
115 		resetResponseAndHandleRequest("GET", "/echo/info", HttpStatus.OK);
116 
117 		assertEquals("application/json;charset=UTF-8", this.servletResponse.getContentType());
118 		assertEquals("no-store, no-cache, must-revalidate, max-age=0", this.servletResponse.getHeader("Cache-Control"));
119 		assertEquals("http://mydomain2.com", this.servletResponse.getHeader("Access-Control-Allow-Origin"));
120 		assertEquals("true", this.servletResponse.getHeader("Access-Control-Allow-Credentials"));
121 		assertEquals("Origin", this.servletResponse.getHeader("Vary"));
122 
123 		String body = this.servletResponse.getContentAsString();
124 		assertEquals("{\"entropy\"", body.substring(0, body.indexOf(':')));
125 		assertEquals(",\"origins\":[\"*:*\"],\"cookie_needed\":true,\"websocket\":true}",
126 				body.substring(body.indexOf(',')));
127 
128 		this.service.setAllowedOrigins(Arrays.asList("http://mydomain1.com"));
129 		resetResponseAndHandleRequest("GET", "/echo/info", HttpStatus.FORBIDDEN);
130 		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Origin"));
131 		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Credentials"));
132 		assertNull(this.servletResponse.getHeader("Vary"));
133 
134 		this.service.setAllowedOrigins(Arrays.asList("http://mydomain1.com", "http://mydomain2.com", "http://mydomain3.com"));
135 		resetResponseAndHandleRequest("GET", "/echo/info", HttpStatus.OK);
136 		assertEquals("http://mydomain2.com", this.servletResponse.getHeader("Access-Control-Allow-Origin"));
137 		assertEquals("true", this.servletResponse.getHeader("Access-Control-Allow-Credentials"));
138 		assertEquals("Origin", this.servletResponse.getHeader("Vary"));
139 
140 		this.service.setAllowedOrigins(Arrays.asList("*"));
141 		resetResponseAndHandleRequest("GET", "/echo/info", HttpStatus.OK);
142 		assertEquals("http://mydomain2.com", this.servletResponse.getHeader("Access-Control-Allow-Origin"));
143 		assertEquals("true", this.servletResponse.getHeader("Access-Control-Allow-Credentials"));
144 		assertEquals("Origin", this.servletResponse.getHeader("Vary"));
145 	}
146 
147 	@Test  // SPR-11443
148 	public void handleInfoGetCorsFilter() throws Exception {
149 
150 		// Simulate scenario where Filter would have already set CORS headers
151 		this.servletResponse.setHeader("Access-Control-Allow-Origin", "foobar:123");
152 
153 		handleRequest("GET", "/echo/info", HttpStatus.OK);
154 
155 		assertEquals("foobar:123", this.servletResponse.getHeader("Access-Control-Allow-Origin"));
156 	}
157 
158 	@Test  // SPR-11919
159 	public void handleInfoGetWildflyNPE() throws Exception {
160 		HttpServletResponse mockResponse = mock(HttpServletResponse.class);
161 		ServletOutputStream ous = mock(ServletOutputStream.class);
162 		given(mockResponse.getHeaders("Access-Control-Allow-Origin")).willThrow(NullPointerException.class);
163 		given(mockResponse.getOutputStream()).willReturn(ous);
164 		this.response = new ServletServerHttpResponse(mockResponse);
165 
166 		handleRequest("GET", "/echo/info", HttpStatus.OK);
167 
168 		verify(mockResponse, times(1)).getOutputStream();
169 	}
170 
171 	@Test  // SPR-12660
172 	public void handleInfoOptions() throws Exception {
173 		this.servletRequest.addHeader("Access-Control-Request-Headers", "Last-Modified");
174 		resetResponseAndHandleRequest("OPTIONS", "/echo/info", HttpStatus.NO_CONTENT);
175 		this.response.flush();
176 
177 		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Origin"));
178 		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Credentials"));
179 		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Headers"));
180 		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Methods"));
181 		assertNull(this.servletResponse.getHeader("Access-Control-Max-Age"));
182 		assertEquals("Origin", this.servletResponse.getHeader("Vary"));
183 
184 		this.service.setAllowedOrigins(Arrays.asList("http://mydomain1.com"));
185 		resetResponseAndHandleRequest("OPTIONS", "/echo/info", HttpStatus.NO_CONTENT);
186 		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Origin"));
187 		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Credentials"));
188 		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Headers"));
189 		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Methods"));
190 		assertNull(this.servletResponse.getHeader("Access-Control-Max-Age"));
191 		assertEquals("Origin", this.servletResponse.getHeader("Vary"));
192 	}
193 
194 	@Test  // SPR-12226 and SPR-12660
195 	public void handleInfoOptionsWithOrigin() throws Exception {
196 		this.servletRequest.setServerName("mydomain2.com");
197 		setOrigin("http://mydomain2.com");
198 		this.request.getHeaders().add("Access-Control-Request-Headers", "Last-Modified");
199 		resetResponseAndHandleRequest("OPTIONS", "/echo/info", HttpStatus.NO_CONTENT);
200 		this.response.flush();
201 		assertEquals("http://mydomain2.com", this.servletResponse.getHeader("Access-Control-Allow-Origin"));
202 		assertEquals("true", this.servletResponse.getHeader("Access-Control-Allow-Credentials"));
203 		assertEquals("Last-Modified", this.servletResponse.getHeader("Access-Control-Allow-Headers"));
204 		assertEquals("OPTIONS, GET", this.servletResponse.getHeader("Access-Control-Allow-Methods"));
205 		assertEquals("31536000", this.servletResponse.getHeader("Access-Control-Max-Age"));
206 		assertEquals("Origin", this.servletResponse.getHeader("Vary"));
207 
208 		this.service.setAllowedOrigins(Arrays.asList("http://mydomain1.com"));
209 		resetResponseAndHandleRequest("OPTIONS", "/echo/info", HttpStatus.FORBIDDEN);
210 		this.response.flush();
211 		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Origin"));
212 		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Credentials"));
213 		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Headers"));
214 		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Methods"));
215 		assertNull(this.servletResponse.getHeader("Access-Control-Max-Age"));
216 		assertNull(this.servletResponse.getHeader("Vary"));
217 
218 		this.service.setAllowedOrigins(Arrays.asList("http://mydomain1.com", "http://mydomain2.com", "http://mydomain3.com"));
219 		resetResponseAndHandleRequest("OPTIONS", "/echo/info", HttpStatus.NO_CONTENT);
220 		this.response.flush();
221 		assertEquals("http://mydomain2.com", this.servletResponse.getHeader("Access-Control-Allow-Origin"));
222 		assertEquals("true", this.servletResponse.getHeader("Access-Control-Allow-Credentials"));
223 		assertEquals("Last-Modified", this.servletResponse.getHeader("Access-Control-Allow-Headers"));
224 		assertEquals("OPTIONS, GET", this.servletResponse.getHeader("Access-Control-Allow-Methods"));
225 		assertEquals("31536000", this.servletResponse.getHeader("Access-Control-Max-Age"));
226 		assertEquals("Origin", this.servletResponse.getHeader("Vary"));
227 
228 		this.service.setAllowedOrigins(Arrays.asList("*"));
229 		resetResponseAndHandleRequest("OPTIONS", "/echo/info", HttpStatus.NO_CONTENT);
230 		this.response.flush();
231 		assertEquals("http://mydomain2.com", this.servletResponse.getHeader("Access-Control-Allow-Origin"));
232 		assertEquals("true", this.servletResponse.getHeader("Access-Control-Allow-Credentials"));
233 		assertEquals("Last-Modified", this.servletResponse.getHeader("Access-Control-Allow-Headers"));
234 		assertEquals("OPTIONS, GET", this.servletResponse.getHeader("Access-Control-Allow-Methods"));
235 		assertEquals("31536000", this.servletResponse.getHeader("Access-Control-Max-Age"));
236 		assertEquals("Origin", this.servletResponse.getHeader("Vary"));
237 	}
238 
239 	@Test  // SPR-12283
240 	public void handleInfoOptionsWithOriginAndCorsHeadersDisabled() throws Exception {
241 		setOrigin("http://mydomain2.com");
242 		this.service.setAllowedOrigins(Arrays.asList("*"));
243 		this.service.setSuppressCors(true);
244 
245 		this.servletRequest.addHeader("Access-Control-Request-Headers", "Last-Modified");
246 		resetResponseAndHandleRequest("OPTIONS", "/echo/info", HttpStatus.NO_CONTENT);
247 		this.response.flush();
248 		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Origin"));
249 		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Credentials"));
250 		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Headers"));
251 		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Methods"));
252 		assertNull(this.servletResponse.getHeader("Access-Control-Max-Age"));
253 		assertEquals("Origin", this.servletResponse.getHeader("Vary"));
254 
255 		this.service.setAllowedOrigins(Arrays.asList("http://mydomain1.com"));
256 		resetResponseAndHandleRequest("OPTIONS", "/echo/info", HttpStatus.FORBIDDEN);
257 		this.response.flush();
258 		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Origin"));
259 		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Credentials"));
260 		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Headers"));
261 		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Methods"));
262 		assertNull(this.servletResponse.getHeader("Access-Control-Max-Age"));
263 		assertNull(this.servletResponse.getHeader("Vary"));
264 
265 		this.service.setAllowedOrigins(Arrays.asList("http://mydomain1.com", "http://mydomain2.com", "http://mydomain3.com"));
266 		resetResponseAndHandleRequest("OPTIONS", "/echo/info", HttpStatus.NO_CONTENT);
267 		this.response.flush();
268 		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Origin"));
269 		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Credentials"));
270 		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Headers"));
271 		assertNull(this.servletResponse.getHeader("Access-Control-Allow-Methods"));
272 		assertNull(this.servletResponse.getHeader("Access-Control-Max-Age"));
273 		assertEquals("Origin", this.servletResponse.getHeader("Vary"));
274 	}
275 
276 	@Test
277 	public void handleIframeRequest() throws Exception {
278 		resetResponseAndHandleRequest("GET", "/echo/iframe.html", HttpStatus.OK);
279 
280 		assertEquals("text/html;charset=UTF-8", this.servletResponse.getContentType());
281 		assertTrue(this.servletResponse.getContentAsString().startsWith("<!DOCTYPE html>\n"));
282 		assertEquals(490, this.servletResponse.getContentLength());
283 		assertEquals("no-store, no-cache, must-revalidate, max-age=0", this.response.getHeaders().getCacheControl());
284 		assertEquals("\"06b486b3208b085d9e3220f456a6caca4\"", this.response.getHeaders().getETag());
285 	}
286 
287 	@Test
288 	public void handleIframeRequestNotModified() throws Exception {
289 		this.servletRequest.addHeader("If-None-Match", "\"06b486b3208b085d9e3220f456a6caca4\"");
290 		resetResponseAndHandleRequest("GET", "/echo/iframe.html", HttpStatus.NOT_MODIFIED);
291 	}
292 
293 	@Test
294 	public void handleRawWebSocketRequest() throws Exception {
295 		resetResponseAndHandleRequest("GET", "/echo", HttpStatus.OK);
296 		assertEquals("Welcome to SockJS!\n", this.servletResponse.getContentAsString());
297 
298 		resetResponseAndHandleRequest("GET", "/echo/websocket", HttpStatus.OK);
299 		assertNull("Raw WebSocket should not open a SockJS session", this.service.sessionId);
300 		assertSame(this.handler, this.service.handler);
301 	}
302 
303 	@Test
304 	public void handleEmptyContentType() throws Exception {
305 		this.servletRequest.setContentType("");
306 		resetResponseAndHandleRequest("GET", "/echo/info", HttpStatus.OK);
307 
308 		assertEquals("Invalid/empty content should have been ignored", 200, this.servletResponse.getStatus());
309 	}
310 
311 
312 	private void resetResponseAndHandleRequest(String httpMethod, String uri, HttpStatus httpStatus) throws IOException {
313 		resetResponse();
314 		handleRequest(httpMethod, uri, httpStatus);
315 	}
316 
317 	private void handleRequest(String httpMethod, String uri, HttpStatus httpStatus) throws IOException {
318 		setRequest(httpMethod, uri);
319 		String sockJsPath = uri.substring("/echo".length());
320 		this.service.handleRequest(this.request, this.response, sockJsPath, this.handler);
321 
322 		assertEquals(httpStatus.value(), this.servletResponse.getStatus());
323 	}
324 
325 
326 	private static class TestSockJsService extends AbstractSockJsService {
327 
328 		private String sessionId;
329 
330 		@SuppressWarnings("unused")
331 		private String transport;
332 
333 		private WebSocketHandler handler;
334 
335 		public TestSockJsService(TaskScheduler scheduler) {
336 			super(scheduler);
337 		}
338 
339 		@Override
340 		protected void handleRawWebSocketRequest(ServerHttpRequest req, ServerHttpResponse res,
341 				WebSocketHandler handler) throws IOException {
342 			this.handler = handler;
343 		}
344 
345 		@Override
346 		protected void handleTransportRequest(ServerHttpRequest req, ServerHttpResponse res, WebSocketHandler handler,
347 				String sessionId, String transport) throws SockJsException {
348 			this.sessionId = sessionId;
349 			this.transport = transport;
350 			this.handler = handler;
351 		}
352 	}
353 
354 }