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.util.StringUtil;
40 import org.eclipse.jetty.util.log.Log;
41 import org.eclipse.jetty.util.log.Logger;
42
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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124 public class CrossOriginFilter implements Filter
125 {
126 private static final Logger LOG = Log.getLogger(CrossOriginFilter.class);
127
128
129 private static final String ORIGIN_HEADER = "Origin";
130 public static final String ACCESS_CONTROL_REQUEST_METHOD_HEADER = "Access-Control-Request-Method";
131 public static final String ACCESS_CONTROL_REQUEST_HEADERS_HEADER = "Access-Control-Request-Headers";
132
133 public static final String ACCESS_CONTROL_ALLOW_ORIGIN_HEADER = "Access-Control-Allow-Origin";
134 public static final String ACCESS_CONTROL_ALLOW_METHODS_HEADER = "Access-Control-Allow-Methods";
135 public static final String ACCESS_CONTROL_ALLOW_HEADERS_HEADER = "Access-Control-Allow-Headers";
136 public static final String ACCESS_CONTROL_MAX_AGE_HEADER = "Access-Control-Max-Age";
137 public static final String ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER = "Access-Control-Allow-Credentials";
138 public static final String ACCESS_CONTROL_EXPOSE_HEADERS_HEADER = "Access-Control-Expose-Headers";
139 public static final String TIMING_ALLOW_ORIGIN_HEADER = "Timing-Allow-Origin";
140
141 public static final String ALLOWED_ORIGINS_PARAM = "allowedOrigins";
142 public static final String ALLOWED_TIMING_ORIGINS_PARAM = "allowedTimingOrigins";
143 public static final String ALLOWED_METHODS_PARAM = "allowedMethods";
144 public static final String ALLOWED_HEADERS_PARAM = "allowedHeaders";
145 public static final String PREFLIGHT_MAX_AGE_PARAM = "preflightMaxAge";
146 public static final String ALLOW_CREDENTIALS_PARAM = "allowCredentials";
147 public static final String EXPOSED_HEADERS_PARAM = "exposedHeaders";
148 public static final String OLD_CHAIN_PREFLIGHT_PARAM = "forwardPreflight";
149 public static final String CHAIN_PREFLIGHT_PARAM = "chainPreflight";
150 private static final String ANY_ORIGIN = "*";
151 private static final String DEFAULT_ALLOWED_ORIGINS = "*";
152 private static final String DEFAULT_ALLOWED_TIMING_ORIGINS = "";
153 private static final List<String> SIMPLE_HTTP_METHODS = Arrays.asList("GET", "POST", "HEAD");
154 private static final List<String> DEFAULT_ALLOWED_METHODS = Arrays.asList("GET", "POST", "HEAD");
155 private static final List<String> DEFAULT_ALLOWED_HEADERS = Arrays.asList("X-Requested-With", "Content-Type", "Accept", "Origin");
156
157 private boolean anyOriginAllowed;
158 private boolean anyTimingOriginAllowed;
159 private boolean anyHeadersAllowed;
160 private List<String> allowedOrigins = new ArrayList<String>();
161 private List<String> allowedTimingOrigins = new ArrayList<String>();
162 private List<String> allowedMethods = new ArrayList<String>();
163 private List<String> allowedHeaders = new ArrayList<String>();
164 private List<String> exposedHeaders = new ArrayList<String>();
165 private int preflightMaxAge;
166 private boolean allowCredentials;
167 private boolean chainPreflight;
168
169 public void init(FilterConfig config) throws ServletException
170 {
171 String allowedOriginsConfig = config.getInitParameter(ALLOWED_ORIGINS_PARAM);
172 String allowedTimingOriginsConfig = config.getInitParameter(ALLOWED_TIMING_ORIGINS_PARAM);
173
174 anyOriginAllowed = generateAllowedOrigins(allowedOrigins, allowedOriginsConfig, DEFAULT_ALLOWED_ORIGINS);
175 anyTimingOriginAllowed = generateAllowedOrigins(allowedTimingOrigins, allowedTimingOriginsConfig, DEFAULT_ALLOWED_TIMING_ORIGINS);
176
177 String allowedMethodsConfig = config.getInitParameter(ALLOWED_METHODS_PARAM);
178 if (allowedMethodsConfig == null)
179 allowedMethods.addAll(DEFAULT_ALLOWED_METHODS);
180 else
181 allowedMethods.addAll(Arrays.asList(StringUtil.csvSplit(allowedMethodsConfig)));
182
183 String allowedHeadersConfig = config.getInitParameter(ALLOWED_HEADERS_PARAM);
184 if (allowedHeadersConfig == null)
185 allowedHeaders.addAll(DEFAULT_ALLOWED_HEADERS);
186 else if ("*".equals(allowedHeadersConfig))
187 anyHeadersAllowed = true;
188 else
189 allowedHeaders.addAll(Arrays.asList(StringUtil.csvSplit(allowedHeadersConfig)));
190
191 String preflightMaxAgeConfig = config.getInitParameter(PREFLIGHT_MAX_AGE_PARAM);
192 if (preflightMaxAgeConfig == null)
193 preflightMaxAgeConfig = "1800";
194 try
195 {
196 preflightMaxAge = Integer.parseInt(preflightMaxAgeConfig);
197 }
198 catch (NumberFormatException x)
199 {
200 LOG.info("Cross-origin filter, could not parse '{}' parameter as integer: {}", PREFLIGHT_MAX_AGE_PARAM, preflightMaxAgeConfig);
201 }
202
203 String allowedCredentialsConfig = config.getInitParameter(ALLOW_CREDENTIALS_PARAM);
204 if (allowedCredentialsConfig == null)
205 allowedCredentialsConfig = "true";
206 allowCredentials = Boolean.parseBoolean(allowedCredentialsConfig);
207
208 String exposedHeadersConfig = config.getInitParameter(EXPOSED_HEADERS_PARAM);
209 if (exposedHeadersConfig == null)
210 exposedHeadersConfig = "";
211 exposedHeaders.addAll(Arrays.asList(StringUtil.csvSplit(exposedHeadersConfig)));
212
213 String chainPreflightConfig = config.getInitParameter(OLD_CHAIN_PREFLIGHT_PARAM);
214 if (chainPreflightConfig != null)
215 LOG.warn("DEPRECATED CONFIGURATION: Use " + CHAIN_PREFLIGHT_PARAM + " instead of " + OLD_CHAIN_PREFLIGHT_PARAM);
216 else
217 chainPreflightConfig = config.getInitParameter(CHAIN_PREFLIGHT_PARAM);
218 if (chainPreflightConfig == null)
219 chainPreflightConfig = "true";
220 chainPreflight = Boolean.parseBoolean(chainPreflightConfig);
221
222 if (LOG.isDebugEnabled())
223 {
224 LOG.debug("Cross-origin filter configuration: " +
225 ALLOWED_ORIGINS_PARAM + " = " + allowedOriginsConfig + ", " +
226 ALLOWED_TIMING_ORIGINS_PARAM + " = " + allowedTimingOriginsConfig + ", " +
227 ALLOWED_METHODS_PARAM + " = " + allowedMethodsConfig + ", " +
228 ALLOWED_HEADERS_PARAM + " = " + allowedHeadersConfig + ", " +
229 PREFLIGHT_MAX_AGE_PARAM + " = " + preflightMaxAgeConfig + ", " +
230 ALLOW_CREDENTIALS_PARAM + " = " + allowedCredentialsConfig + "," +
231 EXPOSED_HEADERS_PARAM + " = " + exposedHeadersConfig + "," +
232 CHAIN_PREFLIGHT_PARAM + " = " + chainPreflightConfig
233 );
234 }
235 }
236
237 private boolean generateAllowedOrigins(List<String> allowedOriginStore, String allowedOriginsConfig, String defaultOrigin)
238 {
239 if (allowedOriginsConfig == null)
240 allowedOriginsConfig = defaultOrigin;
241 String[] allowedOrigins = StringUtil.csvSplit(allowedOriginsConfig);
242 for (String allowedOrigin : allowedOrigins)
243 {
244 if (allowedOrigin.length() > 0)
245 {
246 if (ANY_ORIGIN.equals(allowedOrigin))
247 {
248 allowedOriginStore.clear();
249 return true;
250 }
251 else
252 {
253 allowedOriginStore.add(allowedOrigin);
254 }
255 }
256 }
257 return false;
258 }
259
260 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
261 {
262 handle((HttpServletRequest)request, (HttpServletResponse)response, chain);
263 }
264
265 private void handle(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException
266 {
267 String origin = request.getHeader(ORIGIN_HEADER);
268
269 if (origin != null && isEnabled(request))
270 {
271 if (anyOriginAllowed || originMatches(allowedOrigins, origin))
272 {
273 if (isSimpleRequest(request))
274 {
275 LOG.debug("Cross-origin request to {} is a simple cross-origin request", request.getRequestURI());
276 handleSimpleResponse(request, response, origin);
277 }
278 else if (isPreflightRequest(request))
279 {
280 LOG.debug("Cross-origin request to {} is a preflight cross-origin request", request.getRequestURI());
281 handlePreflightResponse(request, response, origin);
282 if (chainPreflight)
283 LOG.debug("Preflight cross-origin request to {} forwarded to application", request.getRequestURI());
284 else
285 return;
286 }
287 else
288 {
289 LOG.debug("Cross-origin request to {} is a non-simple cross-origin request", request.getRequestURI());
290 handleSimpleResponse(request, response, origin);
291 }
292
293 if (anyTimingOriginAllowed || originMatches(allowedTimingOrigins, origin))
294 {
295 response.setHeader(TIMING_ALLOW_ORIGIN_HEADER, origin);
296 }
297 else
298 {
299 LOG.debug("Cross-origin request to " + request.getRequestURI() + " with origin " + origin + " does not match allowed timing origins " + allowedTimingOrigins);
300 }
301 }
302 else
303 {
304 LOG.debug("Cross-origin request to " + request.getRequestURI() + " with origin " + origin + " does not match allowed origins " + allowedOrigins);
305 }
306 }
307
308 chain.doFilter(request, response);
309 }
310
311 protected boolean isEnabled(HttpServletRequest request)
312 {
313
314
315 for (Enumeration<String> connections = request.getHeaders("Connection"); connections.hasMoreElements();)
316 {
317 String connection = (String)connections.nextElement();
318 if ("Upgrade".equalsIgnoreCase(connection))
319 {
320 for (Enumeration<String> upgrades = request.getHeaders("Upgrade"); upgrades.hasMoreElements();)
321 {
322 String upgrade = (String)upgrades.nextElement();
323 if ("WebSocket".equalsIgnoreCase(upgrade))
324 return false;
325 }
326 }
327 }
328 return true;
329 }
330
331 private boolean originMatches(List<String> allowedOrigins, String originList)
332 {
333 if (originList.trim().length() == 0)
334 return false;
335
336 String[] origins = originList.split(" ");
337 for (String origin : origins)
338 {
339 if (origin.trim().length() == 0)
340 continue;
341
342 for (String allowedOrigin : allowedOrigins)
343 {
344 if (allowedOrigin.contains("*"))
345 {
346 Matcher matcher = createMatcher(origin, allowedOrigin);
347 if (matcher.matches())
348 return true;
349 }
350 else if (allowedOrigin.equals(origin))
351 {
352 return true;
353 }
354 }
355 }
356 return false;
357 }
358
359 private Matcher createMatcher(String origin, String allowedOrigin)
360 {
361 String regex = parseAllowedWildcardOriginToRegex(allowedOrigin);
362 Pattern pattern = Pattern.compile(regex);
363 return pattern.matcher(origin);
364 }
365
366 private String parseAllowedWildcardOriginToRegex(String allowedOrigin)
367 {
368 String regex = allowedOrigin.replace(".", "\\.");
369 return regex.replace("*", ".*");
370 }
371
372 private boolean isSimpleRequest(HttpServletRequest request)
373 {
374 String method = request.getMethod();
375 if (SIMPLE_HTTP_METHODS.contains(method))
376 {
377
378
379
380
381 return request.getHeader(ACCESS_CONTROL_REQUEST_METHOD_HEADER) == null;
382 }
383 return false;
384 }
385
386 private boolean isPreflightRequest(HttpServletRequest request)
387 {
388 String method = request.getMethod();
389 if (!"OPTIONS".equalsIgnoreCase(method))
390 return false;
391 if (request.getHeader(ACCESS_CONTROL_REQUEST_METHOD_HEADER) == null)
392 return false;
393 return true;
394 }
395
396 private void handleSimpleResponse(HttpServletRequest request, HttpServletResponse response, String origin)
397 {
398 response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, origin);
399
400 if (!anyOriginAllowed)
401 response.addHeader("Vary", ORIGIN_HEADER);
402 if (allowCredentials)
403 response.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER, "true");
404 if (!exposedHeaders.isEmpty())
405 response.setHeader(ACCESS_CONTROL_EXPOSE_HEADERS_HEADER, commify(exposedHeaders));
406 }
407
408 private void handlePreflightResponse(HttpServletRequest request, HttpServletResponse response, String origin)
409 {
410 boolean methodAllowed = isMethodAllowed(request);
411
412 if (!methodAllowed)
413 return;
414 List<String> headersRequested = getAccessControlRequestHeaders(request);
415 boolean headersAllowed = areHeadersAllowed(headersRequested);
416 if (!headersAllowed)
417 return;
418 response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, origin);
419
420 if (!anyOriginAllowed)
421 response.addHeader("Vary", ORIGIN_HEADER);
422 if (allowCredentials)
423 response.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER, "true");
424 if (preflightMaxAge > 0)
425 response.setHeader(ACCESS_CONTROL_MAX_AGE_HEADER, String.valueOf(preflightMaxAge));
426 response.setHeader(ACCESS_CONTROL_ALLOW_METHODS_HEADER, commify(allowedMethods));
427 if (anyHeadersAllowed)
428 response.setHeader(ACCESS_CONTROL_ALLOW_HEADERS_HEADER, commify(headersRequested));
429 else
430 response.setHeader(ACCESS_CONTROL_ALLOW_HEADERS_HEADER, commify(allowedHeaders));
431 }
432
433 private boolean isMethodAllowed(HttpServletRequest request)
434 {
435 String accessControlRequestMethod = request.getHeader(ACCESS_CONTROL_REQUEST_METHOD_HEADER);
436 LOG.debug("{} is {}", ACCESS_CONTROL_REQUEST_METHOD_HEADER, accessControlRequestMethod);
437 boolean result = false;
438 if (accessControlRequestMethod != null)
439 result = allowedMethods.contains(accessControlRequestMethod);
440 LOG.debug("Method {} is" + (result ? "" : " not") + " among allowed methods {}", accessControlRequestMethod, allowedMethods);
441 return result;
442 }
443
444 private List<String> getAccessControlRequestHeaders(HttpServletRequest request)
445 {
446 String accessControlRequestHeaders = request.getHeader(ACCESS_CONTROL_REQUEST_HEADERS_HEADER);
447 LOG.debug("{} is {}", ACCESS_CONTROL_REQUEST_HEADERS_HEADER, accessControlRequestHeaders);
448 if (accessControlRequestHeaders == null)
449 return Collections.emptyList();
450
451 List<String> requestedHeaders = new ArrayList<String>();
452 String[] headers = StringUtil.csvSplit(accessControlRequestHeaders);
453 for (String header : headers)
454 {
455 String h = header.trim();
456 if (h.length() > 0)
457 requestedHeaders.add(h);
458 }
459 return requestedHeaders;
460 }
461
462 private boolean areHeadersAllowed(List<String> requestedHeaders)
463 {
464 if (anyHeadersAllowed)
465 {
466 LOG.debug("Any header is allowed");
467 return true;
468 }
469
470 boolean result = true;
471 for (String requestedHeader : requestedHeaders)
472 {
473 boolean headerAllowed = false;
474 for (String allowedHeader : allowedHeaders)
475 {
476 if (requestedHeader.equalsIgnoreCase(allowedHeader.trim()))
477 {
478 headerAllowed = true;
479 break;
480 }
481 }
482 if (!headerAllowed)
483 {
484 result = false;
485 break;
486 }
487 }
488 LOG.debug("Headers [{}] are" + (result ? "" : " not") + " among allowed headers {}", requestedHeaders, allowedHeaders);
489 return result;
490 }
491
492 private String commify(List<String> strings)
493 {
494 StringBuilder builder = new StringBuilder();
495 for (int i = 0; i < strings.size(); ++i)
496 {
497 if (i > 0) builder.append(",");
498 String string = strings.get(i);
499 builder.append(string);
500 }
501 return builder.toString();
502 }
503
504 public void destroy()
505 {
506 anyOriginAllowed = false;
507 allowedOrigins.clear();
508 allowedMethods.clear();
509 allowedHeaders.clear();
510 preflightMaxAge = 0;
511 allowCredentials = false;
512 }
513 }