471604 - Extend CrossOriginFilter to provide a Timing-Allow-Origin header

applied from https://github.com/eclipse/jetty.project/pull/50

Also-by: David Seebacher <dseebacher@gmail.com>
This commit is contained in:
Greg Wilkins 2015-07-02 17:18:21 +10:00
parent 04062a8383
commit 236edce34f
2 changed files with 113 additions and 28 deletions

View File

@ -65,11 +65,20 @@ import org.eclipse.jetty.util.log.Logger;
* https?://*.domain.[a-z]{3} that matches http or https, multiple subdomains
* and any 3 letter top-level domain (.com, .net, .org, etc.).</dd>
*
* <dt>allowedTimingOrigins</dt>
* <dd>a comma separated list of origins that are
* allowed to time the resource. Default value is the empty string, meaning
* no origins.
* <p>
* The check whether the timing header is set, will be performed only if
* the user gets general access to the resource using the <b>allowedOrigins</b>.
*
* <dt>allowedMethods</dt>
* <dd>a comma separated list of HTTP methods that
* are allowed to be used when accessing the resources. Default value is
* <b>GET,POST,HEAD</b></dd>
*
*
* <dt>allowedHeaders</dt>
* <dd>a comma separated list of HTTP headers that
* are allowed to be specified when accessing the resources. Default value
@ -127,8 +136,10 @@ public class CrossOriginFilter implements Filter
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";
public static final String ACCESS_CONTROL_EXPOSE_HEADERS_HEADER = "Access-Control-Expose-Headers";
public static final String TIMING_ALLOW_ORIGIN_HEADER = "Timing-Allow-Origin";
// Implementation constants
public static final String ALLOWED_ORIGINS_PARAM = "allowedOrigins";
public static final String ALLOWED_TIMING_ORIGINS_PARAM = "allowedTimingOrigins";
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";
@ -137,13 +148,17 @@ public class CrossOriginFilter implements Filter
public static final String OLD_CHAIN_PREFLIGHT_PARAM = "forwardPreflight";
public static final String CHAIN_PREFLIGHT_PARAM = "chainPreflight";
private static final String ANY_ORIGIN = "*";
private static final String DEFAULT_ALLOWED_ORIGINS = "*";
private static final String DEFAULT_ALLOWED_TIMING_ORIGINS = "";
private static final List<String> SIMPLE_HTTP_METHODS = Arrays.asList("GET", "POST", "HEAD");
private static final List<String> DEFAULT_ALLOWED_METHODS = Arrays.asList("GET", "POST", "HEAD");
private static final List<String> DEFAULT_ALLOWED_HEADERS = Arrays.asList("X-Requested-With", "Content-Type", "Accept", "Origin");
private boolean anyOriginAllowed;
private boolean anyTimingOriginAllowed;
private boolean anyHeadersAllowed;
private List<String> allowedOrigins = new ArrayList<String>();
private List<String> allowedTimingOrigins = new ArrayList<String>();
private List<String> allowedMethods = new ArrayList<String>();
private List<String> allowedHeaders = new ArrayList<String>();
private List<String> exposedHeaders = new ArrayList<String>();
@ -154,26 +169,10 @@ public class CrossOriginFilter implements Filter
public void init(FilterConfig config) throws ServletException
{
String allowedOriginsConfig = config.getInitParameter(ALLOWED_ORIGINS_PARAM);
if (allowedOriginsConfig == null)
allowedOriginsConfig = "*";
String[] allowedOrigins = StringUtil.csvSplit(allowedOriginsConfig);
for (String allowedOrigin : allowedOrigins)
{
allowedOrigin = allowedOrigin.trim();
if (allowedOrigin.length() > 0)
{
if (ANY_ORIGIN.equals(allowedOrigin))
{
anyOriginAllowed = true;
this.allowedOrigins.clear();
break;
}
else
{
this.allowedOrigins.add(allowedOrigin);
}
}
}
String allowedTimingOriginsConfig = config.getInitParameter(ALLOWED_TIMING_ORIGINS_PARAM);
anyOriginAllowed = generateAllowedOrigins(allowedOrigins, allowedOriginsConfig, DEFAULT_ALLOWED_ORIGINS);
anyTimingOriginAllowed = generateAllowedOrigins(allowedTimingOrigins, allowedTimingOriginsConfig, DEFAULT_ALLOWED_TIMING_ORIGINS);
String allowedMethodsConfig = config.getInitParameter(ALLOWED_METHODS_PARAM);
if (allowedMethodsConfig == null)
@ -224,6 +223,7 @@ public class CrossOriginFilter implements Filter
{
LOG.debug("Cross-origin filter configuration: " +
ALLOWED_ORIGINS_PARAM + " = " + allowedOriginsConfig + ", " +
ALLOWED_TIMING_ORIGINS_PARAM + " = " + allowedTimingOriginsConfig + ", " +
ALLOWED_METHODS_PARAM + " = " + allowedMethodsConfig + ", " +
ALLOWED_HEADERS_PARAM + " = " + allowedHeadersConfig + ", " +
PREFLIGHT_MAX_AGE_PARAM + " = " + preflightMaxAgeConfig + ", " +
@ -234,6 +234,29 @@ public class CrossOriginFilter implements Filter
}
}
private boolean generateAllowedOrigins(List<String> allowedOriginStore, String allowedOriginsConfig, String defaultOrigin)
{
if (allowedOriginsConfig == null)
allowedOriginsConfig = defaultOrigin;
String[] allowedOrigins = StringUtil.csvSplit(allowedOriginsConfig);
for (String allowedOrigin : allowedOrigins)
{
if (allowedOrigin.length() > 0)
{
if (ANY_ORIGIN.equals(allowedOrigin))
{
allowedOriginStore.clear();
return true;
}
else
{
allowedOriginStore.add(allowedOrigin);
}
}
}
return false;
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
{
handle((HttpServletRequest)request, (HttpServletResponse)response, chain);
@ -245,7 +268,7 @@ public class CrossOriginFilter implements Filter
// Is it a cross origin request ?
if (origin != null && isEnabled(request))
{
if (originMatches(origin))
if (anyOriginAllowed || originMatches(allowedOrigins, origin))
{
if (isSimpleRequest(request))
{
@ -266,6 +289,15 @@ public class CrossOriginFilter implements Filter
LOG.debug("Cross-origin request to {} is a non-simple cross-origin request", request.getRequestURI());
handleSimpleResponse(request, response, origin);
}
if (anyTimingOriginAllowed || originMatches(allowedTimingOrigins, origin))
{
response.setHeader(TIMING_ALLOW_ORIGIN_HEADER, origin);
}
else
{
LOG.debug("Cross-origin request to " + request.getRequestURI() + " with origin " + origin + " does not match allowed timing origins " + allowedTimingOrigins);
}
}
else
{
@ -280,12 +312,12 @@ public class CrossOriginFilter implements Filter
{
// WebSocket clients such as Chrome 5 implement a version of the WebSocket
// protocol that does not accept extra response headers on the upgrade response
for (Enumeration connections = request.getHeaders("Connection"); connections.hasMoreElements();)
for (Enumeration<String> connections = request.getHeaders("Connection"); connections.hasMoreElements();)
{
String connection = (String)connections.nextElement();
if ("Upgrade".equalsIgnoreCase(connection))
{
for (Enumeration upgrades = request.getHeaders("Upgrade"); upgrades.hasMoreElements();)
for (Enumeration<String> upgrades = request.getHeaders("Upgrade"); upgrades.hasMoreElements();)
{
String upgrade = (String)upgrades.nextElement();
if ("WebSocket".equalsIgnoreCase(upgrade))
@ -296,11 +328,8 @@ public class CrossOriginFilter implements Filter
return true;
}
private boolean originMatches(String originList)
private boolean originMatches(List<String> allowedOrigins, String originList)
{
if (anyOriginAllowed)
return true;
if (originList.trim().length() == 0)
return false;

View File

@ -173,7 +173,7 @@ public class CrossOriginFilterTest
}
@Test
public void testSimpleRequestWithMatchingOrigin() throws Exception
public void testSimpleRequestWithMatchingOriginAndWithoutTimingOrigin() throws Exception
{
FilterHolder filterHolder = new FilterHolder(new CrossOriginFilter());
String origin = "http://localhost";
@ -193,10 +193,66 @@ public class CrossOriginFilterTest
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.assertFalse(response.contains(CrossOriginFilter.TIMING_ALLOW_ORIGIN_HEADER));
Assert.assertTrue(response.contains("Vary"));
Assert.assertTrue(latch.await(1, TimeUnit.SECONDS));
}
@Test
public void testSimpleRequestWithMatchingOriginAndNonMatchingTimingOrigin() throws Exception
{
FilterHolder filterHolder = new FilterHolder(new CrossOriginFilter());
String origin = "http://localhost";
String timingOrigin = "http://127.0.0.1";
filterHolder.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, origin);
filterHolder.setInitParameter(CrossOriginFilter.ALLOWED_TIMING_ORIGINS_PARAM, timingOrigin);
tester.getContext().addFilter(filterHolder, "/*", EnumSet.of(DispatcherType.REQUEST));
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: close\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.assertFalse(response.contains(CrossOriginFilter.TIMING_ALLOW_ORIGIN_HEADER));
Assert.assertTrue(response.contains("Vary"));
Assert.assertTrue(latch.await(1, TimeUnit.SECONDS));
}
@Test
public void testSimpleRequestWithMatchingOriginAndMatchingTimingOrigin() throws Exception
{
FilterHolder filterHolder = new FilterHolder(new CrossOriginFilter());
String origin = "http://localhost";
filterHolder.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, origin);
filterHolder.setInitParameter(CrossOriginFilter.ALLOWED_TIMING_ORIGINS_PARAM, origin);
tester.getContext().addFilter(filterHolder, "/*", EnumSet.of(DispatcherType.REQUEST));
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: close\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(response.contains(CrossOriginFilter.TIMING_ALLOW_ORIGIN_HEADER));
Assert.assertTrue(response.contains("Vary"));
Assert.assertTrue(latch.await(1, TimeUnit.SECONDS));
}
@Test
public void testSimpleRequestWithMatchingMultipleOrigins() throws Exception
{