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