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