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