diff options
Diffstat (limited to 'jetty-servlets/src/main/java/org/eclipse/jetty/servlets/CrossOriginFilter.java')
-rw-r--r-- | jetty-servlets/src/main/java/org/eclipse/jetty/servlets/CrossOriginFilter.java | 159 |
1 files changed, 104 insertions, 55 deletions
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 e1ba9b988c..a4f8cabc9d 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 @@ -26,6 +26,7 @@ import java.util.Enumeration; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; + import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; @@ -35,48 +36,76 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; /** - * <p>Implementation of the - * <a href="http://www.w3.org/TR/cors/">cross-origin resource sharing</a>.</p> - * <p>A typical example is to use this filter to allow cross-domain + * Implementation of the + * <a href="http://www.w3.org/TR/cors/">cross-origin resource sharing</a>. + * <p> + * A typical example is to use this filter to allow cross-domain * <a href="http://cometd.org">cometd</a> communication using the standard * long polling transport instead of the JSONP transport (that is less - * efficient and less reactive to failures).</p> - * <p>This filter allows the following configuration parameters: - * <ul> - * <li><b>allowedOrigins</b>, a comma separated list of origins that are + * efficient and less reactive to failures). + * <p> + * This filter allows the following configuration parameters: + * <dl> + * <dt>allowedOrigins</dt> + * <dd>a comma separated list of origins that are * allowed to access the resources. Default value is <b>*</b>, meaning all - * origins.<br /> + * origins. + * <p> * If an allowed origin contains one or more * characters (for example * http://*.domain.com), then "*" characters are converted to ".*", "." * characters are escaped to "\." and the resulting allowed origin - * interpreted as a regular expression.<br /> + * interpreted as a regular expression. + * <p> * Allowed origins can therefore be more complex expressions such as * https?://*.domain.[a-z]{3} that matches http or https, multiple subdomains - * and any 3 letter top-level domain (.com, .net, .org, etc.).</li> - * <li><b>allowedMethods</b>, a comma separated list of HTTP methods that + * 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></li> - * <li><b>allowedHeaders</b>, a comma separated list of HTTP headers that + * <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 * is <b>X-Requested-With,Content-Type,Accept,Origin</b>. If the value is a single "*", - * this means that any headers will be accepted.</li> - * <li><b>preflightMaxAge</b>, the number of seconds that preflight requests + * this means that any headers will be accepted.</dd> + * + * <dt>preflightMaxAge</dt> + * <dd>the number of seconds that preflight requests * can be cached by the client. Default value is <b>1800</b> seconds, or 30 - * minutes</li> - * <li><b>allowCredentials</b>, a boolean indicating if the resource allows - * requests with credentials. Default value is <b>true</b></li> - * <li><b>exposedHeaders</b>, a comma separated list of HTTP headers that + * minutes</dd> + * + * <dt>allowCredentials</dt> + * <dd>a boolean indicating if the resource allows + * requests with credentials. Default value is <b>true</b></dd> + * + * <dt>exposedHeaders</dt> + * <dd>a comma separated list of HTTP headers that * are allowed to be exposed on the client. Default value is the - * <b>empty list</b></li> - * <li><b>chainPreflight</b>, if true preflight requests are chained to their + * <b>empty list</b></dd> + * + * <dt>chainPreflight</dt> + * <dd>if true preflight requests are chained to their * target resource for normal handling (as an OPTION request). Otherwise the - * filter will response to the preflight. Default is <b>true</b>.</li> - * </ul></p> - * <p>A typical configuration could be:</p> + * filter will response to the preflight. Default is <b>true</b>.</dd> + * + * </dl> + * A typical configuration could be: * <pre> * <web-app ...> * ... @@ -107,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"; @@ -117,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>(); @@ -134,32 +169,16 @@ 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 = allowedOriginsConfig.split(","); - 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) allowedMethods.addAll(DEFAULT_ALLOWED_METHODS); else - allowedMethods.addAll(Arrays.asList(allowedMethodsConfig.split(","))); + allowedMethods.addAll(Arrays.asList(StringUtil.csvSplit(allowedMethodsConfig))); String allowedHeadersConfig = config.getInitParameter(ALLOWED_HEADERS_PARAM); if (allowedHeadersConfig == null) @@ -167,7 +186,7 @@ public class CrossOriginFilter implements Filter else if ("*".equals(allowedHeadersConfig)) anyHeadersAllowed = true; else - allowedHeaders.addAll(Arrays.asList(allowedHeadersConfig.split(","))); + allowedHeaders.addAll(Arrays.asList(StringUtil.csvSplit(allowedHeadersConfig))); String preflightMaxAgeConfig = config.getInitParameter(PREFLIGHT_MAX_AGE_PARAM); if (preflightMaxAgeConfig == null) @@ -189,7 +208,7 @@ public class CrossOriginFilter implements Filter String exposedHeadersConfig = config.getInitParameter(EXPOSED_HEADERS_PARAM); if (exposedHeadersConfig == null) exposedHeadersConfig = ""; - exposedHeaders.addAll(Arrays.asList(exposedHeadersConfig.split(","))); + exposedHeaders.addAll(Arrays.asList(StringUtil.csvSplit(exposedHeadersConfig))); String chainPreflightConfig = config.getInitParameter(OLD_CHAIN_PREFLIGHT_PARAM); if (chainPreflightConfig != null) @@ -204,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 + ", " + @@ -214,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); @@ -225,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)) { @@ -246,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 { @@ -260,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)) @@ -276,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; @@ -400,7 +449,7 @@ public class CrossOriginFilter implements Filter return Collections.emptyList(); List<String> requestedHeaders = new ArrayList<String>(); - String[] headers = accessControlRequestHeaders.split(","); + String[] headers = StringUtil.csvSplit(accessControlRequestHeaders); for (String header : headers) { String h = header.trim(); |