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
This commit is contained in:
Dave Syer
2014-12-19 12:51:31 +00:00
parent e7956e199e
commit 9939e0ecd9
5 changed files with 255 additions and 4 deletions

View File

@@ -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

View File

@@ -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();

View File

@@ -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<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);
}
}
}

View File

@@ -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) {

View File

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