You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jena.apache.org by an...@apache.org on 2022/09/08 08:25:29 UTC

[jena] branch main updated: GH-1519: Update CORS servlet filter

This is an automated email from the ASF dual-hosted git repository.

andy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/jena.git


The following commit(s) were added to refs/heads/main by this push:
     new a9e7640765 GH-1519: Update CORS servlet filter
     new 062a826446 Merge pull request #1520 from afs/cors
a9e7640765 is described below

commit a9e76407657773f75eb07155ac4d6478184c02e6
Author: Andy Seaborne <an...@apache.org>
AuthorDate: Wed Sep 7 18:21:22 2022 +0100

    GH-1519: Update CORS servlet filter
---
 .../jena/fuseki/servlets/CrossOriginFilter.java    | 391 ++++++++++++---------
 1 file changed, 234 insertions(+), 157 deletions(-)

diff --git a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/CrossOriginFilter.java b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/CrossOriginFilter.java
index c6657578d9..755611145b 100644
--- a/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/CrossOriginFilter.java
+++ b/jena-fuseki2/jena-fuseki-core/src/main/java/org/apache/jena/fuseki/servlets/CrossOriginFilter.java
@@ -20,29 +20,24 @@ package org.apache.jena.fuseki.servlets;
 
 // This is a copy of Jetty's CrossOriginFilter - Fuseki need something
 // that works without Jetty on the classpath when running as a WAR file.
-
+// Copy from Jetty 10.0.11
 // We elect to use and distribute under The Apache License v2.0.
 
-// Changes:
-//   Logger switched to SLF4j
-//   Add @Overides
-//   Suppress warnings.
-//   POST removed from SIMPLE_HTTP_HEADERS (NB it's the same code effect either way).
+//Changes:
+//  * Package declaration
+//  * Comment out casts in to remove warnings in method isEnabled.
+
 
-//========================================================================
-//Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
-//------------------------------------------------------------------------
-//All rights reserved. This program and the accompanying materials
-//are made available under the terms of the Eclipse Public License v1.0
-//and Apache License v2.0 which accompanies this distribution.
 //
-//  The Eclipse Public License is available at
-//  http://www.eclipse.org/legal/epl-v10.html
+//========================================================================
+//Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
 //
-//  The Apache License v2.0 is available at
-//  http://www.opensource.org/licenses/apache2.0.php
+//This program and the accompanying materials are made available under the
+//terms of the Eclipse Public License v. 2.0 which is available at
+//https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+//which is available at https://www.apache.org/licenses/LICENSE-2.0.
 //
-//You may elect to redistribute this code under either of these licenses.
+//SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
 //========================================================================
 //
 
@@ -51,11 +46,12 @@ package org.apache.jena.fuseki.servlets;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.Enumeration;
+import java.util.HashSet;
 import java.util.List;
-import java.util.regex.Matcher;
+import java.util.Set;
 import java.util.regex.Pattern;
-
 import javax.servlet.Filter;
 import javax.servlet.FilterChain;
 import javax.servlet.FilterConfig;
