Commit cd30bea0 by Dave Syer

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
parent 43cab5f9
......@@ -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
......
......@@ -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 {
......
......@@ -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<String> getIgnoredPaths() {
return this.properties.getIgnoredPatterns();
}
public Map<String, String> 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;
......
......@@ -25,4 +25,6 @@ public interface RouteLocator {
Collection<String> getRoutePaths();
Collection<String> getIgnoredPaths();
}
......@@ -41,4 +41,9 @@ public class SimpleRouteLocator implements RouteLocator {
return paths;
}
@Override
public Collection<String> getIgnoredPaths() {
return this.properties.getIgnoredPatterns();
}
}
/*
* 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;
}
}
......@@ -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;
}
......
......@@ -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);
}
......
/*
* 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());
}
}
......@@ -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);
}
......@@ -86,6 +84,28 @@ public class PreDecorationFilterTests {
}
@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");
this.properties.setStripPrefix(true);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment