Added CorsFilter to enable secure cross site scripting
[controller.git] / third-party / org.apache.catalina.filters.CorsFilter / src / main / java / org / apache / catalina / filters / CorsFilter.java
1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements.  See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License.  You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 package org.apache.catalina.filters;
18
19 import java.io.IOException;
20 import java.net.URI;
21 import java.net.URISyntaxException;
22 import java.util.Arrays;
23 import java.util.Collection;
24 import java.util.HashSet;
25 import java.util.LinkedList;
26 import java.util.List;
27 import java.util.Set;
28
29 import javax.servlet.Filter;
30 import javax.servlet.FilterChain;
31 import javax.servlet.FilterConfig;
32 import javax.servlet.ServletException;
33 import javax.servlet.ServletRequest;
34 import javax.servlet.ServletResponse;
35 import javax.servlet.http.HttpServletRequest;
36 import javax.servlet.http.HttpServletResponse;
37
38 import org.apache.catalina.filters.Constants;
39 import org.apache.juli.logging.Log;
40 import org.apache.juli.logging.LogFactory;
41 import org.apache.tomcat.util.res.StringManager;
42
43 /**
44  * <p>
45  * A {@link Filter} that enable client-side cross-origin requests by
46  * implementing W3C's CORS (<b>C</b>ross-<b>O</b>rigin <b>R</b>esource
47  * <b>S</b>haring) specification for resources. Each {@link HttpServletRequest}
48  * request is inspected as per specification, and appropriate response headers
49  * are added to {@link HttpServletResponse}.
50  * </p>
51  *
52  * <p>
53  * By default, it also sets following request attributes, that help to
54  * determine the nature of the request downstream.
55  * <ul>
56  * <li><b>cors.isCorsRequest:</b> Flag to determine if the request is a CORS
57  * request. Set to <code>true</code> if a CORS request; <code>false</code>
58  * otherwise.</li>
59  * <li><b>cors.request.origin:</b> The Origin URL, i.e. the URL of the page from
60  * where the request is originated.</li>
61  * <li>
62  * <b>cors.request.type:</b> Type of request. Possible values:
63  * <ul>
64  * <li>SIMPLE: A request which is not preceded by a pre-flight request.</li>
65  * <li>ACTUAL: A request which is preceded by a pre-flight request.</li>
66  * <li>PRE_FLIGHT: A pre-flight request.</li>
67  * <li>NOT_CORS: A normal same-origin request.</li>
68  * <li>INVALID_CORS: A cross-origin request which is invalid.</li>
69  * </ul>
70  * </li>
71  * <li><b>cors.request.headers:</b> Request headers sent as
72  * 'Access-Control-Request-Headers' header, for pre-flight request.</li>
73  * </ul>
74  * </p>
75  *
76  * @see <a href="http://www.w3.org/TR/cors/">CORS specification</a>
77  *
78  */
79 public final class CorsFilter implements Filter {
80
81     private static final Log log = LogFactory.getLog(CorsFilter.class);
82
83     private static final StringManager sm =
84             StringManager.getManager(Constants.Package);
85
86
87     /**
88      * A {@link Collection} of origins consisting of zero or more origins that
89      * are allowed access to the resource.
90      */
91     private final Collection<String> allowedOrigins;
92
93     /**
94      * Determines if any origin is allowed to make request.
95      */
96     private boolean anyOriginAllowed;
97
98     /**
99      * A {@link Collection} of methods consisting of zero or more methods that
100      * are supported by the resource.
101      */
102     private final Collection<String> allowedHttpMethods;
103
104     /**
105      * A {@link Collection} of headers consisting of zero or more header field
106      * names that are supported by the resource.
107      */
108     private final Collection<String> allowedHttpHeaders;
109
110     /**
111      * A {@link Collection} of exposed headers consisting of zero or more header
112      * field names of headers other than the simple response headers that the
113      * resource might use and can be exposed.
114      */
115     private final Collection<String> exposedHeaders;
116
117     /**
118      * A supports credentials flag that indicates whether the resource supports
119      * user credentials in the request. It is true when the resource does and
120      * false otherwise.
121      */
122     private boolean supportsCredentials;
123
124     /**
125      * Indicates (in seconds) how long the results of a pre-flight request can
126      * be cached in a pre-flight result cache.
127      */
128     private long preflightMaxAge;
129
130     /**
131      * Determines if the request should be decorated or not.
132      */
133     private boolean decorateRequest;
134
135
136     public CorsFilter() {
137         this.allowedOrigins = new HashSet<String>();
138         this.allowedHttpMethods = new HashSet<String>();
139         this.allowedHttpHeaders = new HashSet<String>();
140         this.exposedHeaders = new HashSet<String>();
141     }
142
143
144     @Override
145     public void doFilter(final ServletRequest servletRequest,
146             final ServletResponse servletResponse, final FilterChain filterChain)
147             throws IOException, ServletException {
148         if (!(servletRequest instanceof HttpServletRequest) ||
149                 !(servletResponse instanceof HttpServletResponse)) {
150             throw new ServletException(sm.getString("corsFilter.onlyHttp"));
151         }
152
153         // Safe to downcast at this point.
154         HttpServletRequest request = (HttpServletRequest) servletRequest;
155         HttpServletResponse response = (HttpServletResponse) servletResponse;
156
157         // Determines the CORS request type.
158         CorsFilter.CORSRequestType requestType = checkRequestType(request);
159
160         // Adds CORS specific attributes to request.
161         if (decorateRequest) {
162             CorsFilter.decorateCORSProperties(request, requestType);
163         }
164         switch (requestType) {
165         case SIMPLE:
166             // Handles a Simple CORS request.
167             this.handleSimpleCORS(request, response, filterChain);
168             break;
169         case ACTUAL:
170             // Handles an Actual CORS request.
171             this.handleSimpleCORS(request, response, filterChain);
172             break;
173         case PRE_FLIGHT:
174             // Handles a Pre-flight CORS request.
175             this.handlePreflightCORS(request, response, filterChain);
176             break;
177         case NOT_CORS:
178             // Handles a Normal request that is not a cross-origin request.
179             this.handleNonCORS(request, response, filterChain);
180             break;
181         default:
182             // Handles a CORS request that violates specification.
183             this.handleInvalidCORS(request, response, filterChain);
184             break;
185         }
186     }
187
188
189     @Override
190     public void init(final FilterConfig filterConfig) throws ServletException {
191         // Initialize defaults
192         parseAndStore(DEFAULT_ALLOWED_ORIGINS, DEFAULT_ALLOWED_HTTP_METHODS,
193                 DEFAULT_ALLOWED_HTTP_HEADERS, DEFAULT_EXPOSED_HEADERS,
194                 DEFAULT_SUPPORTS_CREDENTIALS, DEFAULT_PREFLIGHT_MAXAGE,
195                 DEFAULT_DECORATE_REQUEST);
196
197         if (filterConfig != null) {
198             String configAllowedOrigins = filterConfig
199                     .getInitParameter(PARAM_CORS_ALLOWED_ORIGINS);
200             String configAllowedHttpMethods = filterConfig
201                     .getInitParameter(PARAM_CORS_ALLOWED_METHODS);
202             String configAllowedHttpHeaders = filterConfig
203                     .getInitParameter(PARAM_CORS_ALLOWED_HEADERS);
204             String configExposedHeaders = filterConfig
205                     .getInitParameter(PARAM_CORS_EXPOSED_HEADERS);
206             String configSupportsCredentials = filterConfig
207                     .getInitParameter(PARAM_CORS_SUPPORT_CREDENTIALS);
208             String configPreflightMaxAge = filterConfig
209                     .getInitParameter(PARAM_CORS_PREFLIGHT_MAXAGE);
210             String configDecorateRequest = filterConfig
211                     .getInitParameter(PARAM_CORS_REQUEST_DECORATE);
212
213             parseAndStore(configAllowedOrigins, configAllowedHttpMethods,
214                     configAllowedHttpHeaders, configExposedHeaders,
215                     configSupportsCredentials, configPreflightMaxAge,
216                     configDecorateRequest);
217         }
218     }
219
220
221     /**
222      * Handles a CORS request of type {@link CORSRequestType}.SIMPLE.
223      *
224      * @param request
225      *            The {@link HttpServletRequest} object.
226      * @param response
227      *            The {@link HttpServletResponse} object.
228      * @param filterChain
229      *            The {@link FilterChain} object.
230      * @throws IOException
231      * @throws ServletException
232      * @see <a href="http://www.w3.org/TR/cors/#resource-requests">Simple
233      *      Cross-Origin Request, Actual Request, and Redirects</a>
234      */
235     protected void handleSimpleCORS(final HttpServletRequest request,
236             final HttpServletResponse response, final FilterChain filterChain)
237             throws IOException, ServletException {
238
239         CorsFilter.CORSRequestType requestType = checkRequestType(request);
240         if (!(requestType == CorsFilter.CORSRequestType.SIMPLE ||
241                 requestType == CorsFilter.CORSRequestType.ACTUAL)) {
242             throw new IllegalArgumentException(
243                     sm.getString("corsFilter.wrongType2",
244                             CorsFilter.CORSRequestType.SIMPLE,
245                             CorsFilter.CORSRequestType.ACTUAL));
246         }
247
248         final String origin = request
249                 .getHeader(CorsFilter.REQUEST_HEADER_ORIGIN);
250         final String method = request.getMethod();
251
252         // Section 6.1.2
253         if (!isOriginAllowed(origin)) {
254             handleInvalidCORS(request, response, filterChain);
255             return;
256         }
257
258         if (!allowedHttpMethods.contains(method)) {
259             handleInvalidCORS(request, response, filterChain);
260             return;
261         }
262
263         // Section 6.1.3
264         // Add a single Access-Control-Allow-Origin header.
265         if (anyOriginAllowed && !supportsCredentials) {
266             // If resource doesn't support credentials and if any origin is
267             // allowed
268             // to make CORS request, return header with '*'.
269             response.addHeader(
270                     CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
271                     "*");
272         } else {
273             // If the resource supports credentials add a single
274             // Access-Control-Allow-Origin header, with the value of the Origin
275             // header as value.
276             response.addHeader(
277                     CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
278                     origin);
279         }
280
281         // Section 6.1.3
282         // If the resource supports credentials, add a single
283         // Access-Control-Allow-Credentials header with the case-sensitive
284         // string "true" as value.
285         if (supportsCredentials) {
286             response.addHeader(
287                     CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS,
288                     "true");
289         }
290
291         // Section 6.1.4
292         // If the list of exposed headers is not empty add one or more
293         // Access-Control-Expose-Headers headers, with as values the header
294         // field names given in the list of exposed headers.
295         if ((exposedHeaders != null) && (exposedHeaders.size() > 0)) {
296             String exposedHeadersString = join(exposedHeaders, ",");
297             response.addHeader(
298                     CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS,
299                     exposedHeadersString);
300         }
301
302         // Forward the request down the filter chain.
303         filterChain.doFilter(request, response);
304     }
305
306
307     /**
308      * Handles CORS pre-flight request.
309      *
310      * @param request
311      *            The {@link HttpServletRequest} object.
312      * @param response
313      *            The {@link HttpServletResponse} object.
314      * @param filterChain
315      *            The {@link FilterChain} object.
316      * @throws IOException
317      * @throws ServletException
318      */
319     protected void handlePreflightCORS(final HttpServletRequest request,
320             final HttpServletResponse response, final FilterChain filterChain)
321             throws IOException, ServletException {
322
323         CORSRequestType requestType = checkRequestType(request);
324         if (requestType != CORSRequestType.PRE_FLIGHT) {
325             throw new IllegalArgumentException(
326                     sm.getString("corsFilter.wrongType1",
327                             CORSRequestType.PRE_FLIGHT.name().toLowerCase()));
328         }
329
330         final String origin = request
331                 .getHeader(CorsFilter.REQUEST_HEADER_ORIGIN);
332
333         // Section 6.2.2
334         if (!isOriginAllowed(origin)) {
335             handleInvalidCORS(request, response, filterChain);
336             return;
337         }
338
339         // Section 6.2.3
340         String accessControlRequestMethod = request.getHeader(
341                 CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD);
342         if (accessControlRequestMethod == null ||
343                 !HTTP_METHODS.contains(accessControlRequestMethod.trim())) {
344             handleInvalidCORS(request, response, filterChain);
345             return;
346         } else {
347             accessControlRequestMethod = accessControlRequestMethod.trim();
348         }
349
350         // Section 6.2.4
351         String accessControlRequestHeadersHeader = request.getHeader(
352                 CorsFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS);
353         List<String> accessControlRequestHeaders = new LinkedList<String>();
354         if (accessControlRequestHeadersHeader != null &&
355                 !accessControlRequestHeadersHeader.trim().isEmpty()) {
356             String[] headers = accessControlRequestHeadersHeader.trim().split(
357                     ",");
358             for (String header : headers) {
359                 accessControlRequestHeaders.add(header.trim().toLowerCase());
360             }
361         }
362
363         // Section 6.2.5
364         if (!allowedHttpMethods.contains(accessControlRequestMethod)) {
365             handleInvalidCORS(request, response, filterChain);
366             return;
367         }
368
369         // Section 6.2.6
370         if (!accessControlRequestHeaders.isEmpty()) {
371             for (String header : accessControlRequestHeaders) {
372                 if (!allowedHttpHeaders.contains(header)) {
373                     handleInvalidCORS(request, response, filterChain);
374                     return;
375                 }
376             }
377         }
378
379         // Section 6.2.7
380         if (supportsCredentials) {
381             response.addHeader(
382                     CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
383                     origin);
384             response.addHeader(
385                     CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS,
386                     "true");
387         } else {
388             if (anyOriginAllowed) {
389                 response.addHeader(
390                         CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
391                         "*");
392             } else {
393                 response.addHeader(
394                         CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
395                         origin);
396             }
397         }
398
399         // Section 6.2.8
400         if (preflightMaxAge > 0) {
401             response.addHeader(
402                     CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_MAX_AGE,
403                     String.valueOf(preflightMaxAge));
404         }
405
406         // Section 6.2.9
407         response.addHeader(
408                 CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_METHODS,
409                 accessControlRequestMethod);
410
411         // Section 6.2.10
412         if ((allowedHttpHeaders != null) && (!allowedHttpHeaders.isEmpty())) {
413             response.addHeader(
414                     CorsFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_HEADERS,
415                     join(allowedHttpHeaders, ","));
416         }
417
418         // Do not forward the request down the filter chain.
419     }
420
421
422     /**
423      * Handles a request, that's not a CORS request, but is a valid request i.e.
424      * it is not a cross-origin request. This implementation, just forwards the
425      * request down the filter chain.
426      *
427      * @param request
428      *            The {@link HttpServletRequest} object.
429      * @param response
430      *            The {@link HttpServletResponse} object.
431      * @param filterChain
432      *            The {@link FilterChain} object.
433      * @throws IOException
434      * @throws ServletException
435      */
436     private void handleNonCORS(final HttpServletRequest request,
437             final HttpServletResponse response, final FilterChain filterChain)
438             throws IOException, ServletException {
439         // Let request pass.
440         filterChain.doFilter(request, response);
441     }
442
443
444     /**
445      * Handles a CORS request that violates specification.
446      *
447      * @param request
448      *            The {@link HttpServletRequest} object.
449      * @param response
450      *            The {@link HttpServletResponse} object.
451      * @param filterChain
452      *            The {@link FilterChain} object.
453      */
454     private void handleInvalidCORS(final HttpServletRequest request,
455             final HttpServletResponse response, final FilterChain filterChain) {
456         String origin = request.getHeader(CorsFilter.REQUEST_HEADER_ORIGIN);
457         String method = request.getMethod();
458         String accessControlRequestHeaders = request.getHeader(
459                 REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS);
460
461         response.setContentType("text/plain");
462         response.setStatus(HttpServletResponse.SC_FORBIDDEN);
463         response.resetBuffer();
464
465         if (log.isDebugEnabled()) {
466             // Debug so no need for i18n
467             StringBuilder message =
468                     new StringBuilder("Invalid CORS request; Origin=");
469             message.append(origin);
470             message.append(";Method=");
471             message.append(method);
472             if (accessControlRequestHeaders != null) {
473                 message.append(";Access-Control-Request-Headers=");
474                 message.append(accessControlRequestHeaders);
475             }
476             log.debug(message.toString());
477         }
478     }
479
480
481     @Override
482     public void destroy() {
483         // NOOP
484     }
485
486
487     /**
488      * Decorates the {@link HttpServletRequest}, with CORS attributes.
489      * <ul>
490      * <li><b>cors.isCorsRequest:</b> Flag to determine if request is a CORS
491      * request. Set to <code>true</code> if CORS request; <code>false</code>
492      * otherwise.</li>
493      * <li><b>cors.request.origin:</b> The Origin URL.</li>
494      * <li><b>cors.request.type:</b> Type of request. Values:
495      * <code>simple</code> or <code>preflight</code> or <code>not_cors</code> or
496      * <code>invalid_cors</code></li>
497      * <li><b>cors.request.headers:</b> Request headers sent as
498      * 'Access-Control-Request-Headers' header, for pre-flight request.</li>
499      * </ul>
500      *
501      * @param request
502      *            The {@link HttpServletRequest} object.
503      * @param corsRequestType
504      *            The {@link CORSRequestType} object.
505      */
506     protected static void decorateCORSProperties(
507             final HttpServletRequest request,
508             final CORSRequestType corsRequestType) {
509         if (request == null) {
510             throw new IllegalArgumentException(
511                     sm.getString("corsFilter.nullRequest"));
512         }
513
514         if (corsRequestType == null) {
515             throw new IllegalArgumentException(
516                     sm.getString("corsFilter.nullRequestType"));
517         }
518
519         switch (corsRequestType) {
520         case SIMPLE:
521             request.setAttribute(
522                     CorsFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST,
523                     Boolean.TRUE);
524             request.setAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN,
525                     request.getHeader(CorsFilter.REQUEST_HEADER_ORIGIN));
526             request.setAttribute(
527                     CorsFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE,
528                     corsRequestType.name().toLowerCase());
529             break;
530         case ACTUAL:
531             request.setAttribute(
532                     CorsFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST,
533                     Boolean.TRUE);
534             request.setAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN,
535                     request.getHeader(CorsFilter.REQUEST_HEADER_ORIGIN));
536             request.setAttribute(
537                     CorsFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE,
538                     corsRequestType.name().toLowerCase());
539             break;
540         case PRE_FLIGHT:
541             request.setAttribute(
542                     CorsFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST,
543                     Boolean.TRUE);
544             request.setAttribute(CorsFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN,
545                     request.getHeader(CorsFilter.REQUEST_HEADER_ORIGIN));
546             request.setAttribute(
547                     CorsFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE,
548                     corsRequestType.name().toLowerCase());
549             String headers = request.getHeader(
550                     REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS);
551             if (headers == null) {
552                 headers = "";
553             }
554             request.setAttribute(
555                     CorsFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_HEADERS, headers);
556             break;
557         case NOT_CORS:
558             request.setAttribute(
559                     CorsFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST,
560                     Boolean.FALSE);
561             break;
562         default:
563             // Don't set any attributes
564             break;
565         }
566     }
567
568
569     /**
570      * Joins elements of {@link Set} into a string, where each element is
571      * separated by the provided separator.
572      *
573      * @param elements
574      *            The {@link Set} containing elements to join together.
575      * @param joinSeparator
576      *            The character to be used for separating elements.
577      * @return The joined {@link String}; <code>null</code> if elements
578      *         {@link Set} is null.
579      */
580     protected static String join(final Collection<String> elements,
581             final String joinSeparator) {
582         String separator = ",";
583         if (elements == null) {
584             return null;
585         }
586         if (joinSeparator != null) {
587             separator = joinSeparator;
588         }
589         StringBuilder buffer = new StringBuilder();
590         boolean isFirst = true;
591         for (String element : elements) {
592             if (!isFirst) {
593                 buffer.append(separator);
594             } else {
595                 isFirst = false;
596             }
597
598             if (element != null) {
599                 buffer.append(element);
600             }
601         }
602
603         return buffer.toString();
604     }
605
606
607     /**
608      * Determines the request type.
609      *
610      * @param request
611      */
612     protected CORSRequestType checkRequestType(final HttpServletRequest request) {
613         CORSRequestType requestType = CORSRequestType.INVALID_CORS;
614         if (request == null) {
615             throw new IllegalArgumentException(
616                     sm.getString("corsFilter.nullRequest"));
617         }
618         String originHeader = request.getHeader(REQUEST_HEADER_ORIGIN);
619         // Section 6.1.1 and Section 6.2.1
620         if (originHeader != null) {
621             if (originHeader.isEmpty()) {
622                 requestType = CORSRequestType.INVALID_CORS;
623             } else if (!isValidOrigin(originHeader)) {
624                 requestType = CORSRequestType.INVALID_CORS;
625             } else {
626                 String method = request.getMethod();
627                 if (method != null && HTTP_METHODS.contains(method)) {
628                     if ("OPTIONS".equals(method)) {
629                         String accessControlRequestMethodHeader =
630                                 request.getHeader(
631                                         REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD);
632                         if (accessControlRequestMethodHeader != null &&
633                                 !accessControlRequestMethodHeader.isEmpty()) {
634                             requestType = CORSRequestType.PRE_FLIGHT;
635                         } else if (accessControlRequestMethodHeader != null &&
636                                 accessControlRequestMethodHeader.isEmpty()) {
637                             requestType = CORSRequestType.INVALID_CORS;
638                         } else {
639                             requestType = CORSRequestType.ACTUAL;
640                         }
641                     } else if ("GET".equals(method) || "HEAD".equals(method)) {
642                         requestType = CORSRequestType.SIMPLE;
643                     } else if ("POST".equals(method)) {
644                         String contentType = request.getContentType();
645                         if (contentType != null) {
646                             contentType = contentType.toLowerCase().trim();
647                             if (SIMPLE_HTTP_REQUEST_CONTENT_TYPE_VALUES
648                                     .contains(contentType)) {
649                                 requestType = CORSRequestType.SIMPLE;
650                             } else {
651                                 requestType = CORSRequestType.ACTUAL;
652                             }
653                         }
654                     } else if (COMPLEX_HTTP_METHODS.contains(method)) {
655                         requestType = CORSRequestType.ACTUAL;
656                     }
657                 }
658             }
659         } else {
660             requestType = CORSRequestType.NOT_CORS;
661         }
662
663         return requestType;
664     }
665
666
667     /**
668      * Checks if the Origin is allowed to make a CORS request.
669      *
670      * @param origin
671      *            The Origin.
672      * @return <code>true</code> if origin is allowed; <code>false</code>
673      *         otherwise.
674      */
675     private boolean isOriginAllowed(final String origin) {
676         if (anyOriginAllowed) {
677             return true;
678         }
679
680         // If 'Origin' header is a case-sensitive match of any of allowed
681         // origins, then return true, else return false.
682         return allowedOrigins.contains(origin);
683     }
684
685
686     /**
687      * Parses each param-value and populates configuration variables. If a param
688      * is provided, it overrides the default.
689      *
690      * @param allowedOrigins
691      *            A {@link String} of comma separated origins.
692      * @param allowedHttpMethods
693      *            A {@link String} of comma separated HTTP methods.
694      * @param allowedHttpHeaders
695      *            A {@link String} of comma separated HTTP headers.
696      * @param exposedHeaders
697      *            A {@link String} of comma separated headers that needs to be
698      *            exposed.
699      * @param supportsCredentials
700      *            "true" if support credentials needs to be enabled.
701      * @param preflightMaxAge
702      *            The amount of seconds the user agent is allowed to cache the
703      *            result of the pre-flight request.
704      * @throws ServletException
705      */
706     private void parseAndStore(final String allowedOrigins,
707             final String allowedHttpMethods, final String allowedHttpHeaders,
708             final String exposedHeaders, final String supportsCredentials,
709             final String preflightMaxAge, final String decorateRequest)
710                     throws ServletException {
711         if (allowedOrigins != null) {
712             if (allowedOrigins.trim().equals("*")) {
713                 this.anyOriginAllowed = true;
714             } else {
715                 this.anyOriginAllowed = false;
716                 Set<String> setAllowedOrigins =
717                         parseStringToSet(allowedOrigins);
718                 this.allowedOrigins.clear();
719                 this.allowedOrigins.addAll(setAllowedOrigins);
720             }
721         }
722
723         if (allowedHttpMethods != null) {
724             Set<String> setAllowedHttpMethods =
725                     parseStringToSet(allowedHttpMethods);
726             this.allowedHttpMethods.clear();
727             this.allowedHttpMethods.addAll(setAllowedHttpMethods);
728         }
729
730         if (allowedHttpHeaders != null) {
731             Set<String> setAllowedHttpHeaders =
732                     parseStringToSet(allowedHttpHeaders);
733             Set<String> lowerCaseHeaders = new HashSet<String>();
734             for (String header : setAllowedHttpHeaders) {
735                 String lowerCase = header.toLowerCase();
736                 lowerCaseHeaders.add(lowerCase);
737             }
738             this.allowedHttpHeaders.clear();
739             this.allowedHttpHeaders.addAll(lowerCaseHeaders);
740         }
741
742         if (exposedHeaders != null) {
743             Set<String> setExposedHeaders = parseStringToSet(exposedHeaders);
744             this.exposedHeaders.clear();
745             this.exposedHeaders.addAll(setExposedHeaders);
746         }
747
748         if (supportsCredentials != null) {
749             // For any value other then 'true' this will be false.
750             this.supportsCredentials = Boolean
751                     .parseBoolean(supportsCredentials);
752         }
753
754         if (preflightMaxAge != null) {
755             try {
756                 if (!preflightMaxAge.isEmpty()) {
757                     this.preflightMaxAge = Long.parseLong(preflightMaxAge);
758                 } else {
759                     this.preflightMaxAge = 0L;
760                 }
761             } catch (NumberFormatException e) {
762                 throw new ServletException(
763                         sm.getString("corsFilter.invalidPreflightMaxAge"), e);
764             }
765         }
766
767         if (decorateRequest != null) {
768             // For any value other then 'true' this will be false.
769             this.decorateRequest = Boolean.parseBoolean(decorateRequest);
770         }
771     }
772
773     /**
774      * Takes a comma separated list and returns a Set<String>.
775      *
776      * @param data
777      *            A comma separated list of strings.
778      * @return Set<String>
779      */
780     private Set<String> parseStringToSet(final String data) {
781         String[] splits;
782
783         if (data != null && data.length() > 0) {
784             splits = data.split(",");
785         } else {
786             splits = new String[] {};
787         }
788
789         Set<String> set = new HashSet<String>();
790         if (splits.length > 0) {
791             for (String split : splits) {
792                 set.add(split.trim());
793             }
794         }
795
796         return set;
797     }
798
799
800     /**
801      * Checks if a given origin is valid or not. Criteria:
802      * <ul>
803      * <li>If an encoded character is present in origin, it's not valid.</li>
804      * <li>Origin should be a valid {@link URI}</li>
805      * </ul>
806      *
807      * @param origin
808      * @see <a href="http://tools.ietf.org/html/rfc952">RFC952</a>
809      */
810     protected static boolean isValidOrigin(String origin) {
811         // Checks for encoded characters. Helps prevent CRLF injection.
812         if (origin.contains("%")) {
813             return false;
814         }
815
816         URI originURI;
817
818         try {
819             originURI = new URI(origin);
820         } catch (URISyntaxException e) {
821             return false;
822         }
823         // If scheme for URI is null, return false. Return true otherwise.
824         return originURI.getScheme() != null;
825
826     }
827
828
829     /**
830      * Determines if any origin is allowed to make CORS request.
831      *
832      * @return <code>true</code> if it's enabled; false otherwise.
833      */
834     public boolean isAnyOriginAllowed() {
835         return anyOriginAllowed;
836     }
837
838
839     /**
840      * Returns a {@link Set} of headers that should be exposed by browser.
841      */
842     public Collection<String> getExposedHeaders() {
843         return exposedHeaders;
844     }
845
846
847     /**
848      * Determines is supports credentials is enabled.
849      */
850     public boolean isSupportsCredentials() {
851         return supportsCredentials;
852     }
853
854
855     /**
856      * Returns the preflight response cache time in seconds.
857      *
858      * @return Time to cache in seconds.
859      */
860     public long getPreflightMaxAge() {
861         return preflightMaxAge;
862     }
863
864
865     /**
866      * Returns the {@link Set} of allowed origins that are allowed to make
867      * requests.
868      *
869      * @return {@link Set}
870      */
871     public Collection<String> getAllowedOrigins() {
872         return allowedOrigins;
873     }
874
875
876     /**
877      * Returns a {@link Set} of HTTP methods that are allowed to make requests.
878      *
879      * @return {@link Set}
880      */
881     public Collection<String> getAllowedHttpMethods() {
882         return allowedHttpMethods;
883     }
884
885
886     /**
887      * Returns a {@link Set} of headers support by resource.
888      *
889      * @return {@link Set}
890      */
891     public Collection<String> getAllowedHttpHeaders() {
892         return allowedHttpHeaders;
893     }
894
895
896     // -------------------------------------------------- CORS Response Headers
897     /**
898      * The Access-Control-Allow-Origin header indicates whether a resource can
899      * be shared based by returning the value of the Origin request header in
900      * the response.
901      */
902     public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN =
903             "Access-Control-Allow-Origin";
904
905     /**
906      * The Access-Control-Allow-Credentials header indicates whether the
907      * response to request can be exposed when the omit credentials flag is
908      * unset. When part of the response to a preflight request it indicates that
909      * the actual request can include user credentials.
910      */
911     public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS =
912             "Access-Control-Allow-Credentials";
913
914     /**
915      * The Access-Control-Expose-Headers header indicates which headers are safe
916      * to expose to the API of a CORS API specification
917      */
918     public static final String RESPONSE_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS =
919             "Access-Control-Expose-Headers";
920
921     /**
922      * The Access-Control-Max-Age header indicates how long the results of a
923      * preflight request can be cached in a preflight result cache.
924      */
925     public static final String RESPONSE_HEADER_ACCESS_CONTROL_MAX_AGE =
926             "Access-Control-Max-Age";
927
928     /**
929      * The Access-Control-Allow-Methods header indicates, as part of the
930      * response to a preflight request, which methods can be used during the
931      * actual request.
932      */
933     public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_METHODS =
934             "Access-Control-Allow-Methods";
935
936     /**
937      * The Access-Control-Allow-Headers header indicates, as part of the
938      * response to a preflight request, which header field names can be used
939      * during the actual request.
940      */
941     public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_HEADERS =
942             "Access-Control-Allow-Headers";
943
944     // -------------------------------------------------- CORS Request Headers
945     /**
946      * The Origin header indicates where the cross-origin request or preflight
947      * request originates from.
948      */
949     public static final String REQUEST_HEADER_ORIGIN = "Origin";
950
951     /**
952      * The Access-Control-Request-Method header indicates which method will be
953      * used in the actual request as part of the preflight request.
954      */
955     public static final String REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD =
956             "Access-Control-Request-Method";
957
958     /**
959      * The Access-Control-Request-Headers header indicates which headers will be
960      * used in the actual request as part of the preflight request.
961      */
962     public static final String REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS =
963             "Access-Control-Request-Headers";
964
965     // ----------------------------------------------------- Request attributes
966     /**
967      * The prefix to a CORS request attribute.
968      */
969     public static final String HTTP_REQUEST_ATTRIBUTE_PREFIX = "cors.";
970
971     /**
972      * Attribute that contains the origin of the request.
973      */
974     public static final String HTTP_REQUEST_ATTRIBUTE_ORIGIN =
975             HTTP_REQUEST_ATTRIBUTE_PREFIX + "request.origin";
976
977     /**
978      * Boolean value, suggesting if the request is a CORS request or not.
979      */
980     public static final String HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST =
981             HTTP_REQUEST_ATTRIBUTE_PREFIX + "isCorsRequest";
982
983     /**
984      * Type of CORS request, of type {@link CORSRequestType}.
985      */
986     public static final String HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE =
987             HTTP_REQUEST_ATTRIBUTE_PREFIX + "request.type";
988
989     /**
990      * Request headers sent as 'Access-Control-Request-Headers' header, for
991      * pre-flight request.
992      */
993     public static final String HTTP_REQUEST_ATTRIBUTE_REQUEST_HEADERS =
994             HTTP_REQUEST_ATTRIBUTE_PREFIX + "request.headers";
995
996     // -------------------------------------------------------------- Constants
997     /**
998      * Enumerates varies types of CORS requests. Also, provides utility methods
999      * to determine the request type.
1000      */
1001     protected static enum CORSRequestType {
1002         /**
1003          * A simple HTTP request, i.e. it shouldn't be pre-flighted.
1004          */
1005         SIMPLE,
1006         /**
1007          * A HTTP request that needs to be pre-flighted.
1008          */
1009         ACTUAL,
1010         /**
1011          * A pre-flight CORS request, to get meta information, before a
1012          * non-simple HTTP request is sent.
1013          */
1014         PRE_FLIGHT,
1015         /**
1016          * Not a CORS request, but a normal request.
1017          */
1018         NOT_CORS,
1019         /**
1020          * An invalid CORS request, i.e. it qualifies to be a CORS request, but
1021          * fails to be a valid one.
1022          */
1023         INVALID_CORS
1024     }
1025
1026     /**
1027      * {@link Collection} of HTTP methods. Case sensitive.
1028      *
1029      * @see  <a href="http://tools.ietf.org/html/rfc2616#section-5.1.1"
1030      *       >http://tools.ietf.org/html/rfc2616#section-5.1.1</a>
1031      *
1032      */
1033     public static final Collection<String> HTTP_METHODS =
1034             new HashSet<String>(Arrays.asList("OPTIONS", "GET", "HEAD", "POST",
1035                     "PUT", "DELETE", "TRACE", "CONNECT"));
1036     /**
1037      * {@link Collection} of non-simple HTTP methods. Case sensitive.
1038      */
1039     public static final Collection<String> COMPLEX_HTTP_METHODS =
1040             new HashSet<String>(Arrays.asList("PUT", "DELETE", "TRACE",
1041                     "CONNECT"));
1042     /**
1043      * {@link Collection} of Simple HTTP methods. Case sensitive.
1044      *
1045      * @see  <a href="http://www.w3.org/TR/cors/#terminology"
1046      *       >http://www.w3.org/TR/cors/#terminology</a>
1047      */
1048     public static final Collection<String> SIMPLE_HTTP_METHODS =
1049             new HashSet<String>(Arrays.asList("GET", "POST", "HEAD"));
1050
1051     /**
1052      * {@link Collection} of Simple HTTP request headers. Case in-sensitive.
1053      *
1054      * @see  <a href="http://www.w3.org/TR/cors/#terminology"
1055      *       >http://www.w3.org/TR/cors/#terminology</a>
1056      */
1057     public static final Collection<String> SIMPLE_HTTP_REQUEST_HEADERS =
1058             new HashSet<String>(Arrays.asList("Accept", "Accept-Language",
1059                     "Content-Language"));
1060
1061     /**
1062      * {@link Collection} of Simple HTTP request headers. Case in-sensitive.
1063      *
1064      * @see  <a href="http://www.w3.org/TR/cors/#terminology"
1065      *       >http://www.w3.org/TR/cors/#terminology</a>
1066      */
1067     public static final Collection<String> SIMPLE_HTTP_RESPONSE_HEADERS =
1068             new HashSet<String>(Arrays.asList("Cache-Control",
1069                     "Content-Language", "Content-Type", "Expires",
1070                     "Last-Modified", "Pragma"));
1071
1072     /**
1073      * {@link Collection} of Simple HTTP request headers. Case in-sensitive.
1074      *
1075      * @see  <a href="http://www.w3.org/TR/cors/#terminology"
1076      *       >http://www.w3.org/TR/cors/#terminology</a>
1077      */
1078     public static final Collection<String> SIMPLE_HTTP_REQUEST_CONTENT_TYPE_VALUES =
1079             new HashSet<String>(Arrays.asList(
1080                     "application/x-www-form-urlencoded",
1081                     "multipart/form-data", "text/plain"));
1082
1083     // ------------------------------------------------ Configuration Defaults
1084     /**
1085      * By default, all origins are allowed to make requests.
1086      */
1087     public static final String DEFAULT_ALLOWED_ORIGINS = "*";
1088
1089     /**
1090      * By default, following methods are supported: GET, POST, HEAD and OPTIONS.
1091      */
1092     public static final String DEFAULT_ALLOWED_HTTP_METHODS =
1093             "GET,POST,HEAD,OPTIONS";
1094
1095     /**
1096      * By default, time duration to cache pre-flight response is 30 mins.
1097      */
1098     public static final String DEFAULT_PREFLIGHT_MAXAGE = "1800";
1099
1100     /**
1101      * By default, support credentials is turned on.
1102      */
1103     public static final String DEFAULT_SUPPORTS_CREDENTIALS = "true";
1104
1105     /**
1106      * By default, following headers are supported:
1107      * Origin,Accept,X-Requested-With, Content-Type,
1108      * Access-Control-Request-Method, and Access-Control-Request-Headers.
1109      */
1110     public static final String DEFAULT_ALLOWED_HTTP_HEADERS =
1111             "Origin,Accept,X-Requested-With,Content-Type," +
1112             "Access-Control-Request-Method,Access-Control-Request-Headers";
1113
1114     /**
1115      * By default, none of the headers are exposed in response.
1116      */
1117     public static final String DEFAULT_EXPOSED_HEADERS = "";
1118
1119     /**
1120      * By default, request is decorated with CORS attributes.
1121      */
1122     public static final String DEFAULT_DECORATE_REQUEST = "true";
1123
1124     // ----------------------------------------Filter Config Init param-name(s)
1125     /**
1126      * Key to retrieve allowed origins from {@link FilterConfig}.
1127      */
1128     public static final String PARAM_CORS_ALLOWED_ORIGINS =
1129             "cors.allowed.origins";
1130
1131     /**
1132      * Key to retrieve support credentials from {@link FilterConfig}.
1133      */
1134     public static final String PARAM_CORS_SUPPORT_CREDENTIALS =
1135             "cors.support.credentials";
1136
1137     /**
1138      * Key to retrieve exposed headers from {@link FilterConfig}.
1139      */
1140     public static final String PARAM_CORS_EXPOSED_HEADERS =
1141             "cors.exposed.headers";
1142
1143     /**
1144      * Key to retrieve allowed headers from {@link FilterConfig}.
1145      */
1146     public static final String PARAM_CORS_ALLOWED_HEADERS =
1147             "cors.allowed.headers";
1148
1149     /**
1150      * Key to retrieve allowed methods from {@link FilterConfig}.
1151      */
1152     public static final String PARAM_CORS_ALLOWED_METHODS =
1153             "cors.allowed.methods";
1154
1155     /**
1156      * Key to retrieve preflight max age from {@link FilterConfig}.
1157      */
1158     public static final String PARAM_CORS_PREFLIGHT_MAXAGE =
1159             "cors.preflight.maxage";
1160
1161     /**
1162      * Key to determine if request should be decorated.
1163      */
1164     public static final String PARAM_CORS_REQUEST_DECORATE =
1165             "cors.request.decorate";
1166 }