Browse Source

Support for re-writing Location Headers (#1863)

* Support for re-writing Location Headers

* Added additional constructor for Route, removed auto-configuration of LocationRewriteFilter

* Polished docs, more tests
pull/6/head
Biju Kunjummen 7 years ago committed by Spencer Gibb
parent
commit
0579b326be
  1. 26
      docs/src/main/asciidoc/spring-cloud-netflix.adoc
  2. 8
      spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/Route.java
  3. 3
      spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/SimpleRouteLocator.java
  4. 3
      spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/ZuulProperties.java
  5. 160
      spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/post/LocationRewriteFilter.java
  6. 109
      spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/post/LocationRewriteFilterIntegrationTests.java
  7. 178
      spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/post/LocationRewriteFilterTests.java

26
docs/src/main/asciidoc/spring-cloud-netflix.adoc

@ -1864,6 +1864,32 @@ class MyFallbackProvider implements ZuulFallbackProvider { @@ -1864,6 +1864,32 @@ class MyFallbackProvider implements ZuulFallbackProvider {
}
----
[[zuul-redirect-location-rewrite]]
=== Rewriting `Location` header
If Zuul is fronting a web application then there may be a need to re-write the `Location` header when the web application redirects through a http status code of 3XX, otherwise the browser will end up redirecting to the web application's url instead of the Zuul url.
A `LocationRewriteFilter` Zuul filter can be configured to re-write the Location header to the Zuul's url, it also adds back the stripped global and route specific prefixes. The filter can be added the following way via a Spring Configuration file:
[source,java]
----
import org.springframework.cloud.netflix.zuul.filters.post.LocationRewriteFilter;
...
@Configuration
@EnableZuulProxy
public class ZuulConfig {
@Bean
public LocationRewriteFilter locationRewriteFilter() {
return new LocationRewriteFilter();
}
}
----
[WARNING]
====
Use this filter with caution though, the filter acts on the `Location` header of ALL 3XX response codes which may not be appropriate in all scenarios, say if the user is redirecting to an external URL.
====
[[zuul-developer-guide]]
=== Zuul Developer Guide

8
spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/Route.java

@ -42,6 +42,12 @@ public class Route { @@ -42,6 +42,12 @@ public class Route {
}
}
}
public Route(String id, String path, String location, String prefix,
Boolean retryable, Set<String> ignoredHeaders, boolean prefixStripped) {
this(id, path, location, prefix, retryable, ignoredHeaders);
this.prefixStripped = prefixStripped;
}
private String id;
@ -58,6 +64,8 @@ public class Route { @@ -58,6 +64,8 @@ public class Route {
private Set<String> sensitiveHeaders = new LinkedHashSet<>();
private boolean customSensitiveHeaders;
private boolean prefixStripped = true;
public boolean isCustomSensitiveHeaders() {
return this.customSensitiveHeaders;

3
spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/SimpleRouteLocator.java

@ -151,7 +151,8 @@ public class SimpleRouteLocator implements RouteLocator, Ordered { @@ -151,7 +151,8 @@ public class SimpleRouteLocator implements RouteLocator, Ordered {
}
return new Route(route.getId(), targetPath, route.getLocation(), prefix,
retryable,
route.isCustomSensitiveHeaders() ? route.getSensitiveHeaders() : null);
route.isCustomSensitiveHeaders() ? route.getSensitiveHeaders() : null,
route.isStripPrefix());
}
/**

3
spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/ZuulProperties.java

@ -304,7 +304,8 @@ public class ZuulProperties { @@ -304,7 +304,8 @@ public class ZuulProperties {
public Route getRoute(String prefix) {
return new Route(this.id, this.path, getLocation(), prefix, this.retryable,
isCustomSensitiveHeaders() ? this.sensitiveHeaders : null);
isCustomSensitiveHeaders() ? this.sensitiveHeaders : null,
this.stripPrefix);
}
public void setSensitiveHeaders(Set<String> headers) {

160
spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/post/LocationRewriteFilter.java

@ -0,0 +1,160 @@ @@ -0,0 +1,160 @@
/*
* Copyright 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.netflix.zuul.filters.post;
import com.netflix.util.Pair;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.filters.Route;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UrlPathHelper;
import java.net.URI;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.POST_TYPE;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SEND_RESPONSE_FILTER_ORDER;
/**
* {@link ZuulFilter} Responsible for rewriting the Location header to be the Zuul URL
*
* @author Biju Kunjummen
*/
public class LocationRewriteFilter extends ZuulFilter {
private final UrlPathHelper urlPathHelper = new UrlPathHelper();
@Autowired
private ZuulProperties zuulProperties;
@Autowired
private RouteLocator routeLocator;
private static final String LOCATION_HEADER = "Location";
public LocationRewriteFilter() {
}
public LocationRewriteFilter(ZuulProperties zuulProperties,
RouteLocator routeLocator) {
this.routeLocator = routeLocator;
this.zuulProperties = zuulProperties;
}
@Override
public String filterType() {
return POST_TYPE;
}
@Override
public int filterOrder() {
return SEND_RESPONSE_FILTER_ORDER - 100;
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
int statusCode = ctx.getResponseStatusCode();
return HttpStatus.valueOf(statusCode).is3xxRedirection();
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
Route route = routeLocator.getMatchingRoute(
urlPathHelper.getPathWithinApplication(ctx.getRequest()));
if (route != null) {
Pair<String, String> lh = locationHeader(ctx);
if (lh != null) {
String location = lh.second();
URI originalRequestUri = UriComponentsBuilder
.fromHttpRequest(new ServletServerHttpRequest(ctx.getRequest()))
.build().toUri();
UriComponentsBuilder redirectedUriBuilder = UriComponentsBuilder
.fromUriString(location);
UriComponents redirectedUriComps = redirectedUriBuilder.build();
String newPath = getRestoredPath(this.zuulProperties, route,
redirectedUriComps);
String modifiedLocation = redirectedUriBuilder
.scheme(originalRequestUri.getScheme())
.host(originalRequestUri.getHost())
.port(originalRequestUri.getPort()).replacePath(newPath).build()
.toUriString();
lh.setSecond(modifiedLocation);
}
}
return null;
}
private String getRestoredPath(ZuulProperties zuulProperties, Route route,
UriComponents redirectedUriComps) {
StringBuilder path = new StringBuilder();
String redirectedPathWithoutGlobal = downstreamHasGlobalPrefix(zuulProperties)
? redirectedUriComps.getPath()
.substring(("/" + zuulProperties.getPrefix()).length())
: redirectedUriComps.getPath();
if (downstreamHasGlobalPrefix(zuulProperties)) {
path.append("/" + zuulProperties.getPrefix());
}
else {
path.append(zuulHasGlobalPrefix(zuulProperties)
? "/" + zuulProperties.getPrefix() : "");
}
path.append(downstreamHasRoutePrefix(route) ? "" : "/" + route.getPrefix())
.append(redirectedPathWithoutGlobal);
return path.toString();
}
private boolean downstreamHasGlobalPrefix(ZuulProperties zuulProperties) {
return (!zuulProperties.isStripPrefix()
&& StringUtils.hasText(zuulProperties.getPrefix()));
}
private boolean zuulHasGlobalPrefix(ZuulProperties zuulProperties) {
return StringUtils.hasText(zuulProperties.getPrefix());
}
private boolean downstreamHasRoutePrefix(Route route) {
return (!route.isPrefixStripped() && StringUtils.hasText(route.getPrefix()));
}
private Pair<String, String> locationHeader(RequestContext ctx) {
if (ctx.getZuulResponseHeaders() != null) {
for (Pair<String, String> pair : ctx.getZuulResponseHeaders()) {
if (pair.first().equals(LOCATION_HEADER)) {
return pair;
}
}
}
return null;
}
}

109
spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/post/LocationRewriteFilterIntegrationTests.java

@ -0,0 +1,109 @@ @@ -0,0 +1,109 @@
/*
* Copyright 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.netflix.zuul.filters.post;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ServerList;
import com.netflix.zuul.context.RequestContext;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.embedded.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.netflix.ribbon.StaticServerList;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Biju Kunjummen
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = {
"zuul.routes.aservice.path:/service/**", "zuul.routes.aservice.strip-prefix:true",
"eureka.client.enabled:false" })
@DirtiesContext
public class LocationRewriteFilterIntegrationTests {
@LocalServerPort
private int port;
@Before
public void before() {
RequestContext context = new RequestContext();
RequestContext.testSetCurrentContext(context);
}
@Test
public void testWithRedirectPrefixStripped() {
String url = "http://localhost:" + port + "/service/redirectingUri";
ResponseEntity<String> response = new TestRestTemplate().getForEntity(url,
String.class);
List<String> locationHeaders = response.getHeaders().get("Location");
assertThat(locationHeaders).hasSize(1);
String locationHeader = locationHeaders.get(0);
assertThat(locationHeader).withFailMessage("Location should have prefix")
.isEqualTo(
String.format("http://localhost:%d/service/redirectedUri", port));
}
@SpringBootConfiguration
@EnableAutoConfiguration
@EnableZuulProxy
@Controller
@RibbonClient(name = "aservice", configuration = RibbonConfig.class)
protected static class Config {
@RequestMapping("/redirectingUri")
public String redirect1() {
return "redirect:/redirectedUri";
}
@Bean
public LocationRewriteFilter locationRewriteFilter() {
return new LocationRewriteFilter();
}
}
public static class RibbonConfig {
@LocalServerPort
private int port;
@Bean
public ServerList<Server> ribbonServerList() {
return new StaticServerList<>(new Server("localhost", this.port));
}
}
}

178
spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/post/LocationRewriteFilterTests.java

@ -0,0 +1,178 @@ @@ -0,0 +1,178 @@
/*
* Copyright 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.netflix.zuul.filters.post;
import com.netflix.util.Pair;
import com.netflix.zuul.context.RequestContext;
import org.junit.Before;
import org.junit.Test;
import org.springframework.cloud.netflix.zuul.filters.Route;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import java.util.Collections;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* @author Biju Kunjummen
*/
public class LocationRewriteFilterTests {
private final String ZUUL_HOST = "myzuul.com";
private final String ZUUL_SCHEME = "https";
private final int ZUUL_PORT = 8443;
private final String ZUUL_BASE_URL = String.format("%s://%s:%d", ZUUL_SCHEME,
ZUUL_HOST, ZUUL_PORT);
private final String SERVER_HOST = "someserver.com";
private final String SERVER_SCHEME = "http";
private final int SERVER_PORT = 8564;
private final String SERVER_BASE_URL = String.format("%s://%s:%d", SERVER_SCHEME,
SERVER_HOST, SERVER_PORT);
@Before
public void before() {
RequestContext context = new RequestContext();
RequestContext.testSetCurrentContext(context);
}
@Test
public void shouldRewriteLocationHeadersWithRoutePrefix() {
RequestContext context = RequestContext.getCurrentContext();
ZuulProperties zuulProperties = new ZuulProperties();
LocationRewriteFilter filter = setFilterUpWith(context, zuulProperties,
new Route("service1", "/redirectingUri", "service1", "prefix", false,
Collections.EMPTY_SET, true),
"/prefix/redirectingUri", "/redirectedUri;someparam?param1=abc");
filter.run();
assertThat(getLocationHeader(context).second()).isEqualTo(String
.format("%s/prefix/redirectedUri;someparam?param1=abc", ZUUL_BASE_URL));
}
@Test
public void shouldBeUntouchedIfNoRoutesFound() {
RequestContext context = RequestContext.getCurrentContext();
ZuulProperties zuulProperties = new ZuulProperties();
LocationRewriteFilter filter = setFilterUpWith(context, zuulProperties, null,
"/prefix/redirectingUri", "/redirectedUri;someparam?param1=abc");
filter.run();
assertThat(getLocationHeader(context).second()).isEqualTo(
String.format("%s/redirectedUri;someparam?param1=abc", SERVER_BASE_URL));
}
@Test
public void shouldRewriteLocationHeadersIfPrefixIsNotStripped() {
RequestContext context = RequestContext.getCurrentContext();
ZuulProperties zuulProperties = new ZuulProperties();
LocationRewriteFilter filter = setFilterUpWith(context, zuulProperties,
new Route("service1", "/something/redirectingUri", "service1", "prefix",
false, Collections.EMPTY_SET, false),
"/prefix/redirectingUri",
"/something/redirectedUri;someparam?param1=abc");
filter.run();
assertThat(getLocationHeader(context).second()).isEqualTo(String.format(
"%s/something/redirectedUri;someparam?param1=abc", ZUUL_BASE_URL));
}
@Test
public void shouldRewriteLocationHeadersIfPrefixIsEmpty() {
RequestContext context = RequestContext.getCurrentContext();
ZuulProperties zuulProperties = new ZuulProperties();
LocationRewriteFilter filter = setFilterUpWith(context, zuulProperties,
new Route("service1", "/something/redirectingUri", "service1", "", false,
Collections.EMPTY_SET, true),
"/redirectingUri", "/something/redirectedUri;someparam?param1=abc");
filter.run();
assertThat(getLocationHeader(context).second()).isEqualTo(String.format(
"%s/something/redirectedUri;someparam?param1=abc", ZUUL_BASE_URL));
}
@Test
public void shouldAddBackGlobalPrefixIfPresent() {
RequestContext context = RequestContext.getCurrentContext();
ZuulProperties zuulProperties = new ZuulProperties();
zuulProperties.setPrefix("global");
zuulProperties.setStripPrefix(true);
LocationRewriteFilter filter = setFilterUpWith(context, zuulProperties,
new Route("service1", "/something/redirectingUri", "service1", "prefix",
false, Collections.EMPTY_SET, true),
"/global/prefix/redirectingUri",
"/something/redirectedUri;someparam?param1=abc");
filter.run();
assertThat(getLocationHeader(context).second()).isEqualTo(String.format(
"%s/global/prefix/something/redirectedUri;someparam?param1=abc",
ZUUL_BASE_URL));
}
@Test
public void shouldNotAddBackGlobalPrefixIfNotStripped() {
RequestContext context = RequestContext.getCurrentContext();
ZuulProperties zuulProperties = new ZuulProperties();
zuulProperties.setPrefix("global");
zuulProperties.setStripPrefix(false);
LocationRewriteFilter filter = setFilterUpWith(context, zuulProperties,
new Route("service1", "/something/redirectingUri", "service1", "prefix",
false, Collections.EMPTY_SET, true),
"/global/prefix/redirectingUri",
"/global/something/redirectedUri;someparam?param1=abc");
filter.run();
assertThat(getLocationHeader(context).second()).isEqualTo(String.format(
"%s/global/prefix/something/redirectedUri;someparam?param1=abc",
ZUUL_BASE_URL));
}
private LocationRewriteFilter setFilterUpWith(RequestContext context,
ZuulProperties zuulProperties, Route route, String toZuulRequestUri,
String redirectedUri) {
MockHttpServletRequest httpServletRequest = new MockHttpServletRequest();
httpServletRequest.setRequestURI(toZuulRequestUri);
httpServletRequest.setServerName(ZUUL_HOST);
httpServletRequest.setScheme(ZUUL_SCHEME);
httpServletRequest.setServerPort(ZUUL_PORT);
context.setRequest(httpServletRequest);
MockHttpServletResponse httpServletResponse = new MockHttpServletResponse();
context.getZuulResponseHeaders().add(new Pair<>("Location",
String.format("%s%s", SERVER_BASE_URL, redirectedUri)));
context.setResponse(httpServletResponse);
RouteLocator routeLocator = mock(RouteLocator.class);
when(routeLocator.getMatchingRoute(toZuulRequestUri)).thenReturn(route);
LocationRewriteFilter filter = new LocationRewriteFilter(zuulProperties,
routeLocator);
return filter;
}
private Pair<String, String> getLocationHeader(RequestContext ctx) {
if (ctx.getZuulResponseHeaders() != null) {
for (Pair<String, String> pair : ctx.getZuulResponseHeaders()) {
if (pair.first().equals("Location")) {
return pair;
}
}
}
return null;
}
}
Loading…
Cancel
Save