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();
+ }
+ }
+}