1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.eclipse.jetty.servlets;
20
21 import java.io.IOException;
22 import java.util.ArrayList;
23 import java.util.Arrays;
24 import java.util.Collections;
25 import java.util.Enumeration;
26 import java.util.List;
27 import java.util.regex.Matcher;
28 import java.util.regex.Pattern;
29
30 import javax.servlet.Filter;
31 import javax.servlet.FilterChain;
32 import javax.servlet.FilterConfig;
33 import javax.servlet.ServletException;
34 import javax.servlet.ServletRequest;
35 import javax.servlet.ServletResponse;
36 import javax.servlet.http.HttpServletRequest;
37 import javax.servlet.http.HttpServletResponse;
38
39 import org.eclipse.jetty.http.HttpField;
40 import org.eclipse.jetty.http.HttpFields;
41 import org.eclipse.jetty.util.log.Log;
42 import org.eclipse.jetty.util.log.Logger;
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98 public class CrossOriginFilter implements Filter
99 {
100 private static final Logger LOG = Log.getLogger(CrossOriginFilter.class);
101
102
103 private static final String ORIGIN_HEADER = "Origin";
104 public static final String ACCESS_CONTROL_REQUEST_METHOD_HEADER = "Access-Control-Request-Method";
105 public static final String ACCESS_CONTROL_REQUEST_HEADERS_HEADER = "Access-Control-Request-Headers";
106
107 public static final String ACCESS_CONTROL_ALLOW_ORIGIN_HEADER = "Access-Control-Allow-Origin";
108 public static final String ACCESS_CONTROL_ALLOW_METHODS_HEADER = "Access-Control-Allow-Methods";
109 public static final String ACCESS_CONTROL_ALLOW_HEADERS_HEADER = "Access-Control-Allow-Headers";
110 public static final String ACCESS_CONTROL_MAX_AGE_HEADER = "Access-Control-Max-Age";
111 public static final String ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER = "Access-Control-Allow-Credentials";
112 public static final String ACCESS_CONTROL_EXPOSE_HEADERS_HEADER = "Access-Control-Expose-Headers";
113
114 public static final String ALLOWED_ORIGINS_PARAM = "allowedOrigins";
115 public static final String ALLOWED_METHODS_PARAM = "allowedMethods";
116 public static final String ALLOWED_HEADERS_PARAM = "allowedHeaders";
117 public static final String PREFLIGHT_MAX_AGE_PARAM = "preflightMaxAge";
118 public static final String ALLOW_CREDENTIALS_PARAM = "allowCredentials";
119 public static final String EXPOSED_HEADERS_PARAM = "exposedHeaders";
120 public static final String OLD_CHAIN_PREFLIGHT_PARAM = "forwardPreflight";
121 public static final String CHAIN_PREFLIGHT_PARAM = "chainPreflight";
122 private static final String ANY_ORIGIN = "*";
123 private static final List<String> SIMPLE_HTTP_METHODS = Arrays.asList("GET", "POST", "HEAD");
124 private static final List<String> DEFAULT_ALLOWED_METHODS = Arrays.asList("GET", "POST", "HEAD");
125 private static final List<String> DEFAULT_ALLOWED_HEADERS = Arrays.asList("X-Requested-With", "Content-Type", "Accept", "Origin");
126
127 private boolean anyOriginAllowed;
128 private boolean anyHeadersAllowed;
129 private List<String> allowedOrigins = new ArrayList<String>();
130 private List<String> allowedMethods = new ArrayList<String>();
131 private List<String> allowedHeaders = new ArrayList<String>();
132 private List<String> exposedHeaders = new ArrayList<String>();
133 private int preflightMaxAge;
134 private boolean allowCredentials;
135 private boolean chainPreflight;
136
137 public void init(FilterConfig config) throws ServletException
138 {
139 String allowedOriginsConfig = config.getInitParameter(ALLOWED_ORIGINS_PARAM);
140 if (allowedOriginsConfig == null)
141 allowedOriginsConfig = "*";
142 String[] allowedOrigins = allowedOriginsConfig.split(",");
143 for (String allowedOrigin : allowedOrigins)
144 {
145 allowedOrigin = allowedOrigin.trim();
146 if (allowedOrigin.length() > 0)
147 {
148 if (ANY_ORIGIN.equals(allowedOrigin))
149 {
150 anyOriginAllowed = true;
151 this.allowedOrigins.clear();
152 break;
153 }
154 else
155 {
156 this.allowedOrigins.add(allowedOrigin);
157 }
158 }
159 }
160
161 String allowedMethodsConfig = config.getInitParameter(ALLOWED_METHODS_PARAM);
162 if (allowedMethodsConfig == null)
163 allowedMethods.addAll(DEFAULT_ALLOWED_METHODS);
164 else
165 allowedMethods.addAll(Arrays.asList(allowedMethodsConfig.split(",")));
166
167 String allowedHeadersConfig = config.getInitParameter(ALLOWED_HEADERS_PARAM);
168 if (allowedHeadersConfig == null)
169 allowedHeaders.addAll(DEFAULT_ALLOWED_HEADERS);
170 else if ("*".equals(allowedHeadersConfig))
171 anyHeadersAllowed = true;
172 else
173 allowedHeaders.addAll(Arrays.asList(allowedHeadersConfig.split(",")));
174
175 String preflightMaxAgeConfig = config.getInitParameter(PREFLIGHT_MAX_AGE_PARAM);
176 if (preflightMaxAgeConfig == null)
177 preflightMaxAgeConfig = "1800";
178 try
179 {
180 preflightMaxAge = Integer.parseInt(preflightMaxAgeConfig);
181 }
182 catch (NumberFormatException x)
183 {
184 LOG.info("Cross-origin filter, could not parse '{}' parameter as integer: {}", PREFLIGHT_MAX_AGE_PARAM, preflightMaxAgeConfig);
185 }
186
187 String allowedCredentialsConfig = config.getInitParameter(ALLOW_CREDENTIALS_PARAM);
188 if (allowedCredentialsConfig == null)
189 allowedCredentialsConfig = "true";
190 allowCredentials = Boolean.parseBoolean(allowedCredentialsConfig);
191
192 String exposedHeadersConfig = config.getInitParameter(EXPOSED_HEADERS_PARAM);
193 if (exposedHeadersConfig == null)
194 exposedHeadersConfig = "";
195 exposedHeaders.addAll(Arrays.asList(exposedHeadersConfig.split(",")));
196
197 String chainPreflightConfig = config.getInitParameter(OLD_CHAIN_PREFLIGHT_PARAM);
198 if (chainPreflightConfig!=null)
199 LOG.warn("DEPRECATED CONFIGURATION: Use "+CHAIN_PREFLIGHT_PARAM+ " instead of "+OLD_CHAIN_PREFLIGHT_PARAM);
200 else
201 chainPreflightConfig = config.getInitParameter(CHAIN_PREFLIGHT_PARAM);
202 if (chainPreflightConfig == null)
203 chainPreflightConfig = "true";
204 chainPreflight = Boolean.parseBoolean(chainPreflightConfig);
205
206 if (LOG.isDebugEnabled())
207 {
208 LOG.debug("Cross-origin filter configuration: " +
209 ALLOWED_ORIGINS_PARAM + " = " + allowedOriginsConfig + ", " +
210 ALLOWED_METHODS_PARAM + " = " + allowedMethodsConfig + ", " +
211 ALLOWED_HEADERS_PARAM + " = " + allowedHeadersConfig + ", " +
212 PREFLIGHT_MAX_AGE_PARAM + " = " + preflightMaxAgeConfig + ", " +
213 ALLOW_CREDENTIALS_PARAM + " = " + allowedCredentialsConfig + "," +
214 EXPOSED_HEADERS_PARAM + " = " + exposedHeadersConfig + "," +
215 CHAIN_PREFLIGHT_PARAM + " = " + chainPreflightConfig
216 );
217 }
218 }
219
220 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
221 {
222 handle((HttpServletRequest)request, (HttpServletResponse)response, chain);
223 }
224
225 private void handle(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException
226 {
227 String origin = request.getHeader(ORIGIN_HEADER);
228
229 if (origin != null && isEnabled(request))
230 {
231 if (originMatches(origin))
232 {
233 if (isSimpleRequest(request))
234 {
235 LOG.debug("Cross-origin request to {} is a simple cross-origin request", request.getRequestURI());
236 handleSimpleResponse(request, response, origin);
237 }
238 else if (isPreflightRequest(request))
239 {
240 LOG.debug("Cross-origin request to {} is a preflight cross-origin request", request.getRequestURI());
241 handlePreflightResponse(request, response, origin);
242 if (chainPreflight)
243 LOG.debug("Preflight cross-origin request to {} forwarded to application", request.getRequestURI());
244 else
245 return;
246 }
247 else
248 {
249 LOG.debug("Cross-origin request to {} is a non-simple cross-origin request", request.getRequestURI());
250 handleSimpleResponse(request, response, origin);
251 }
252 }
253 else
254 {
255 LOG.debug("Cross-origin request to " + request.getRequestURI() + " with origin " + origin + " does not match allowed origins " + allowedOrigins);
256 }
257 }
258
259 chain.doFilter(request, response);
260 }
261
262 protected boolean isEnabled(HttpServletRequest request)
263 {
264
265
266 for (Enumeration connections = request.getHeaders("Connection"); connections.hasMoreElements();)
267 {
268 String connection = (String)connections.nextElement();
269 if ("Upgrade".equalsIgnoreCase(connection))
270 {
271 for (Enumeration upgrades = request.getHeaders("Upgrade"); upgrades.hasMoreElements();)
272 {
273 String upgrade = (String)upgrades.nextElement();
274 if ("WebSocket".equalsIgnoreCase(upgrade))
275 return false;
276 }
277 }
278 }
279 return true;
280 }
281
282 private boolean originMatches(String originList)
283 {
284 if (anyOriginAllowed)
285 return true;
286
287 if (originList.trim().length() == 0)
288 return false;
289
290 String[] origins = originList.split(" ");
291 for (String origin : origins)
292 {
293 if (origin.trim().length() == 0)
294 continue;
295
296 for (String allowedOrigin : allowedOrigins)
297 {
298 if (allowedOrigin.contains("*"))
299 {
300 Matcher matcher = createMatcher(origin,allowedOrigin);
301 if (matcher.matches())
302 return true;
303 }
304 else if (allowedOrigin.equals(origin))
305 {
306 return true;
307 }
308 }
309 }
310 return false;
311 }
312
313 private Matcher createMatcher(String origin, String allowedOrigin)
314 {
315 String regex = parseAllowedWildcardOriginToRegex(allowedOrigin);
316 Pattern pattern = Pattern.compile(regex);
317 return pattern.matcher(origin);
318 }
319
320 private String parseAllowedWildcardOriginToRegex(String allowedOrigin)
321 {
322 String regex = allowedOrigin.replace(".","\\.");
323 return regex.replace("*",".*");
324 }
325
326 private boolean isSimpleRequest(HttpServletRequest request)
327 {
328 String method = request.getMethod();
329 if (SIMPLE_HTTP_METHODS.contains(method))
330 {
331
332
333
334
335 return request.getHeader(ACCESS_CONTROL_REQUEST_METHOD_HEADER) == null;
336 }
337 return false;
338 }
339
340 private boolean isPreflightRequest(HttpServletRequest request)
341 {
342 String method = request.getMethod();
343 if (!"OPTIONS".equalsIgnoreCase(method))
344 return false;
345 if (request.getHeader(ACCESS_CONTROL_REQUEST_METHOD_HEADER) == null)
346 return false;
347 return true;
348 }
349
350 private void handleSimpleResponse(HttpServletRequest request, HttpServletResponse response, String origin)
351 {
352 response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, origin);
353
354 if (!anyOriginAllowed)
355 response.addHeader("Vary", ORIGIN_HEADER);
356 if (allowCredentials)
357 response.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER, "true");
358 if (!exposedHeaders.isEmpty())
359 response.setHeader(ACCESS_CONTROL_EXPOSE_HEADERS_HEADER, commify(exposedHeaders));
360 }
361
362 private void handlePreflightResponse(HttpServletRequest request, HttpServletResponse response, String origin)
363 {
364 boolean methodAllowed = isMethodAllowed(request);
365
366 if (!methodAllowed)
367 return;
368 List<String> headersRequested = getAccessControlRequestHeaders(request);
369 boolean headersAllowed = areHeadersAllowed(headersRequested);
370 if (!headersAllowed)
371 return;
372 response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, origin);
373
374 if (!anyOriginAllowed)
375 response.addHeader("Vary", ORIGIN_HEADER);
376 if (allowCredentials)
377 response.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER, "true");
378 if (preflightMaxAge > 0)
379 response.setHeader(ACCESS_CONTROL_MAX_AGE_HEADER, String.valueOf(preflightMaxAge));
380 response.setHeader(ACCESS_CONTROL_ALLOW_METHODS_HEADER, commify(allowedMethods));
381 if (anyHeadersAllowed)
382 response.setHeader(ACCESS_CONTROL_ALLOW_HEADERS_HEADER, commify(headersRequested));
383 else
384 response.setHeader(ACCESS_CONTROL_ALLOW_HEADERS_HEADER, commify(allowedHeaders));
385 }
386
387 private boolean isMethodAllowed(HttpServletRequest request)
388 {
389 String accessControlRequestMethod = request.getHeader(ACCESS_CONTROL_REQUEST_METHOD_HEADER);
390 LOG.debug("{} is {}", ACCESS_CONTROL_REQUEST_METHOD_HEADER, accessControlRequestMethod);
391 boolean result = false;
392 if (accessControlRequestMethod != null)
393 result = allowedMethods.contains(accessControlRequestMethod);
394 LOG.debug("Method {} is" + (result ? "" : " not") + " among allowed methods {}", accessControlRequestMethod, allowedMethods);
395 return result;
396 }
397
398 List<String> getAccessControlRequestHeaders (HttpServletRequest request)
399 {
400 String accessControlRequestHeaders = request.getHeader(ACCESS_CONTROL_REQUEST_HEADERS_HEADER);
401 LOG.debug("{} is {}", ACCESS_CONTROL_REQUEST_HEADERS_HEADER, accessControlRequestHeaders);
402 if (accessControlRequestHeaders == null)
403 return Collections.emptyList();
404
405 List<String> requestedHeaders = new ArrayList<String>();
406 String[] headers = accessControlRequestHeaders.split(",");
407 for (String header : headers)
408 {
409 String h = header.trim();
410 if (h.length() > 0)
411 requestedHeaders.add(h);
412 }
413 return requestedHeaders;
414 }
415
416
417 private boolean areHeadersAllowed(List<String> requestedHeaders)
418 {
419 if (anyHeadersAllowed)
420 {
421 LOG.debug("Any header is allowed");
422 return true;
423 }
424
425 boolean result = true;
426 for (String requestedHeader:requestedHeaders)
427 {
428 boolean headerAllowed = false;
429 for (String allowedHeader:allowedHeaders)
430 {
431 if (requestedHeader.equalsIgnoreCase(allowedHeader.trim()))
432 {
433 headerAllowed = true;
434 break;
435 }
436 }
437 if (!headerAllowed)
438 {
439 result = false;
440 break;
441 }
442 }
443 LOG.debug("Headers [{}] are" + (result ? "" : " not") + " among allowed headers {}", requestedHeaders, allowedHeaders);
444 return result;
445 }
446
447 private String commify(List<String> strings)
448 {
449 StringBuilder builder = new StringBuilder();
450 for (int i = 0; i < strings.size(); ++i)
451 {
452 if (i > 0) builder.append(",");
453 String string = strings.get(i);
454 builder.append(string);
455 }
456 return builder.toString();
457 }
458
459 public void destroy()
460 {
461 anyOriginAllowed = false;
462 allowedOrigins.clear();
463 allowedMethods.clear();
464 allowedHeaders.clear();
465 preflightMaxAge = 0;
466 allowCredentials = false;
467 }
468 }