diff --git a/docs/src/main/asciidoc/spring-cloud-netflix.adoc b/docs/src/main/asciidoc/spring-cloud-netflix.adoc index 16e0c1226..ce57e3c76 100644 --- a/docs/src/main/asciidoc/spring-cloud-netflix.adoc +++ b/docs/src/main/asciidoc/spring-cloud-netflix.adoc @@ -1661,6 +1661,26 @@ public class ZuulConfig { CAUTION: Use this filter carefully. The filter acts on the `Location` header of ALL `3XX` response codes, which may not be appropriate in all scenarios, such as when redirecting the user to an external URL. +=== Enabling Cross Origin Requests + +By default Zuul routes all Cross Origin requests (CORS) to the services. If you want instead Zuul to handle these requests it can be done by providing custom `WebMvcConfigurer` bean: +[source,java] +---- +@Bean +public WebMvcConfigurer corsConfigurer() { + return new WebMvcConfigurer() { + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/path-1/**") + .allowedOrigins("http://allowed-origin.com") + .allowedMethods("GET", "POST"); + } + }; +} +---- +In the example above, we allow `GET` and `POST` methods from `http://allowed-origin.com` to send cross-origin requests to the endpoints starting with `path-1`. +You can apply CORS configuration to a specific path pattern or globally for the whole application, using `/**` mapping. +You can customize properties: `allowedOrigins`,`allowedMethods`,`allowedHeaders`,`exposedHeaders`,`allowCredentials` and `maxAge` via this configuration. + === Metrics Zuul will provide metrics under the Actuator metrics endpoint for any failures that might occur when routing requests. @@ -2021,6 +2041,8 @@ info: url: https://github.com/spring-cloud-samples ---- +To enable the health check request to accept all certificates when using HTTPs set `sidecar.accept-all-ssl-certificates` to `true. + [[retrying-failed-requests]] == Retrying Failed Requests @@ -2033,15 +2055,15 @@ When Spring Retry is present, load-balanced `RestTemplates`, Feign, and Zuul aut === BackOff Policies By default, no backoff policy is used when retrying requests. -If you would like to configure a backoff policy, you need to create a bean of type `LoadBalancedBackOffPolicyFactory`, which is used to create a `BackOffPolicy` for a given service, as shown in the following example: +If you would like to configure a backoff policy, you need to create a bean of type `LoadBalancedRetryFactory` and override the `createBackOffPolicy` method for a given service, as shown in the following example: [source,java,indent=0] ---- @Configuration public class MyConfiguration { @Bean - LoadBalancedBackOffPolicyFactory backOffPolicyFactory() { - return new LoadBalancedBackOffPolicyFactory() { + LoadBalancedRetryFactory retryFactory() { + return new LoadBalancedRetryFactory() { @Override public BackOffPolicy createBackOffPolicy(String service) { return new ExponentialBackOffPolicy(); diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaClientAutoConfiguration.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaClientAutoConfiguration.java index d1efe9660..464c4b7a3 100644 --- a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaClientAutoConfiguration.java +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaClientAutoConfiguration.java @@ -23,6 +23,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.Map; +import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; @@ -53,6 +54,7 @@ import org.springframework.cloud.netflix.eureka.metadata.ManagementMetadataProvi import org.springframework.cloud.netflix.eureka.serviceregistry.EurekaAutoServiceRegistration; import org.springframework.cloud.netflix.eureka.serviceregistry.EurekaRegistration; import org.springframework.cloud.netflix.eureka.serviceregistry.EurekaServiceRegistry; +import org.springframework.cloud.util.ProxyUtils; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; @@ -285,10 +287,22 @@ public class EurekaClientAutoConfiguration { @ConditionalOnMissingBean(value = EurekaClient.class, search = SearchStrategy.CURRENT) @org.springframework.cloud.context.config.annotation.RefreshScope @Lazy - public EurekaClient eurekaClient(ApplicationInfoManager manager, EurekaClientConfig config, EurekaInstanceConfig instance) { - manager.getInfo(); // force initialization - return new CloudEurekaClient(manager, config, this.optionalArgs, + public EurekaClient eurekaClient(ApplicationInfoManager manager, EurekaClientConfig config, EurekaInstanceConfig instance, + @Autowired(required = false) HealthCheckHandler healthCheckHandler) { + //If we use the proxy of the ApplicationInfoManager we could run into a problem + //when shutdown is called on the CloudEurekaClient where the ApplicationInfoManager bean is + //requested but wont be allowed because we are shutting down. To avoid this we use the + //object directly. + ApplicationInfoManager appManager; + if(AopUtils.isAopProxy(manager)) { + appManager = ProxyUtils.getTargetObject(manager); + } else { + appManager = manager; + } + CloudEurekaClient cloudEurekaClient = new CloudEurekaClient(appManager, config, this.optionalArgs, this.context); + cloudEurekaClient.registerHealthCheck(healthCheckHandler); + return cloudEurekaClient; } @Bean diff --git a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/serviceregistry/EurekaServiceRegistry.java b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/serviceregistry/EurekaServiceRegistry.java index 47b4ddbbc..84f3f3d96 100644 --- a/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/serviceregistry/EurekaServiceRegistry.java +++ b/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/serviceregistry/EurekaServiceRegistry.java @@ -39,7 +39,7 @@ public class EurekaServiceRegistry implements ServiceRegistry status = new HashMap<>(); diff --git a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/serviceregistry/EurekaServiceRegistryTests.java b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/serviceregistry/EurekaServiceRegistryTests.java index eba2ce617..d97376442 100644 --- a/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/serviceregistry/EurekaServiceRegistryTests.java +++ b/spring-cloud-netflix-eureka-client/src/test/java/org/springframework/cloud/netflix/eureka/serviceregistry/EurekaServiceRegistryTests.java @@ -31,6 +31,7 @@ import com.netflix.appinfo.ApplicationInfoManager; import com.netflix.appinfo.InstanceInfo; import static com.netflix.appinfo.InstanceInfo.InstanceStatus.DOWN; +import static com.netflix.appinfo.InstanceInfo.InstanceStatus.OUT_OF_SERVICE; import static com.netflix.appinfo.InstanceInfo.InstanceStatus.UNKNOWN; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -70,20 +71,29 @@ public class EurekaServiceRegistryTests { config.setAppname("myapp"); config.setInstanceId("1234"); - CloudEurekaClient eurekaClient = mock(CloudEurekaClient.class); - - InstanceInfo instanceInfo = InstanceInfo.Builder.newBuilder() + InstanceInfo local = InstanceInfo.Builder.newBuilder() .setAppName("myapp") .setInstanceId("1234") .setStatus(DOWN) - .setOverriddenStatus(UNKNOWN) .build(); - when(eurekaClient.getInstanceInfo("myapp", "1234")) - .thenReturn(instanceInfo); + + InstanceInfo remote = InstanceInfo.Builder.newBuilder() + .setAppName("myapp") + .setInstanceId("1234") + .setStatus(DOWN) + .setOverriddenStatus(OUT_OF_SERVICE) + .build(); + + CloudEurekaClient eurekaClient = mock(CloudEurekaClient.class); + when(eurekaClient.getInstanceInfo(local.getAppName(), local.getId())) + .thenReturn(remote); + + ApplicationInfoManager applicationInfoManager = mock(ApplicationInfoManager.class); + when(applicationInfoManager.getInfo()).thenReturn(local); EurekaRegistration registration = EurekaRegistration.builder(config) .with(eurekaClient) - .with(mock(ApplicationInfoManager.class)) + .with(applicationInfoManager) .with(new EurekaClientConfigBean(), mock(ApplicationEventPublisher.class)) .build(); @@ -95,7 +105,7 @@ public class EurekaServiceRegistryTests { assertThat(map).hasSize(2) .containsEntry("status", DOWN.toString()) - .containsEntry("overriddenStatus", UNKNOWN.toString()); + .containsEntry("overriddenStatus", OUT_OF_SERVICE.toString()); } @@ -112,9 +122,12 @@ public class EurekaServiceRegistryTests { when(eurekaClient.getInstanceInfo("myapp", "1234")) .thenReturn(null); + ApplicationInfoManager applicationInfoManager = mock(ApplicationInfoManager.class); + when(applicationInfoManager.getInfo()).thenReturn(mock(InstanceInfo.class)); + EurekaRegistration registration = EurekaRegistration.builder(config) .with(eurekaClient) - .with(mock(ApplicationInfoManager.class)) + .with(applicationInfoManager) .with(new EurekaClientConfigBean(), mock(ApplicationEventPublisher.class)) .build(); diff --git a/spring-cloud-netflix-ribbon/src/main/java/org/springframework/cloud/netflix/ribbon/support/RibbonCommandContext.java b/spring-cloud-netflix-ribbon/src/main/java/org/springframework/cloud/netflix/ribbon/support/RibbonCommandContext.java index 05796e60f..3d548e831 100644 --- a/spring-cloud-netflix-ribbon/src/main/java/org/springframework/cloud/netflix/ribbon/support/RibbonCommandContext.java +++ b/spring-cloud-netflix-ribbon/src/main/java/org/springframework/cloud/netflix/ribbon/support/RibbonCommandContext.java @@ -140,6 +140,12 @@ public class RibbonCommandContext { if (requestEntity == null) { return null; } + //If the route is not retryable there is no point in copying the RequestEntity. This + //has memory implications in all cases but especially when uploading large files through + //Zuul + if(!retryable) { + return requestEntity; + } try { if (!(requestEntity instanceof ResettableServletInputStreamWrapper)) { diff --git a/spring-cloud-netflix-sidecar/src/main/java/org/springframework/cloud/netflix/sidecar/LocalApplicationHealthIndicator.java b/spring-cloud-netflix-sidecar/src/main/java/org/springframework/cloud/netflix/sidecar/LocalApplicationHealthIndicator.java index 19e1d1f13..6646b7871 100644 --- a/spring-cloud-netflix-sidecar/src/main/java/org/springframework/cloud/netflix/sidecar/LocalApplicationHealthIndicator.java +++ b/spring-cloud-netflix-sidecar/src/main/java/org/springframework/cloud/netflix/sidecar/LocalApplicationHealthIndicator.java @@ -16,22 +16,26 @@ package org.springframework.cloud.netflix.sidecar; -import java.net.URI; -import java.util.Map; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.health.AbstractHealthIndicator; import org.springframework.boot.actuate.health.Health; import org.springframework.web.client.RestTemplate; +import java.net.URI; +import java.util.Map; + /** * @author Spencer Gibb + * @author Fabrizio Di Napoli */ public class LocalApplicationHealthIndicator extends AbstractHealthIndicator { @Autowired private SidecarProperties properties; + @Autowired + private RestTemplate restTemplate; + @SuppressWarnings("unchecked") @Override protected void doHealthCheck(Health.Builder builder) throws Exception { @@ -40,12 +44,13 @@ public class LocalApplicationHealthIndicator extends AbstractHealthIndicator { builder.up(); return; } - Map map = new RestTemplate().getForObject(uri, Map.class); + + Map map = restTemplate.getForObject(uri, Map.class); Object status = map.get("status"); - if (status != null && status instanceof String) { + if (status instanceof String) { builder.status(status.toString()); } - else if (status != null && status instanceof Map) { + else if (status instanceof Map) { Map statusMap = (Map) status; Object code = statusMap.get("code"); if (code != null) { @@ -63,5 +68,4 @@ public class LocalApplicationHealthIndicator extends AbstractHealthIndicator { private Health.Builder getWarning(Health.Builder builder) { return builder.unknown().withDetail("warning", "no status field in response"); } - } diff --git a/spring-cloud-netflix-sidecar/src/main/java/org/springframework/cloud/netflix/sidecar/SidecarConfiguration.java b/spring-cloud-netflix-sidecar/src/main/java/org/springframework/cloud/netflix/sidecar/SidecarConfiguration.java index 096d0cc09..0c4a2f802 100644 --- a/spring-cloud-netflix-sidecar/src/main/java/org/springframework/cloud/netflix/sidecar/SidecarConfiguration.java +++ b/spring-cloud-netflix-sidecar/src/main/java/org/springframework/cloud/netflix/sidecar/SidecarConfiguration.java @@ -16,6 +16,11 @@ package org.springframework.cloud.netflix.sidecar; +import org.apache.http.client.HttpClient; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import static org.springframework.cloud.commons.util.IdUtils.getDefaultInstanceId; import org.springframework.beans.factory.annotation.Autowired; @@ -24,6 +29,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.cloud.client.actuator.HasFeatures; import org.springframework.cloud.commons.util.InetUtils; import org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean; @@ -33,10 +39,12 @@ import org.springframework.cloud.netflix.eureka.metadata.ManagementMetadataProvi import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.util.StringUtils; import com.netflix.appinfo.HealthCheckHandler; import com.netflix.discovery.EurekaClientConfig; +import org.springframework.web.client.RestTemplate; import java.util.Map; @@ -54,6 +62,7 @@ import java.util.Map; * * @author Spencer Gibb * @author Ryan Baxter + * @author Fabrizio Di Napoli * * @see EurekaInstanceConfigBeanConfiguration */ @@ -153,7 +162,27 @@ public class SidecarConfiguration { final LocalApplicationHealthIndicator healthIndicator) { return new LocalApplicationHealthCheckHandler(healthIndicator); } + } + @Bean + @ConditionalOnMissingClass("org.apache.http.client.HttpClient") + public RestTemplate restTemplate() { + return new RestTemplateBuilder().build(); + } + + @Bean + @ConditionalOnClass(HttpClient.class) + public RestTemplate sslRestTemplate(SidecarProperties properties) { + RestTemplateBuilder builder = new RestTemplateBuilder(); + if(properties.acceptAllSslCertificates()) { + CloseableHttpClient httpClient = HttpClients.custom() + .setSSLHostnameVerifier(new NoopHostnameVerifier()) + .build(); + HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(); + requestFactory.setHttpClient(httpClient); + builder = builder.requestFactory(() -> requestFactory); + } + return builder.build(); } @Bean diff --git a/spring-cloud-netflix-sidecar/src/main/java/org/springframework/cloud/netflix/sidecar/SidecarProperties.java b/spring-cloud-netflix-sidecar/src/main/java/org/springframework/cloud/netflix/sidecar/SidecarProperties.java index 4f8aa8e55..1104be691 100644 --- a/spring-cloud-netflix-sidecar/src/main/java/org/springframework/cloud/netflix/sidecar/SidecarProperties.java +++ b/spring-cloud-netflix-sidecar/src/main/java/org/springframework/cloud/netflix/sidecar/SidecarProperties.java @@ -27,6 +27,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; /** * @author Spencer Gibb * @author Gregor Zurowski + * @author Fabrizio Di Napoli */ @ConfigurationProperties("sidecar") public class SidecarProperties { @@ -43,6 +44,8 @@ public class SidecarProperties { private String ipAddress; + private boolean acceptAllSslCertificates; + public URI getHealthUri() { return healthUri; } @@ -83,6 +86,14 @@ public class SidecarProperties { this.ipAddress = ipAddress; } + public boolean acceptAllSslCertificates() { + return acceptAllSslCertificates; + } + + public void setAcceptAllSslCertificates(boolean acceptAllSslCertificates) { + this.acceptAllSslCertificates = acceptAllSslCertificates; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -92,12 +103,13 @@ public class SidecarProperties { Objects.equals(homePageUri, that.homePageUri) && port == that.port && Objects.equals(hostname, that.hostname) && - Objects.equals(ipAddress, that.ipAddress); + Objects.equals(ipAddress, that.ipAddress) && + Objects.equals(acceptAllSslCertificates, that.acceptAllSslCertificates); } @Override public int hashCode() { - return Objects.hash(healthUri, homePageUri, port, hostname, ipAddress); + return Objects.hash(healthUri, homePageUri, port, hostname, ipAddress, acceptAllSslCertificates); } @Override @@ -107,7 +119,8 @@ public class SidecarProperties { .append("homePageUri=").append(homePageUri).append(", ") .append("port=").append(port).append(", ") .append("hostname='").append(hostname).append("', ") - .append("ipAddress='").append(ipAddress).append("'}") + .append("ipAddress='").append(ipAddress).append("', ") + .append("acceptAllSslCertificates='").append(acceptAllSslCertificates).append("'}") .toString(); } diff --git a/spring-cloud-netflix-sidecar/src/test/java/org/springframework/cloud/netflix/sidecar/SidecarApplicationTests.java b/spring-cloud-netflix-sidecar/src/test/java/org/springframework/cloud/netflix/sidecar/SidecarApplicationTests.java index 2d8da11a7..5401f635f 100644 --- a/spring-cloud-netflix-sidecar/src/test/java/org/springframework/cloud/netflix/sidecar/SidecarApplicationTests.java +++ b/spring-cloud-netflix-sidecar/src/test/java/org/springframework/cloud/netflix/sidecar/SidecarApplicationTests.java @@ -16,6 +16,7 @@ package org.springframework.cloud.netflix.sidecar; +import static org.junit.Assert.assertNull; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -26,6 +27,7 @@ import org.springframework.test.context.junit4.SpringRunner; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; +import org.springframework.web.client.RestTemplate; public class SidecarApplicationTests { @@ -128,4 +130,16 @@ public class SidecarApplicationTests { assertThat(this.config.getHealthCheckUrl(), equalTo("http://mhhost2:0/foo/health")); } } + + @RunWith(SpringRunner.class) + @SpringBootTest(classes = SidecarApplication.class, webEnvironment = RANDOM_PORT, value = {"sidecar.accept-all-ssl-certificates=false"}) + public static class AcceptAllSslCertificatesContext { + @Autowired + RestTemplate restTemplate; + + @Test + public void testUseRestTemplateWhenHttpClientIsNotAvailable() { + assertNull(restTemplate.getRequestFactory()); + } + } } diff --git a/spring-cloud-netflix-sidecar/src/test/resources/application.yml b/spring-cloud-netflix-sidecar/src/test/resources/application.yml index d6dac53b5..5a78e0959 100644 --- a/spring-cloud-netflix-sidecar/src/test/resources/application.yml +++ b/spring-cloud-netflix-sidecar/src/test/resources/application.yml @@ -7,6 +7,7 @@ spring: sidecar: port: 8000 health-uri: http://localhost:8000/src/test/resources/health.json + accept-all-ssl-certificates: true eureka: instance: diff --git a/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/ZuulServerAutoConfiguration.java b/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/ZuulServerAutoConfiguration.java index aa4bb7b7d..4583a1d2e 100644 --- a/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/ZuulServerAutoConfiguration.java +++ b/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/ZuulServerAutoConfiguration.java @@ -19,6 +19,7 @@ package org.springframework.cloud.netflix.zuul; import java.util.Collection; import java.util.Map; +import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -59,6 +60,9 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import com.netflix.zuul.FilterLoader; import com.netflix.zuul.ZuulFilter; @@ -69,6 +73,8 @@ import com.netflix.zuul.monitoring.TracerFactory; import io.micrometer.core.instrument.MeterRegistry; +import static java.util.Collections.emptyList; + /** * @author Spencer Gibb * @author Dave Syer @@ -91,6 +97,11 @@ public class ZuulServerAutoConfiguration { @Autowired(required = false) private ErrorController errorController; + private Map corsConfigurations; + + @Autowired(required = false) + private List configurers = emptyList(); + @Bean public HasFeatures zuulFeature() { return HasFeatures.namedFeature("Zuul (Simple)", ZuulServerAutoConfiguration.class); @@ -119,9 +130,20 @@ public class ZuulServerAutoConfiguration { public ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes) { ZuulHandlerMapping mapping = new ZuulHandlerMapping(routes, zuulController()); mapping.setErrorController(this.errorController); + mapping.setCorsConfigurations(getCorsConfigurations()); return mapping; } + protected final Map getCorsConfigurations() { + if (this.corsConfigurations == null) { + ZuulCorsRegistry registry = new ZuulCorsRegistry(); + this.configurers + .forEach(configurer -> configurer.addCorsMappings(registry)); + this.corsConfigurations = registry.getCorsConfigurations(); + } + return this.corsConfigurations; + } + @Bean public ApplicationListener zuulRefreshRoutesListener() { return new ZuulRefreshListener(); @@ -267,4 +289,12 @@ public class ZuulServerAutoConfiguration { this.zuulHandlerMapping.setDirty(true); } } + + private static class ZuulCorsRegistry extends CorsRegistry { + + @Override + protected Map getCorsConfigurations() { + return super.getCorsConfigurations(); + } + } } diff --git a/spring-cloud-netflix-zuul/src/test/java/org/springframework/cloud/netflix/zuul/ZuulProxyApplicationTests.java b/spring-cloud-netflix-zuul/src/test/java/org/springframework/cloud/netflix/zuul/ZuulProxyApplicationTests.java index 1da92e593..c4e6db24a 100644 --- a/spring-cloud-netflix-zuul/src/test/java/org/springframework/cloud/netflix/zuul/ZuulProxyApplicationTests.java +++ b/spring-cloud-netflix-zuul/src/test/java/org/springframework/cloud/netflix/zuul/ZuulProxyApplicationTests.java @@ -20,12 +20,11 @@ package org.springframework.cloud.netflix.zuul; import com.netflix.loadbalancer.Server; import com.netflix.loadbalancer.ServerList; import com.netflix.zuul.context.RequestContext; - import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; - +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; @@ -38,27 +37,41 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import static java.util.Collections.singletonList; import static org.junit.Assert.assertEquals; @RunWith(SpringJUnit4ClassRunner.class) -@SpringBootTest(classes = ZuulProxyApplicationTests.ZuulProxyApplication.class, webEnvironment = WebEnvironment.RANDOM_PORT, properties = { - "zuul.routes.simplezpat:/simplezpat/**", "logging.level.org.apache.http: DEBUG" }) +@SpringBootTest( + classes = ZuulProxyApplicationTests.ZuulProxyApplication.class, + webEnvironment = WebEnvironment.RANDOM_PORT, + properties = { + "zuul.routes.simplezpat:/simplezpat/**", + "logging.level.org.apache.http: DEBUG" + }) @DirtiesContext public class ZuulProxyApplicationTests { @LocalServerPort private int port; + @Autowired + private TestRestTemplate testRestTemplate; + @Before public void setTestRequestcontext() { RequestContext context = new RequestContext(); @@ -72,22 +85,58 @@ public class ZuulProxyApplicationTests { @Test public void getHasCorrectTransferEncoding() { - ResponseEntity result = new TestRestTemplate().getForEntity( - "http://localhost:" + this.port + "/simplezpat/transferencoding", - String.class); + ResponseEntity result = testRestTemplate.getForEntity(url(), String.class); + assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals("missing", result.getBody()); } @Test public void postHasCorrectTransferEncoding() { - ResponseEntity result = new TestRestTemplate().postForEntity( - "http://localhost:" + this.port + "/simplezpat/transferencoding", - new HttpEntity<>("hello"), String.class); + ResponseEntity result = testRestTemplate.postForEntity(url(), new HttpEntity<>("hello"), String.class); + assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals("missing", result.getBody()); } + @Test + public void preflightRequestSucceedsForGetRequest() { + MultiValueMap headers = new LinkedMultiValueMap<>(); + headers.put("Origin", singletonList("http://hello.com")); + headers.put("Access-Control-Request-Method", singletonList("GET")); + ResponseEntity result = testRestTemplate.exchange(url(), HttpMethod.OPTIONS, + new HttpEntity<>(headers), Void.class); + + assertEquals(HttpStatus.OK, result.getStatusCode()); + } + + @Test + public void preflightRequestIsForbiddenForUnsupportedMethod() { + MultiValueMap headers = new LinkedMultiValueMap<>(); + headers.put("Origin", singletonList("http://hello.com")); + headers.put("Access-Control-Request-Method", singletonList("PUT")); + ResponseEntity result = testRestTemplate.exchange(url(), HttpMethod.OPTIONS, + new HttpEntity<>(headers), Void.class); + + assertEquals(HttpStatus.FORBIDDEN, result.getStatusCode()); + } + + @Test + public void preflightRequestIsForbiddenForUnsupportedorigin() { + MultiValueMap headers = new LinkedMultiValueMap<>(); + headers.put("Origin", singletonList("http://unknown-origin.com")); + headers.put("Access-Control-Request-Method", singletonList("GET")); + ResponseEntity result = testRestTemplate.exchange(url(), HttpMethod.OPTIONS, + new HttpEntity<>(headers), Void.class); + + assertEquals(HttpStatus.FORBIDDEN, result.getStatusCode()); + } + + + private String url() { + return "http://localhost:" + this.port + "/simplezpat/transferencoding"; + } + // Don't use @SpringBootApplication because we don't want to component scan @Configuration @EnableAutoConfiguration @@ -116,6 +165,18 @@ public class ZuulProxyApplicationTests { return transferEncoding; } + @Bean + public WebMvcConfigurer corsConfigurer() { + return new WebMvcConfigurer() { + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/simplezpat/**") + .allowedOrigins("http://hello.com") + .allowedMethods("GET", "POST") + .allowedHeaders("Authorization"); + } + }; + } + } // Load balancer with fixed server list for "simplezpat" pointing to localhost