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