View Javadoc

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