From 9939e0ecd92d45d7fdb11dc1aec84f519bbcf883 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Fri, 19 Dec 2014 12:51:31 +0000 Subject: [PATCH] Convert form-encoded request to byte[] for proxy With just a bit more hackery on the Zuul request wrapper we can mask off the input stream and content lengths, and fix them so they contain the expected content. Doesn't work (yet) for multipart content. Fixes gh-109 --- Guardfile | 6 +- .../cloud/netflix/zuul/ZuulConfiguration.java | 6 + .../filters/pre/FormBodyWrapperFilter.java | 118 ++++++++++++++++ .../filters/pre/Servlet30WrapperFilter.java | 2 +- .../zuul/FormZuulProxyApplicationTests.java | 127 ++++++++++++++++++ 5 files changed, 255 insertions(+), 4 deletions(-) create mode 100644 spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/FormBodyWrapperFilter.java create mode 100644 spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/FormZuulProxyApplicationTests.java diff --git a/Guardfile b/Guardfile index 19e40db7..356b3854 100644 --- a/Guardfile +++ b/Guardfile @@ -4,8 +4,8 @@ require 'erb' options = {:mkdirs => true, :safe => :unsafe, :attributes => 'linkcss'} guard 'shell' do - watch(/^src\/[A-Za-z].*\.adoc$/) {|m| - Asciidoctor.load_file('src/main/asciidoc/README.adoc', :to_file => './README.adoc', safe: :safe, parse: false, attributes: 'allow-uri-read') - Asciidoctor.render_file('src/main/asciidoc/spring-cloud-netflix.adoc', options.merge(:to_dir => 'target/generated-docs')) + watch(/^docs\/[A-Za-z].*\.adoc$/) {|m| + Asciidoctor.load_file('docs/src/main/asciidoc/README.adoc', :to_file => './README.adoc', safe: :safe, parse: false, attributes: 'allow-uri-read') + Asciidoctor.render_file('docs/src/main/asciidoc/spring-cloud-netflix.adoc', options.merge(:to_dir => 'target/generated-docs')) } end 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 b396bfbc..89a2f25b 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 @@ -9,6 +9,7 @@ import org.springframework.cloud.context.scope.refresh.RefreshScopeRefreshedEven import org.springframework.cloud.netflix.zuul.filters.post.SendErrorFilter; 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; import org.springframework.cloud.netflix.zuul.filters.pre.Servlet30WrapperFilter; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; @@ -80,6 +81,11 @@ public class ZuulConfiguration { } // pre filters + @Bean + public FormBodyWrapperFilter formBodyWrapperFilter() { + return new FormBodyWrapperFilter(); + } + @Bean public DebugFilter debugFilter() { return new DebugFilter(); diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/FormBodyWrapperFilter.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/FormBodyWrapperFilter.java new file mode 100644 index 00000000..bdd64d12 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/FormBodyWrapperFilter.java @@ -0,0 +1,118 @@ +package org.springframework.cloud.netflix.zuul.filters.pre; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.Map.Entry; + +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.io.Charsets; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +import com.google.common.base.Throwables; +import com.netflix.zuul.ZuulFilter; +import com.netflix.zuul.context.RequestContext; +import com.netflix.zuul.http.HttpServletRequestWrapper; +import com.netflix.zuul.http.ServletInputStreamWrapper; + +/** + * @author Spencer Gibb + */ +public class FormBodyWrapperFilter extends ZuulFilter { + protected Field requestField = null; + + public FormBodyWrapperFilter() { + requestField = ReflectionUtils.findField(HttpServletRequestWrapper.class, "req", + HttpServletRequest.class); + Assert.notNull(requestField, "HttpServletRequestWrapper.req field not found"); + requestField.setAccessible(true); + } + + @Override + public String filterType() { + return "pre"; + } + + @Override + public int filterOrder() { + return -1; + } + + @Override + public boolean shouldFilter() { + RequestContext ctx = RequestContext.getCurrentContext(); + HttpServletRequest request = ctx.getRequest(); + if (MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType())) { + return true; + } + return false; + } + + @Override + public Object run() { + RequestContext ctx = RequestContext.getCurrentContext(); + HttpServletRequest request = ctx.getRequest(); + if (request instanceof HttpServletRequestWrapper) { + try { + HttpServletRequest wrapped = (HttpServletRequest) requestField.get(request); + requestField.set(request, new FormBodyRequestWrapper(wrapped)); + } + catch (IllegalAccessException e) { + Throwables.propagate(e); + } + } + else { + ctx.setRequest(new FormBodyRequestWrapper(request)); + } + return null; + } + + private class FormBodyRequestWrapper extends HttpServletRequestWrapper { + + private HttpServletRequest request; + private byte[] contentData; + + public FormBodyRequestWrapper(HttpServletRequest request) { + super(request); + this.request = request; + } + + @Override + public int getContentLength() { + if (contentData == null) { + contentData = buildContentData(); + } + return contentData.length; + } + + @Override + public ServletInputStream getInputStream() throws IOException { + if (RequestContext.getCurrentContext().isChunkedRequestBody()) { + return request.getInputStream(); + } + else { + if (contentData == null) { + contentData = buildContentData(); + } + return new ServletInputStreamWrapper(contentData); + } + } + + private byte[] buildContentData() { + StringBuilder builder = new StringBuilder(); + for (Entry entry : request.getParameterMap().entrySet()) { + for (String value : entry.getValue()) { + if (builder.length() != 0) { + builder.append("&"); + } + builder.append(entry.getKey()).append("=").append(value); + } + } + return builder.toString().getBytes(Charsets.UTF_8); + } + + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/Servlet30WrapperFilter.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/Servlet30WrapperFilter.java index cde75061..558b414c 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/Servlet30WrapperFilter.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/Servlet30WrapperFilter.java @@ -59,7 +59,7 @@ public class Servlet30WrapperFilter extends ZuulFilter { return null; } - class Servlet30RequestWrapper extends HttpServletRequestWrapper { + private class Servlet30RequestWrapper extends HttpServletRequestWrapper { private HttpServletRequest request; Servlet30RequestWrapper(HttpServletRequest request) { diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/FormZuulProxyApplicationTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/FormZuulProxyApplicationTests.java new file mode 100644 index 00000000..4b6d3ec7 --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/FormZuulProxyApplicationTests.java @@ -0,0 +1,127 @@ +package org.springframework.cloud.netflix.zuul; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.TestRestTemplate; +import org.springframework.cloud.netflix.ribbon.RibbonClient; +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.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +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.RestController; + +import com.netflix.appinfo.EurekaInstanceConfig; +import com.netflix.loadbalancer.BaseLoadBalancer; +import com.netflix.loadbalancer.ILoadBalancer; +import com.netflix.loadbalancer.Server; +import com.netflix.zuul.ZuulFilter; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = FormZuulProxyApplication.class) +@WebAppConfiguration +@IntegrationTest({ "server.port: 0", + "zuul.routes.simple: /simple/**" }) +@DirtiesContext +public class FormZuulProxyApplicationTests { + + @Value("${local.server.port}") + private int port; + + @Autowired + private ProxyRouteLocator routes; + + @Autowired + private RoutesEndpoint endpoint; + + @Test + public void postWithForm() { + MultiValueMap form = new LinkedMultiValueMap(); + form.set("foo", "bar"); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + ResponseEntity result = new TestRestTemplate().exchange( + "http://localhost:" + port + "/simple", HttpMethod.POST, + new HttpEntity>(form, headers), String.class); + assertEquals(HttpStatus.OK, result.getStatusCode()); + assertEquals("Posted! {foo=[bar]}", result.getBody()); + } + +} + +//Don't use @SpringBootApplication because we don't want to component scan +@Configuration +@EnableAutoConfiguration +@RestController +@EnableZuulProxy +@RibbonClient(name = "simple", configuration = FormRibbonClientConfiguration.class) +class FormZuulProxyApplication { + + @RequestMapping(value = "/", method = RequestMethod.POST) + public String delete(@RequestBody MultiValueMap form) { + return "Posted! " + form; + } + + @Bean + public ZuulFilter sampleFilter() { + return new ZuulFilter() { + @Override + public String filterType() { + return "pre"; + } + + @Override + public boolean shouldFilter() { + return true; + } + + @Override + public Object run() { + return null; + } + + @Override + public int filterOrder() { + return 0; + } + }; + } + + public static void main(String[] args) { + SpringApplication.run(SampleZuulProxyApplication.class, args); + } + +} + +//Load balancer with fixed server list for "simple" pointing to localhost +@Configuration +class FormRibbonClientConfiguration { + @Bean + public ILoadBalancer ribbonLoadBalancer(EurekaInstanceConfig instance) { + BaseLoadBalancer balancer = new BaseLoadBalancer(); + balancer.setServersList(Arrays.asList(new Server("localhost", instance + .getNonSecurePort()))); + return balancer; + } +}