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