org.springframework.web.cors.DefaultCorsProcessor Maven / Gradle / Ivy
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.cors;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
/**
* The default implementation of {@link CorsProcessor}, as defined by the
* CORS W3C recommendation.
*
* Note that when the supplied {@link CorsConfiguration} is {@code null}, this
* implementation does not reject simple or actual requests outright but simply
* avoids adding CORS headers to the response. CORS processing is also skipped
* if the response already contains CORS headers.
*
* @author Sebastien Deleuze
* @author Rossen Stoyanchev
* @since 4.2
*/
public class DefaultCorsProcessor implements CorsProcessor {
private static final Log logger = LogFactory.getLog(DefaultCorsProcessor.class);
/**
* The {@code Access-Control-Request-Private-Network} request header field name.
* @see Private Network Access specification
*/
static final String ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK = "Access-Control-Request-Private-Network";
/**
* The {@code Access-Control-Allow-Private-Network} response header field name.
* @see Private Network Access specification
*/
static final String ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK = "Access-Control-Allow-Private-Network";
@Override
@SuppressWarnings("resource")
public boolean processRequest(@Nullable CorsConfiguration config, HttpServletRequest request,
HttpServletResponse response) throws IOException {
Collection varyHeaders = response.getHeaders(HttpHeaders.VARY);
if (!varyHeaders.contains(HttpHeaders.ORIGIN)) {
response.addHeader(HttpHeaders.VARY, HttpHeaders.ORIGIN);
}
if (!varyHeaders.contains(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD)) {
response.addHeader(HttpHeaders.VARY, HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD);
}
if (!varyHeaders.contains(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS)) {
response.addHeader(HttpHeaders.VARY, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
}
if (!CorsUtils.isCorsRequest(request)) {
return true;
}
if (response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN) != null) {
logger.trace("Skip: response already contains \"Access-Control-Allow-Origin\"");
return true;
}
boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);
if (config == null) {
if (preFlightRequest) {
rejectRequest(new ServletServerHttpResponse(response));
return false;
}
else {
return true;
}
}
return handleInternal(new ServletServerHttpRequest(request), new ServletServerHttpResponse(response), config, preFlightRequest);
}
/**
* Invoked when one of the CORS checks failed.
* The default implementation sets the response status to 403 and writes
* "Invalid CORS request" to the response.
*/
protected void rejectRequest(ServerHttpResponse response) throws IOException {
response.setStatusCode(HttpStatus.FORBIDDEN);
response.getBody().write("Invalid CORS request".getBytes(StandardCharsets.UTF_8));
response.flush();
}
/**
* Handle the given request.
*/
protected boolean handleInternal(ServerHttpRequest request, ServerHttpResponse response,
CorsConfiguration config, boolean preFlightRequest) throws IOException {
String requestOrigin = request.getHeaders().getOrigin();
String allowOrigin = checkOrigin(config, requestOrigin);
HttpHeaders responseHeaders = response.getHeaders();
if (allowOrigin == null) {
logger.debug("Reject: '" + requestOrigin + "' origin is not allowed");
rejectRequest(response);
return false;
}
HttpMethod requestMethod = getMethodToUse(request, preFlightRequest);
List allowMethods = checkMethods(config, requestMethod);
if (allowMethods == null) {
logger.debug("Reject: HTTP '" + requestMethod + "' is not allowed");
rejectRequest(response);
return false;
}
List requestHeaders = getHeadersToUse(request, preFlightRequest);
List allowHeaders = checkHeaders(config, requestHeaders);
if (preFlightRequest && allowHeaders == null) {
logger.debug("Reject: headers '" + requestHeaders + "' are not allowed");
rejectRequest(response);
return false;
}
responseHeaders.setAccessControlAllowOrigin(allowOrigin);
if (preFlightRequest) {
responseHeaders.setAccessControlAllowMethods(allowMethods);
}
if (preFlightRequest && !allowHeaders.isEmpty()) {
responseHeaders.setAccessControlAllowHeaders(allowHeaders);
}
if (!CollectionUtils.isEmpty(config.getExposedHeaders())) {
responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders());
}
if (Boolean.TRUE.equals(config.getAllowCredentials())) {
responseHeaders.setAccessControlAllowCredentials(true);
}
if (Boolean.TRUE.equals(config.getAllowPrivateNetwork()) &&
Boolean.parseBoolean(request.getHeaders().getFirst(ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK))) {
responseHeaders.set(ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK, Boolean.toString(true));
}
if (preFlightRequest && config.getMaxAge() != null) {
responseHeaders.setAccessControlMaxAge(config.getMaxAge());
}
response.flush();
return true;
}
/**
* Check the origin and determine the origin for the response. The default
* implementation simply delegates to
* {@link org.springframework.web.cors.CorsConfiguration#checkOrigin(String)}.
*/
@Nullable
protected String checkOrigin(CorsConfiguration config, @Nullable String requestOrigin) {
return config.checkOrigin(requestOrigin);
}
/**
* Check the HTTP method and determine the methods for the response of a
* pre-flight request. The default implementation simply delegates to
* {@link org.springframework.web.cors.CorsConfiguration#checkHttpMethod(HttpMethod)}.
*/
@Nullable
protected List checkMethods(CorsConfiguration config, @Nullable HttpMethod requestMethod) {
return config.checkHttpMethod(requestMethod);
}
@Nullable
private HttpMethod getMethodToUse(ServerHttpRequest request, boolean isPreFlight) {
return (isPreFlight ? request.getHeaders().getAccessControlRequestMethod() : request.getMethod());
}
/**
* Check the headers and determine the headers for the response of a
* pre-flight request. The default implementation simply delegates to
* {@link org.springframework.web.cors.CorsConfiguration#checkHeaders(List)}.
*/
@Nullable
protected List checkHeaders(CorsConfiguration config, List requestHeaders) {
return config.checkHeaders(requestHeaders);
}
private List getHeadersToUse(ServerHttpRequest request, boolean isPreFlight) {
HttpHeaders headers = request.getHeaders();
return (isPreFlight ? headers.getAccessControlRequestHeaders() : new ArrayList<>(headers.keySet()));
}
}