View Javadoc

1   /*
2    * Copyright (c) 2009-2009 Mort Bay Consulting Pty. Ltd.
3    *
4    * All rights reserved. This program and the accompanying materials
5    * are made available under the terms of the Eclipse Public License v1.0
6    * and Apache License v2.0 which accompanies this distribution.
7    * The Eclipse Public License is available at
8    * http://www.eclipse.org/legal/epl-v10.html
9    * The Apache License v2.0 is available at
10   * http://www.opensource.org/licenses/apache2.0.php
11   *
12   * You may elect to redistribute this code under either of these licenses.
13   */
14  
15  package org.eclipse.jetty.servlets;
16  
17  import java.io.IOException;
18  import java.util.ArrayList;
19  import java.util.Arrays;
20  import java.util.List;
21  import javax.servlet.Filter;
22  import javax.servlet.FilterChain;
23  import javax.servlet.FilterConfig;
24  import javax.servlet.ServletException;
25  import javax.servlet.ServletRequest;
26  import javax.servlet.ServletResponse;
27  import javax.servlet.http.HttpServletRequest;
28  import javax.servlet.http.HttpServletResponse;
29  
30  import org.eclipse.jetty.util.log.Log;
31  import org.eclipse.jetty.util.log.Logger;
32  
33  /**
34   * <p>Implementation of the
35   * <a href="http://dev.w3.org/2006/waf/access-control/">cross-origin resource sharing</a>.</p>
36   * <p>A typical example is to use this filter to allow cross-domain
37   * <a href="http://cometd.org">cometd</a> communication using the standard
38   * long polling transport instead of the JSONP transport (that is less
39   * efficient and less reactive to failures).</p>
40   * <p>This filter allows the following configuration parameters:
41   * <ul>
42   * <li><b>allowedOrigins</b>, a comma separated list of origins that are
43   * allowed to access the resources. Default value is <b>*</b>, meaning all
44   * origins</li>
45   * <li><b>allowedMethods</b>, a comma separated list of HTTP methods that
46   * are allowed to be used when accessing the resources. Default value is
47   * <b>GET,POST</b></li>
48   * <li><b>allowedHeaders</b>, a comma separated list of HTTP headers that
49   * are allowed to be specified when accessing the resources. Default value
50   * is <b>X-Requested-With</b></li>
51   * <li><b>preflightMaxAge</b>, the number of seconds that preflight requests
52   * can be cached by the client. Default value is <b>1800</b> seconds, or 30
53   * minutes</li>
54   * <li><b>allowCredentials</b>, a boolean indicating if the resource allows
55   * requests with credentials. Default value is <b>false</b></li>
56   * </ul></p>
57   * <p>A typical configuration could be:
58   * <pre>
59   * &lt;web-app ...&gt;
60   *     ...
61   *     &lt;filter&gt;
62   *         &lt;filter-name&gt;cross-origin&lt;/filter-name&gt;
63   *         &lt;filter-class&gt;org.eclipse.jetty.servlets.CrossOriginFilter&lt;/filter-class&gt;
64   *     &lt;/filter&gt;
65   *     &lt;filter-mapping&gt;
66   *         &lt;filter-name&gt;cross-origin&lt;/filter-name&gt;
67   *         &lt;url-pattern&gt;/cometd/*&lt;/url-pattern&gt;
68   *     &lt;/filter-mapping&gt;
69   *     ...
70   * &lt;/web-app&gt;
71   * </pre></p>
72   *
73   * @version $Revision$ $Date$
74   */
75  public class CrossOriginFilter implements Filter
76  {
77      private static final Logger LOG = Log.getLogger(CrossOriginFilter.class);
78  
79      // Request headers
80      private static final String ORIGIN_HEADER = "Origin";
81      private static final String ACCESS_CONTROL_REQUEST_METHOD_HEADER = "Access-Control-Request-Method";
82      private static final String ACCESS_CONTROL_REQUEST_HEADERS_HEADER = "Access-Control-Request-Headers";
83      // Response headers
84      private static final String ACCESS_CONTROL_ALLOW_ORIGIN_HEADER = "Access-Control-Allow-Origin";
85      private static final String ACCESS_CONTROL_ALLOW_METHODS_HEADER = "Access-Control-Allow-Methods";
86      private static final String ACCESS_CONTROL_ALLOW_HEADERS_HEADER = "Access-Control-Allow-Headers";
87      private static final String ACCESS_CONTROL_MAX_AGE_HEADER = "Access-Control-Max-Age";
88      private static final String ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER = "Access-Control-Allow-Credentials";
89      // Implementation constants
90      private static final String ALLOWED_ORIGINS_PARAM = "allowedOrigins";
91      private static final String ALLOWED_METHODS_PARAM = "allowedMethods";
92      private static final String ALLOWED_HEADERS_PARAM = "allowedHeaders";
93      private static final String PREFLIGHT_MAX_AGE_PARAM = "preflightMaxAge";
94      private static final String ALLOWED_CREDENTIALS_PARAM = "allowCredentials";
95      private static final String ANY_ORIGIN = "*";
96      private static final List<String> SIMPLE_HTTP_METHODS = Arrays.asList("GET", "POST", "HEAD");
97  
98      private boolean anyOriginAllowed = false;
99      private List<String> allowedOrigins = new ArrayList<String>();
100     private List<String> allowedMethods = new ArrayList<String>();
101     private List<String> allowedHeaders = new ArrayList<String>();
102     private int preflightMaxAge = 0;
103     private boolean allowCredentials = true;
104 
105     public void init(FilterConfig config) throws ServletException
106     {
107         String allowedOriginsConfig = config.getInitParameter(ALLOWED_ORIGINS_PARAM);
108         if (allowedOriginsConfig == null) allowedOriginsConfig = "*";
109         String[] allowedOrigins = allowedOriginsConfig.split(",");
110         for (String allowedOrigin : allowedOrigins)
111         {
112             allowedOrigin = allowedOrigin.trim();
113             if (allowedOrigin.length() > 0)
114             {
115                 if (ANY_ORIGIN.equals(allowedOrigin))
116                 {
117                     anyOriginAllowed = true;
118                     this.allowedOrigins.clear();
119                     break;
120                 }
121                 else
122                 {
123                     this.allowedOrigins.add(allowedOrigin);
124                 }
125             }
126         }
127 
128         String allowedMethodsConfig = config.getInitParameter(ALLOWED_METHODS_PARAM);
129         if (allowedMethodsConfig == null) allowedMethodsConfig = "GET,POST";
130         allowedMethods.addAll(Arrays.asList(allowedMethodsConfig.split(",")));
131 
132         String allowedHeadersConfig = config.getInitParameter(ALLOWED_HEADERS_PARAM);
133         if (allowedHeadersConfig == null) allowedHeadersConfig = "X-Requested-With,Content-Type,Accept,Origin";
134         allowedHeaders.addAll(Arrays.asList(allowedHeadersConfig.split(",")));
135 
136         String preflightMaxAgeConfig = config.getInitParameter(PREFLIGHT_MAX_AGE_PARAM);
137         if (preflightMaxAgeConfig == null) preflightMaxAgeConfig = "1800"; // Default is 30 minutes
138         try
139         {
140             preflightMaxAge = Integer.parseInt(preflightMaxAgeConfig);
141         }
142         catch (NumberFormatException x)
143         {
144             LOG.info("Cross-origin filter, could not parse '{}' parameter as integer: {}", PREFLIGHT_MAX_AGE_PARAM, preflightMaxAgeConfig);
145         }
146 
147         String allowedCredentialsConfig = config.getInitParameter(ALLOWED_CREDENTIALS_PARAM);
148         if (allowedCredentialsConfig == null) allowedCredentialsConfig = "false";
149         allowCredentials = Boolean.parseBoolean(allowedCredentialsConfig);
150 
151         LOG.debug("Cross-origin filter configuration: " +
152                   ALLOWED_ORIGINS_PARAM + " = " + allowedOriginsConfig + ", " +
153                   ALLOWED_METHODS_PARAM + " = " + allowedMethodsConfig + ", " +
154                   ALLOWED_HEADERS_PARAM + " = " + allowedHeadersConfig + ", " +
155                   PREFLIGHT_MAX_AGE_PARAM + " = " + preflightMaxAgeConfig + ", " +
156                   ALLOWED_CREDENTIALS_PARAM + " = " + allowedCredentialsConfig);
157     }
158 
159     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
160     {
161         handle((HttpServletRequest)request, (HttpServletResponse)response, chain);
162     }
163 
164     private void handle(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException
165     {
166         String origin = request.getHeader(ORIGIN_HEADER);
167         // Is it a cross origin request ?
168         if (origin != null && isEnabled(request))
169         {
170             if (originMatches(origin))
171             {
172                 if (isSimpleRequest(request))
173                 {
174                     LOG.debug("Cross-origin request to {} is a simple cross-origin request", request.getRequestURI());
175                     handleSimpleResponse(request, response, origin);
176                 }
177                 else
178                 {
179                     LOG.debug("Cross-origin request to {} is a preflight cross-origin request", request.getRequestURI());
180                     handlePreflightResponse(request, response, origin);
181                 }
182             }
183             else
184             {
185                 LOG.debug("Cross-origin request to " + request.getRequestURI() + " with origin " + origin + " does not match allowed origins " + allowedOrigins);
186             }
187         }
188 
189         chain.doFilter(request, response);
190     }
191 
192     protected boolean isEnabled(HttpServletRequest request)
193     {
194         // WebSocket clients such as Chrome 5 implement a version of the WebSocket
195         // protocol that does not accept extra response headers on the upgrade response
196         if ("Upgrade".equalsIgnoreCase(request.getHeader("Connection")) &&
197             "WebSocket".equalsIgnoreCase(request.getHeader("Upgrade")))
198         {
199             return false;
200         }
201         return true;
202     }
203 
204     private boolean originMatches(String origin)
205     {
206         if (anyOriginAllowed) return true;
207         for (String allowedOrigin : allowedOrigins)
208         {
209             if (allowedOrigin.equals(origin)) return true;
210         }
211         return false;
212     }
213 
214     private boolean isSimpleRequest(HttpServletRequest request)
215     {
216         String method = request.getMethod();
217         if (SIMPLE_HTTP_METHODS.contains(method))
218         {
219             // TODO: implement better section 6.1
220             // Section 6.1 says that for a request to be simple, custom request headers must be simple.
221             // Here for simplicity I just check if there is a Access-Control-Request-Method header,
222             // which is required for preflight requests
223             return request.getHeader(ACCESS_CONTROL_REQUEST_METHOD_HEADER) == null;
224         }
225         return false;
226     }
227 
228     private void handleSimpleResponse(HttpServletRequest request, HttpServletResponse response, String origin)
229     {
230         response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, origin);
231         if (allowCredentials) response.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER, "true");
232     }
233 
234     private void handlePreflightResponse(HttpServletRequest request, HttpServletResponse response, String origin)
235     {
236         // Implementation of section 5.2
237 
238         // 5.2.3 and 5.2.5
239         boolean methodAllowed = isMethodAllowed(request);
240         if (!methodAllowed) return;
241         // 5.2.4 and 5.2.6
242         boolean headersAllowed = areHeadersAllowed(request);
243         if (!headersAllowed) return;
244         // 5.2.7
245         response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, origin);
246         if (allowCredentials) response.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER, "true");
247         // 5.2.8
248         if (preflightMaxAge > 0) response.setHeader(ACCESS_CONTROL_MAX_AGE_HEADER, String.valueOf(preflightMaxAge));
249         // 5.2.9
250         response.setHeader(ACCESS_CONTROL_ALLOW_METHODS_HEADER, commify(allowedMethods));
251         // 5.2.10
252         response.setHeader(ACCESS_CONTROL_ALLOW_HEADERS_HEADER, commify(allowedHeaders));
253     }
254 
255     private boolean isMethodAllowed(HttpServletRequest request)
256     {
257         String accessControlRequestMethod = request.getHeader(ACCESS_CONTROL_REQUEST_METHOD_HEADER);
258         LOG.debug("{} is {}", ACCESS_CONTROL_REQUEST_METHOD_HEADER, accessControlRequestMethod);
259         boolean result = false;
260         if (accessControlRequestMethod != null)
261         {
262             result = allowedMethods.contains(accessControlRequestMethod);
263         }
264         LOG.debug("Method {} is" + (result ? "" : " not") + " among allowed methods {}", accessControlRequestMethod, allowedMethods);
265         return result;
266     }
267 
268     private boolean areHeadersAllowed(HttpServletRequest request)
269     {
270         String accessControlRequestHeaders = request.getHeader(ACCESS_CONTROL_REQUEST_HEADERS_HEADER);
271         LOG.debug("{} is {}", ACCESS_CONTROL_REQUEST_HEADERS_HEADER, accessControlRequestHeaders);
272         boolean result = true;
273         if (accessControlRequestHeaders != null)
274         {
275             String[] headers = accessControlRequestHeaders.split(",");
276             for (String header : headers)
277             {
278                 boolean headerAllowed = false;
279                 for (String allowedHeader : allowedHeaders)
280                 {
281                     if (header.trim().equalsIgnoreCase(allowedHeader.trim()))
282                     {
283                         headerAllowed = true;
284                         break;
285                     }
286                 }
287                 if (!headerAllowed)
288                 {
289                     result = false;
290                     break;
291                 }
292             }
293         }
294         LOG.debug("Headers [{}] are" + (result ? "" : " not") + " among allowed headers {}", accessControlRequestHeaders, allowedHeaders);
295         return result;
296     }
297 
298     private String commify(List<String> strings)
299     {
300         StringBuilder builder = new StringBuilder();
301         for (int i = 0; i < strings.size(); ++i)
302         {
303             if (i > 0) builder.append(",");
304             String string = strings.get(i);
305             builder.append(string);
306         }
307         return builder.toString();
308     }
309 
310     public void destroy()
311     {
312         anyOriginAllowed = false;
313         allowedOrigins.clear();
314         allowedMethods.clear();
315         allowedHeaders.clear();
316         preflightMaxAge = 0;
317         allowCredentials = false;
318     }
319 }