diff --git a/docs/src/main/asciidoc/spring-cloud-netflix.adoc b/docs/src/main/asciidoc/spring-cloud-netflix.adoc index fb8ab60e..cca26222 100644 --- a/docs/src/main/asciidoc/spring-cloud-netflix.adoc +++ b/docs/src/main/asciidoc/spring-cloud-netflix.adoc @@ -1060,13 +1060,17 @@ independently for all the backends. To enable it, annotate a Spring Boot main class with `@EnableZuulProxy`, and this forwards local calls to the appropriate -service. By convention, a service with the Eureka ID "users", will +service. By convention, a service with the ID "users", will receive requests from the proxy located at `/users` (with the prefix stripped). The proxy uses Ribbon to locate an instance to forward to -via Eureka, and all requests are executed in a hystrix command, so +via discovery, and all requests are executed in a hystrix command, so failures will show up in Hystrix metrics, and once the circuit is open the proxy will not try to contact the service. +NOTE: the Zuul starter does not include a discovery client, so for +routes based on service IDs you need to provide one of those +on the classpath as well (e.g. Eureka is one choice). + To skip having a service automatically added, set `zuul.ignored-services` to a list of service id patterns. If a service matches a pattern that is ignored, but also included in the explicitly diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/rx/ObservableSseEmitter.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/rx/ObservableSseEmitter.java index 14a5ae58..a267c54c 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/rx/ObservableSseEmitter.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/rx/ObservableSseEmitter.java @@ -18,6 +18,7 @@ package org.springframework.cloud.netflix.rx; import org.springframework.http.MediaType; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + import rx.Observable; /** @@ -28,18 +29,17 @@ import rx.Observable; */ class ObservableSseEmitter extends SseEmitter { - private final ResponseBodyEmitterSubscriber subscriber; + public ObservableSseEmitter(Observable observable) { + this(null, observable); + } - public ObservableSseEmitter(Observable observable) { - this(null, observable); - } + public ObservableSseEmitter(MediaType mediaType, Observable observable) { + this(null, mediaType, observable); + } - public ObservableSseEmitter(MediaType mediaType, Observable observable) { - this(null, mediaType, observable); - } - - public ObservableSseEmitter(Long timeout, MediaType mediaType, Observable observable) { - super(timeout); - this.subscriber = new ResponseBodyEmitterSubscriber<>(mediaType, observable, this); - } + public ObservableSseEmitter(Long timeout, MediaType mediaType, + Observable observable) { + super(timeout); + new ResponseBodyEmitterSubscriber<>(mediaType, observable, this); + } } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/rx/SingleDeferredResult.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/rx/SingleDeferredResult.java index af313392..232a053e 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/rx/SingleDeferredResult.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/rx/SingleDeferredResult.java @@ -29,22 +29,19 @@ import rx.Single; */ class SingleDeferredResult extends DeferredResult { - private static final Object EMPTY_RESULT = new Object(); + private static final Object EMPTY_RESULT = new Object(); - private final DeferredResultSubscriber subscriber; + public SingleDeferredResult(Single single) { + this(null, EMPTY_RESULT, single); + } - public SingleDeferredResult(Single single) { - this(null, EMPTY_RESULT, single); - } + public SingleDeferredResult(long timeout, Single single) { + this(timeout, EMPTY_RESULT, single); + } - public SingleDeferredResult(long timeout, Single single) { - this(timeout, EMPTY_RESULT, single); - } - - public SingleDeferredResult(Long timeout, Object timeoutResult, Single single) { - super(timeout, timeoutResult); - Assert.notNull(single, "single can not be null"); - - subscriber = new DeferredResultSubscriber<>(single.toObservable(), this); - } + public SingleDeferredResult(Long timeout, Object timeoutResult, Single single) { + super(timeout, timeoutResult); + Assert.notNull(single, "single can not be null"); + new DeferredResultSubscriber<>(single.toObservable(), this); + } } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/rx/SingleReturnValueHandler.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/rx/SingleReturnValueHandler.java index 190a06d7..1db91b76 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/rx/SingleReturnValueHandler.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/rx/SingleReturnValueHandler.java @@ -31,42 +31,46 @@ import rx.Single; import rx.functions.Func1; /** - * A specialized {@link AsyncHandlerMethodReturnValueHandler} that handles {@link Single} return types. + * A specialized {@link AsyncHandlerMethodReturnValueHandler} that handles {@link Single} + * return types. * * @author Spencer Gibb * @author Jakub Narloch */ public class SingleReturnValueHandler implements AsyncHandlerMethodReturnValueHandler { - @Override - public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) { - return returnValue != null && supportsReturnType(returnType); - } + @Override + public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) { + return returnValue != null && supportsReturnType(returnType); + } - @Override - public boolean supportsReturnType(MethodParameter returnType) { - return Single.class.isAssignableFrom(returnType.getParameterType()) || isResponseEntity(returnType); - } + @Override + public boolean supportsReturnType(MethodParameter returnType) { + return Single.class.isAssignableFrom(returnType.getParameterType()) + || isResponseEntity(returnType); + } private boolean isResponseEntity(MethodParameter returnType) { - if(ResponseEntity.class.isAssignableFrom(returnType.getParameterType())) { - Class bodyType = ResolvableType.forMethodParameter(returnType).getGeneric(0).resolve(); + if (ResponseEntity.class.isAssignableFrom(returnType.getParameterType())) { + Class bodyType = ResolvableType.forMethodParameter(returnType) + .getGeneric(0).resolve(); return bodyType != null && Single.class.isAssignableFrom(bodyType); } return false; } - @SuppressWarnings("unchecked") - @Override - public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { + @Override + public void handleReturnValue(Object returnValue, MethodParameter returnType, + ModelAndViewContainer mavContainer, NativeWebRequest webRequest) + throws Exception { - if (returnValue == null) { - mavContainer.setRequestHandled(true); - return; - } + if (returnValue == null) { + mavContainer.setRequestHandled(true); + return; + } ResponseEntity> responseEntity = getResponseEntity(returnValue); - if(responseEntity != null) { + if (responseEntity != null) { returnValue = responseEntity.getBody(); if (returnValue == null) { mavContainer.setRequestHandled(true); @@ -74,10 +78,10 @@ public class SingleReturnValueHandler implements AsyncHandlerMethodReturnValueHa } } - final Single single = Single.class.cast(returnValue); - WebAsyncUtils.getAsyncManager(webRequest) - .startDeferredResultProcessing(convertToDeferredResult(responseEntity, single), mavContainer); - } + final Single single = Single.class.cast(returnValue); + WebAsyncUtils.getAsyncManager(webRequest).startDeferredResultProcessing( + convertToDeferredResult(responseEntity, single), mavContainer); + } @SuppressWarnings("unchecked") private ResponseEntity> getResponseEntity(Object returnValue) { @@ -88,28 +92,32 @@ public class SingleReturnValueHandler implements AsyncHandlerMethodReturnValueHa return null; } - protected DeferredResult convertToDeferredResult(final ResponseEntity> responseEntity, Single single) { + protected DeferredResult convertToDeferredResult( + final ResponseEntity> responseEntity, Single single) { - //TODO: fix when java8 :-) - Single singleResponse = single.map(new Func1() { - @Override - public ResponseEntity call(Object object) { - return new ResponseEntity<>(object, getHttpHeaders(responseEntity), getHttpStatus(responseEntity)); - } - }); + // TODO: use lambda when java8 :-) + Single> singleResponse = single + .map(new Func1>() { + @Override + public ResponseEntity call(Object object) { + return new ResponseEntity(object, + getHttpHeaders(responseEntity), + getHttpStatus(responseEntity)); + } + }); return new SingleDeferredResult<>(singleResponse); } private HttpStatus getHttpStatus(ResponseEntity responseEntity) { - if(responseEntity == null) { + if (responseEntity == null) { return HttpStatus.OK; } return responseEntity.getStatusCode(); } private HttpHeaders getHttpHeaders(ResponseEntity responseEntity) { - if(responseEntity == null) { + if (responseEntity == null) { return new HttpHeaders(); } return responseEntity.getHeaders(); diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProxyConfiguration.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProxyConfiguration.java index 3570c8e0..6f81583e 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProxyConfiguration.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProxyConfiguration.java @@ -85,10 +85,12 @@ public class ZuulProxyConfiguration extends ZuulConfiguration { // pre filters @Bean - public PreDecorationFilter preDecorationFilter(RouteLocator routeLocator) { + public PreDecorationFilter preDecorationFilter(RouteLocator routeLocator, + ProxyRequestHelper proxyRequestHelper) { return new PreDecorationFilter(routeLocator, this.server.getServletPrefix(), - this.zuulProperties); + this.zuulProperties, + proxyRequestHelper); } // route filters diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilter.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilter.java index 866bb164..e1ff53b7 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilter.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilter.java @@ -43,13 +43,17 @@ public class PreDecorationFilter extends ZuulFilter { private ZuulProperties properties; private UrlPathHelper urlPathHelper = new UrlPathHelper(); + + private ProxyRequestHelper proxyRequestHelper; public PreDecorationFilter(RouteLocator routeLocator, - String dispatcherServletPath, ZuulProperties properties) { + String dispatcherServletPath, ZuulProperties properties, + ProxyRequestHelper proxyRequestHelper) { this.routeLocator = routeLocator; this.properties = properties; this.urlPathHelper.setRemoveSemicolonContent(properties.isRemoveSemicolonContent()); this.dispatcherServletPath = dispatcherServletPath; + this.proxyRequestHelper = proxyRequestHelper; } @Override @@ -81,9 +85,9 @@ public class PreDecorationFilter extends ZuulFilter { ctx.put("requestURI", route.getPath()); ctx.put("proxy", route.getId()); if (route.getSensitiveHeaders().isEmpty()) { - ctx.put(ProxyRequestHelper.IGNORED_HEADERS, this.properties.getSensitiveHeaders()); + proxyRequestHelper.addIgnoredHeaders(this.properties.getSensitiveHeaders().toArray(new String[0])); } else { - ctx.put(ProxyRequestHelper.IGNORED_HEADERS, route.getSensitiveHeaders()); + proxyRequestHelper.addIgnoredHeaders(route.getSensitiveHeaders().toArray(new String[0])); } if (route.getRetryable() != null) { diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/RibbonAutoConfigurationIntegrationTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/RibbonAutoConfigurationIntegrationTests.java new file mode 100644 index 00000000..3153e79b --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/ribbon/RibbonAutoConfigurationIntegrationTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2013-2014 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.netflix.ribbon; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.cloud.netflix.archaius.ArchaiusAutoConfiguration; +import org.springframework.cloud.netflix.ribbon.RibbonAutoConfigurationIntegrationTests.TestConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.netflix.client.config.CommonClientConfigKey; +import com.netflix.client.config.IClientConfig; + +import static org.junit.Assert.assertEquals; + +/** + * @author Dave Syer + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = TestConfiguration.class) +@DirtiesContext +@IntegrationTest("ribbon.ConnectTimeout=25000") +public class RibbonAutoConfigurationIntegrationTests { + + @Autowired + private SpringClientFactory factory; + + @Test + public void serverListIsConfigured() throws Exception { + IClientConfig config = this.factory.getClientConfig("client"); + assertEquals(25000, + config.getPropertyAsInteger(CommonClientConfigKey.ConnectTimeout, 3000)); + } + + @Configuration + @RibbonClient("client") + @Import({ PropertyPlaceholderAutoConfiguration.class, ArchaiusAutoConfiguration.class, + RibbonAutoConfiguration.class }) + protected static class TestConfiguration { + } + +} diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilterTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilterTests.java index 13245117..016802d1 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilterTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilterTests.java @@ -16,7 +16,9 @@ package org.springframework.cloud.netflix.zuul.filters.pre; +import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Set; @@ -53,6 +55,8 @@ public class PreDecorationFilterTests { private DiscoveryClientRouteLocator routeLocator; private MockHttpServletRequest request = new MockHttpServletRequest(); + + private ProxyRequestHelper proxyRequestHelper = new ProxyRequestHelper(); @Before public void init() { @@ -60,7 +64,7 @@ public class PreDecorationFilterTests { this.properties = new ZuulProperties(); this.routeLocator = new DiscoveryClientRouteLocator("/", this.discovery, this.properties); - this.filter = new PreDecorationFilter(this.routeLocator, "/", this.properties); + this.filter = new PreDecorationFilter(this.routeLocator, "/", this.properties, proxyRequestHelper); RequestContext ctx = RequestContext.getCurrentContext(); ctx.clear(); ctx.setRequest(this.request); @@ -81,7 +85,7 @@ public class PreDecorationFilterTests { @Test public void skippedIfForwardToSet() throws Exception { - RequestContext.getCurrentContext().set("forward.to", "mycontext"); + RequestContext.getCurrentContext().set("forward.to", "myconteext"); assertEquals(false, this.filter.shouldFilter()); } @@ -189,7 +193,7 @@ public class PreDecorationFilterTests { new ZuulRoute("foo", "/foo/**", null, "forward:/foo", true, null, null)); this.filter = new PreDecorationFilter(this.routeLocator, - "/special", this.properties); + "/special", this.properties, proxyRequestHelper); this.request.setRequestURI("/api/bar/1"); @@ -233,7 +237,7 @@ public class PreDecorationFilterTests { this.routeLocator.addRoute( new ZuulRoute("foo", "/foo/**", null, "forward:/foo", true, null, null)); this.filter = new PreDecorationFilter(this.routeLocator, - "/special", this.properties); + "/special", this.properties, proxyRequestHelper); this.filter.run(); @@ -258,7 +262,7 @@ public class PreDecorationFilterTests { new ZuulRoute("foo", "/foo/**", null, "forward:/foo", true, null, null)); this.filter = new PreDecorationFilter(this.routeLocator, - "/special", this.properties); + "/special", this.properties, proxyRequestHelper); this.filter.run(); @@ -296,6 +300,51 @@ public class PreDecorationFilterTests { assertTrue("sensitiveHeaders is wrong", sensitiveHeaders.containsAll(Collections.singletonList("x-bar"))); assertFalse("sensitiveHeaders is wrong", sensitiveHeaders.contains("Cookie")); } + + @Test + public void sensitiveHeadersCaseInsensitive() throws Exception { + this.properties.setPrefix("/api"); + this.properties.setStripPrefix(true); + this.properties.setSensitiveHeaders(Collections.singleton("X-bAr")); + this.request.setRequestURI("/api/foo/1"); + this.routeLocator.addRoute("/foo/**", "foo"); + this.filter.run(); + RequestContext ctx = RequestContext.getCurrentContext(); + @SuppressWarnings("unchecked") + Set sensitiveHeaders = (Set) ctx.get(ProxyRequestHelper.IGNORED_HEADERS); + assertTrue("sensitiveHeaders is wrong", sensitiveHeaders.containsAll(Collections.singletonList("x-bar"))); + } + + @Test + public void sensitiveHeadersOverrideCaseInsensitive() throws Exception { + this.properties.setPrefix("/api"); + this.properties.setStripPrefix(true); + this.properties.setSensitiveHeaders(Collections.singleton("X-bAr")); + this.request.setRequestURI("/api/foo/1"); + ZuulRoute route = new ZuulRoute("/foo/**", "foo"); + route.setSensitiveHeaders(Collections.singleton("X-Foo")); + this.routeLocator.addRoute(route); + this.filter.run(); + RequestContext ctx = RequestContext.getCurrentContext(); + @SuppressWarnings("unchecked") + Set sensitiveHeaders = (Set) ctx.get(ProxyRequestHelper.IGNORED_HEADERS); + assertTrue("sensitiveHeaders is wrong", sensitiveHeaders.containsAll(Collections.singletonList("x-foo"))); + } + + @Test + public void ignoredHeadersAlreadySetInRequestContextDontGetOverridden() throws Exception { + this.properties.setPrefix("/api"); + this.properties.setStripPrefix(true); + this.properties.setSensitiveHeaders(Collections.singleton("x-bar")); + this.request.setRequestURI("/api/foo/1"); + this.routeLocator.addRoute("/foo/**", "foo"); + RequestContext ctx = RequestContext.getCurrentContext(); + ctx.set(ProxyRequestHelper.IGNORED_HEADERS, new HashSet<>(Arrays.asList("x-foo"))); + this.filter.run(); + @SuppressWarnings("unchecked") + Set sensitiveHeaders = (Set) ctx.get(ProxyRequestHelper.IGNORED_HEADERS); + assertTrue("sensitiveHeaders is wrong", sensitiveHeaders.containsAll(Arrays.asList("x-bar","x-foo"))); + } private Object getHeader(List> headers, String key) { String value = null;