From d5a2ec877cd98022ade64cafefc83bfcc7ae0782 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Wed, 19 Oct 2011 13:50:53 +0200 Subject: [PATCH] 360912 CrossOriginFilter does not send Access-Control-Allow-Origin on responses. 355103 Make allowCredentials default to true in CrossOriginFilter. The actual response now contains the Access-Control-Allow-Origin header, and fixed also 355103 that was not really fixed. Added test suite. --- .../jetty/servlets/CrossOriginFilter.java | 152 +++++--- .../jetty/servlets/CrossOriginFilterTest.java | 343 ++++++++++++++++++ 2 files changed, 436 insertions(+), 59 deletions(-) create mode 100644 jetty-servlets/src/test/java/org/eclipse/jetty/servlets/CrossOriginFilterTest.java diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/CrossOriginFilter.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/CrossOriginFilter.java index 67657cfad0a..e38c73de306 100644 --- a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/CrossOriginFilter.java +++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/CrossOriginFilter.java @@ -32,7 +32,7 @@ import org.eclipse.jetty.util.log.Logger; /** *

Implementation of the - * cross-origin resource sharing.

+ * cross-origin resource sharing.

*

A typical example is to use this filter to allow cross-domain * cometd communication using the standard * long polling transport instead of the JSONP transport (that is less @@ -74,38 +74,39 @@ import org.eclipse.jetty.util.log.Logger; */ public class CrossOriginFilter implements Filter { - private static final Logger LOG = Log.getLogger(CrossOriginFilter.class); + private static final Logger logger = Log.getLogger(CrossOriginFilter.class); // Request headers private static final String ORIGIN_HEADER = "Origin"; - private static final String ACCESS_CONTROL_REQUEST_METHOD_HEADER = "Access-Control-Request-Method"; - private static final String ACCESS_CONTROL_REQUEST_HEADERS_HEADER = "Access-Control-Request-Headers"; + public static final String ACCESS_CONTROL_REQUEST_METHOD_HEADER = "Access-Control-Request-Method"; + public static final String ACCESS_CONTROL_REQUEST_HEADERS_HEADER = "Access-Control-Request-Headers"; // Response headers - private static final String ACCESS_CONTROL_ALLOW_ORIGIN_HEADER = "Access-Control-Allow-Origin"; - private static final String ACCESS_CONTROL_ALLOW_METHODS_HEADER = "Access-Control-Allow-Methods"; - private static final String ACCESS_CONTROL_ALLOW_HEADERS_HEADER = "Access-Control-Allow-Headers"; - private static final String ACCESS_CONTROL_MAX_AGE_HEADER = "Access-Control-Max-Age"; - private static final String ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER = "Access-Control-Allow-Credentials"; + public static final String ACCESS_CONTROL_ALLOW_ORIGIN_HEADER = "Access-Control-Allow-Origin"; + public static final String ACCESS_CONTROL_ALLOW_METHODS_HEADER = "Access-Control-Allow-Methods"; + public static final String ACCESS_CONTROL_ALLOW_HEADERS_HEADER = "Access-Control-Allow-Headers"; + public static final String ACCESS_CONTROL_MAX_AGE_HEADER = "Access-Control-Max-Age"; + public static final String ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER = "Access-Control-Allow-Credentials"; // Implementation constants - private static final String ALLOWED_ORIGINS_PARAM = "allowedOrigins"; - private static final String ALLOWED_METHODS_PARAM = "allowedMethods"; - private static final String ALLOWED_HEADERS_PARAM = "allowedHeaders"; - private static final String PREFLIGHT_MAX_AGE_PARAM = "preflightMaxAge"; - private static final String ALLOWED_CREDENTIALS_PARAM = "allowCredentials"; + public static final String ALLOWED_ORIGINS_PARAM = "allowedOrigins"; + public static final String ALLOWED_METHODS_PARAM = "allowedMethods"; + public static final String ALLOWED_HEADERS_PARAM = "allowedHeaders"; + public static final String PREFLIGHT_MAX_AGE_PARAM = "preflightMaxAge"; + public static final String ALLOW_CREDENTIALS_PARAM = "allowCredentials"; private static final String ANY_ORIGIN = "*"; private static final List SIMPLE_HTTP_METHODS = Arrays.asList("GET", "POST", "HEAD"); - private boolean anyOriginAllowed = false; + private boolean anyOriginAllowed; private List allowedOrigins = new ArrayList(); private List allowedMethods = new ArrayList(); private List allowedHeaders = new ArrayList(); private int preflightMaxAge = 0; - private boolean allowCredentials = true; + private boolean allowCredentials; public void init(FilterConfig config) throws ServletException { String allowedOriginsConfig = config.getInitParameter(ALLOWED_ORIGINS_PARAM); - if (allowedOriginsConfig == null) allowedOriginsConfig = "*"; + if (allowedOriginsConfig == null) + allowedOriginsConfig = "*"; String[] allowedOrigins = allowedOriginsConfig.split(","); for (String allowedOrigin : allowedOrigins) { @@ -126,34 +127,38 @@ public class CrossOriginFilter implements Filter } String allowedMethodsConfig = config.getInitParameter(ALLOWED_METHODS_PARAM); - if (allowedMethodsConfig == null) allowedMethodsConfig = "GET,POST"; + if (allowedMethodsConfig == null) + allowedMethodsConfig = "GET,POST,HEAD"; allowedMethods.addAll(Arrays.asList(allowedMethodsConfig.split(","))); String allowedHeadersConfig = config.getInitParameter(ALLOWED_HEADERS_PARAM); - if (allowedHeadersConfig == null) allowedHeadersConfig = "X-Requested-With,Content-Type,Accept,Origin"; + if (allowedHeadersConfig == null) + allowedHeadersConfig = "X-Requested-With,Content-Type,Accept,Origin"; allowedHeaders.addAll(Arrays.asList(allowedHeadersConfig.split(","))); String preflightMaxAgeConfig = config.getInitParameter(PREFLIGHT_MAX_AGE_PARAM); - if (preflightMaxAgeConfig == null) preflightMaxAgeConfig = "1800"; // Default is 30 minutes + if (preflightMaxAgeConfig == null) + preflightMaxAgeConfig = "1800"; // Default is 30 minutes try { preflightMaxAge = Integer.parseInt(preflightMaxAgeConfig); } catch (NumberFormatException x) { - LOG.info("Cross-origin filter, could not parse '{}' parameter as integer: {}", PREFLIGHT_MAX_AGE_PARAM, preflightMaxAgeConfig); + logger.info("Cross-origin filter, could not parse '{}' parameter as integer: {}", PREFLIGHT_MAX_AGE_PARAM, preflightMaxAgeConfig); } - String allowedCredentialsConfig = config.getInitParameter(ALLOWED_CREDENTIALS_PARAM); - if (allowedCredentialsConfig == null) allowedCredentialsConfig = "false"; + String allowedCredentialsConfig = config.getInitParameter(ALLOW_CREDENTIALS_PARAM); + if (allowedCredentialsConfig == null) + allowedCredentialsConfig = "true"; allowCredentials = Boolean.parseBoolean(allowedCredentialsConfig); - LOG.debug("Cross-origin filter configuration: " + - ALLOWED_ORIGINS_PARAM + " = " + allowedOriginsConfig + ", " + - ALLOWED_METHODS_PARAM + " = " + allowedMethodsConfig + ", " + - ALLOWED_HEADERS_PARAM + " = " + allowedHeadersConfig + ", " + - PREFLIGHT_MAX_AGE_PARAM + " = " + preflightMaxAgeConfig + ", " + - ALLOWED_CREDENTIALS_PARAM + " = " + allowedCredentialsConfig); + logger.debug("Cross-origin filter configuration: " + + ALLOWED_ORIGINS_PARAM + " = " + allowedOriginsConfig + ", " + + ALLOWED_METHODS_PARAM + " = " + allowedMethodsConfig + ", " + + ALLOWED_HEADERS_PARAM + " = " + allowedHeadersConfig + ", " + + PREFLIGHT_MAX_AGE_PARAM + " = " + preflightMaxAgeConfig + ", " + + ALLOW_CREDENTIALS_PARAM + " = " + allowedCredentialsConfig); } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException @@ -171,18 +176,23 @@ public class CrossOriginFilter implements Filter { if (isSimpleRequest(request)) { - LOG.debug("Cross-origin request to {} is a simple cross-origin request", request.getRequestURI()); + logger.debug("Cross-origin request to {} is a simple cross-origin request", request.getRequestURI()); handleSimpleResponse(request, response, origin); } + else if (isPreflightRequest(request)) + { + logger.debug("Cross-origin request to {} is a preflight cross-origin request", request.getRequestURI()); + handlePreflightResponse(request, response, origin); + } else { - LOG.debug("Cross-origin request to {} is a preflight cross-origin request", request.getRequestURI()); - handlePreflightResponse(request, response, origin); + logger.debug("Cross-origin request to {} is a non-simple cross-origin request", request.getRequestURI()); + handleSimpleResponse(request, response, origin); } } else { - LOG.debug("Cross-origin request to " + request.getRequestURI() + " with origin " + origin + " does not match allowed origins " + allowedOrigins); + logger.debug("Cross-origin request to " + request.getRequestURI() + " with origin " + origin + " does not match allowed origins " + allowedOrigins); } } @@ -201,14 +211,33 @@ public class CrossOriginFilter implements Filter return true; } - private boolean originMatches(String origin) + private boolean originMatches(String originList) { - if (anyOriginAllowed) return true; - for (String allowedOrigin : allowedOrigins) + if (anyOriginAllowed) + return true; + + if (originList.trim().length() == 0) + return false; + + String[] origins = originList.split(" "); + for (String origin : origins) { - if (allowedOrigin.equals(origin)) return true; + if (origin.trim().length() == 0) + continue; + + boolean allowed = false; + for (String allowedOrigin : allowedOrigins) + { + if (allowedOrigin.equals(origin)) + { + allowed = true; + break; + } + } + if (!allowed) + return false; } - return false; + return true; } private boolean isSimpleRequest(HttpServletRequest request) @@ -216,8 +245,8 @@ public class CrossOriginFilter implements Filter String method = request.getMethod(); if (SIMPLE_HTTP_METHODS.contains(method)) { - // TODO: implement better section 6.1 - // Section 6.1 says that for a request to be simple, custom request headers must be simple. + // TODO: implement better detection of simple headers + // The specification says that for a request to be simple, custom request headers must be simple. // Here for simplicity I just check if there is a Access-Control-Request-Method header, // which is required for preflight requests return request.getHeader(ACCESS_CONTROL_REQUEST_METHOD_HEADER) == null; @@ -225,50 +254,55 @@ public class CrossOriginFilter implements Filter return false; } + private boolean isPreflightRequest(HttpServletRequest request) + { + String method = request.getMethod(); + if (!"OPTIONS".equalsIgnoreCase(method)) + return false; + if (request.getHeader(ACCESS_CONTROL_REQUEST_METHOD_HEADER) == null) + return false; + return true; + } + private void handleSimpleResponse(HttpServletRequest request, HttpServletResponse response, String origin) { response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, origin); - if (allowCredentials) response.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER, "true"); + if (allowCredentials) + response.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER, "true"); } private void handlePreflightResponse(HttpServletRequest request, HttpServletResponse response, String origin) { - // Implementation of section 5.2 - - // 5.2.3 and 5.2.5 boolean methodAllowed = isMethodAllowed(request); - if (!methodAllowed) return; - // 5.2.4 and 5.2.6 + if (!methodAllowed) + return; boolean headersAllowed = areHeadersAllowed(request); - if (!headersAllowed) return; - // 5.2.7 + if (!headersAllowed) + return; response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, origin); - if (allowCredentials) response.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER, "true"); - // 5.2.8 - if (preflightMaxAge > 0) response.setHeader(ACCESS_CONTROL_MAX_AGE_HEADER, String.valueOf(preflightMaxAge)); - // 5.2.9 + if (allowCredentials) + response.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER, "true"); + if (preflightMaxAge > 0) + response.setHeader(ACCESS_CONTROL_MAX_AGE_HEADER, String.valueOf(preflightMaxAge)); response.setHeader(ACCESS_CONTROL_ALLOW_METHODS_HEADER, commify(allowedMethods)); - // 5.2.10 response.setHeader(ACCESS_CONTROL_ALLOW_HEADERS_HEADER, commify(allowedHeaders)); } private boolean isMethodAllowed(HttpServletRequest request) { String accessControlRequestMethod = request.getHeader(ACCESS_CONTROL_REQUEST_METHOD_HEADER); - LOG.debug("{} is {}", ACCESS_CONTROL_REQUEST_METHOD_HEADER, accessControlRequestMethod); + logger.debug("{} is {}", ACCESS_CONTROL_REQUEST_METHOD_HEADER, accessControlRequestMethod); boolean result = false; if (accessControlRequestMethod != null) - { result = allowedMethods.contains(accessControlRequestMethod); - } - LOG.debug("Method {} is" + (result ? "" : " not") + " among allowed methods {}", accessControlRequestMethod, allowedMethods); + logger.debug("Method {} is" + (result ? "" : " not") + " among allowed methods {}", accessControlRequestMethod, allowedMethods); return result; } private boolean areHeadersAllowed(HttpServletRequest request) { String accessControlRequestHeaders = request.getHeader(ACCESS_CONTROL_REQUEST_HEADERS_HEADER); - LOG.debug("{} is {}", ACCESS_CONTROL_REQUEST_HEADERS_HEADER, accessControlRequestHeaders); + logger.debug("{} is {}", ACCESS_CONTROL_REQUEST_HEADERS_HEADER, accessControlRequestHeaders); boolean result = true; if (accessControlRequestHeaders != null) { @@ -291,7 +325,7 @@ public class CrossOriginFilter implements Filter } } } - LOG.debug("Headers [{}] are" + (result ? "" : " not") + " among allowed headers {}", accessControlRequestHeaders, allowedHeaders); + logger.debug("Headers [{}] are" + (result ? "" : " not") + " among allowed headers {}", accessControlRequestHeaders, allowedHeaders); return result; } diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/CrossOriginFilterTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/CrossOriginFilterTest.java new file mode 100644 index 00000000000..e59051ffbf0 --- /dev/null +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/CrossOriginFilterTest.java @@ -0,0 +1,343 @@ +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 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 final CountDownLatch latch; + + public ResourceServlet(CountDownLatch latch) + { + this.latch = latch; + } + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + latch.countDown(); + } + } +}