package org.eclipse.jetty.servlets; import java.io.IOException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.FilterMapping; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.testing.ServletTester; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; public class CrossOriginFilterTest { private ServletTester tester; @Before public void init() throws Exception { tester = new ServletTester(); tester.start(); } @After public void destroy() throws Exception { if (tester != null) tester.stop(); } @Test public void testRequestWithNoOriginArrivesToApplication() throws Exception { tester.getContext().addFilter(CrossOriginFilter.class, "/*", FilterMapping.DEFAULT); final CountDownLatch latch = new CountDownLatch(1); tester.getContext().addServlet(new ServletHolder(new ResourceServlet(latch)), "/*"); String request = "" + "GET / HTTP/1.1\r\n" + "Host: localhost\r\n" + "\r\n"; String response = tester.getResponses(request); Assert.assertTrue(response.contains("HTTP/1.1 200")); Assert.assertTrue(latch.await(1, TimeUnit.SECONDS)); } @Test public void testSimpleRequestWithNonMatchingOrigin() throws Exception { FilterHolder filterHolder = new FilterHolder(new CrossOriginFilter()); String origin = "http://localhost"; filterHolder.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, origin); tester.getContext().addFilter(filterHolder, "/*", FilterMapping.DEFAULT); CountDownLatch latch = new CountDownLatch(1); tester.getContext().addServlet(new ServletHolder(new ResourceServlet(latch)), "/*"); String otherOrigin = origin.replace("localhost", "127.0.0.1"); String request = "" + "GET / HTTP/1.1\r\n" + "Host: localhost\r\n" + "Origin: " + otherOrigin + "\r\n" + "\r\n"; String response = tester.getResponses(request); Assert.assertTrue(response.contains("HTTP/1.1 200")); Assert.assertFalse(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER)); Assert.assertFalse(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER)); Assert.assertTrue(latch.await(1, TimeUnit.SECONDS)); } @Test public void testSimpleRequestWithMatchingWildcardOrigin() throws Exception { FilterHolder filterHolder = new FilterHolder(new CrossOriginFilter()); String origin = "http://subdomain.example.com"; filterHolder.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "http://*.example.com"); tester.getContext().addFilter(filterHolder, "/*", FilterMapping.DEFAULT); CountDownLatch latch = new CountDownLatch(1); tester.getContext().addServlet(new ServletHolder(new ResourceServlet(latch)), "/*"); String request = "" + "GET / HTTP/1.1\r\n" + "Host: localhost\r\n" + "Origin: " + origin + "\r\n" + "\r\n"; String response = tester.getResponses(request); Assert.assertTrue(response.contains("HTTP/1.1 200")); Assert.assertTrue(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER)); Assert.assertTrue(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER)); Assert.assertTrue(latch.await(1, TimeUnit.SECONDS)); } @Test public void testSimpleRequestWithMatchingWildcardOriginAndMultipleSubdomains() throws Exception { FilterHolder filterHolder = new FilterHolder(new CrossOriginFilter()); String origin = "http://subdomain.subdomain.example.com"; filterHolder.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "http://*.example.com"); tester.getContext().addFilter(filterHolder, "/*", FilterMapping.DEFAULT); CountDownLatch latch = new CountDownLatch(1); tester.getContext().addServlet(new ServletHolder(new ResourceServlet(latch)), "/*"); String request = "" + "GET / HTTP/1.1\r\n" + "Host: localhost\r\n" + "Origin: " + origin + "\r\n" + "\r\n"; String response = tester.getResponses(request); Assert.assertTrue(response.contains("HTTP/1.1 200")); Assert.assertTrue(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER)); Assert.assertTrue(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER)); Assert.assertTrue(latch.await(1, TimeUnit.SECONDS)); } @Test public void testSimpleRequestWithMatchingOrigin() throws Exception { FilterHolder filterHolder = new FilterHolder(new CrossOriginFilter()); String origin = "http://localhost"; filterHolder.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, origin); tester.getContext().addFilter(filterHolder, "/*", FilterMapping.DEFAULT); CountDownLatch latch = new CountDownLatch(1); tester.getContext().addServlet(new ServletHolder(new ResourceServlet(latch)), "/*"); String request = "" + "GET / HTTP/1.1\r\n" + "Host: localhost\r\n" + "Origin: " + origin + "\r\n" + "\r\n"; String response = tester.getResponses(request); Assert.assertTrue(response.contains("HTTP/1.1 200")); Assert.assertTrue(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER)); Assert.assertTrue(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER)); Assert.assertTrue(latch.await(1, TimeUnit.SECONDS)); } @Test public void testSimpleRequestWithMatchingMultipleOrigins() throws Exception { FilterHolder filterHolder = new FilterHolder(new CrossOriginFilter()); String origin = "http://localhost"; String otherOrigin = origin.replace("localhost", "127.0.0.1"); filterHolder.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, origin + "," + otherOrigin); tester.getContext().addFilter(filterHolder, "/*", FilterMapping.DEFAULT); CountDownLatch latch = new CountDownLatch(1); tester.getContext().addServlet(new ServletHolder(new ResourceServlet(latch)), "/*"); String request = "" + "GET / HTTP/1.1\r\n" + "Host: localhost\r\n" + // Use 2 spaces as separator to test that the implementation does not fail "Origin: " + otherOrigin + " " + " " + origin + "\r\n" + "\r\n"; String response = tester.getResponses(request); Assert.assertTrue(response.contains("HTTP/1.1 200")); Assert.assertTrue(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER)); Assert.assertTrue(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER)); Assert.assertTrue(latch.await(1, TimeUnit.SECONDS)); } @Test public void testSimpleRequestWithoutCredentials() throws Exception { FilterHolder filterHolder = new FilterHolder(new CrossOriginFilter()); filterHolder.setInitParameter(CrossOriginFilter.ALLOW_CREDENTIALS_PARAM, "false"); tester.getContext().addFilter(filterHolder, "/*", FilterMapping.DEFAULT); CountDownLatch latch = new CountDownLatch(1); tester.getContext().addServlet(new ServletHolder(new ResourceServlet(latch)), "/*"); String request = "" + "GET / HTTP/1.1\r\n" + "Host: localhost\r\n" + "Origin: http://localhost\r\n" + "\r\n"; String response = tester.getResponses(request); Assert.assertTrue(response.contains("HTTP/1.1 200")); Assert.assertTrue(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER)); Assert.assertFalse(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER)); Assert.assertTrue(latch.await(1, TimeUnit.SECONDS)); } @Test public void testNonSimpleRequestWithoutPreflight() throws Exception { // We cannot know if an actual request has performed the preflight before: // we'll trust browsers to do it right, so responses to actual requests // will contain the CORS response headers. FilterHolder filterHolder = new FilterHolder(new CrossOriginFilter()); tester.getContext().addFilter(filterHolder, "/*", FilterMapping.DEFAULT); CountDownLatch latch = new CountDownLatch(1); tester.getContext().addServlet(new ServletHolder(new ResourceServlet(latch)), "/*"); String request = "" + "PUT / HTTP/1.1\r\n" + "Host: localhost\r\n" + "Origin: http://localhost\r\n" + "\r\n"; String response = tester.getResponses(request); Assert.assertTrue(response.contains("HTTP/1.1 200")); Assert.assertTrue(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER)); Assert.assertTrue(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER)); Assert.assertTrue(latch.await(1, TimeUnit.SECONDS)); } @Test public void testOptionsRequestButNotPreflight() throws Exception { // We cannot know if an actual request has performed the preflight before: // we'll trust browsers to do it right, so responses to actual requests // will contain the CORS response headers. FilterHolder filterHolder = new FilterHolder(new CrossOriginFilter()); tester.getContext().addFilter(filterHolder, "/*", FilterMapping.DEFAULT); CountDownLatch latch = new CountDownLatch(1); tester.getContext().addServlet(new ServletHolder(new ResourceServlet(latch)), "/*"); String request = "" + "OPTIONS / HTTP/1.1\r\n" + "Host: localhost\r\n" + "Origin: http://localhost\r\n" + "\r\n"; String response = tester.getResponses(request); Assert.assertTrue(response.contains("HTTP/1.1 200")); Assert.assertTrue(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER)); Assert.assertTrue(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER)); Assert.assertTrue(latch.await(1, TimeUnit.SECONDS)); } @Test public void testPUTRequestWithPreflight() throws Exception { FilterHolder filterHolder = new FilterHolder(new CrossOriginFilter()); filterHolder.setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM, "PUT"); tester.getContext().addFilter(filterHolder, "/*", FilterMapping.DEFAULT); CountDownLatch latch = new CountDownLatch(1); tester.getContext().addServlet(new ServletHolder(new ResourceServlet(latch)), "/*"); // Preflight request String request = "" + "OPTIONS / HTTP/1.1\r\n" + "Host: localhost\r\n" + CrossOriginFilter.ACCESS_CONTROL_REQUEST_METHOD_HEADER + ": PUT\r\n" + "Origin: http://localhost\r\n" + "\r\n"; String response = tester.getResponses(request); Assert.assertTrue(response.contains("HTTP/1.1 200")); Assert.assertTrue(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER)); Assert.assertTrue(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER)); Assert.assertTrue(response.contains(CrossOriginFilter.ACCESS_CONTROL_MAX_AGE_HEADER)); Assert.assertTrue(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_METHODS_HEADER)); Assert.assertTrue(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_HEADERS_HEADER)); Assert.assertTrue(latch.await(1, TimeUnit.SECONDS)); // Preflight request was ok, now make the actual request request = "" + "PUT / HTTP/1.1\r\n" + "Host: localhost\r\n" + "Origin: http://localhost\r\n" + "\r\n"; response = tester.getResponses(request); Assert.assertTrue(response.contains("HTTP/1.1 200")); Assert.assertTrue(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER)); Assert.assertTrue(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER)); } @Test public void testDELETERequestWithPreflightAndAllowedCustomHeaders() throws Exception { FilterHolder filterHolder = new FilterHolder(new CrossOriginFilter()); filterHolder.setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM, "GET,HEAD,POST,PUT,DELETE"); filterHolder.setInitParameter(CrossOriginFilter.ALLOWED_HEADERS_PARAM, "X-Requested-With,Content-Type,Accept,Origin,X-Custom"); tester.getContext().addFilter(filterHolder, "/*", FilterMapping.DEFAULT); CountDownLatch latch = new CountDownLatch(1); tester.getContext().addServlet(new ServletHolder(new ResourceServlet(latch)), "/*"); // Preflight request String request = "" + "OPTIONS / HTTP/1.1\r\n" + "Host: localhost\r\n" + CrossOriginFilter.ACCESS_CONTROL_REQUEST_METHOD_HEADER + ": DELETE\r\n" + CrossOriginFilter.ACCESS_CONTROL_REQUEST_HEADERS_HEADER + ": origin,x-custom,x-requested-with\r\n" + "Origin: http://localhost\r\n" + "\r\n"; String response = tester.getResponses(request); Assert.assertTrue(response.contains("HTTP/1.1 200")); Assert.assertTrue(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER)); Assert.assertTrue(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER)); Assert.assertTrue(response.contains(CrossOriginFilter.ACCESS_CONTROL_MAX_AGE_HEADER)); Assert.assertTrue(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_METHODS_HEADER)); Assert.assertTrue(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_HEADERS_HEADER)); Assert.assertTrue(latch.await(1, TimeUnit.SECONDS)); // Preflight request was ok, now make the actual request request = "" + "DELETE / HTTP/1.1\r\n" + "Host: localhost\r\n" + "X-Custom: value\r\n" + "X-Requested-With: local\r\n" + "Origin: http://localhost\r\n" + "\r\n"; response = tester.getResponses(request); Assert.assertTrue(response.contains("HTTP/1.1 200")); Assert.assertTrue(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER)); Assert.assertTrue(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER)); } @Test public void testDELETERequestWithPreflightAndNotAllowedCustomHeaders() throws Exception { FilterHolder filterHolder = new FilterHolder(new CrossOriginFilter()); filterHolder.setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM, "GET,HEAD,POST,PUT,DELETE"); tester.getContext().addFilter(filterHolder, "/*", FilterMapping.DEFAULT); CountDownLatch latch = new CountDownLatch(1); tester.getContext().addServlet(new ServletHolder(new ResourceServlet(latch)), "/*"); // Preflight request String request = "" + "OPTIONS / HTTP/1.1\r\n" + "Host: localhost\r\n" + CrossOriginFilter.ACCESS_CONTROL_REQUEST_METHOD_HEADER + ": DELETE\r\n" + CrossOriginFilter.ACCESS_CONTROL_REQUEST_HEADERS_HEADER + ": origin,x-custom,x-requested-with\r\n" + "Origin: http://localhost\r\n" + "\r\n"; String response = tester.getResponses(request); Assert.assertTrue(response.contains("HTTP/1.1 200")); Assert.assertFalse(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER)); Assert.assertFalse(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER)); Assert.assertTrue(latch.await(1, TimeUnit.SECONDS)); // The preflight request failed because header X-Custom is not allowed, actual request not issued } @Test public void testCrossOriginFilterDisabledForWebSocketUpgrade() throws Exception { FilterHolder filterHolder = new FilterHolder(new CrossOriginFilter()); tester.getContext().addFilter(filterHolder, "/*", FilterMapping.DEFAULT); CountDownLatch latch = new CountDownLatch(1); tester.getContext().addServlet(new ServletHolder(new ResourceServlet(latch)), "/*"); String request = "" + "GET / HTTP/1.1\r\n" + "Host: localhost\r\n" + "Connection: Upgrade\r\n" + "Upgrade: WebSocket\r\n" + "Origin: http://localhost\r\n" + "\r\n"; String response = tester.getResponses(request); Assert.assertTrue(response.contains("HTTP/1.1 200")); Assert.assertFalse(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER)); Assert.assertFalse(response.contains(CrossOriginFilter.ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER)); Assert.assertTrue(latch.await(1, TimeUnit.SECONDS)); } public static class ResourceServlet extends HttpServlet { private static final long serialVersionUID = 1L; private final CountDownLatch latch; public ResourceServlet(CountDownLatch latch) { this.latch = latch; } @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { latch.countDown(); } } }