@@ -65,68 +61,95 @@ import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import org.eclipse.jetty.util.StringUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-//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
-* <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
-* allowed to access the resources. Default value is <b>*</b>, meaning all
-* origins.<br />
-* 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 />
-* 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
-* 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
-* are allowed to be specified when accessing the resources. Default value
-* is <b>X-Requested-With,Content-Type,Accept,Origin</b></li>
-* <li><b>preflightMaxAge</b>, 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>false</b></li>
-* <li><b>exposeHeaders</b>, 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
-* target resource for normal handling (as an OPTION request).  Otherwise the
-* filter will response to the preflight. Default is true.</li>
-* </ul>1
-* <p>A typical configuration could be:
-* <pre>
-* &lt;web-app ...&gt;
-*     ...
-*     &lt;filter&gt;
-*         &lt;filter-name&gt;cross-origin&lt;/filter-name&gt;
-*         &lt;filter-class&gt;org.eclipse.jetty.servlets.CrossOriginFilter&lt;/filter-class&gt;
-*     &lt;/filter&gt;
-*     &lt;filter-mapping&gt;
-*         &lt;filter-name&gt;cross-origin&lt;/filter-name&gt;
-*         &lt;url-pattern&gt;/cometd/*&lt;/url-pattern&gt;
-*     &lt;/filter-mapping&gt;
-*     ...
-* &lt;/web-app&gt;
-* </pre>
-*/
+ * 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>
+ * 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.    Note that using wild cards can result in security problems
+ * for requests identifying hosts that do not exist.
+ * <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.
+ * <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.).</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
+ * is <b>X-Requested-With,Content-Type,Accept,Origin</b>. If the value is a single "*",
+ * 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</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></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>.</dd>
+ *
+ * </dl>
+ * A typical configuration could be:
+ * <pre>
+ * &lt;web-app ...&gt;
+ *     ...
+ *     &lt;filter&gt;
+ *         &lt;filter-name&gt;cross-origin&lt;/filter-name&gt;
+ *         &lt;filter-class&gt;org.eclipse.jetty.servlets.CrossOriginFilter&lt;/filter-class&gt;
+ *     &lt;/filter&gt;
+ *     &lt;filter-mapping&gt;
+ *         &lt;filter-name&gt;cross-origin&lt;/filter-name&gt;
+ *         &lt;url-pattern&gt;/cometd/*&lt;/url-pattern&gt;
+ *     &lt;/filter-mapping&gt;
+ *     ...
+ * &lt;/web-app&gt;
+ * </pre>
+ */
 public class CrossOriginFilter implements Filter
 {
-    //private static final Logger LOG = Log.getLogger(CrossOriginFilter.class);
     private static final Logger LOG = LoggerFactory.getLogger(CrossOriginFilter.class);
 
     // Request headers
@@ -140,8 +163,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";
@@ -150,13 +175,22 @@ 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 List<String> SIMPLE_HTTP_METHODS = Arrays.asList("GET", /*"POST",*/ "HEAD");
+    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 List<String> allowedOrigins = new ArrayList<>();
-    private List<String> allowedMethods = new ArrayList<>();
-    private List<String> allowedHeaders = new ArrayList<>();
-    private List<String> exposedHeaders = new ArrayList<>();
+    private boolean anyTimingOriginAllowed;
+    private boolean anyHeadersAllowed;
+    private Set<String> allowedOrigins = new HashSet<String>();
+    private List<Pattern> allowedOriginPatterns = new ArrayList<Pattern>();
+    private Set<String> allowedTimingOrigins = new HashSet<String>();
+    private List<Pattern> allowedTimingOriginPatterns = new ArrayList<Pattern>();
+    private List<String> allowedMethods = new ArrayList<String>();
+    private List<String> allowedHeaders = new ArrayList<String>();
+    private List<String> exposedHeaders = new ArrayList<String>();
     private int preflightMaxAge;
     private boolean allowCredentials;
     private boolean chainPreflight;
@@ -165,36 +199,24 @@ 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, allowedOriginPatterns, allowedOriginsConfig, DEFAULT_ALLOWED_ORIGINS);
+        anyTimingOriginAllowed = generateAllowedOrigins(allowedTimingOrigins, allowedTimingOriginPatterns, allowedTimingOriginsConfig, DEFAULT_ALLOWED_TIMING_ORIGINS);
 
         String allowedMethodsConfig = config.getInitParameter(ALLOWED_METHODS_PARAM);
         if (allowedMethodsConfig == null)
-            allowedMethodsConfig = "GET,POST,HEAD";
-        allowedMethods.addAll(Arrays.asList(allowedMethodsConfig.split(",")));
+            allowedMethods.addAll(DEFAULT_ALLOWED_METHODS);
+        else
+            allowedMethods.addAll(Arrays.asList(StringUtil.csvSplit(allowedMethodsConfig)));
 
         String allowedHeadersConfig = config.getInitParameter(ALLOWED_HEADERS_PARAM);
         if (allowedHeadersConfig == null)
-            allowedHeadersConfig = "X-Requested-With,Content-Type,Accept,Origin";
-        allowedHeaders.addAll(Arrays.asList(allowedHeadersConfig.split(",")));
+            allowedHeaders.addAll(DEFAULT_ALLOWED_HEADERS);
+        else if ("*".equals(allowedHeadersConfig))
+            anyHeadersAllowed = true;
+        else
+            allowedHeaders.addAll(Arrays.asList(StringUtil.csvSplit(allowedHeadersConfig)));
 
         String preflightMaxAgeConfig = config.getInitParameter(PREFLIGHT_MAX_AGE_PARAM);
         if (preflightMaxAgeConfig == null)
@@ -216,11 +238,11 @@ 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)
-            LOG.warn("DEPRECATED CONFIGURATION: Use "+CHAIN_PREFLIGHT_PARAM+ " instead of "+OLD_CHAIN_PREFLIGHT_PARAM);
+        if (chainPreflightConfig != null)
+            LOG.warn("DEPRECATED CONFIGURATION: Use {} instead of {}", CHAIN_PREFLIGHT_PARAM, OLD_CHAIN_PREFLIGHT_PARAM);
         else
             chainPreflightConfig = config.getInitParameter(CHAIN_PREFLIGHT_PARAM);
         if (chainPreflightConfig == null)
