From cd30bea0c8bfd75bd27e74c5ff18f69bd0c0b532 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Sun, 18 Oct 2015 07:35:51 -0400 Subject: [PATCH] Add smarts for local route handling in Zuul proxy User can provide a url in the route that starts with "forward:" (instead of "http:" etc.) to handle the request locally. Also properly ignores ignored routes, so they can be handled locally (rather than giving up and sending 404). Fixes gh-536 --- .../main/asciidoc/spring-cloud-netflix.adoc | 45 +++++++++++- .../cloud/netflix/zuul/ZuulConfiguration.java | 6 ++ .../zuul/filters/ProxyRouteLocator.java | 17 +++-- .../netflix/zuul/filters/RouteLocator.java | 2 + .../zuul/filters/SimpleRouteLocator.java | 5 ++ .../zuul/filters/post/SendForwardFilter.java | 69 +++++++++++++++++++ .../zuul/filters/pre/PreDecorationFilter.java | 29 ++++---- .../netflix/zuul/web/ZuulHandlerMapping.java | 5 ++ .../filters/post/SendForwardFilterTests.java | 65 +++++++++++++++++ .../filters/pre/PreDecorationFilterTests.java | 32 +++++++-- 10 files changed, 248 insertions(+), 27 deletions(-) create mode 100644 spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/post/SendForwardFilter.java create mode 100644 spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/post/SendForwardFilterTests.java diff --git a/docs/src/main/asciidoc/spring-cloud-netflix.adoc b/docs/src/main/asciidoc/spring-cloud-netflix.adoc index a58b013b..6239c6fb 100644 --- a/docs/src/main/asciidoc/spring-cloud-netflix.adoc +++ b/docs/src/main/asciidoc/spring-cloud-netflix.adoc @@ -1096,7 +1096,7 @@ server if you set a default route ("/"), for example `zuul.route.home: /` would route all traffic (i.e. "/**") to the "home" service. If more fine-grained ignoring is needed, you can specify specific patterns to ignore. -These patterns are being evaluated at the end of the route location process, which +These patterns are being evaluated at the start of the route location process, which means prefixes should be included in the pattern to warrant a match. Ignored patterns span all services and supersede any other route specification. @@ -1104,7 +1104,7 @@ span all services and supersede any other route specification. [source,yaml] ---- zuul: - ignoredPatterns: */admin/** + ignoredPatterns: /**/admin/** routes: users: /myusers/** ---- @@ -1112,6 +1112,47 @@ span all services and supersede any other route specification. This means that all calls such as "/myusers/101" will be forwarded to "/101" on the "users" service. But calls including "/admin/" will not resolve. +=== Strangulation Patterns and Local Forwards + +A common pattern when migrating an existing application or API is to +"strangle" old endpoints, slowly replacing them with different +implementations. The Zuul proxy is a useful tool for this because you +can use it to handle all traffic from clients of the old endpoints, +but redirect some of the requests to new ones. + +Example configuration: + +.application.yml +[source,yaml] +---- + zuul: + routes: + first: + path: /first/** + url: http://first.example.com + second: + path: /second/** + url: forward:/second + third: + path: /third/** + url: forward:/3rd + legacy: + path: /** + url: http://legacy.example.com +---- + +In this example we are strangling the "legacy" app which is mapped to +all requests that do not match one of the other patterns. Paths in +`/first/**` have been extracted into a new service with an external +URL. And paths in `/second/**` are forwared so they can be handled +locally, e.g. with a normal Spring `@RequestMapping`. Paths in +`/third/**` are also forwarded, but with a different prefix +(i.e. `/third/foo` is forwarded to `/3rd/foo`). + +NOTE: The ignored pattterns aren't completely ignored, they just +aren't handled by the proxy (so they are also effectively forwarded +locally). + === Uploading Files through Zuul If you `@EnableZuulProxy` you can use the proxy paths to diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulConfiguration.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulConfiguration.java index cf4008d9..10249d89 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulConfiguration.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulConfiguration.java @@ -29,6 +29,7 @@ import org.springframework.cloud.netflix.zuul.filters.RouteLocator; import org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator; import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; import org.springframework.cloud.netflix.zuul.filters.post.SendErrorFilter; +import org.springframework.cloud.netflix.zuul.filters.post.SendForwardFilter; import org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter; import org.springframework.cloud.netflix.zuul.filters.pre.DebugFilter; import org.springframework.cloud.netflix.zuul.filters.pre.FormBodyWrapperFilter; @@ -121,6 +122,11 @@ public class ZuulConfiguration { return new SendErrorFilter(); } + @Bean + public SendForwardFilter sendForwardFilter() { + return new SendForwardFilter(); + } + @Configuration protected static class ZuulFilterConfiguration { diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/ProxyRouteLocator.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/ProxyRouteLocator.java index f3a062f6..31b6212a 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/ProxyRouteLocator.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/ProxyRouteLocator.java @@ -23,10 +23,6 @@ import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.atomic.AtomicReference; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.extern.apachecommons.CommonsLog; - import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.netflix.zuul.filters.ZuulProperties.ZuulRoute; @@ -35,6 +31,10 @@ import org.springframework.util.PathMatcher; import org.springframework.util.PatternMatchUtils; import org.springframework.util.StringUtils; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.extern.apachecommons.CommonsLog; + /** * @author Spencer Gibb */ @@ -93,6 +93,11 @@ public class ProxyRouteLocator implements RouteLocator { return getRoutes().keySet(); } + @Override + public Collection getIgnoredPaths() { + return this.properties.getIgnoredPatterns(); + } + public Map getRoutes() { if (this.routes.get() == null) { this.routes.set(locateRoutes()); @@ -106,8 +111,8 @@ public class ProxyRouteLocator implements RouteLocator { } public ProxyRouteSpec getMatchingRoute(String path) { - log.info("Finding route for path: " + path); - + log.info("Finding route for path: " + path); + String location = null; String targetPath = null; String id = null; diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/RouteLocator.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/RouteLocator.java index 91acf405..e5967cd7 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/RouteLocator.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/RouteLocator.java @@ -25,4 +25,6 @@ public interface RouteLocator { Collection getRoutePaths(); + Collection getIgnoredPaths(); + } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/SimpleRouteLocator.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/SimpleRouteLocator.java index d8ec1fae..c21c53ca 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/SimpleRouteLocator.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/SimpleRouteLocator.java @@ -41,4 +41,9 @@ public class SimpleRouteLocator implements RouteLocator { return paths; } + @Override + public Collection getIgnoredPaths() { + return this.properties.getIgnoredPatterns(); + } + } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/post/SendForwardFilter.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/post/SendForwardFilter.java new file mode 100644 index 00000000..0b1e77ce --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/post/SendForwardFilter.java @@ -0,0 +1,69 @@ +/* + * Copyright 2013-2015 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.zuul.filters.post; + +import javax.servlet.RequestDispatcher; + +import org.springframework.util.ReflectionUtils; + +import com.netflix.zuul.ZuulFilter; +import com.netflix.zuul.context.RequestContext; + +/** + * @author Dave Syer + */ +public class SendForwardFilter extends ZuulFilter { + + protected static final String SEND_FORWARD_FILTER_RAN = "sendForwardFilter.ran"; + + @Override + public String filterType() { + return "post"; + } + + @Override + public int filterOrder() { + return 2000; + } + + @Override + public boolean shouldFilter() { + RequestContext ctx = RequestContext.getCurrentContext(); + return ctx.containsKey("forward.to") + && !ctx.getBoolean(SEND_FORWARD_FILTER_RAN, false); + } + + @Override + public Object run() { + try { + RequestContext ctx = RequestContext.getCurrentContext(); + String path = (String) ctx.get("forward.to"); + RequestDispatcher dispatcher = ctx.getRequest().getRequestDispatcher(path); + if (dispatcher != null) { + ctx.set(SEND_FORWARD_FILTER_RAN, true); + if (!ctx.getResponse().isCommitted()) { + dispatcher.forward(ctx.getRequest(), ctx.getResponse()); + } + } + } + catch (Exception ex) { + ReflectionUtils.rethrowRuntimeException(ex); + } + return null; + } + +} 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 90d9495e..338438af 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 @@ -19,19 +19,17 @@ package org.springframework.cloud.netflix.zuul.filters.pre; import java.net.MalformedURLException; import java.net.URL; -import javax.servlet.http.HttpServletResponse; - -import com.netflix.zuul.constants.ZuulHeaders; -import lombok.extern.apachecommons.CommonsLog; - import org.springframework.cloud.netflix.zuul.filters.ProxyRouteLocator; import org.springframework.cloud.netflix.zuul.filters.ProxyRouteLocator.ProxyRouteSpec; import org.springframework.util.StringUtils; import org.springframework.web.util.UrlPathHelper; import com.netflix.zuul.ZuulFilter; +import com.netflix.zuul.constants.ZuulHeaders; import com.netflix.zuul.context.RequestContext; +import lombok.extern.apachecommons.CommonsLog; + @CommonsLog public class PreDecorationFilter extends ZuulFilter { @@ -58,14 +56,15 @@ public class PreDecorationFilter extends ZuulFilter { @Override public boolean shouldFilter() { - return true; + RequestContext ctx = RequestContext.getCurrentContext(); + return !ctx.containsKey("forward.to"); } @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); - final String requestURI = this.urlPathHelper.getPathWithinApplication(ctx - .getRequest()); + final String requestURI = this.urlPathHelper + .getPathWithinApplication(ctx.getRequest()); ProxyRouteSpec route = this.routeLocator.getMatchingRoute(requestURI); if (route != null) { String location = route.getLocation(); @@ -81,6 +80,12 @@ public class PreDecorationFilter extends ZuulFilter { ctx.setRouteHost(getUrl(location)); ctx.addOriginResponseHeader("X-Zuul-Service", location); } + else if (location.startsWith("forward:")) { + ctx.set("forward.to", StringUtils.cleanPath( + location.substring("forward:".length()) + route.getPath())); + ctx.setRouteHost(null); + return null; + } else { // set serviceId for use in filters.route.RibbonRequest ctx.set("serviceId", location); @@ -88,12 +93,10 @@ public class PreDecorationFilter extends ZuulFilter { ctx.addOriginResponseHeader("X-Zuul-ServiceId", location); } if (this.addProxyHeaders) { - ctx.addZuulRequestHeader( - "X-Forwarded-Host", + ctx.addZuulRequestHeader("X-Forwarded-Host", ctx.getRequest().getServerName() + ":" + String.valueOf(ctx.getRequest().getServerPort())); - ctx.addZuulRequestHeader( - ZuulHeaders.X_FORWARDED_PROTO, + ctx.addZuulRequestHeader(ZuulHeaders.X_FORWARDED_PROTO, ctx.getRequest().getScheme()); if (StringUtils.hasText(route.getPrefix())) { ctx.addZuulRequestHeader("X-Forwarded-Prefix", route.getPrefix()); @@ -103,7 +106,7 @@ public class PreDecorationFilter extends ZuulFilter { } else { log.warn("No route found for uri: " + requestURI); - ctx.set("error.status_code", HttpServletResponse.SC_NOT_FOUND); + ctx.set("forward.to", requestURI); } return null; } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/web/ZuulHandlerMapping.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/web/ZuulHandlerMapping.java index 92668599..f4273276 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/web/ZuulHandlerMapping.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/web/ZuulHandlerMapping.java @@ -22,6 +22,7 @@ import javax.servlet.http.HttpServletRequest; import org.springframework.boot.autoconfigure.web.ErrorController; import org.springframework.cloud.netflix.zuul.filters.RouteLocator; +import org.springframework.util.PatternMatchUtils; import org.springframework.web.servlet.handler.AbstractUrlHandlerMapping; /** @@ -55,6 +56,10 @@ public class ZuulHandlerMapping extends AbstractUrlHandlerMapping { && urlPath.equals(this.errorController.getErrorPath())) { return null; } + String[] ignored = this.routeLocator.getIgnoredPaths().toArray(new String[0]); + if (PatternMatchUtils.simpleMatch(ignored, urlPath)) { + return null; + } return super.lookupHandler(urlPath, request); } diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/post/SendForwardFilterTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/post/SendForwardFilterTests.java new file mode 100644 index 00000000..c66c1010 --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/post/SendForwardFilterTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2013-2015 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.zuul.filters.post; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.After; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import com.netflix.zuul.context.RequestContext; + +/** + * @author Dave Syer + */ +public class SendForwardFilterTests { + + @After + public void reset() { + RequestContext.testSetCurrentContext(null); + } + + @Test + public void runsNormally() { + SendForwardFilter filter = createSendForwardFilter(new MockHttpServletRequest()); + assertTrue("shouldFilter returned false", filter.shouldFilter()); + filter.run(); + } + + private SendForwardFilter createSendForwardFilter(HttpServletRequest request) { + RequestContext context = new RequestContext(); + context.setRequest(request); + context.setResponse(new MockHttpServletResponse()); + context.set("forward.to", "/foo"); + RequestContext.testSetCurrentContext(context); + SendForwardFilter filter = new SendForwardFilter(); + return filter; + } + + @Test + public void doesNotRunTwice() { + SendForwardFilter filter = createSendForwardFilter(new MockHttpServletRequest()); + assertTrue("shouldFilter returned false", filter.shouldFilter()); + filter.run(); + assertFalse("shouldFilter returned true", filter.shouldFilter()); + } +} 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 7ea2a8f9..b513f608 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,12 +16,14 @@ package org.springframework.cloud.netflix.zuul.filters.pre; +import static org.junit.Assert.assertEquals; +import static org.mockito.MockitoAnnotations.initMocks; + import java.util.List; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; -import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.netflix.zuul.filters.ProxyRouteLocator; import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; @@ -31,9 +33,6 @@ import org.springframework.mock.web.MockHttpServletRequest; import com.netflix.util.Pair; import com.netflix.zuul.context.RequestContext; -import static org.junit.Assert.assertEquals; -import static org.mockito.MockitoAnnotations.initMocks; - /** * @author Dave Syer */ @@ -50,8 +49,6 @@ public class PreDecorationFilterTests { private MockHttpServletRequest request = new MockHttpServletRequest(); - private ServerProperties server = new ServerProperties(); - @Before public void init() { initMocks(this); @@ -59,6 +56,7 @@ public class PreDecorationFilterTests { this.properties); this.filter = new PreDecorationFilter(this.routeLocator, true); RequestContext ctx = RequestContext.getCurrentContext(); + ctx.clear(); ctx.setRequest(this.request); } @@ -85,6 +83,28 @@ public class PreDecorationFilterTests { assertEquals("foo", getHeader(ctx.getOriginResponseHeaders(), "x-zuul-serviceid")); } + @Test + public void forwardRouteAddsLocation() throws Exception { + this.properties.setPrefix("/api"); + this.properties.setStripPrefix(true); + this.request.setRequestURI("/api/foo/1"); + this.routeLocator.addRoute(new ZuulRoute("foo", "/foo/**", null, "forward:/foo", true, + null)); + this.filter.run(); + RequestContext ctx = RequestContext.getCurrentContext(); + assertEquals("/foo/1", ctx.get("forward.to")); + } + + @Test + public void forwardWithoutStripPrefixAppendsPath() throws Exception { + this.request.setRequestURI("/foo/1"); + this.routeLocator.addRoute(new ZuulRoute("foo", "/foo/**", null, "forward:/bar", false, + null)); + this.filter.run(); + RequestContext ctx = RequestContext.getCurrentContext(); + assertEquals("/bar/foo/1", ctx.get("forward.to")); + } + @Test public void prefixRouteWithRouteStrippingAddsHeader() throws Exception { this.properties.setPrefix("/api");