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; + } +}