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