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