diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/SimpleHostRoutingFilter.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/SimpleHostRoutingFilter.java index 8e120965..1488092a 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/SimpleHostRoutingFilter.java +++ b/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; 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; 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; 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; 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 { .getInstance().getIntProperty(ZuulConstants.ZUUL_HOST_CONNECT_TIMEOUT_MILLIS, 2000); - private static final AtomicReference CLIENT = new AtomicReference( - newClient()); - - private static final Timer CONNECTION_MANAGER_TIMER = new Timer( - "SimpleHostRoutingFilter.CONNECTION_MANAGER_TIMER", true); - - // 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); - } - } - }, 30000, 5000); - } + private final Timer connectionManagerTimer = new Timer( + "SimpleHostRoutingFilter.connectionManagerTimer", true); private ProxyRequestHelper helper; + private PoolingHttpClientConnectionManager connectionManager; + private CloseableHttpClient httpClient; + + private final Runnable clientloader = new Runnable() { + @Override + public void run() { + try { + httpClient.close(); + } catch (IOException ex) { + log.error("error closing client", ex); + } + httpClient = newClient(); + } + }; public SimpleHostRoutingFilter() { this(new ProxyRequestHelper()); @@ -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 { .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 { 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 registry = RegistryBuilder.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 headers, - MultiValueMap params, InputStream requestEntity) + HttpServletRequest request, MultiValueMap headers, + MultiValueMap params, InputStream requestEntity) throws Exception { Map info = this.helper.debug(verb, uri, headers, params, requestEntity); @@ -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); } } diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/CustomHostRoutingFilterTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/CustomHostRoutingFilterTests.java new file mode 100644 index 00000000..59e79b22 --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/CustomHostRoutingFilterTests.java @@ -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 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 params = new LinkedMultiValueMap<>(); + params.add("id", "2"); + ResponseEntity 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 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 params = new LinkedMultiValueMap<>(); + params.add("patch", "5"); + ResponseEntity 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 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 result1 = restTemplate.getForEntity( + "http://localhost:" + this.port + "/app/self/cookie/1", String.class); + + ResponseEntity 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(); + } + } + } + +}