Add CORS support for Private Network Access
This commit adds CORS support for Private Network Access by adding an Access-Control-Allow-Private-Network response header when the preflight request is sent with an Access-Control-Request-Private-Network header and that Private Network Access has been enabled in the CORS configuration. See https://developer.chrome.com/blog/private-network-access-preflight/ for more details. Closes gh-28546
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -50,6 +50,8 @@ class CorsConfigurationTests {
|
||||
assertThat(config.getExposedHeaders()).isNull();
|
||||
config.setAllowCredentials(null);
|
||||
assertThat(config.getAllowCredentials()).isNull();
|
||||
config.setAllowPrivateNetwork(null);
|
||||
assertThat(config.getAllowPrivateNetwork()).isNull();
|
||||
config.setMaxAge((Long) null);
|
||||
assertThat(config.getMaxAge()).isNull();
|
||||
}
|
||||
@@ -63,6 +65,7 @@ class CorsConfigurationTests {
|
||||
config.addAllowedMethod("*");
|
||||
config.addExposedHeader("*");
|
||||
config.setAllowCredentials(true);
|
||||
config.setAllowPrivateNetwork(true);
|
||||
config.setMaxAge(123L);
|
||||
|
||||
assertThat(config.getAllowedOrigins()).containsExactly("*");
|
||||
@@ -71,6 +74,7 @@ class CorsConfigurationTests {
|
||||
assertThat(config.getAllowedMethods()).containsExactly("*");
|
||||
assertThat(config.getExposedHeaders()).containsExactly("*");
|
||||
assertThat(config.getAllowCredentials()).isTrue();
|
||||
assertThat(config.getAllowPrivateNetwork()).isTrue();
|
||||
assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(123));
|
||||
}
|
||||
|
||||
@@ -93,6 +97,7 @@ class CorsConfigurationTests {
|
||||
config.addAllowedMethod(HttpMethod.GET.name());
|
||||
config.setMaxAge(123L);
|
||||
config.setAllowCredentials(true);
|
||||
config.setAllowPrivateNetwork(true);
|
||||
|
||||
CorsConfiguration other = new CorsConfiguration();
|
||||
config = config.combine(other);
|
||||
@@ -105,6 +110,7 @@ class CorsConfigurationTests {
|
||||
assertThat(config.getAllowedMethods()).containsExactly(HttpMethod.GET.name());
|
||||
assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(123));
|
||||
assertThat(config.getAllowCredentials()).isTrue();
|
||||
assertThat(config.getAllowPrivateNetwork()).isTrue();
|
||||
}
|
||||
|
||||
@Test // SPR-15772
|
||||
@@ -258,6 +264,7 @@ class CorsConfigurationTests {
|
||||
config.addAllowedMethod(HttpMethod.GET.name());
|
||||
config.setMaxAge(123L);
|
||||
config.setAllowCredentials(true);
|
||||
config.setAllowPrivateNetwork(true);
|
||||
|
||||
CorsConfiguration other = new CorsConfiguration();
|
||||
other.addAllowedOrigin("https://domain2.com");
|
||||
@@ -267,6 +274,7 @@ class CorsConfigurationTests {
|
||||
other.addAllowedMethod(HttpMethod.PUT.name());
|
||||
other.setMaxAge(456L);
|
||||
other.setAllowCredentials(false);
|
||||
other.setAllowPrivateNetwork(false);
|
||||
|
||||
config = config.combine(other);
|
||||
assertThat(config).isNotNull();
|
||||
@@ -277,6 +285,7 @@ class CorsConfigurationTests {
|
||||
assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(456));
|
||||
assertThat(config).isNotNull();
|
||||
assertThat(config.getAllowCredentials()).isFalse();
|
||||
assertThat(config.getAllowPrivateNetwork()).isFalse();
|
||||
assertThat(config.getAllowedOriginPatterns()).containsExactly("http://*.domain1.com", "http://*.domain2.com");
|
||||
}
|
||||
|
||||
|
||||
@@ -351,6 +351,32 @@ public class DefaultCorsProcessorTests {
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preflightRequestPrivateNetworkWithWildcardOrigin() throws Exception {
|
||||
this.request.setMethod(HttpMethod.OPTIONS.name());
|
||||
this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com");
|
||||
this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
|
||||
this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, "Header1");
|
||||
this.request.addHeader(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true");
|
||||
this.conf.setAllowedOrigins(Arrays.asList("https://domain1.com", "*", "http://domain3.example"));
|
||||
this.conf.addAllowedHeader("Header1");
|
||||
this.conf.setAllowPrivateNetwork(true);
|
||||
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
this.processor.processRequest(this.conf, this.request, this.response));
|
||||
|
||||
this.conf.setAllowedOrigins(null);
|
||||
this.conf.addAllowedOriginPattern("*");
|
||||
|
||||
this.processor.processRequest(this.conf, this.request, this.response);
|
||||
assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
|
||||
assertThat(this.response.containsHeader(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isTrue();
|
||||
assertThat(this.response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isEqualTo("https://domain2.com");
|
||||
assertThat(this.response.getHeaders(HttpHeaders.VARY)).contains(HttpHeaders.ORIGIN,
|
||||
HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preflightRequestAllowedHeaders() throws Exception {
|
||||
this.request.setMethod(HttpMethod.OPTIONS.name());
|
||||
@@ -434,4 +460,49 @@ public class DefaultCorsProcessorTests {
|
||||
HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preflightRequestWithoutAccessControlRequestPrivateNetwork() throws Exception {
|
||||
this.request.setMethod(HttpMethod.OPTIONS.name());
|
||||
this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com");
|
||||
this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
|
||||
this.conf.addAllowedHeader("*");
|
||||
this.conf.addAllowedOrigin("https://domain2.com");
|
||||
|
||||
this.processor.processRequest(this.conf, this.request, this.response);
|
||||
assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
|
||||
assertThat(this.response.containsHeader(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isFalse();
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preflightRequestWithAccessControlRequestPrivateNetworkNotAllowed() throws Exception {
|
||||
this.request.setMethod(HttpMethod.OPTIONS.name());
|
||||
this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com");
|
||||
this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
|
||||
this.request.addHeader(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true");
|
||||
this.conf.addAllowedHeader("*");
|
||||
this.conf.addAllowedOrigin("https://domain2.com");
|
||||
|
||||
this.processor.processRequest(this.conf, this.request, this.response);
|
||||
assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
|
||||
assertThat(this.response.containsHeader(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isFalse();
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preflightRequestWithAccessControlRequestPrivateNetworkAllowed() throws Exception {
|
||||
this.request.setMethod(HttpMethod.OPTIONS.name());
|
||||
this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com");
|
||||
this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
|
||||
this.request.addHeader(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true");
|
||||
this.conf.addAllowedHeader("*");
|
||||
this.conf.addAllowedOrigin("https://domain2.com");
|
||||
this.conf.setAllowPrivateNetwork(true);
|
||||
|
||||
this.processor.processRequest(this.conf, this.request, this.response);
|
||||
assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
|
||||
assertThat(this.response.containsHeader(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isTrue();
|
||||
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -364,6 +364,33 @@ public class DefaultCorsProcessorTests {
|
||||
assertThat(response.getStatusCode()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preflightRequestPrivateNetworkWithWildcardOrigin() {
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest()
|
||||
.header(ACCESS_CONTROL_REQUEST_METHOD, "GET")
|
||||
.header(ACCESS_CONTROL_REQUEST_HEADERS, "Header1")
|
||||
.header(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true"));
|
||||
|
||||
this.conf.addAllowedOrigin("https://domain1.com");
|
||||
this.conf.addAllowedOrigin("*");
|
||||
this.conf.addAllowedOrigin("http://domain3.example");
|
||||
this.conf.addAllowedHeader("Header1");
|
||||
this.conf.setAllowPrivateNetwork(true);
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.processor.process(this.conf, exchange));
|
||||
|
||||
this.conf.setAllowedOrigins(null);
|
||||
this.conf.addAllowedOriginPattern("*");
|
||||
this.processor.process(this.conf, exchange);
|
||||
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
|
||||
assertThat(response.getHeaders().containsKey(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isTrue();
|
||||
assertThat(response.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN)).isEqualTo("https://domain2.com");
|
||||
assertThat(response.getHeaders().get(VARY)).contains(ORIGIN,
|
||||
ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS);
|
||||
assertThat(response.getStatusCode()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preflightRequestAllowedHeaders() {
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest()
|
||||
@@ -460,6 +487,57 @@ public class DefaultCorsProcessorTests {
|
||||
ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preflightRequestWithoutAccessControlRequestPrivateNetwork() {
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest()
|
||||
.header(ACCESS_CONTROL_REQUEST_METHOD, "GET"));
|
||||
|
||||
this.conf.addAllowedHeader("*");
|
||||
this.conf.addAllowedOrigin("https://domain2.com");
|
||||
|
||||
this.processor.process(this.conf, exchange);
|
||||
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
|
||||
assertThat(response.getHeaders().containsKey(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isFalse();
|
||||
assertThat(response.getStatusCode()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preflightRequestWithAccessControlRequestPrivateNetworkNotAllowed() {
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest()
|
||||
.header(ACCESS_CONTROL_REQUEST_METHOD, "GET")
|
||||
.header(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true"));
|
||||
|
||||
this.conf.addAllowedHeader("*");
|
||||
this.conf.addAllowedOrigin("https://domain2.com");
|
||||
|
||||
this.processor.process(this.conf, exchange);
|
||||
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
|
||||
assertThat(response.getHeaders().containsKey(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isFalse();
|
||||
assertThat(response.getStatusCode()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preflightRequestWithAccessControlRequestPrivateNetworkAllowed() {
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest()
|
||||
.header(ACCESS_CONTROL_REQUEST_METHOD, "GET")
|
||||
.header(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true"));
|
||||
|
||||
this.conf.addAllowedHeader("*");
|
||||
this.conf.addAllowedOrigin("https://domain2.com");
|
||||
this.conf.setAllowPrivateNetwork(true);
|
||||
|
||||
this.processor.process(this.conf, exchange);
|
||||
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue();
|
||||
assertThat(response.getHeaders().containsKey(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isTrue();
|
||||
assertThat(response.getStatusCode()).isNull();
|
||||
}
|
||||
|
||||
|
||||
private ServerWebExchange actualRequest() {
|
||||
return MockServerWebExchange.from(corsRequest(HttpMethod.GET));
|
||||
|
||||
Reference in New Issue
Block a user