Browse Source

Refactor SimpleHostRoutingFilter

Removed deprecated HttpClient APIs, exposed method to override client, fixed ignoredHeaders
pull/6/head
Greg Liebowitz 9 years ago committed by Spencer Gibb
parent
commit
940921ac15
  1. 287
      spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/SimpleHostRoutingFilter.java
  2. 211
      spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/CustomHostRoutingFilterTests.java

287
spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/SimpleHostRoutingFilter.java

@ -19,15 +19,9 @@ package org.springframework.cloud.netflix.zuul.filters.route; @@ -19,15 +19,9 @@ package org.springframework.cloud.netflix.zuul.filters.route;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.URL;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
@ -35,8 +29,7 @@ import java.util.List; @@ -35,8 +29,7 @@ import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
@ -44,30 +37,31 @@ import javax.net.ssl.X509TrustManager; @@ -44,30 +37,31 @@ import javax.net.ssl.X509TrustManager;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.extern.apachecommons.CommonsLog;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.ProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.RedirectStrategy;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicHttpRequest;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.HttpContext;
import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper;
import org.springframework.util.LinkedMultiValueMap;
@ -80,19 +74,11 @@ import com.netflix.zuul.ZuulFilter; @@ -80,19 +74,11 @@ import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.constants.ZuulConstants;
import com.netflix.zuul.context.RequestContext;
import lombok.extern.apachecommons.CommonsLog;
@CommonsLog
@SuppressWarnings("deprecation")
public class SimpleHostRoutingFilter extends ZuulFilter {
public static final String CONTENT_ENCODING = "Content-Encoding";
private static final Runnable CLIENTLOADER = new Runnable() {
@Override
public void run() {
loadClient();
}
};
private static final DynamicIntProperty SOCKET_TIMEOUT = DynamicPropertyFactory
.getInstance().getIntProperty(ZuulConstants.ZUUL_HOST_SOCKET_TIMEOUT_MILLIS,
10000);
@ -101,34 +87,24 @@ public class SimpleHostRoutingFilter extends ZuulFilter { @@ -101,34 +87,24 @@ public class SimpleHostRoutingFilter extends ZuulFilter {
.getInstance().getIntProperty(ZuulConstants.ZUUL_HOST_CONNECT_TIMEOUT_MILLIS,
2000);
private static final AtomicReference<HttpClient> CLIENT = new AtomicReference<HttpClient>(
newClient());
private final Timer connectionManagerTimer = new Timer(
"SimpleHostRoutingFilter.connectionManagerTimer", true);
private static final Timer CONNECTION_MANAGER_TIMER = new Timer(
"SimpleHostRoutingFilter.CONNECTION_MANAGER_TIMER", true);
private ProxyRequestHelper helper;
private PoolingHttpClientConnectionManager connectionManager;
private CloseableHttpClient httpClient;
// cleans expired connections at an interval
static {
SOCKET_TIMEOUT.addCallback(CLIENTLOADER);
CONNECTION_TIMEOUT.addCallback(CLIENTLOADER);
CONNECTION_MANAGER_TIMER.schedule(new TimerTask() {
@Override
public void run() {
try {
final HttpClient hc = CLIENT.get();
if (hc == null) {
return;
}
hc.getConnectionManager().closeExpiredConnections();
}
catch (Throwable ex) {
log.error("error closing expired connections", ex);
}
private final Runnable clientloader = new Runnable() {
@Override
public void run() {
try {
httpClient.close();
} catch (IOException ex) {
log.error("error closing client", ex);
}
}, 30000, 5000);
}
private ProxyRequestHelper helper;
httpClient = newClient();
}
};
public SimpleHostRoutingFilter() {
this(new ProxyRequestHelper());
@ -138,9 +114,25 @@ public class SimpleHostRoutingFilter extends ZuulFilter { @@ -138,9 +114,25 @@ public class SimpleHostRoutingFilter extends ZuulFilter {
this.helper = helper;
}
@PostConstruct
private void initialize() {
this.httpClient = newClient();
SOCKET_TIMEOUT.addCallback(clientloader);
CONNECTION_TIMEOUT.addCallback(clientloader);
connectionManagerTimer.schedule(new TimerTask() {
@Override
public void run() {
if (connectionManager == null) {
return;
}
connectionManager.closeExpiredConnections();
}
}, 30000, 5000);
}
@PreDestroy
public void stop() {
CONNECTION_MANAGER_TIMER.cancel();
connectionManagerTimer.cancel();
}
@Override
@ -169,12 +161,11 @@ public class SimpleHostRoutingFilter extends ZuulFilter { @@ -169,12 +161,11 @@ public class SimpleHostRoutingFilter extends ZuulFilter {
.buildZuulRequestQueryParams(request);
String verb = getVerb(request);
InputStream requestEntity = getRequestBody(request);
HttpClient httpclient = CLIENT.get();
String uri = this.helper.buildZuulRequestURI(request);
try {
HttpResponse response = forward(httpclient, verb, uri, request, headers,
HttpResponse response = forward(httpClient, verb, uri, request, headers,
params, requestEntity);
setResponse(response);
}
@ -185,9 +176,66 @@ public class SimpleHostRoutingFilter extends ZuulFilter { @@ -185,9 +176,66 @@ public class SimpleHostRoutingFilter extends ZuulFilter {
return null;
}
protected PoolingHttpClientConnectionManager newConnectionManager() {
try {
final SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, new TrustManager[]{new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}}, new SecureRandom());
final Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.INSTANCE)
.register("https", new SSLConnectionSocketFactory(sslContext))
.build();
connectionManager = new PoolingHttpClientConnectionManager(registry);
connectionManager.setMaxTotal(Integer.parseInt(System.getProperty("zuul.max.host.connections", "200")));
connectionManager.setDefaultMaxPerRoute(Integer.parseInt(System.getProperty("zuul.max.host.connections", "20")));
return connectionManager;
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
protected CloseableHttpClient newClient() {
final RequestConfig requestConfig = RequestConfig.custom()
.setSocketTimeout(SOCKET_TIMEOUT.get())
.setConnectTimeout(CONNECTION_TIMEOUT.get())
.setCookieSpec(CookieSpecs.IGNORE_COOKIES)
.build();
return HttpClients.custom()
.setConnectionManager(newConnectionManager())
.setDefaultRequestConfig(requestConfig)
.setRetryHandler(new DefaultHttpRequestRetryHandler(0, false))
.setRedirectStrategy(new RedirectStrategy() {
@Override
public boolean isRedirected(HttpRequest request, HttpResponse response, HttpContext context) throws ProtocolException {
return false;
}
@Override
public HttpUriRequest getRedirect(HttpRequest request, HttpResponse response, HttpContext context) throws ProtocolException {
return null;
}
})
.build();
}
private HttpResponse forward(HttpClient httpclient, String verb, String uri,
HttpServletRequest request, MultiValueMap<String, String> headers,
MultiValueMap<String, String> params, InputStream requestEntity)
HttpServletRequest request, MultiValueMap<String, String> headers,
MultiValueMap<String, String> params, InputStream requestEntity)
throws Exception {
Map<String, Object> info = this.helper.debug(verb, uri, headers, params,
requestEntity);
@ -306,117 +354,12 @@ public class SimpleHostRoutingFilter extends ZuulFilter { @@ -306,117 +354,12 @@ public class SimpleHostRoutingFilter extends ZuulFilter {
revertHeaders(response.getAllHeaders()));
}
private static ClientConnectionManager newConnectionManager() throws Exception {
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null, null);
SSLSocketFactory sf = new MySSLSocketFactory(trustStore);
sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
registry.register(new Scheme("https", sf, 443));
ThreadSafeClientConnManager cm = new ThreadSafeClientConnManager(registry);
cm.setMaxTotal(Integer.parseInt(System.getProperty("zuul.max.host.connections",
"200")));
cm.setDefaultMaxPerRoute(Integer.parseInt(System.getProperty(
"zuul.max.host.connections", "20")));
return cm;
}
private static void loadClient() {
final HttpClient oldClient = CLIENT.get();
CLIENT.set(newClient());
if (oldClient != null) {
CONNECTION_MANAGER_TIMER.schedule(new TimerTask() {
@Override
public void run() {
try {
oldClient.getConnectionManager().shutdown();
}
catch (Throwable ex) {
log.error("error shutting down old connection manager", ex);
}
}
}, 30000);
}
}
private static HttpClient newClient() {
// I could statically cache the connection manager but we will probably want to
// make some of its properties
// dynamic in the near future also
try {
DefaultHttpClient httpclient = new DefaultHttpClient(newConnectionManager());
HttpParams httpParams = httpclient.getParams();
httpParams.setIntParameter(CoreConnectionPNames.SO_TIMEOUT,
SOCKET_TIMEOUT.get());
httpParams.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT,
CONNECTION_TIMEOUT.get());
httpclient.setHttpRequestRetryHandler(new DefaultHttpRequestRetryHandler(0,
false));
httpParams.setParameter(ClientPNames.COOKIE_POLICY,
org.apache.http.client.params.CookiePolicy.IGNORE_COOKIES);
httpclient.setRedirectStrategy(new org.apache.http.client.RedirectStrategy() {
@Override
public boolean isRedirected(HttpRequest httpRequest,
HttpResponse httpResponse, HttpContext httpContext) {
return false;
}
@Override
public org.apache.http.client.methods.HttpUriRequest getRedirect(
HttpRequest httpRequest, HttpResponse httpResponse,
HttpContext httpContext) {
return null;
}
});
return httpclient;
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
}
public static class MySSLSocketFactory extends SSLSocketFactory {
private SSLContext sslContext = SSLContext.getInstance("TLS");
public MySSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException,
KeyManagementException, KeyStoreException, UnrecoverableKeyException {
super(truststore);
TrustManager tm = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
TrustManager[] tms = new TrustManager[1];
tms[0] = tm;
this.sslContext.init(null, tms, null);
}
@Override
public Socket createSocket(Socket socket, String host, int port, boolean autoClose)
throws IOException, UnknownHostException {
return this.sslContext.getSocketFactory().createSocket(socket, host, port,
autoClose);
}
@Override
public Socket createSocket() throws IOException {
return this.sslContext.getSocketFactory().createSocket();
}
/**
* Add header names to exclude from proxied response in the current request.
* @param names
*/
protected void addIgnoredHeaders(String... names) {
helper.addIgnoredHeaders(names);
}
}

211
spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/CustomHostRoutingFilterTests.java

@ -0,0 +1,211 @@ @@ -0,0 +1,211 @@
package org.springframework.cloud.netflix.zuul.filters;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
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.zuul.EnableZuulProxy;
import org.springframework.cloud.netflix.zuul.RoutesEndpoint;
import org.springframework.cloud.netflix.zuul.ZuulProxyConfiguration;
import org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
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.PathVariable;
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.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.assertTrue;
import static org.junit.Assert.assertEquals;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = SampleCustomZuulProxyApplication.class)
@WebAppConfiguration
@IntegrationTest({"server.port: 0", "server.contextPath: /app"})
@DirtiesContext
public class CustomHostRoutingFilterTests {
@Value("${local.server.port}")
private int port;
@Autowired
private ProxyRouteLocator routes;
@Autowired
private RoutesEndpoint endpoint;
@Test
public void getOnSelfViaCustomHostRoutingFilter() {
this.routes.addRoute("/self/**", "http://localhost:" + this.port + "/app");
this.endpoint.reset();
ResponseEntity<String> result = new TestRestTemplate().getForEntity(
"http://localhost:" + this.port + "/app/self/get/1", String.class);
assertEquals(HttpStatus.OK, result.getStatusCode());
assertEquals("Get 1", result.getBody());
}
@Test
public void postOnSelfViaCustomHostRoutingFilter() {
this.routes.addRoute("/self/**", "http://localhost:" + this.port + "/app");
this.endpoint.reset();
MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
params.add("id", "2");
ResponseEntity<String> result = new TestRestTemplate().postForEntity(
"http://localhost:" + this.port + "/app/self/post", params, String.class);
assertEquals(HttpStatus.OK, result.getStatusCode());
assertEquals("Post 2", result.getBody());
}
@Test
public void putOnSelfViaCustomHostRoutingFilter() {
this.routes.addRoute("/self/**", "http://localhost:" + this.port + "/app");
this.endpoint.reset();
ResponseEntity<String> result = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/app/self/put/3", HttpMethod.PUT,
new HttpEntity<>((Void) null), String.class);
assertEquals(HttpStatus.OK, result.getStatusCode());
assertEquals("Put 3", result.getBody());
}
@Test
public void patchOnSelfViaCustomHostRoutingFilter() {
this.routes.addRoute("/self/**", "http://localhost:" + this.port + "/app");
this.endpoint.reset();
MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
params.add("patch", "5");
ResponseEntity<String> result = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/app/self/patch/4", HttpMethod.PATCH,
new HttpEntity<>(params), String.class);
assertEquals(HttpStatus.OK, result.getStatusCode());
assertEquals("Patch 45", result.getBody());
}
@Test
public void getOnSelfIgnoredHeaders() {
this.routes.addRoute("/self/**", "http://localhost:" + this.port + "/app");
this.endpoint.reset();
ResponseEntity<String> result = new TestRestTemplate().getForEntity(
"http://localhost:" + this.port + "/app/self/get/1", String.class);
assertEquals(HttpStatus.OK, result.getStatusCode());
assertTrue(result.getHeaders().containsKey("X-NotIgnored"));
assertFalse(result.getHeaders().containsKey("X-Ignored"));
}
@Test
public void getOnSelfWithSessionCookie() {
this.routes.addRoute("/self/**", "http://localhost:" + this.port + "/app");
this.endpoint.reset();
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> result1 = restTemplate.getForEntity(
"http://localhost:" + this.port + "/app/self/cookie/1", String.class);
ResponseEntity<String> result2 = restTemplate.getForEntity(
"http://localhost:" + this.port + "/app/self/cookie/2", String.class);
assertEquals("SetCookie 1", result1.getBody());
assertEquals("GetCookie 1", result2.getBody());
}
}
@Configuration
@EnableAutoConfiguration
@RestController
class SampleCustomZuulProxyApplication {
@RequestMapping(value = "/get/{id}", method = RequestMethod.GET)
public String get(@PathVariable String id, HttpServletResponse response) {
response.setHeader("X-Ignored", "foo");
response.setHeader("X-NotIgnored", "bar");
return "Get " + id;
}
@RequestMapping(value = "/cookie/{id}", method = RequestMethod.GET)
public String getWithCookie(@PathVariable String id, HttpSession session) {
Object testCookie = session.getAttribute("testCookie");
if (testCookie != null) {
return "GetCookie " + testCookie;
}
session.setAttribute("testCookie", id);
return "SetCookie " + id;
}
@RequestMapping(value = "/post", method = RequestMethod.POST)
public String post(@RequestParam("id") String id) {
return "Post " + id;
}
@RequestMapping(value = "/put/{id}", method = RequestMethod.PUT)
public String put(@PathVariable String id) {
return "Put " + id;
}
@RequestMapping(value = "/patch/{id}", method = RequestMethod.PATCH)
public String patch(@PathVariable String id, @RequestParam("patch") String patch) {
return "Patch " + id + patch;
}
public static void main(String[] args) {
SpringApplication.run(SampleCustomZuulProxyApplication.class, args);
}
@Configuration
@EnableZuulProxy
protected static class CustomZuulProxyConfig extends ZuulProxyConfiguration {
@Bean
@Override
public SimpleHostRoutingFilter simpleHostRoutingFilter() {
return new CustomHostRoutingFilter();
}
private class CustomHostRoutingFilter extends SimpleHostRoutingFilter {
@Override
public Object run() {
super.addIgnoredHeaders("X-Ignored");
return super.run();
}
@Override
protected CloseableHttpClient newClient() {
// Custom client with cookie support.
// In practice, we would want a custom cookie store using a multimap with a user key.
return HttpClients.custom()
.setConnectionManager(newConnectionManager())
.setDefaultCookieStore(new BasicCookieStore())
.setDefaultRequestConfig(RequestConfig.custom()
.setCookieSpec(CookieSpecs.DEFAULT)
.build())
.build();
}
}
}
}
Loading…
Cancel
Save