diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 48505644..cd2895fb 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -982,6 +982,26 @@ The Gateway can be configured to create routes based on services registered with To enable this, set `spring.cloud.gateway.discovery.locator.enabled=true` and make sure a `DiscoveryClient` implementation is on the classpath and enabled (such as Netflix Eureka, Consul or Zookeeper). +== CORS Configuration + +The gateway can be configured to control CORS behavior. The "global" CORS configuration is a map of URL patterns to https://docs.spring.io/spring/docs/5.0.x/javadoc-api/org/springframework/web/cors/CorsConfiguration.html[Spring Framework `CorsConfiguration`]. + +.application.yml +[source,yaml] +---- +spring: + cloud: + gateway: + globalcors: + corsConfigurations: + '[/**]': + allowedOrigins: "docs.spring.io" + allowedMethods: + - GET +---- + +In the example above, CORS requests will be allowed from requests that originate from docs.spring.io for all GET requested paths. + == Actuator API TODO: document the `/gateway` actuator endpoint diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index 0f3d9b8b..cc0e2f68 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -285,9 +285,14 @@ public class GatewayAutoConfiguration { } @Bean - public RoutePredicateHandlerMapping routePredicateHandlerMapping(FilteringWebHandler webHandler, - RouteLocator routeLocator) { - return new RoutePredicateHandlerMapping(webHandler, routeLocator); + public GlobalCorsProperties globalCorsProperties() { + return new GlobalCorsProperties(); + } + + @Bean + public RoutePredicateHandlerMapping routePredicateHandlerMapping(FilteringWebHandler webHandler, RouteLocator routeLocator, + GlobalCorsProperties globalCorsProperties) { + return new RoutePredicateHandlerMapping(webHandler, routeLocator, globalCorsProperties); } // ConfigurationProperty beans diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GlobalCorsProperties.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GlobalCorsProperties.java new file mode 100644 index 00000000..8675ac41 --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GlobalCorsProperties.java @@ -0,0 +1,40 @@ +/* + * Copyright 2013-2018 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 + * + * http://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.cloud.gateway.config; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping; +import org.springframework.web.cors.CorsConfiguration; + +/** + * Configuration properties for global configuration of cors. See + * {@link RoutePredicateHandlerMapping} + */ +@ConfigurationProperties("spring.cloud.gateway.globalcors") +public class GlobalCorsProperties { + + private final Map corsConfigurations = new LinkedHashMap<>(); + + public Map getCorsConfigurations() { + return corsConfigurations; + } + +} diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilter.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilter.java index f5671412..d3941636 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilter.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilter.java @@ -25,7 +25,6 @@ import java.util.List; import java.util.Map; import org.jetbrains.annotations.Nullable; - import org.springframework.core.Ordered; import org.springframework.http.HttpHeaders; import org.springframework.http.server.reactive.ServerHttpRequest; @@ -58,7 +57,7 @@ public class ForwardedHeadersFilter implements HttpHeadersFilter, Ordered { List forwardeds = parse(original.get(FORWARDED_HEADER)); for (Forwarded f : forwardeds) { - updated.add(FORWARDED_HEADER, f.toString()); + updated.add(FORWARDED_HEADER, f.toHeaderValue()); } //TODO: add new forwarded diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMapping.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMapping.java index d8212eea..84e862b8 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMapping.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMapping.java @@ -19,8 +19,8 @@ package org.springframework.cloud.gateway.handler; import java.util.function.Function; +import org.springframework.cloud.gateway.config.GlobalCorsProperties; import reactor.core.publisher.Mono; - import org.springframework.cloud.gateway.route.Route; import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.web.cors.CorsConfiguration; @@ -39,11 +39,12 @@ public class RoutePredicateHandlerMapping extends AbstractHandlerMapping { private final FilteringWebHandler webHandler; private final RouteLocator routeLocator; - public RoutePredicateHandlerMapping(FilteringWebHandler webHandler, RouteLocator routeLocator) { + public RoutePredicateHandlerMapping(FilteringWebHandler webHandler, RouteLocator routeLocator, GlobalCorsProperties globalCorsProperties) { this.webHandler = webHandler; this.routeLocator = routeLocator; - setOrder(1); + setOrder(1); + setCorsConfigurations(globalCorsProperties.getCorsConfigurations()); } @Override @@ -70,10 +71,10 @@ public class RoutePredicateHandlerMapping extends AbstractHandlerMapping { @Override protected CorsConfiguration getCorsConfiguration(Object handler, ServerWebExchange exchange) { - //TODO: support cors configuration via global properties and - // properties on a route see gh-229 + // TODO: support cors configuration via properties on a route see gh-229 // see RequestMappingHandlerMapping.initCorsConfiguration() - // also see https://github.com/spring-projects/spring-framework/blob/master/spring-web/src/test/java/org/springframework/web/cors/reactive/CorsWebFilterTests.java + // also see https://github.com/spring-projects/spring-framework/blob/master/spring-web/src/test/java/org/springframework/web/cors/reactive/CorsWebFilterTests.java + return super.getCorsConfiguration(handler, exchange); } diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/cors/CorsTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/cors/CorsTests.java new file mode 100644 index 00000000..3e8eeb76 --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/cors/CorsTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2013-2017 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 + * + * http://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.cloud.gateway.cors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +import java.util.Arrays; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.gateway.test.BaseWebClientTests; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.reactive.function.client.ClientResponse; + +import reactor.core.publisher.Mono; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = RANDOM_PORT) +@DirtiesContext +public class CorsTests extends BaseWebClientTests { + + @Test + public void testPreFlightCorsRequest() { + ClientResponse clientResponse = webClient.options().uri("/abc/123/function") + .header("Origin", "domain.com") + .header("Access-Control-Request-Method", "GET").exchange().block(); + HttpHeaders asHttpHeaders = clientResponse.headers().asHttpHeaders(); + Mono bodyToMono = clientResponse.bodyToMono(String.class); + // pre-flight request shouldn't return the response body + assertNull(bodyToMono.block()); + assertEquals( + "Missing header value in response: " + + HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, + "*", asHttpHeaders.getAccessControlAllowOrigin()); + assertEquals( + "Missing header value in response: " + + HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, + Arrays.asList(new HttpMethod[] { HttpMethod.GET, HttpMethod.HEAD }), + asHttpHeaders.getAccessControlAllowMethods()); + assertEquals("Pre Flight call failed.", HttpStatus.OK, + clientResponse.statusCode()); + } + + @Test + public void testCorsRequest() { + ClientResponse clientResponse = webClient.get().uri("/abc/123/function") + .header("Origin", "domain.com").header(HttpHeaders.HOST, "www.path.org") + .exchange().block(); + HttpHeaders asHttpHeaders = clientResponse.headers().asHttpHeaders(); + Mono bodyToMono = clientResponse.bodyToMono(String.class); + assertNotNull(bodyToMono.block()); + assertEquals( + "Missing header value in response: " + + HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, + "*", asHttpHeaders.getAccessControlAllowOrigin()); + assertEquals("CORS request failed.", HttpStatus.OK, + clientResponse.statusCode()); + } + + @EnableAutoConfiguration + @SpringBootConfiguration + @Import(DefaultTestConfig.class) + public static class TestConfig { + + } + +} diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilterTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilterTests.java index 21e343fa..dc1df9e3 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilterTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilterTests.java @@ -17,23 +17,18 @@ package org.springframework.cloud.gateway.filter.headers; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - import org.junit.Test; - import org.springframework.cloud.gateway.filter.headers.ForwardedHeadersFilter.Forwarded; import org.springframework.http.HttpHeaders; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.web.server.MockServerWebExchange; import org.springframework.util.StringUtils; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.*; + import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.cloud.gateway.filter.headers.ForwardedHeadersFilter.FORWARDED_HEADER; @@ -67,6 +62,36 @@ public class ForwardedHeadersFilterTests { .containsEntry("for", "\"10.0.0.1:80\""); } + @Test + public void forwardedHeaderExists() throws UnknownHostException { + MockServerHttpRequest request = MockServerHttpRequest + .get("http://localhost/get") + .remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80)) + .header(FORWARDED_HEADER, "for=12.34.56.78;host=example.com;proto=https; for=23.45.67.89") + .build(); + + ForwardedHeadersFilter filter = new ForwardedHeadersFilter(); + + HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request)); + + assertThat(headers.get(FORWARDED_HEADER)).hasSize(2); + + + List forwardeds = ForwardedHeadersFilter.parse(headers.get(FORWARDED_HEADER)); + + assertThat(forwardeds).hasSize(2); + Forwarded addedForwardedHeader = forwardeds.get(0); + Forwarded existingForwardedHeader = forwardeds.get(1); + + assertThat(existingForwardedHeader.getValues()) + .containsEntry("proto", "http") + .containsEntry("for", "\"10.0.0.1:80\""); + + assertThat(addedForwardedHeader.getValues()) + .containsEntry("proto", "https") + .containsEntry("for", "23.45.67.89"); + } + @Test public void noHostHeader() throws UnknownHostException { MockServerHttpRequest request = MockServerHttpRequest @@ -92,7 +117,7 @@ public class ForwardedHeadersFilterTests { @Test public void forwardedParsedCorrectly() { - String[] valid = new String[] { + String[] valid = new String[]{ "for=\"_gazonk\"", "for=192.0.2.60;proto=http;by=203.0.113.43", "for=192.0.2.43, for=198.51.100.17", @@ -103,15 +128,15 @@ public class ForwardedHeadersFilterTests { @SuppressWarnings("unchecked") List>> expectedFor = new ArrayList>>() {{ - add(list(map("for", "\"_gazonk\""))); - add(list(map("for", "192.0.2.60", "proto", "http", "by", "203.0.113.43"))); - add(list(map("for", "192.0.2.43"), map("for", "198.51.100.17"))); - add(list(map("for", "12.34.56.78", "host", "example.com", "proto", "https"), - map("for", "23.45.67.89"))); - add(list(map("for", "12.34.56.78"), - map("for", "23.45.67.89", "secret", "egah2CGj55fSJFs"), - map("for", "10.1.2.3"))); - add(list(map("for", "\"[2001:db8:cafe::17]:4711\""))); + add(list(map("for", "\"_gazonk\""))); + add(list(map("for", "192.0.2.60", "proto", "http", "by", "203.0.113.43"))); + add(list(map("for", "192.0.2.43"), map("for", "198.51.100.17"))); + add(list(map("for", "12.34.56.78", "host", "example.com", "proto", "https"), + map("for", "23.45.67.89"))); + add(list(map("for", "12.34.56.78"), + map("for", "23.45.67.89", "secret", "egah2CGj55fSJFs"), + map("for", "10.1.2.3"))); + add(list(map("for", "\"[2001:db8:cafe::17]:4711\""))); }}; for (int i = 0; i < valid.length; i++) { diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMappingTest.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMappingTest.java index 49773462..1b2613f3 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMappingTest.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMappingTest.java @@ -4,6 +4,7 @@ import org.junit.Rule; import org.junit.Test; import org.mockito.Mockito; import org.springframework.boot.test.rule.OutputCapture; +import org.springframework.cloud.gateway.config.GlobalCorsProperties; import org.springframework.cloud.gateway.route.Route; import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.web.server.ServerWebExchange; @@ -41,7 +42,7 @@ public class RoutePredicateHandlerMappingTest { RouteLocator routeLocator = () -> Flux.just(routeFalse, routeFail, routeTrue).hide(); RoutePredicateHandlerMapping mapping = - new RoutePredicateHandlerMapping(null, routeLocator); + new RoutePredicateHandlerMapping(null, routeLocator, new GlobalCorsProperties()); final Mono routeMono = mapping.lookupRoute(Mockito.mock(ServerWebExchange.class)); @@ -79,7 +80,7 @@ public class RoutePredicateHandlerMappingTest { RouteLocator routeLocator = () -> Flux.just(routeFalse, routeError, routeFail, routeTrue).hide(); RoutePredicateHandlerMapping mapping = - new RoutePredicateHandlerMapping(null, routeLocator); + new RoutePredicateHandlerMapping(null, routeLocator, new GlobalCorsProperties()); final Mono routeMono = mapping.lookupRoute(Mockito.mock(ServerWebExchange.class)); diff --git a/spring-cloud-gateway-core/src/test/resources/application.yml b/spring-cloud-gateway-core/src/test/resources/application.yml index 2db2461e..6ae8ed00 100644 --- a/spring-cloud-gateway-core/src/test/resources/application.yml +++ b/spring-cloud-gateway-core/src/test/resources/application.yml @@ -7,6 +7,13 @@ test: spring: cloud: gateway: + globalcors: + corsConfigurations: + '[/**]': + maxAge: 10 + allowedOrigins: "*" + allowedMethods: + - GET default-filters: - AddResponseHeader=X-Response-Default-Foo, Default-Bar - PrefixPath=/httpbin