001/*
002 * Copyright 2002-2020 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.web.cors;
018
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collections;
022import java.util.LinkedHashSet;
023import java.util.List;
024import java.util.Set;
025
026import org.springframework.http.HttpMethod;
027import org.springframework.util.CollectionUtils;
028import org.springframework.util.ObjectUtils;
029import org.springframework.util.StringUtils;
030
031/**
032 * A container for CORS configuration along with methods to check against the
033 * actual origin, HTTP methods, and headers of a given request.
034 *
035 * <p>By default a newly created {@code CorsConfiguration} does not permit any
036 * cross-origin requests and must be configured explicitly to indicate what
037 * should be allowed. Use {@link #applyPermitDefaultValues()} to flip the
038 * initialization model to start with open defaults that permit all cross-origin
039 * requests for GET, HEAD, and POST requests.
040 *
041 * @author Sebastien Deleuze
042 * @author Rossen Stoyanchev
043 * @author Juergen Hoeller
044 * @author Sam Brannen
045 * @since 4.2
046 * @see <a href="https://www.w3.org/TR/cors/">CORS spec</a>
047 */
048public class CorsConfiguration {
049
050        /** Wildcard representing <em>all</em> origins, methods, or headers. */
051        public static final String ALL = "*";
052
053        private static final List<HttpMethod> DEFAULT_METHODS;
054
055        static {
056                List<HttpMethod> rawMethods = new ArrayList<HttpMethod>(2);
057                rawMethods.add(HttpMethod.GET);
058                rawMethods.add(HttpMethod.HEAD);
059                DEFAULT_METHODS = Collections.unmodifiableList(rawMethods);
060        }
061
062
063        private List<String> allowedOrigins;
064
065        private List<String> allowedMethods;
066
067        private List<HttpMethod> resolvedMethods = DEFAULT_METHODS;
068
069        private List<String> allowedHeaders;
070
071        private List<String> exposedHeaders;
072
073        private Boolean allowCredentials;
074
075        private Long maxAge;
076
077
078        /**
079         * Construct a new {@code CorsConfiguration} instance with no cross-origin
080         * requests allowed for any origin by default.
081         * @see #applyPermitDefaultValues()
082         */
083        public CorsConfiguration() {
084        }
085
086        /**
087         * Construct a new {@code CorsConfiguration} instance by copying all
088         * values from the supplied {@code CorsConfiguration}.
089         */
090        public CorsConfiguration(CorsConfiguration other) {
091                this.allowedOrigins = other.allowedOrigins;
092                this.allowedMethods = other.allowedMethods;
093                this.resolvedMethods = other.resolvedMethods;
094                this.allowedHeaders = other.allowedHeaders;
095                this.exposedHeaders = other.exposedHeaders;
096                this.allowCredentials = other.allowCredentials;
097                this.maxAge = other.maxAge;
098        }
099
100
101        /**
102         * Set the origins to allow, e.g. {@code "https://domain1.com"}.
103         * <p>The special value {@code "*"} allows all domains.
104         * <p>By default this is not set.
105         * <p><strong>Note:</strong> CORS checks use values from "Forwarded"
106         * (<a href="https://tools.ietf.org/html/rfc7239">RFC 7239</a>),
107         * "X-Forwarded-Host", "X-Forwarded-Port", and "X-Forwarded-Proto" headers,
108         * if present, in order to reflect the client-originated address.
109         * Consider using the {@code ForwardedHeaderFilter} in order to choose from a
110         * central place whether to extract and use, or to discard such headers.
111         * See the Spring Framework reference for more on this filter.
112         */
113        public void setAllowedOrigins(List<String> allowedOrigins) {
114                this.allowedOrigins = (allowedOrigins != null ? new ArrayList<String>(allowedOrigins) : null);
115        }
116
117        /**
118         * Return the configured origins to allow, or {@code null} if none.
119         * @see #addAllowedOrigin(String)
120         * @see #setAllowedOrigins(List)
121         */
122        public List<String> getAllowedOrigins() {
123                return this.allowedOrigins;
124        }
125
126        /**
127         * Add an origin to allow.
128         */
129        public void addAllowedOrigin(String origin) {
130                if (this.allowedOrigins == null) {
131                        this.allowedOrigins = new ArrayList<String>(4);
132                }
133                this.allowedOrigins.add(origin);
134        }
135
136        /**
137         * Set the HTTP methods to allow, e.g. {@code "GET"}, {@code "POST"},
138         * {@code "PUT"}, etc.
139         * <p>The special value {@code "*"} allows all methods.
140         * <p>If not set, only {@code "GET"} and {@code "HEAD"} are allowed.
141         * <p>By default this is not set.
142         */
143        public void setAllowedMethods(List<String> allowedMethods) {
144                this.allowedMethods = (allowedMethods != null ? new ArrayList<String>(allowedMethods) : null);
145                if (!CollectionUtils.isEmpty(allowedMethods)) {
146                        this.resolvedMethods = new ArrayList<HttpMethod>(allowedMethods.size());
147                        for (String method : allowedMethods) {
148                                if (ALL.equals(method)) {
149                                        this.resolvedMethods = null;
150                                        break;
151                                }
152                                this.resolvedMethods.add(HttpMethod.resolve(method));
153                        }
154                }
155                else {
156                        this.resolvedMethods = DEFAULT_METHODS;
157                }
158        }
159
160        /**
161         * Return the allowed HTTP methods, or {@code null} in which case
162         * only {@code "GET"} and {@code "HEAD"} allowed.
163         * @see #addAllowedMethod(HttpMethod)
164         * @see #addAllowedMethod(String)
165         * @see #setAllowedMethods(List)
166         */
167        public List<String> getAllowedMethods() {
168                return this.allowedMethods;
169        }
170
171        /**
172         * Add an HTTP method to allow.
173         */
174        public void addAllowedMethod(HttpMethod method) {
175                if (method != null) {
176                        addAllowedMethod(method.name());
177                }
178        }
179
180        /**
181         * Add an HTTP method to allow.
182         */
183        public void addAllowedMethod(String method) {
184                if (StringUtils.hasText(method)) {
185                        if (this.allowedMethods == null) {
186                                this.allowedMethods = new ArrayList<String>(4);
187                                this.resolvedMethods = new ArrayList<HttpMethod>(4);
188                        }
189                        this.allowedMethods.add(method);
190                        if (ALL.equals(method)) {
191                                this.resolvedMethods = null;
192                        }
193                        else if (this.resolvedMethods != null) {
194                                this.resolvedMethods.add(HttpMethod.resolve(method));
195                        }
196                }
197        }
198
199        /**
200         * Set the list of headers that a pre-flight request can list as allowed
201         * for use during an actual request.
202         * <p>The special value {@code "*"} allows actual requests to send any
203         * header.
204         * <p>A header name is not required to be listed if it is one of:
205         * {@code Cache-Control}, {@code Content-Language}, {@code Expires},
206         * {@code Last-Modified}, or {@code Pragma}.
207         * <p>By default this is not set.
208         */
209        public void setAllowedHeaders(List<String> allowedHeaders) {
210                this.allowedHeaders = (allowedHeaders != null ? new ArrayList<String>(allowedHeaders) : null);
211        }
212
213        /**
214         * Return the allowed actual request headers, or {@code null} if none.
215         * @see #addAllowedHeader(String)
216         * @see #setAllowedHeaders(List)
217         */
218        public List<String> getAllowedHeaders() {
219                return this.allowedHeaders;
220        }
221
222        /**
223         * Add an actual request header to allow.
224         */
225        public void addAllowedHeader(String allowedHeader) {
226                if (this.allowedHeaders == null) {
227                        this.allowedHeaders = new ArrayList<String>(4);
228                }
229                this.allowedHeaders.add(allowedHeader);
230        }
231
232        /**
233         * Set the list of response headers other than simple headers (i.e.
234         * {@code Cache-Control}, {@code Content-Language}, {@code Content-Type},
235         * {@code Expires}, {@code Last-Modified}, or {@code Pragma}) that an
236         * actual response might have and can be exposed.
237         * <p>Note that {@code "*"} is not a valid exposed header value.
238         * <p>By default this is not set.
239         */
240        public void setExposedHeaders(List<String> exposedHeaders) {
241                if (exposedHeaders != null && exposedHeaders.contains(ALL)) {
242                        throw new IllegalArgumentException("'*' is not a valid exposed header value");
243                }
244                this.exposedHeaders = (exposedHeaders != null ? new ArrayList<String>(exposedHeaders) : null);
245        }
246
247        /**
248         * Return the configured response headers to expose, or {@code null} if none.
249         * @see #addExposedHeader(String)
250         * @see #setExposedHeaders(List)
251         */
252        public List<String> getExposedHeaders() {
253                return this.exposedHeaders;
254        }
255
256        /**
257         * Add a response header to expose.
258         * <p>Note that {@code "*"} is not a valid exposed header value.
259         */
260        public void addExposedHeader(String exposedHeader) {
261                if (ALL.equals(exposedHeader)) {
262                        throw new IllegalArgumentException("'*' is not a valid exposed header value");
263                }
264                if (this.exposedHeaders == null) {
265                        this.exposedHeaders = new ArrayList<String>(4);
266                }
267                this.exposedHeaders.add(exposedHeader);
268        }
269
270        /**
271         * Whether user credentials are supported.
272         * <p>By default this is not set (i.e. user credentials are not supported).
273         */
274        public void setAllowCredentials(Boolean allowCredentials) {
275                this.allowCredentials = allowCredentials;
276        }
277
278        /**
279         * Return the configured {@code allowCredentials} flag, or {@code null} if none.
280         * @see #setAllowCredentials(Boolean)
281         */
282        public Boolean getAllowCredentials() {
283                return this.allowCredentials;
284        }
285
286        /**
287         * Configure how long, in seconds, the response from a pre-flight request
288         * can be cached by clients.
289         * <p>By default this is not set.
290         */
291        public void setMaxAge(Long maxAge) {
292                this.maxAge = maxAge;
293        }
294
295        /**
296         * Return the configured {@code maxAge} value, or {@code null} if none.
297         * @see #setMaxAge(Long)
298         */
299        public Long getMaxAge() {
300                return this.maxAge;
301        }
302
303
304        /**
305         * By default a newly created {@code CorsConfiguration} does not permit any
306         * cross-origin requests and must be configured explicitly to indicate what
307         * should be allowed.
308         * <p>Use this method to flip the initialization model to start with open
309         * defaults that permit all cross-origin requests for GET, HEAD, and POST
310         * requests. Note however that this method will not override any existing
311         * values already set.
312         * <p>The following defaults are applied if not already set:
313         * <ul>
314         * <li>Allow all origins.</li>
315         * <li>Allow "simple" methods {@code GET}, {@code HEAD} and {@code POST}.</li>
316         * <li>Allow all headers.</li>
317         * <li>Set max age to 1800 seconds (30 minutes).</li>
318         * </ul>
319         */
320        public CorsConfiguration applyPermitDefaultValues() {
321                if (this.allowedOrigins == null) {
322                        this.addAllowedOrigin(ALL);
323                }
324                if (this.allowedMethods == null) {
325                        this.setAllowedMethods(Arrays.asList(
326                                        HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name()));
327                }
328                if (this.allowedHeaders == null) {
329                        this.addAllowedHeader(ALL);
330                }
331                if (this.maxAge == null) {
332                        this.setMaxAge(1800L);
333                }
334                return this;
335        }
336
337        /**
338         * Combine the supplied {@code CorsConfiguration} with this one.
339         * <p>Properties of this configuration are overridden by any non-null
340         * properties of the supplied one.
341         * @return the combined {@code CorsConfiguration} or {@code this}
342         * configuration if the supplied configuration is {@code null}
343         */
344        public CorsConfiguration combine(CorsConfiguration other) {
345                if (other == null) {
346                        return this;
347                }
348                CorsConfiguration config = new CorsConfiguration(this);
349                config.setAllowedOrigins(combine(getAllowedOrigins(), other.getAllowedOrigins()));
350                config.setAllowedMethods(combine(getAllowedMethods(), other.getAllowedMethods()));
351                config.setAllowedHeaders(combine(getAllowedHeaders(), other.getAllowedHeaders()));
352                config.setExposedHeaders(combine(getExposedHeaders(), other.getExposedHeaders()));
353                Boolean allowCredentials = other.getAllowCredentials();
354                if (allowCredentials != null) {
355                        config.setAllowCredentials(allowCredentials);
356                }
357                Long maxAge = other.getMaxAge();
358                if (maxAge != null) {
359                        config.setMaxAge(maxAge);
360                }
361                return config;
362        }
363
364        private List<String> combine(List<String> source, List<String> other) {
365                if (other == null || other.contains(ALL)) {
366                        return source;
367                }
368                if (source == null || source.contains(ALL)) {
369                        return other;
370                }
371                Set<String> combined = new LinkedHashSet<String>(source);
372                combined.addAll(other);
373                return new ArrayList<String>(combined);
374        }
375
376        /**
377         * Check the origin of the request against the configured allowed origins.
378         * @param requestOrigin the origin to check
379         * @return the origin to use for the response, or {@code null} which
380         * means the request origin is not allowed
381         */
382        public String checkOrigin(String requestOrigin) {
383                if (!StringUtils.hasText(requestOrigin)) {
384                        return null;
385                }
386                if (ObjectUtils.isEmpty(this.allowedOrigins)) {
387                        return null;
388                }
389
390                if (this.allowedOrigins.contains(ALL)) {
391                        if (this.allowCredentials != Boolean.TRUE) {
392                                return ALL;
393                        }
394                        else {
395                                return requestOrigin;
396                        }
397                }
398                for (String allowedOrigin : this.allowedOrigins) {
399                        if (requestOrigin.equalsIgnoreCase(allowedOrigin)) {
400                                return requestOrigin;
401                        }
402                }
403
404                return null;
405        }
406
407        /**
408         * Check the HTTP request method (or the method from the
409         * {@code Access-Control-Request-Method} header on a pre-flight request)
410         * against the configured allowed methods.
411         * @param requestMethod the HTTP request method to check
412         * @return the list of HTTP methods to list in the response of a pre-flight
413         * request, or {@code null} if the supplied {@code requestMethod} is not allowed
414         */
415        public List<HttpMethod> checkHttpMethod(HttpMethod requestMethod) {
416                if (requestMethod == null) {
417                        return null;
418                }
419                if (this.resolvedMethods == null) {
420                        return Collections.singletonList(requestMethod);
421                }
422                return (this.resolvedMethods.contains(requestMethod) ? this.resolvedMethods : null);
423        }
424
425        /**
426         * Check the supplied request headers (or the headers listed in the
427         * {@code Access-Control-Request-Headers} of a pre-flight request) against
428         * the configured allowed headers.
429         * @param requestHeaders the request headers to check
430         * @return the list of allowed headers to list in the response of a pre-flight
431         * request, or {@code null} if none of the supplied request headers is allowed
432         */
433        public List<String> checkHeaders(List<String> requestHeaders) {
434                if (requestHeaders == null) {
435                        return null;
436                }
437                if (requestHeaders.isEmpty()) {
438                        return Collections.emptyList();
439                }
440                if (ObjectUtils.isEmpty(this.allowedHeaders)) {
441                        return null;
442                }
443
444                boolean allowAnyHeader = this.allowedHeaders.contains(ALL);
445                List<String> result = new ArrayList<String>(requestHeaders.size());
446                for (String requestHeader : requestHeaders) {
447                        if (StringUtils.hasText(requestHeader)) {
448                                requestHeader = requestHeader.trim();
449                                if (allowAnyHeader) {
450                                        result.add(requestHeader);
451                                }
452                                else {
453                                        for (String allowedHeader : this.allowedHeaders) {
454                                                if (requestHeader.equalsIgnoreCase(allowedHeader)) {
455                                                        result.add(requestHeader);
456                                                        break;
457                                                }
458                                        }
459                                }
460                        }
461                }
462                return (result.isEmpty() ? null : result);
463        }
464
465}