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: 1910 $ $Date: 2010-06-01 07:02:07 -0500 (Tue, 01 Jun 2010) $
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 && isEnabled(request))
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     protected boolean isEnabled(HttpServletRequest request)
190     {
191         // WebSocket clients such as Chrome 5 implement a version of the WebSocket
192         // protocol that does not accept extra response headers on the upgrade response
193         if ("Upgrade".equalsIgnoreCase(request.getHeader("Connection")) &&
194             "WebSocket".equalsIgnoreCase(request.getHeader("Upgrade")))
195         {
196             return false;
197         }
198         return true;
199     }
200 
201     private boolean originMatches(String origin)
202     {
203         if (anyOriginAllowed) return true;
204         for (String allowedOrigin : allowedOrigins)
205         {
206             if (allowedOrigin.equals(origin)) return true;
207         }
208         return false;
209     }
210 
211     private boolean isSimpleRequest(HttpServletRequest request)
212     {
213         String method = request.getMethod();
214         if (SIMPLE_HTTP_METHODS.contains(method))
215         {
216             // TODO: implement better section 6.1
217             // Section 6.1 says that for a request to be simple, custom request headers must be simple.
218             // Here for simplicity I just check if there is a Access-Control-Request-Method header,
219             // which is required for preflight requests
220             return request.getHeader(ACCESS_CONTROL_REQUEST_METHOD_HEADER) == null;
221         }
222         return false;
223     }
224 
225     private void handleSimpleResponse(HttpServletRequest request, HttpServletResponse response, String origin)
226     {
227         response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, origin);
228         if (allowCredentials) response.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER, "true");
229     }
230 
231     private void handlePreflightResponse(HttpServletRequest request, HttpServletResponse response, String origin)
232     {
233         // Implementation of section 5.2
234 
235         // 5.2.3 and 5.2.5
236         boolean methodAllowed = isMethodAllowed(request);
237         if (!methodAllowed) return;
238         // 5.2.4 and 5.2.6
239         boolean headersAllowed = areHeadersAllowed(request);
240         if (!headersAllowed) return;
241         // 5.2.7
242         response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, origin);
243         if (allowCredentials) response.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER, "true");
244         // 5.2.8
245         if (preflightMaxAge > 0) response.setHeader(ACCESS_CONTROL_MAX_AGE_HEADER, String.valueOf(preflightMaxAge));
246         // 5.2.9
247         response.setHeader(ACCESS_CONTROL_ALLOW_METHODS_HEADER, commify(allowedMethods));
248         // 5.2.10
249         response.setHeader(ACCESS_CONTROL_ALLOW_HEADERS_HEADER, commify(allowedHeaders));
250     }
251 
252     private boolean isMethodAllowed(HttpServletRequest request)
253     {
254         String accessControlRequestMethod = request.getHeader(ACCESS_CONTROL_REQUEST_METHOD_HEADER);
255         Log.debug("{} is {}", ACCESS_CONTROL_REQUEST_METHOD_HEADER, accessControlRequestMethod);
256         boolean result = false;
257         if (accessControlRequestMethod != null)
258         {
259             result = allowedMethods.contains(accessControlRequestMethod);
260         }
261         Log.debug("Method {} is" + (result ? "" : " not") + " among allowed methods {}", accessControlRequestMethod, allowedMethods);
262         return result;
263     }
264 
265     private boolean areHeadersAllowed(HttpServletRequest request)
266     {
267         String accessControlRequestHeaders = request.getHeader(ACCESS_CONTROL_REQUEST_HEADERS_HEADER);
268         Log.debug("{} is {}", ACCESS_CONTROL_REQUEST_HEADERS_HEADER, accessControlRequestHeaders);
269         boolean result = true;
270         if (accessControlRequestHeaders != null)
271         {
272             String[] headers = accessControlRequestHeaders.split(",");
273             for (String header : headers)
274             {
275                 boolean headerAllowed = false;
276                 for (String allowedHeader : allowedHeaders)
277                 {
278                     if (header.trim().equalsIgnoreCase(allowedHeader.trim()))
279                     {
280                         headerAllowed = true;
281                         break;
282                     }
283                 }
284                 if (!headerAllowed)
285                 {
286                     result = false;
287                     break;
288                 }
289             }
290         }
291         Log.debug("Headers [{}] are" + (result ? "" : " not") + " among allowed headers {}", accessControlRequestHeaders, allowedHeaders);
292         return result;
293     }
294 
295     private String commify(List<String> strings)
296     {
297         StringBuilder builder = new StringBuilder();
298         for (int i = 0; i < strings.size(); ++i)
299         {
300             if (i > 0) builder.append(",");
301             String string = strings.get(i);
302             builder.append(string);
303         }
304         return builder.toString();
305     }
306 
307     public void destroy()
308     {
309         anyOriginAllowed = false;
310         allowedOrigins.clear();
311         allowedMethods.clear();
312         allowedHeaders.clear();
313         preflightMaxAge = 0;
314         allowCredentials = false;
315     }
316 }