@@ -231,16 +253,45 @@ 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 + ", " +
                 ALLOW_CREDENTIALS_PARAM + " = " + allowedCredentialsConfig + "," +
                 EXPOSED_HEADERS_PARAM + " = " + exposedHeadersConfig + "," +
                 CHAIN_PREFLIGHT_PARAM + " = " + chainPreflightConfig
-                );
+            );
         }
     }
 
+    private boolean generateAllowedOrigins(Set<String> allowedOriginStore, List<Pattern> allowedOriginPatternStore, 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();
+                    allowedOriginPatternStore.clear();
+                    return true;
+                }
+                else if (allowedOrigin.contains("*"))
+                {
+                    allowedOriginPatternStore.add(Pattern.compile(parseAllowedWildcardOriginToRegex(allowedOrigin)));
+                }
+                else
+                {
+                    allowedOriginStore.add(allowedOrigin);
+                }
+            }
+        }
+        return false;
+    }
+
     @Override
     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
     {
@@ -253,7 +304,7 @@ public class CrossOriginFilter implements Filter
         // Is it a cross origin request ?
         if (origin != null && isEnabled(request))
         {
-            if (originMatches(origin))
+            if (anyOriginAllowed || originMatches(allowedOrigins, allowedOriginPatterns, origin))
             {
                 if (isSimpleRequest(request))
                 {
@@ -274,29 +325,37 @@ 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, allowedTimingOriginPatterns, origin))
+                {
+                    response.setHeader(TIMING_ALLOW_ORIGIN_HEADER, origin);
+                }
+                else if (LOG.isDebugEnabled())
+                {
+                    LOG.debug("Cross-origin request to {} with origin {} does not match allowed timing origins {}", request.getRequestURI(), origin, allowedTimingOrigins);
+                }
             }
-            else
+            else if (LOG.isDebugEnabled())
             {
-                LOG.debug("Cross-origin request to " + request.getRequestURI() + " with origin " + origin + " does not match allowed origins " + allowedOrigins);
+                LOG.debug("Cross-origin request to {} with origin {} does not match allowed origins {}", request.getRequestURI(), origin, allowedOrigins);
             }
         }
 
         chain.doFilter(request, response);
     }
 
-    @SuppressWarnings("rawtypes")
     protected boolean isEnabled(HttpServletRequest request)
     {
         // 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();
+            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();
+                    String upgrade = /*(String)*/upgrades.nextElement();
                     if ("WebSocket".equalsIgnoreCase(upgrade))
                         return false;
                 }
@@ -305,11 +364,8 @@ public class CrossOriginFilter implements Filter
         return true;
     }
 
-    private boolean originMatches(String originList)
+    private boolean originMatches(Set<String> allowedOrigins, List<Pattern> allowedOriginPatterns, String originList)
     {
-        if (anyOriginAllowed)
-            return true;
-
         if (originList.trim().length() == 0)
             return false;
 
@@ -319,34 +375,22 @@ public class CrossOriginFilter implements Filter
             if (origin.trim().length() == 0)
                 continue;
 
-            for (String allowedOrigin : allowedOrigins)
+            if (allowedOrigins.contains(origin))
+                return true;
+
+            for (Pattern allowedOrigin : allowedOriginPatterns)
             {
-                if (allowedOrigin.contains("*"))
-                {
-                    Matcher matcher = createMatcher(origin,allowedOrigin);
-                    if (matcher.matches())
-                        return true;
-                }
-                else if (allowedOrigin.equals(origin))
-                {
+                if (allowedOrigin.matcher(origin).matches())
                     return true;
-                }
             }
         }
         return false;
     }
 
-    private Matcher createMatcher(String origin, String allowedOrigin)
-    {
-        String regex = parseAllowedWildcardOriginToRegex(allowedOrigin);
-        Pattern pattern = Pattern.compile(regex);
-        return pattern.matcher(origin);
-    }
-
     private String parseAllowedWildcardOriginToRegex(String allowedOrigin)
     {
-        String regex = allowedOrigin.replace(".","\\.");
-        return regex.replace("*",".*"); // we want to be greedy here to match multiple subdomains, thus we use .*
+        String regex = StringUtil.replace(allowedOrigin, ".", "\\.");
+        return StringUtil.replace(regex, "*", ".*"); // we want to be greedy here to match multiple subdomains, thus we use .*
     }
 
     private boolean isSimpleRequest(HttpServletRequest request)
@@ -376,6 +420,8 @@ public class CrossOriginFilter implements Filter
     private void handleSimpleResponse(HttpServletRequest request, HttpServletResponse response, String origin)
     {
         response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, origin);
