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