Commit 9939e0ec by Dave Syer

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
parent e7956e19
......@@ -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
......@@ -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;
......@@ -81,6 +82,11 @@ public class ZuulConfiguration {
// pre filters
@Bean
public FormBodyWrapperFilter formBodyWrapperFilter() {
return new FormBodyWrapperFilter();
}
@Bean
public DebugFilter debugFilter() {
return new DebugFilter();
}
......
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<String, String[]> 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);
}
}
}
......@@ -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) {
......
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<String, String> form = new LinkedMultiValueMap<String, String>();
form.set("foo", "bar");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
ResponseEntity<String> result = new TestRestTemplate().exchange(
"http://localhost:" + port + "/simple", HttpMethod.POST,
new HttpEntity<MultiValueMap<String,String>>(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<String, String> 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;
}
}
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