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