/*
* Copyright 2013-2016 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.route.support;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.servlet.http.HttpServletRequest;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.web.BasicErrorController;
import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.cloud.netflix.ribbon.StaticServerList;
import org.springframework.cloud.netflix.zuul.RoutesMvcEndpoint;
import org.springframework.cloud.netflix.zuul.filters.Route;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.cloud.netflix.zuul.filters.discovery.DiscoveryClientRouteLocator;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandFactory;
import org.springframework.cloud.netflix.zuul.filters.route.ZuulFallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ServerList;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assume.assumeThat;
/**
* @author Spencer Gibb
* @author Ryan Baxter
*/
public abstract class ZuulProxyTestBase {
@Value("${local.server.port}")
protected int port;
@Autowired
protected DiscoveryClientRouteLocator routes;
@Autowired
protected RoutesMvcEndpoint endpoint;
@Autowired
protected RibbonCommandFactory<?> ribbonCommandFactory;
@Autowired
protected MyErrorController myErrorController;
@Before
public void cleanup() {
this.myErrorController.clear();
}
@Before
public void setTestRequestcontext() {
RequestContext.testSetCurrentContext(null);
RequestContext.getCurrentContext().unset();
}
/**
* used to disable patch tests if client doesn't support it
*/
protected boolean supportsPatch() {
return true;
}
/**
* used to switch delete tests with a boyd if client doesn't support it
*/
protected boolean supportsDeleteWithBody() {
return true;
}
protected String getRoute(String path) {
for (Route route : this.routes.getRoutes()) {
if (path.equals(route.getFullPath())) {
return route.getLocation();
}
}
return null;
}
@Test
public void bindRouteUsingPhysicalRoute() {
assertEquals("http://localhost:7777/local", getRoute("/test/**"));
}
@Test
public void bindRouteUsingOnlyPath() {
assertEquals("simple", getRoute("/simple/**"));
}
@Test
public void getOnSelfViaRibbonRoutingFilter() {
ResponseEntity<String> result = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/simple/local/1", HttpMethod.GET,
new HttpEntity<>((Void) null), String.class);
assertEquals(HttpStatus.OK, result.getStatusCode());
assertEquals("Gotten 1!", result.getBody());
}
@Test
public void deleteOnSelfViaSimpleHostRoutingFilter() {
this.routes.addRoute("/self/**", "http://localhost:" + this.port + "/local");
this.endpoint.reset();
ResponseEntity<String> result = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/self/1", HttpMethod.DELETE,
new HttpEntity<>((Void) null), String.class);
assertEquals(HttpStatus.OK, result.getStatusCode());
assertEquals("Deleted 1!", result.getBody());
}
@Test
public void stripPrefixFalseAppendsPath() {
this.routes.addRoute(new ZuulProperties.ZuulRoute("strip", "/strip/**", "strip",
"http://localhost:" + this.port + "/local", false, false, null));
this.endpoint.reset();
ResponseEntity<String> result = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/strip", HttpMethod.GET,
new HttpEntity<>((Void) null), String.class);
assertEquals(HttpStatus.OK, result.getStatusCode());
// Prefix not stripped to it goes to /local/strip
assertEquals("Gotten strip!", result.getBody());
}
@Test
public void testNotFoundFromApp() {
ResponseEntity<String> result = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/simple/local/notfound",
HttpMethod.GET, new HttpEntity<>((Void) null), String.class);
assertEquals(HttpStatus.NOT_FOUND, result.getStatusCode());
}
@Test
public void testNotFoundOnProxy() {
ResponseEntity<String> result = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/myinvalidpath", HttpMethod.GET,
new HttpEntity<>((Void) null), String.class);
assertEquals(HttpStatus.NOT_FOUND, result.getStatusCode());
}
@Test
public void getSecondLevel() {
ResponseEntity<String> result = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/another/twolevel/local/1",
HttpMethod.GET, new HttpEntity<>((Void) null), String.class);
assertEquals(HttpStatus.OK, result.getStatusCode());
assertEquals("Gotten 1!", result.getBody());
}
@Test
public void ribbonRouteWithSpace() {
String uri = "/simple/spa ce";
this.myErrorController.setUriToMatch(uri);
ResponseEntity<String> result = new TestRestTemplate().exchange(
"http://localhost:" + this.port + uri, HttpMethod.GET,
new HttpEntity<>((Void) null), String.class);
assertEquals(HttpStatus.OK, result.getStatusCode());
assertEquals("Hello space", result.getBody());
assertFalse(myErrorController.wasControllerUsed());
}
@Test
public void ribbonDeleteWithBody() {
this.endpoint.reset();
ResponseEntity<String> result = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/simple/deletewithbody", HttpMethod.DELETE,
new HttpEntity<>("deleterequestbody"), String.class);
assertEquals(HttpStatus.OK, result.getStatusCode());
if (supportsDeleteWithBody()) {
assertEquals("Deleted deleterequestbody", result.getBody());
} else {
assertEquals("Deleted null", result.getBody());
}
}
@Test
public void ribbonRouteWithNonExistentUri() {
String uri = "/simple/nonExistent";
this.myErrorController.setUriToMatch(uri);
ResponseEntity<String> result = new TestRestTemplate().exchange(
"http://localhost:" + this.port + uri, HttpMethod.GET,
new HttpEntity<>((Void) null), String.class);
assertEquals(HttpStatus.NOT_FOUND, result.getStatusCode());
assertFalse(myErrorController.wasControllerUsed());
}
@Test
public void simpleHostRouteWithSpace() {
this.routes.addRoute("/self/**", "http://localhost:" + this.port);
this.endpoint.reset();
ResponseEntity<String> result = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/self/spa ce", HttpMethod.GET,
new HttpEntity<>((Void) null), String.class);
assertEquals(HttpStatus.OK, result.getStatusCode());
assertEquals("Hello space", result.getBody());
}
@Test
public void simpleHostRouteWithOriginalQueryString() {
this.routes.addRoute("/self/**", "http://localhost:" + this.port);
this.endpoint.reset();
ResponseEntity<String> result = new TestRestTemplate().exchange(
"http://localhost:" + this.port
+ "/self/qstring?original=value1&original=value2",
HttpMethod.GET, new HttpEntity<>((Void) null), String.class);
assertEquals(HttpStatus.OK, result.getStatusCode());
assertEquals("Received {original=[value1, value2]}", result.getBody());
}
@Test
public void simpleHostRouteWithOverriddenQString() {
this.routes.addRoute("/self/**", "http://localhost:" + this.port);
this.endpoint.reset();
ResponseEntity<String> result = new TestRestTemplate().exchange(
"http://localhost:" + this.port
+ "/self/qstring?override=true&different=key",
HttpMethod.GET, new HttpEntity<>((Void) null), String.class);
assertEquals(HttpStatus.OK, result.getStatusCode());
assertEquals("Received {key=[overridden]}", result.getBody());
}
@Test
public void patchOnSelfViaSimpleHostRoutingFilter() {
assumeThat(supportsPatch(), is(true));
this.routes.addRoute("/self/**", "http://localhost:" + this.port + "/local");
this.endpoint.reset();
ResponseEntity<String> result = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/self/1", HttpMethod.PATCH,
new HttpEntity<>("TestPatch"), String.class);
assertEquals(HttpStatus.OK, result.getStatusCode());
assertEquals("Patched 1!", result.getBody());
}
@SuppressWarnings("deprecation")
@Test
public void javascriptEncodedFormParams() {
TestRestTemplate testRestTemplate = new TestRestTemplate();
ArrayList<HttpMessageConverter<?>> converters = new ArrayList<>();
converters.addAll(Arrays.asList(new StringHttpMessageConverter(),
new NoEncodingFormHttpMessageConverter()));
testRestTemplate.getRestTemplate().setMessageConverters(converters);
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("foo", "(bar)");
ResponseEntity<String> result = testRestTemplate.postForEntity(
"http://localhost:" + this.port + "/simple/local", map, String.class);
assertEquals(HttpStatus.OK, result.getStatusCode());
assertEquals("Posted [(bar)] and Content-Length was: 13!", result.getBody());
}
public static abstract class AbstractZuulProxyApplication
extends DelegatingWebMvcConfiguration {
@RequestMapping(value = "/local/{id}", method = RequestMethod.PATCH)
public String patch(@PathVariable final String id,
@RequestBody final String body) {
return "Patched " + id + "!";
}
@RequestMapping("/testing123")
public String testing123() {
throw new RuntimeException("myerror");
}
@RequestMapping("/local")
public String local() {
return "Hello local";
}
@RequestMapping(value = "/local", method = RequestMethod.POST)
public String postWithFormParam(HttpServletRequest request,
@RequestBody MultiValueMap<String, String> body) {
return "Posted " + body.get("foo") + " and Content-Length was: " + request.getContentLength() + "!";
}
@RequestMapping(value = "/deletewithbody", method = RequestMethod.DELETE)
public String deleteWithBody(@RequestBody(required = false) String body) {
return "Deleted " + body;
}
@RequestMapping(value = "/local/{id}", method = RequestMethod.DELETE)
public String delete(@PathVariable String id) {
return "Deleted " + id + "!";
}
@RequestMapping(value = "/local/{id}", method = RequestMethod.GET)
public ResponseEntity<?> get(@PathVariable String id) {
if ("notfound".equalsIgnoreCase(id)) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok("Gotten " + id + "!");
}
@RequestMapping(value = "/local/{id}", method = RequestMethod.POST)
public String post(@PathVariable String id, @RequestBody String body) {
return "Posted " + id + "!";
}
@RequestMapping(value = "/qstring")
public String qstring(@RequestParam MultiValueMap<String, String> params) {
return "Received " + params.toString();
}
@RequestMapping("/")
public String home() {
return "Hello world";
}
@RequestMapping("/spa ce")
public String space() {
return "Hello space";
}
@RequestMapping("/slow")
public String slow() {
try {
Thread.sleep(80000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "slow";
}
@Bean
public ZuulFallbackProvider fallbackProvider() {
return new FallbackProvider();
}
@Bean
public ZuulFilter sampleFilter() {
return new ZuulFilter() {
@Override
public String filterType() {
return "pre";
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
if (RequestContext.getCurrentContext().getRequest().getParameterMap()
.containsKey("override")) {
Map<String, List<String>> overridden = new HashMap<>();
overridden.put("key", Arrays.asList("overridden"));
RequestContext.getCurrentContext()
.setRequestQueryParams(overridden);
}
return null;
}
@Override
public int filterOrder() {
return 0;
}
};
}
@Override
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
RequestMappingHandlerMapping mapping = super.requestMappingHandlerMapping();
mapping.setRemoveSemicolonContent(false);
return mapping;
}
}
public static class FallbackProvider implements ZuulFallbackProvider {
@Override
public String getRoute() {
return "simple";
}
@Override
public ClientHttpResponse fallbackResponse() {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return 200;
}
@Override
public String getStatusText() throws IOException {
return null;
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("fallback".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_HTML);
return headers;
}
};
}
}
@Configuration
public class FormEncodedMessageConverterConfiguration extends WebMvcConfigurerAdapter {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
FormHttpMessageConverter converter = new FormHttpMessageConverter();
MediaType mediaType = new MediaType("application", "x-www-form-urlencoded", Charset.forName("UTF-8"));
converter.setSupportedMediaTypes(Arrays.asList(mediaType));
converters.add(converter);
super.configureMessageConverters(converters);
}
}
// Load balancer with fixed server list for "simple" pointing to localhost
@Configuration
public static class SimpleRibbonClientConfiguration {
@Value("${local.server.port}")
private int port;
@Bean
public ServerList<Server> ribbonServerList() {
return new StaticServerList<>(new Server("localhost", this.port));
}
}
@Configuration
public static class AnotherRibbonClientConfiguration {
@Value("${local.server.port}")
private int port;
@Bean
public ServerList<Server> ribbonServerList() {
return new StaticServerList<>(new Server("localhost", this.port));
}
}
public static class MyErrorController extends BasicErrorController {
ThreadLocal<String> uriToMatch = new ThreadLocal<>();
AtomicBoolean controllerUsed = new AtomicBoolean();
public MyErrorController(ErrorAttributes errorAttributes) {
super(errorAttributes, new ErrorProperties());
}
@Override
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
String errorUri = (String) request.getAttribute("javax.servlet.error.request_uri");
if (errorUri != null && errorUri.equals(this.uriToMatch.get())) {
controllerUsed.set(true);
}
this.uriToMatch.remove();
return super.error(request);
}
public void setUriToMatch(String uri) {
this.uriToMatch.set(uri);
}
public boolean wasControllerUsed() {
return this.controllerUsed.get();
}
public void clear() {
this.controllerUsed.set(false);
}
}
}