Encapsulate CORS checking within CorsConfiguration

CorsConfiguration now provides methods to check and determine the
allowed origin, method, and headers according to its own configuration.

This simplifies significantly the work that needs to be done from
DefaultCorsProcessor. However an alternative CorsProcessor can still
access the raw CorsConfiguration and perform its own checks.

Issue: SPR-12885
This commit is contained in:
Rossen Stoyanchev
2015-04-21 15:20:38 -04:00
committed by Sebastien Deleuze
parent 83f269b512
commit e306098155
7 changed files with 371 additions and 274 deletions

View File

@@ -49,6 +49,8 @@ public class ServletServerHttpResponse implements ServerHttpResponse {
private boolean headersWritten = false;
private boolean bodyUsed = false;
/**
* Construct a new instance of the ServletServerHttpResponse based on the given {@link HttpServletResponse}.
@@ -80,6 +82,7 @@ public class ServletServerHttpResponse implements ServerHttpResponse {
@Override
public OutputStream getBody() throws IOException {
this.bodyUsed = true;
writeHeaders();
return this.servletResponse.getOutputStream();
}
@@ -87,7 +90,9 @@ public class ServletServerHttpResponse implements ServerHttpResponse {
@Override
public void flush() throws IOException {
writeHeaders();
this.servletResponse.flushBuffer();
if (this.bodyUsed) {
this.servletResponse.flushBuffer();
}
}
@Override

View File

@@ -21,9 +21,11 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.springframework.http.HttpMethod;
/**
* Represents the CORS configuration that stores various properties used to check if a
* CORS request is allowed and to generate CORS response headers.
* A container for CORS configuration also providing methods to check actual or
* or requested origin, HTTP method, and headers.
*
* @author Sebastien Deleuze
* @author Rossen Stoyanchev
@@ -45,130 +47,79 @@ public class CorsConfiguration {
private Long maxAge;
/**
* Default constructor.
*/
public CorsConfiguration() {
}
public CorsConfiguration(CorsConfiguration config) {
if (config.allowedOrigins != null) {
this.allowedOrigins = new ArrayList<String>(config.allowedOrigins);
}
if (config.allowCredentials != null) {
this.allowCredentials = config.allowCredentials;
}
if (config.exposedHeaders != null) {
this.exposedHeaders = new ArrayList<String>(config.exposedHeaders);
}
if (config.allowedMethods != null) {
this.allowedMethods = new ArrayList<String>(config.allowedMethods);
}
if (config.allowedHeaders != null) {
this.allowedHeaders = new ArrayList<String>(config.allowedHeaders);
}
if (config.maxAge != null) {
this.maxAge = config.maxAge;
}
}
public CorsConfiguration combine(CorsConfiguration other) {
CorsConfiguration config = new CorsConfiguration(this);
if (other.getAllowedOrigins() != null) {
config.setAllowedOrigins(other.getAllowedOrigins());
}
if (other.getAllowedMethods() != null) {
config.setAllowedMethods(other.getAllowedMethods());
}
if (other.getAllowedHeaders() != null) {
config.setAllowedHeaders(other.getAllowedHeaders());
}
if (other.getExposedHeaders() != null) {
config.setExposedHeaders(other.getExposedHeaders());
}
if (other.getMaxAge() != null) {
config.setMaxAge(other.getMaxAge());
}
if (other.isAllowCredentials() != null) {
config.setAllowCredentials(other.isAllowCredentials());
}
return config;
/**
* Configure origins to allow, e.g. "http://domain1.com". The special value
* "*" allows all domains.
* <p>By default this is not set.
*/
public void setAllowedOrigins(List<String> origins) {
this.allowedOrigins = origins;
}
/**
* @see #setAllowedOrigins(java.util.List)
* Add an origin to allow.
*/
public List<String> getAllowedOrigins() {
if (this.allowedOrigins != null) {
return this.allowedOrigins.contains("*") ? Arrays.asList("*") : Collections.unmodifiableList(this.allowedOrigins);
}
return null;
}
/**
* Set allowed allowedOrigins that will define Access-Control-Allow-Origin response
* header values (mandatory). For example "http://domain1.com", "http://domain2.com" ...
* "*" means that all domains are allowed.
*/
public void setAllowedOrigins(List<String> allowedOrigins) {
this.allowedOrigins = allowedOrigins;
}
/**
* @see #setAllowedOrigins(java.util.List)
*/
public void addAllowedOrigin(String allowedOrigin) {
public void addAllowedOrigin(String origin) {
if (this.allowedOrigins == null) {
this.allowedOrigins = new ArrayList<String>();
}
this.allowedOrigins.add(allowedOrigin);
this.allowedOrigins.add(origin);
}
/**
* @see #setAllowedMethods(java.util.List)
* Return the configured origins to allow, possibly {@code null}.
*/
public List<String> getAllowedMethods() {
return this.allowedMethods == null ? null : Collections.unmodifiableList(this.allowedMethods);
public List<String> getAllowedOrigins() {
return this.allowedOrigins;
}
/**
* Set allow methods that will define Access-Control-Allow-Methods response header
* values. For example "GET", "POST", "PUT" ... "*" means that all methods requested
* by the client are allowed. If not set, allowed method is set to "GET".
*
* Configure HTTP methods to allow, e.g. "GET", "POST", "PUT". The special
* value "*" allows all method. When not set only "GET is allowed.
* <p>By default this is not set.
*/
public void setAllowedMethods(List<String> allowedMethods) {
this.allowedMethods = allowedMethods;
public void setAllowedMethods(List<String> methods) {
this.allowedMethods = methods;
}
/**
* @see #setAllowedMethods(java.util.List)
* Add an HTTP method to allow.
*/
public void addAllowedMethod(String allowedMethod) {
public void addAllowedMethod(String method) {
if (this.allowedMethods == null) {
this.allowedMethods = new ArrayList<String>();
}
this.allowedMethods.add(allowedMethod);
this.allowedMethods.add(method);
}
/**
* @see #setAllowedHeaders(java.util.List)
* Return the allowed HTTP methods, possibly {@code null} in which case only
* HTTP GET is allowed.
*/
public List<String> getAllowedHeaders() {
return this.allowedHeaders == null ? null : Collections.unmodifiableList(this.allowedHeaders);
public List<String> getAllowedMethods() {
return this.allowedMethods;
}
/**
* Set a list of request headers that will define Access-Control-Allow-Methods response
* header values. If a header field name is one of the following, it is not required
* to be listed: Cache-Control, Content-Language, Expires, Last-Modified, Pragma.
* "*" means that all headers asked by the client will be allowed.
* Configure the list of headers that a pre-flight request can list as allowed
* for use during an actual request. The special value of "*" allows actual
* requests to send any header. A header name is not required to be listed if
* it is one of: Cache-Control, Content-Language, Expires, Last-Modified, Pragma.
* <p>By default this is not set.
*/
public void setAllowedHeaders(List<String> allowedHeaders) {
this.allowedHeaders = allowedHeaders;
}
/**
* @see #setAllowedHeaders(java.util.List)
* Add one actual request header to allow.
*/
public void addAllowedHeader(String allowedHeader) {
if (this.allowedHeaders == null) {
@@ -178,23 +129,24 @@ public class CorsConfiguration {
}
/**
* @see #setExposedHeaders(java.util.List)
* Return the allowed actual request headers, possibly {@code null}.
*/
public List<String> getExposedHeaders() {
return this.exposedHeaders == null ? null : Collections.unmodifiableList(this.exposedHeaders);
public List<String> getAllowedHeaders() {
return this.allowedHeaders;
}
/**
* Set a list of response headers other than simple headers that the resource might use
* and can be exposed. Simple response headers are: Cache-Control, Content-Language,
* Content-Type, Expires, Last-Modified, Pragma.
* Configure the list of response headers other than simple headers (i.e.
* Cache-Control, Content-Language, Content-Type, Expires, Last-Modified,
* Pragma) that an actual response might have and can be exposed.
* <p>By default this is not set.
*/
public void setExposedHeaders(List<String> exposedHeaders) {
this.exposedHeaders = exposedHeaders;
}
/**
* @see #setExposedHeaders(java.util.List)
* Add a single response header to expose.
*/
public void addExposedHeader(String exposedHeader) {
if (this.exposedHeaders == null) {
@@ -204,33 +156,131 @@ public class CorsConfiguration {
}
/**
* @see #setAllowCredentials(Boolean)
* Return the configured response headers to expose, possibly {@code null}.
*/
public Boolean isAllowCredentials() {
return this.allowCredentials;
public List<String> getExposedHeaders() {
return this.exposedHeaders;
}
/**
* Indicates whether the resource supports user credentials.
* Set the value of Access-Control-Allow-Credentials response header.
* Whether user credentials are supported.
* <p>By default this is not set (i.e. user credentials not supported).
*/
public void setAllowCredentials(Boolean allowCredentials) {
this.allowCredentials = allowCredentials;
}
/**
* @see #setMaxAge(Long)
* Return the configured allowCredentials, possibly {@code null}.
*/
public Long getMaxAge() {
return maxAge;
public Boolean getAllowCredentials() {
return this.allowCredentials;
}
/**
* Indicates how long (seconds) the results of a preflight request can be cached
* in a preflight result cache.
* Configure how long, in seconds, the response from a pre-flight request
* can be cached by clients.
* <p>By default this is not set.
*/
public void setMaxAge(Long maxAge) {
this.maxAge = maxAge;
}
/**
* Return the configure maxAge value, possibly {@code null}.
*/
public Long getMaxAge() {
return maxAge;
}
/**
* Check the origin of the request against the configured allowed origins.
* @param requestOrigin the origin to check.
* @return the origin to use for the response, possibly {@code null} which
* means the request origin is not allowed.
*/
public String checkOrigin(String requestOrigin) {
if (requestOrigin == null) {
return null;
}
List<String> allowedOrigins = this.allowedOrigins == null ?
new ArrayList<String>() : this.allowedOrigins;
if (allowedOrigins.contains("*")) {
if (this.allowCredentials == null || !this.allowCredentials) {
return "*";
} else {
return requestOrigin;
}
}
for (String allowedOrigin : allowedOrigins) {
if (requestOrigin.equalsIgnoreCase(allowedOrigin)) {
return requestOrigin;
}
}
return null;
}
/**
* Check the request HTTP method (or the method from the
* Access-Control-Request-Method header on a pre-flight request) against the
* configured allowed methods.
* @param requestMethod the HTTP method to check.
* @return the list of HTTP methods to list in the response of a pre-flight
* request, or {@code null} if the requestMethod is not allowed.
*/
public List<HttpMethod> checkHttpMethod(HttpMethod requestMethod) {
if (requestMethod == null) {
return null;
}
List<String> allowedMethods = this.allowedMethods == null ?
new ArrayList<String>() : this.allowedMethods;
if (allowedMethods.contains("*")) {
return Arrays.asList(requestMethod);
}
if (allowedMethods.isEmpty()) {
allowedMethods.add(HttpMethod.GET.name());
}
List<HttpMethod> result = new ArrayList<HttpMethod>(allowedMethods.size());
boolean allowed = false;
for (String method : allowedMethods) {
if (method.equals(requestMethod.name())) {
allowed = true;
}
result.add(HttpMethod.valueOf(method));
}
return allowed ? result : null;
}
/**
* Check the request headers (or the headers listed in the
* Access-Control-Request-Headers of a pre-flight request) against the
* configured allowed headers.
* @param requestHeaders the headers to check.
* @return the list of allowed headers to list in the response of a pre-flight
* request, or {@code null} if a requestHeader is not allowed.
*/
public List<String> checkHeaders(List<String> requestHeaders) {
if (requestHeaders == null) {
return null;
}
if (requestHeaders.isEmpty()) {
return Collections.emptyList();
}
List<String> allowedHeaders = this.allowedHeaders == null ?
new ArrayList<String>() : this.allowedHeaders;
boolean allowAnyHeader = allowedHeaders.contains("*");
List<String> result = new ArrayList<String>();
for (String requestHeader : requestHeaders) {
requestHeader = requestHeader.trim();
for (String allowedHeader : allowedHeaders) {
if (allowAnyHeader || requestHeader.equalsIgnoreCase(allowedHeader)) {
result.add(requestHeader);
break;
}
}
}
return result.isEmpty() ? null : result;
}
}

View File

@@ -19,7 +19,6 @@ package org.springframework.web.cors;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
@@ -36,12 +35,14 @@ import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* Default implementation of {@link CorsProcessor}, as defined by the
* <a href="http://www.w3.org/TR/cors/">CORS W3C recommandation</a>.
*
* @author Sebastien Deleuze
* @author Rossen Stoyanhcev
* @since 4.2
*/
public class DefaultCorsProcessor implements CorsProcessor {
@@ -56,23 +57,19 @@ public class DefaultCorsProcessor implements CorsProcessor {
public boolean processPreFlightRequest(CorsConfiguration config, HttpServletRequest request,
HttpServletResponse response) throws IOException {
ServerHttpRequest serverRequest = new ServletServerHttpRequest(request);
Assert.isTrue(CorsUtils.isPreFlightRequest(request));
ServerHttpResponse serverResponse = new ServletServerHttpResponse(response);
boolean isPreFlight = CorsUtils.isPreFlightRequest(request);
Assert.isTrue(isPreFlight);
if (skip(serverResponse)) {
if (responseHasCors(serverResponse)) {
return true;
}
if (check(serverRequest, serverResponse, config, isPreFlight)) {
setAllowOrigin(serverRequest, serverResponse, config.getAllowedOrigins(), config.isAllowCredentials());
setAllowCredentials(serverResponse, config.isAllowCredentials());
setAllowMethods(serverRequest, serverResponse, config.getAllowedMethods());
setAllowHeadersHeader(serverRequest, serverResponse, config.getAllowedHeaders());
setMaxAgeHeader(serverResponse, config.getMaxAge());
serverResponse.close();
ServerHttpRequest serverRequest = new ServletServerHttpRequest(request);
if (handleInternal(serverRequest, serverResponse, config, true)) {
serverResponse.flush();
return true;
}
return false;
}
@@ -80,185 +77,124 @@ public class DefaultCorsProcessor implements CorsProcessor {
public boolean processActualRequest(CorsConfiguration config, HttpServletRequest request,
HttpServletResponse response) throws IOException {
ServerHttpRequest serverRequest = new ServletServerHttpRequest(request);
ServerHttpResponse serverResponse = new ServletServerHttpResponse(response);
boolean isPreFlight = CorsUtils.isPreFlightRequest(request);
Assert.isTrue(CorsUtils.isCorsRequest(request) && !isPreFlight);
if (skip(serverResponse)) {
Assert.isTrue(CorsUtils.isCorsRequest(request) && !CorsUtils.isPreFlightRequest(request));
ServletServerHttpResponse serverResponse = new ServletServerHttpResponse(response);
if (responseHasCors(serverResponse)) {
return true;
}
if (check(serverRequest, serverResponse, config, isPreFlight)) {
setAllowOrigin(serverRequest, serverResponse, config.getAllowedOrigins(), config.isAllowCredentials());
setAllowCredentials(serverResponse, config.isAllowCredentials());
setExposeHeadersHeader(serverResponse, config.getExposedHeaders());
serverResponse.close();
ServletServerHttpRequest serverRequest = new ServletServerHttpRequest(request);
if (handleInternal(serverRequest, serverResponse, config, false)) {
serverResponse.flush();
return true;
}
return false;
}
private boolean skip(ServerHttpResponse response) {
if (hasAllowOriginHeader(response)) {
logger.debug("Skip adding CORS headers, response already contains \"Access-Control-Allow-Origin\"");
return true;
}
return false;
}
private boolean check(ServerHttpRequest request, ServerHttpResponse response,
CorsConfiguration config, boolean isPreFlight) throws IOException {
if (!checkOrigin(request, config.getAllowedOrigins()) ||
!checkMethod(request, config.getAllowedMethods(), isPreFlight) ||
!checkHeaders(request, config.getAllowedHeaders(), isPreFlight)) {
response.setStatusCode(HttpStatus.FORBIDDEN);
response.getBody().write("Invalid CORS request".getBytes(UTF8_CHARSET));
return false;
}
return true;
}
private boolean hasAllowOriginHeader(ServerHttpResponse response) {
boolean hasCorsResponseHeaders = false;
private boolean responseHasCors(ServerHttpResponse response) {
boolean hasAllowOrigin = false;
try {
// Perhaps a CORS Filter has already added this?
hasCorsResponseHeaders = response.getHeaders().getAccessControlAllowOrigin() != null;
hasAllowOrigin = (response.getHeaders().getAccessControlAllowOrigin() != null);
}
catch (NullPointerException npe) {
// See SPR-11919 and https://issues.jboss.org/browse/WFLY-3474
// SPR-11919 and https://issues.jboss.org/browse/WFLY-3474
}
return hasCorsResponseHeaders;
if (hasAllowOrigin) {
logger.debug("Skip adding CORS headers, response already contains \"Access-Control-Allow-Origin\"");
}
return hasAllowOrigin;
}
private boolean checkOrigin(ServerHttpRequest request, List<String> allowedOrigins) {
String originHeader = request.getHeaders().getOrigin();
if (originHeader == null || allowedOrigins == null) {
protected boolean handleInternal(ServerHttpRequest request, ServerHttpResponse response,
CorsConfiguration config, boolean isPreFlight) throws IOException {
String requestOrigin = request.getHeaders().getOrigin();
String allowOrigin = checkOrigin(config, requestOrigin);
HttpMethod requestMethod = getMethodToUse(request, isPreFlight);
List<HttpMethod> allowMethods = checkMethods(config, requestMethod);
List<String> requestHeaders = getHeadersToUse(request, isPreFlight);
List<String> allowHeaders = checkHeaders(config, requestHeaders);
if (allowOrigin == null || allowMethods == null || (isPreFlight && allowHeaders == null)) {
handleInvalidCorsRequest(response);
return false;
}
if (allowedOrigins.contains("*")) {
return true;
}
for (String allowedOrigin : allowedOrigins) {
if (originHeader.equalsIgnoreCase(allowedOrigin)) {
return true;
}
}
return false;
}
private boolean checkMethod(ServerHttpRequest request, List<String> allowedMethods, boolean isPreFlight) {
HttpMethod requestMethod = isPreFlight ?
request.getHeaders().getAccessControlRequestMethod() :
request.getMethod();
if (allowedMethods == null) {
allowedMethods = Arrays.asList(HttpMethod.GET.name());
}
if (allowedMethods.contains("*")) {
return true;
}
for (String allowedMethod : allowedMethods) {
if (allowedMethod.equalsIgnoreCase(requestMethod.name())) {
return true;
}
}
return false;
}
HttpHeaders responseHeaders = response.getHeaders();
responseHeaders.setAccessControlAllowOrigin(allowOrigin);
responseHeaders.add(HttpHeaders.VARY, HttpHeaders.ORIGIN);
private boolean checkHeaders(ServerHttpRequest request, List<String> allowedHeaders, boolean isPreFlight) {
List<String> requestHeaders = isPreFlight ? request.getHeaders().getAccessControlRequestHeaders() :
new ArrayList<String>(request.getHeaders().keySet());
if ((allowedHeaders != null) && allowedHeaders.contains("*")) {
return true;
if (isPreFlight) {
responseHeaders.setAccessControlAllowMethods(allowMethods);
}
for (String requestHeader : requestHeaders) {
if (!HttpHeaders.ORIGIN.equals(requestHeader)) {
requestHeader = requestHeader.trim();
boolean found = false;
if (allowedHeaders != null) {
for (String header : allowedHeaders) {
if (requestHeader.equalsIgnoreCase(header)) {
found = true;
break;
}
}
}
if (!found) {
return false;
}
}
if (isPreFlight && !allowHeaders.isEmpty()) {
responseHeaders.setAccessControlAllowHeaders(allowHeaders);
}
if (!CollectionUtils.isEmpty(config.getExposedHeaders())) {
responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders());
}
if (Boolean.TRUE.equals(config.getAllowCredentials())) {
responseHeaders.setAccessControlAllowCredentials(true);
}
if (isPreFlight && config.getMaxAge() != null) {
responseHeaders.setAccessControlMaxAge(config.getMaxAge());
}
return true;
}
private void setAllowOrigin(ServerHttpRequest request, ServerHttpResponse response,
List<String> allowedOrigins, Boolean allowCredentials) {
String origin = request.getHeaders().getOrigin();
if (allowedOrigins.contains("*") && (allowCredentials == null || !allowCredentials)) {
response.getHeaders().setAccessControlAllowOrigin("*");
return;
}
response.getHeaders().setAccessControlAllowOrigin(origin);
response.getHeaders().add(HttpHeaders.VARY, HttpHeaders.ORIGIN);
/**
* Check the origin and determine the origin for the response. The default
* implementation simply delegates to
* {@link org.springframework.web.cors.CorsConfiguration#checkOrigin(String)}
*/
protected String checkOrigin(CorsConfiguration config, String requestOrigin) {
return config.checkOrigin(requestOrigin);
}
private void setAllowMethods(ServerHttpRequest request, ServerHttpResponse response,
List<String> allowedMethods) {
if (allowedMethods == null) {
allowedMethods = Arrays.asList(HttpMethod.GET.name());
}
if (allowedMethods.contains("*")) {
HttpMethod method = request.getHeaders().getAccessControlRequestMethod();
response.getHeaders().setAccessControlAllowMethods(Arrays.asList(method));
}
else {
List<HttpMethod> methods = new ArrayList<HttpMethod>();
for (String method : allowedMethods) {
methods.add(HttpMethod.valueOf(method));
}
response.getHeaders().setAccessControlAllowMethods(methods);
}
/**
* 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#checkOrigin(String)}
*/
protected List<HttpMethod> checkMethods(CorsConfiguration config, HttpMethod requestMethod) {
return config.checkHttpMethod(requestMethod);
}
private void setAllowHeadersHeader(ServerHttpRequest request, ServerHttpResponse response,
List<String> allowedHeaders) {
if ((allowedHeaders != null) && !allowedHeaders.isEmpty()) {
List<String> requestHeaders = request.getHeaders().getAccessControlRequestHeaders();
boolean matchAll = allowedHeaders.contains("*");
List<String> matchingHeaders = new ArrayList<String>();
for (String requestHeader : requestHeaders) {
for (String header : allowedHeaders) {
requestHeader = requestHeader.trim();
if (matchAll || requestHeader.equalsIgnoreCase(header)) {
matchingHeaders.add(requestHeader);
break;
}
}
}
if (!matchingHeaders.isEmpty()) {
response.getHeaders().setAccessControlAllowHeaders(matchingHeaders);
}
}
private HttpMethod getMethodToUse(ServerHttpRequest request, boolean isPreFlight) {
return (isPreFlight ? request.getHeaders().getAccessControlRequestMethod() : request.getMethod());
}
private void setExposeHeadersHeader(ServerHttpResponse response, List<String> exposedHeaders) {
if ((exposedHeaders != null) && !exposedHeaders.isEmpty()) {
response.getHeaders().setAccessControlExposeHeaders(exposedHeaders);
}
/**
* 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#checkOrigin(String)}
*/
protected List<String> checkHeaders(CorsConfiguration config, List<String> requestHeaders) {
return config.checkHeaders(requestHeaders);
}
private void setAllowCredentials(ServerHttpResponse response, Boolean allowCredentials) {
if ((allowCredentials != null) && allowCredentials) {
response.getHeaders().setAccessControlAllowCredentials(allowCredentials);
}
private List<String> getHeadersToUse(ServerHttpRequest request, boolean isPreFlight) {
HttpHeaders headers = request.getHeaders();
return (isPreFlight ? headers.getAccessControlRequestHeaders() : new ArrayList<String>(headers.keySet()));
}
private void setMaxAgeHeader(ServerHttpResponse response, Long maxAge) {
if (maxAge != null) {
response.getHeaders().setAccessControlMaxAge(maxAge);
}
/**
* 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 handleInvalidCorsRequest(ServerHttpResponse response) throws IOException {
response.setStatusCode(HttpStatus.FORBIDDEN);
response.getBody().write("Invalid CORS request".getBytes(UTF8_CHARSET));
}
}