+        //W3C CORS spec http://www.w3.org/TR/cors/#resource-implementation
+        response.addHeader("Vary", ORIGIN_HEADER);
         if (allowCredentials)
             response.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER, "true");
         if (!exposedHeaders.isEmpty())
@@ -385,18 +431,26 @@ public class CrossOriginFilter implements Filter
     private void handlePreflightResponse(HttpServletRequest request, HttpServletResponse response, String origin)
     {
         boolean methodAllowed = isMethodAllowed(request);
+
         if (!methodAllowed)
             return;
-        boolean headersAllowed = areHeadersAllowed(request);
+        List<String> headersRequested = getAccessControlRequestHeaders(request);
+        boolean headersAllowed = areHeadersAllowed(headersRequested);
         if (!headersAllowed)
             return;
         response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, origin);
+        //W3C CORS spec http://www.w3.org/TR/cors/#resource-implementation
+        if (!anyOriginAllowed)
+            response.addHeader("Vary", ORIGIN_HEADER);
         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));
-        response.setHeader(ACCESS_CONTROL_ALLOW_HEADERS_HEADER, commify(allowedHeaders));
+        if (anyHeadersAllowed)
+            response.setHeader(ACCESS_CONTROL_ALLOW_HEADERS_HEADER, commify(headersRequested));
+        else
+            response.setHeader(ACCESS_CONTROL_ALLOW_HEADERS_HEADER, commify(allowedHeaders));
     }
 
     private boolean isMethodAllowed(HttpServletRequest request)
@@ -410,33 +464,51 @@ public class CrossOriginFilter implements Filter
         return result;
     }
 
-    private boolean areHeadersAllowed(HttpServletRequest request)
+    private List<String> getAccessControlRequestHeaders(HttpServletRequest request)
     {
         String accessControlRequestHeaders = request.getHeader(ACCESS_CONTROL_REQUEST_HEADERS_HEADER);
         LOG.debug("{} is {}", ACCESS_CONTROL_REQUEST_HEADERS_HEADER, accessControlRequestHeaders);
+        if (accessControlRequestHeaders == null)
+            return Collections.emptyList();
+
+        List<String> requestedHeaders = new ArrayList<String>();
+        String[] headers = StringUtil.csvSplit(accessControlRequestHeaders);
+        for (String header : headers)
+        {
+            String h = header.trim();
+            if (h.length() > 0)
+                requestedHeaders.add(h);
+        }
+        return requestedHeaders;
+    }
+
+    private boolean areHeadersAllowed(List<String> requestedHeaders)
+    {
+        if (anyHeadersAllowed)
+        {
+            LOG.debug("Any header is allowed");
+            return true;
+        }
+
         boolean result = true;
-        if (accessControlRequestHeaders != null)
+        for (String requestedHeader : requestedHeaders)
         {
-            String[] headers = accessControlRequestHeaders.split(",");
-            for (String header : headers)
+            boolean headerAllowed = false;
+            for (String allowedHeader : allowedHeaders)
             {
-                boolean headerAllowed = false;
-                for (String allowedHeader : allowedHeaders)
-                {
-                    if (header.trim().equalsIgnoreCase(allowedHeader.trim()))
-                    {
-                        headerAllowed = true;
-                        break;
-                    }
-                }
-                if (!headerAllowed)
+                if (requestedHeader.equalsIgnoreCase(allowedHeader.trim()))
                 {
-                    result = false;
+                    headerAllowed = true;
                     break;
                 }
             }
+            if (!headerAllowed)
+            {
+                result = false;
+                break;
+            }
         }
-        LOG.debug("Headers [{}] are" + (result ? "" : " not") + " among allowed headers {}", accessControlRequestHeaders, allowedHeaders);
+        LOG.debug("Headers [{}] are" + (result ? "" : " not") + " among allowed headers {}", requestedHeaders, allowedHeaders);
         return result;
     }
 
@@ -445,7 +517,8 @@ public class CrossOriginFilter implements Filter
         StringBuilder builder = new StringBuilder();
         for (int i = 0; i < strings.size(); ++i)
         {
-            if (i > 0) builder.append(",");
+            if (i > 0)
+                builder.append(",");
             String string = strings.get(i);
             builder.append(string);
         }
@@ -456,7 +529,11 @@ public class CrossOriginFilter implements Filter
     public void destroy()
     {
         anyOriginAllowed = false;
+        anyTimingOriginAllowed = false;
         allowedOrigins.clear();
+        allowedOriginPatterns.clear();
+        allowedTimingOrigins.clear();
+        allowedTimingOriginPatterns.clear();
         allowedMethods.clear();
         allowedHeaders.clear();
         preflightMaxAge = 0;