From 8fbba2b0933a4c61f5b2b6e2649a660f6d0ee93c Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Wed, 6 Aug 2014 01:47:47 -0600 Subject: [PATCH] @EnableZuulProxy initial implementation --- spring-platform-netflix-core/pom.xml | 10 + .../eureka/EurekaInstanceConfigBean.java | 2 +- .../netflix/feign/FeignConfigurer.java | 4 +- .../netflix/zuul/EnableZuulProxy.java | 15 + .../netflix/zuul/FilterIntializer.java | 72 +++ .../platform/netflix/zuul/RibbonCommand.java | 2 +- .../platform/netflix/zuul/Routes.java | 17 +- .../platform/netflix/zuul/SpringFilter.java | 2 +- .../netflix/zuul/ZuulProxyConfiguration.java | 94 ++++ .../netflix/zuul/ZuulProxyProperties.java | 13 + .../zuul/filters/post/SendResponseFilter.java | 181 +++++++ .../zuul/filters/post/StatsFilter.java | 50 ++ .../netflix/zuul/filters/pre/DebugFilter.java | 48 ++ .../zuul/filters/pre/DebugRequestFilter.java | 62 +++ .../zuul/filters/pre/PreDecorationFilter.java | 76 +++ .../filters/route/RibbonRoutingFilter.java | 302 ++++++++++++ .../route/SimpleHostRoutingFilter.java | 456 ++++++++++++++++++ .../netflix/zuul/sample/Application.java | 34 ++ .../netflix/zuul/sample/ApplicationTests.java | 18 + .../src/test/resources/application.yml | 14 +- .../platform/netflix/zuul/Application.java | 1 + .../netflix/zuul/FilterIntializer.java | 9 +- .../filters/pre/PreDecorationFilter.groovy | 4 +- .../filters/route/RibbonRequest.groovy | 6 +- 24 files changed, 1465 insertions(+), 27 deletions(-) create mode 100644 spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/EnableZuulProxy.java create mode 100644 spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/FilterIntializer.java rename {spring-platform-netflix-zuul/src/main/java/io/spring => spring-platform-netflix-core/src/main/java/org/springframework}/platform/netflix/zuul/RibbonCommand.java (98%) rename {spring-platform-netflix-zuul/src/main/java/io/spring => spring-platform-netflix-core/src/main/java/org/springframework}/platform/netflix/zuul/Routes.java (88%) rename {spring-platform-netflix-zuul/src/main/java/io/spring => spring-platform-netflix-core/src/main/java/org/springframework}/platform/netflix/zuul/SpringFilter.java (96%) create mode 100644 spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/ZuulProxyConfiguration.java create mode 100644 spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/ZuulProxyProperties.java create mode 100644 spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/filters/post/SendResponseFilter.java create mode 100644 spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/filters/post/StatsFilter.java create mode 100644 spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/filters/pre/DebugFilter.java create mode 100644 spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/filters/pre/DebugRequestFilter.java create mode 100644 spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/filters/pre/PreDecorationFilter.java create mode 100644 spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/filters/route/RibbonRoutingFilter.java create mode 100644 spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/filters/route/SimpleHostRoutingFilter.java create mode 100644 spring-platform-netflix-core/src/test/java/org/springframework/platform/netflix/zuul/sample/Application.java create mode 100644 spring-platform-netflix-core/src/test/java/org/springframework/platform/netflix/zuul/sample/ApplicationTests.java diff --git a/spring-platform-netflix-core/pom.xml b/spring-platform-netflix-core/pom.xml index 0031b704..6b27c259 100644 --- a/spring-platform-netflix-core/pom.xml +++ b/spring-platform-netflix-core/pom.xml @@ -81,6 +81,16 @@ ribbon-eureka true + + com.netflix.ribbon + ribbon-httpclient + true + + + com.netflix.zuul + zuul-core + true + org.projectlombok lombok diff --git a/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/eureka/EurekaInstanceConfigBean.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/eureka/EurekaInstanceConfigBean.java index 62ecf112..01a6ce98 100644 --- a/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/eureka/EurekaInstanceConfigBean.java +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/eureka/EurekaInstanceConfigBean.java @@ -66,7 +66,7 @@ public class EurekaInstanceConfigBean implements EurekaInstanceConfig { private int leaseExpirationDurationInSeconds = 90; - @Value("${spring.application.name:unknown}.mydomain.net") + @Value("${spring.application.name:unknown}") //TODO: why is .mydomain.net here? private String virtualHostName; private String secureVirtualHostName; diff --git a/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/feign/FeignConfigurer.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/feign/FeignConfigurer.java index 98907b09..943e7918 100644 --- a/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/feign/FeignConfigurer.java +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/feign/FeignConfigurer.java @@ -50,11 +50,11 @@ public class FeignConfigurer { protected T loadBalance(Feign.Builder builder, Class type, String schemeName) { String name = URI.create(schemeName).getHost(); - setServiceListClass(name); + setServiceListClassAndVIP(name); return builder.target(LoadBalancingTarget.create(type, schemeName)); } - public static void setServiceListClass(String serviceId) { + public static void setServiceListClassAndVIP(String serviceId) { setProp(serviceId, "NIWSServerListClassName", DiscoveryEnabledNIWSServerList.class.getName()); setProp(serviceId, "DeploymentContextBasedVipAddresses", serviceId); //FIXME: what should this be? } diff --git a/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/EnableZuulProxy.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/EnableZuulProxy.java new file mode 100644 index 00000000..a938b665 --- /dev/null +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/EnableZuulProxy.java @@ -0,0 +1,15 @@ +package org.springframework.platform.netflix.zuul; + +import java.lang.annotation.*; + +import org.springframework.context.annotation.Import; + +/** + * @author Spencer Gibb + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Import(ZuulProxyConfiguration.class) +public @interface EnableZuulProxy { +} diff --git a/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/FilterIntializer.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/FilterIntializer.java new file mode 100644 index 00000000..4bcbe67d --- /dev/null +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/FilterIntializer.java @@ -0,0 +1,72 @@ +package org.springframework.platform.netflix.zuul; + +import com.netflix.zuul.FilterFileManager; +import com.netflix.zuul.FilterLoader; +import com.netflix.zuul.ZuulFilter; +import com.netflix.zuul.filters.FilterRegistry; +import com.netflix.zuul.groovy.GroovyCompiler; +import com.netflix.zuul.groovy.GroovyFileFilter; +import com.netflix.zuul.monitoring.MonitoringHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import java.util.List; +import java.util.Map; + +/** + * User: spencergibb + * Date: 4/24/14 + * Time: 9:23 PM + * TODO: .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + */ +public class FilterIntializer implements ServletContextListener { + + private static final Logger LOGGER = LoggerFactory.getLogger(FilterIntializer.class); + + @Autowired + private Map filters; + + @Override + public void contextInitialized(ServletContextEvent sce) { + + LOGGER.info("Starting filter initialzer context listener"); + + //FIXME: mocks monitoring infrastructure as we don't need it for this simple app + MonitoringHelper.initMocks(); + + FilterRegistry registry = FilterRegistry.instance(); + + for (Map.Entry entry : filters.entrySet()) { + registry.put(entry.getKey(), entry.getValue()); + } + } + + @Override + public void contextDestroyed(ServletContextEvent sce) { + LOGGER.info("Stopping filter initializer context listener"); + } + + /*private void initGroovyFilterManager() { + + //TODO: support groovy filters loaded from filesystem in proxy + FilterLoader.getInstance().setCompiler(new GroovyCompiler()); + + final String scriptRoot = props.getFilterRoot(); + LOGGER.info("Using file system script: " + scriptRoot); + + try { + FilterFileManager.setFilenameFilter(new GroovyFileFilter()); + FilterFileManager.init(5, + scriptRoot + "/pre", + scriptRoot + "/route", + scriptRoot + "/post" + ); + } + catch (Exception e) { + throw new RuntimeException(e); + } + }*/ +} diff --git a/spring-platform-netflix-zuul/src/main/java/io/spring/platform/netflix/zuul/RibbonCommand.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/RibbonCommand.java similarity index 98% rename from spring-platform-netflix-zuul/src/main/java/io/spring/platform/netflix/zuul/RibbonCommand.java rename to spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/RibbonCommand.java index acdcbc77..1d30d43a 100644 --- a/spring-platform-netflix-zuul/src/main/java/io/spring/platform/netflix/zuul/RibbonCommand.java +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/RibbonCommand.java @@ -1,4 +1,4 @@ -package io.spring.platform.netflix.zuul; +package org.springframework.platform.netflix.zuul; import com.netflix.client.http.HttpRequest; import com.netflix.client.http.HttpResponse; diff --git a/spring-platform-netflix-zuul/src/main/java/io/spring/platform/netflix/zuul/Routes.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/Routes.java similarity index 88% rename from spring-platform-netflix-zuul/src/main/java/io/spring/platform/netflix/zuul/Routes.java rename to spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/Routes.java index ae532151..41c83f50 100644 --- a/spring-platform-netflix-zuul/src/main/java/io/spring/platform/netflix/zuul/Routes.java +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/Routes.java @@ -1,4 +1,4 @@ -package io.spring.platform.netflix.zuul; +package org.springframework.platform.netflix.zuul; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,9 +22,20 @@ public class Routes { @Autowired ConfigurableEnvironment env; - private final Field propertySourcesField; + private Field propertySourcesField; + private String keyPrefix; public Routes() { + keyPrefix = "zuul.route."; + initField(); + } + + public Routes(String keyPrefix) { + this.keyPrefix = keyPrefix; + initField(); + } + + private void initField() { propertySourcesField = ReflectionUtils.findField(CompositePropertySource.class, "propertySources"); propertySourcesField.setAccessible(true); } @@ -63,7 +74,7 @@ public class Routes { //EnumerablePropertySource enumerable = (EnumerablePropertySource) propertySource; MutablePropertySources propertySources = new MutablePropertySources(); propertySources.addLast(propertySource); - Map routeEntries = PropertySourceUtils.getSubProperties(propertySources, "zuul.route."); + Map routeEntries = PropertySourceUtils.getSubProperties(propertySources, keyPrefix); for (Map.Entry entry : routeEntries.entrySet()) { String serviceId = entry.getKey(); String route = entry.getValue().toString(); diff --git a/spring-platform-netflix-zuul/src/main/java/io/spring/platform/netflix/zuul/SpringFilter.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/SpringFilter.java similarity index 96% rename from spring-platform-netflix-zuul/src/main/java/io/spring/platform/netflix/zuul/SpringFilter.java rename to spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/SpringFilter.java index db42a98d..bba3a258 100644 --- a/spring-platform-netflix-zuul/src/main/java/io/spring/platform/netflix/zuul/SpringFilter.java +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/SpringFilter.java @@ -1,4 +1,4 @@ -package io.spring.platform.netflix.zuul; +package org.springframework.platform.netflix.zuul; import com.netflix.zuul.ZuulFilter; import org.springframework.web.context.WebApplicationContext; diff --git a/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/ZuulProxyConfiguration.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/ZuulProxyConfiguration.java new file mode 100644 index 00000000..d263dc2b --- /dev/null +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/ZuulProxyConfiguration.java @@ -0,0 +1,94 @@ +package org.springframework.platform.netflix.zuul; + +import com.netflix.zuul.context.ContextLifecycleFilter; +import com.netflix.zuul.http.ZuulServlet; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.context.embedded.FilterRegistrationBean; +import org.springframework.boot.context.embedded.ServletRegistrationBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.platform.netflix.zuul.filters.post.SendResponseFilter; +import org.springframework.platform.netflix.zuul.filters.post.StatsFilter; +import org.springframework.platform.netflix.zuul.filters.pre.DebugFilter; +import org.springframework.platform.netflix.zuul.filters.pre.DebugRequestFilter; +import org.springframework.platform.netflix.zuul.filters.pre.PreDecorationFilter; +import org.springframework.platform.netflix.zuul.filters.route.RibbonRoutingFilter; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * @author Spencer Gibb + */ +@Configuration +@EnableConfigurationProperties(ZuulProxyProperties.class) +@ConditionalOnClass(ZuulServlet.class) +@ConditionalOnExpression("${zuul.proxy.enabled:true}") +public class ZuulProxyConfiguration { + + @Autowired + private ZuulProxyProperties props; + + @Bean + public FilterRegistrationBean contextLifecycleFilter() { + Collection urlPatterns = new ArrayList<>(); + urlPatterns.add(props.getMapping()+"/*"); + + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new ContextLifecycleFilter()); + filterRegistrationBean.setUrlPatterns(urlPatterns); + + return filterRegistrationBean; + } + + @Bean + public ServletRegistrationBean zuulServlet() { + return new ServletRegistrationBean(new ZuulServlet(), props.getMapping()+"/*"); + } + + @Bean + Routes routes() { + return new Routes("zuul.proxy.route."); + } + + @Bean + FilterIntializer filterIntializer() { + return new FilterIntializer(); + } + + // pre filters + @Bean + public DebugFilter debugFilter() { + return new DebugFilter(); + } + + @Bean + public DebugRequestFilter debugRequestFilter() { + return new DebugRequestFilter(); + } + + @Bean + public PreDecorationFilter preDecorationFilter() { + return new PreDecorationFilter(); + } + + // route filters + @Bean + public RibbonRoutingFilter ribbonRoutingFilter() { + return new RibbonRoutingFilter(); + } + + // post filters + @Bean + public SendResponseFilter sendResponseFilter() { + return new SendResponseFilter(); + } + + @Bean + public StatsFilter statsFilter() { + return new StatsFilter(); + } + +} diff --git a/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/ZuulProxyProperties.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/ZuulProxyProperties.java new file mode 100644 index 00000000..9f62b76a --- /dev/null +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/ZuulProxyProperties.java @@ -0,0 +1,13 @@ +package org.springframework.platform.netflix.zuul; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Created by sgibb on 8/5/14. + */ +@Data +@ConfigurationProperties("zuul.proxy") +public class ZuulProxyProperties { + private String mapping = "/proxy"; +} diff --git a/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/filters/post/SendResponseFilter.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/filters/post/SendResponseFilter.java new file mode 100644 index 00000000..b2ab31f7 --- /dev/null +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/filters/post/SendResponseFilter.java @@ -0,0 +1,181 @@ +package org.springframework.platform.netflix.zuul.filters.post; + +import com.google.common.base.Throwables; +import com.netflix.config.DynamicBooleanProperty; + +import com.netflix.config.DynamicIntProperty; +import com.netflix.config.DynamicPropertyFactory; +import com.netflix.util.Pair; +import com.netflix.zuul.ZuulFilter; +import com.netflix.zuul.constants.ZuulConstants; +import com.netflix.zuul.constants.ZuulHeaders; +import com.netflix.zuul.context.Debug; +import com.netflix.zuul.context.RequestContext; + +import javax.servlet.http.HttpServletResponse; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; +import java.util.zip.GZIPInputStream; + +public class SendResponseFilter extends ZuulFilter { + + static DynamicBooleanProperty INCLUDE_DEBUG_HEADER = + DynamicPropertyFactory.getInstance().getBooleanProperty(ZuulConstants.ZUUL_INCLUDE_DEBUG_HEADER, false); + + static DynamicIntProperty INITIAL_STREAM_BUFFER_SIZE = + DynamicPropertyFactory.getInstance().getIntProperty(ZuulConstants.ZUUL_INITIAL_STREAM_BUFFER_SIZE, 1024); + + static DynamicBooleanProperty SET_CONTENT_LENGTH = DynamicPropertyFactory.getInstance().getBooleanProperty(ZuulConstants.ZUUL_SET_CONTENT_LENGTH, false); + + @Override + public String filterType() { + return "post"; + } + + @Override + public int filterOrder() { + return 1000; + } + + public boolean shouldFilter() { + return !RequestContext.getCurrentContext().getZuulResponseHeaders().isEmpty() || + RequestContext.getCurrentContext().getResponseDataStream() != null || + RequestContext.getCurrentContext().getResponseBody() != null; + } + + public Object run() { + try { + addResponseHeaders(); + writeResponse(); + } catch (Exception e) { + Throwables.propagate(e); + } + return null; + } + + void writeResponse() throws Exception { + RequestContext context = RequestContext.getCurrentContext(); + + // there is no body to send + if (context.getResponseBody() == null && context.getResponseDataStream() == null) return; + + HttpServletResponse servletResponse = context.getResponse(); + servletResponse.setCharacterEncoding("UTF-8"); + + OutputStream outStream = servletResponse.getOutputStream(); + InputStream is = null; + try { + if (RequestContext.getCurrentContext().getResponseBody() != null) { + String body = RequestContext.getCurrentContext().getResponseBody(); + writeResponse(new ByteArrayInputStream(body.getBytes()), outStream); + return; + } + + boolean isGzipRequested = false; + final String requestEncoding = context.getRequest().getHeader(ZuulHeaders.ACCEPT_ENCODING); + if (requestEncoding != null && requestEncoding.equals("gzip")) + isGzipRequested = true; + + is = context.getResponseDataStream(); + InputStream inputStream = is; + if (is != null) { + if (context.sendZuulResponse()) { + // if origin response is gzipped, and client has not requested gzip, decompress stream + // before sending to client + // else, stream gzip directly to client + if (context.getResponseGZipped() && !isGzipRequested) + try { + inputStream = new GZIPInputStream(is); + + } catch (java.util.zip.ZipException e) { + System.out.println("gzip expected but not received assuming unencoded response" + + RequestContext.getCurrentContext().getRequest().getRequestURL().toString()); + inputStream = is; + } + else if (context.getResponseGZipped() && isGzipRequested) + servletResponse.setHeader(ZuulHeaders.CONTENT_ENCODING, "gzip"); + writeResponse(inputStream, outStream); + } + } + + } finally { + try { + if (is != null) + is.close(); + + outStream.flush(); + outStream.close(); + } catch (IOException e) { + + } + } + } + + private void writeResponse(InputStream zin, OutputStream out) throws Exception { + byte[] bytes = new byte[INITIAL_STREAM_BUFFER_SIZE.get()]; + int bytesRead = -1; + while ((bytesRead = zin.read(bytes)) != -1) { +// if (Debug.debugRequest() && !Debug.debugRequestHeadersOnly()) { +// Debug.addRequestDebug("OUTBOUND: < " + new String(bytes, 0, bytesRead)); +// } + + try { + out.write(bytes, 0, bytesRead); + out.flush(); + } catch (IOException e) { + //ignore + e.printStackTrace(); + } + + // doubles buffer size if previous read filled it + if (bytesRead == bytes.length) { + bytes = new byte[bytes.length * 2]; + } + } + } + + private void addResponseHeaders() { + RequestContext context = RequestContext.getCurrentContext(); + HttpServletResponse servletResponse = context.getResponse(); + List> zuulResponseHeaders = context.getZuulResponseHeaders(); + String debugHeader = ""; + + List rd = (List) RequestContext.getCurrentContext().get("routingDebug"); + for (String it : rd) { + debugHeader += "[[["+it+"]]]"; + } + + /* + rd = (List) RequestContext.getCurrentContext().get("requestDebug"); + rd?.each { + debugHeader += "[[[REQUEST_DEBUG::${it}]]]"; + } + */ + + if (INCLUDE_DEBUG_HEADER.get()) servletResponse.addHeader("X-Zuul-Debug-Header", debugHeader); + + if (Debug.debugRequest() && zuulResponseHeaders != null) { + for (Pair it : zuulResponseHeaders) { + servletResponse.addHeader(it.first(), it.second()); + Debug.addRequestDebug("OUTBOUND: < " + it.first() + ":" + it.second()); + } + } else if (zuulResponseHeaders != null) { + for (Pair it : zuulResponseHeaders) { + servletResponse.addHeader(it.first(), it.second()); + } + } + + RequestContext ctx = RequestContext.getCurrentContext(); + Integer contentLength = ctx.getOriginContentLength(); + + // only inserts Content-Length if origin provides it and origin response is not gzipped + if (SET_CONTENT_LENGTH.get()) { + if (contentLength != null && !ctx.getResponseGZipped()) + servletResponse.setContentLength(contentLength); + } + } + +} \ No newline at end of file diff --git a/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/filters/post/StatsFilter.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/filters/post/StatsFilter.java new file mode 100644 index 00000000..d3959000 --- /dev/null +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/filters/post/StatsFilter.java @@ -0,0 +1,50 @@ +package org.springframework.platform.netflix.zuul.filters.post; + +import com.netflix.zuul.ZuulFilter; + +import com.netflix.zuul.context.RequestContext; + +import java.util.List; + +public class StatsFilter extends ZuulFilter { + @Override + public String filterType() { + return "post"; + } + + @Override + public int filterOrder() { + return 2000; + } + + @Override + public boolean shouldFilter() { + return true; + } + + @Override + public Object run() { + dumpRoutingDebug(); + dumpRequestDebug(); + return null; + } + + public void dumpRequestDebug() { + List rd = (List) RequestContext.getCurrentContext().get("requestDebug"); + if (rd != null) { + for (String it : rd) { + System.out.println("REQUEST_DEBUG::" + it); + } + } + } + + public void dumpRoutingDebug() { + List rd = (List) RequestContext.getCurrentContext().get("routingDebug"); + if (rd != null) { + for (String it : rd) { + System.out.println("ZUUL_DEBUG::"+it); + } + } + } + +} diff --git a/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/filters/pre/DebugFilter.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/filters/pre/DebugFilter.java new file mode 100644 index 00000000..45cade94 --- /dev/null +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/filters/pre/DebugFilter.java @@ -0,0 +1,48 @@ +package org.springframework.platform.netflix.zuul.filters.pre; + +import com.netflix.config.DynamicBooleanProperty; +import com.netflix.config.DynamicPropertyFactory; +import com.netflix.config.DynamicStringProperty; +import com.netflix.zuul.ZuulFilter; +import com.netflix.zuul.constants.ZuulConstants; +import com.netflix.zuul.context.RequestContext; + +import javax.servlet.http.HttpServletRequest; + +public class DebugFilter extends ZuulFilter { + + static final DynamicBooleanProperty routingDebug = DynamicPropertyFactory.getInstance() + .getBooleanProperty(ZuulConstants.ZUUL_DEBUG_REQUEST, true); + static final DynamicStringProperty debugParameter = DynamicPropertyFactory.getInstance() + .getStringProperty(ZuulConstants.ZUUL_DEBUG_PARAMETER, "d"); + + @Override + public String filterType() { + return "pre"; + } + + @Override + public int filterOrder() { + return 1; + } + + public boolean shouldFilter() { + HttpServletRequest request = RequestContext.getCurrentContext().getRequest(); + if ("true".equals(request.getParameter(debugParameter.get()))) + return true; + + return routingDebug.get(); + } + + public Object run() { + RequestContext ctx = RequestContext.getCurrentContext(); + ctx.setDebugRouting(true); + ctx.setDebugRequest(true); + return null; + } + + +} + + + diff --git a/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/filters/pre/DebugRequestFilter.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/filters/pre/DebugRequestFilter.java new file mode 100644 index 00000000..5724f3a9 --- /dev/null +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/filters/pre/DebugRequestFilter.java @@ -0,0 +1,62 @@ +package org.springframework.platform.netflix.zuul.filters.pre; + +import com.netflix.zuul.ZuulFilter; + +import com.netflix.zuul.context.Debug; +import com.netflix.zuul.context.RequestContext; +import org.apache.commons.io.IOUtils; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.io.InputStream; +import java.util.Enumeration; + +public class DebugRequestFilter extends ZuulFilter { + @Override + public String filterType() { + return "pre"; + } + + @Override + public int filterOrder() { + return 10000; + } + + @Override + public boolean shouldFilter() { + return Debug.debugRequest(); + } + + @Override + public Object run() { + HttpServletRequest req = RequestContext.getCurrentContext().getRequest(); + + Debug.addRequestDebug("REQUEST:: " + req.getScheme() + " " + req.getRemoteAddr() + ":" + req.getRemotePort()); + + Debug.addRequestDebug("REQUEST:: > " + req.getMethod() + " " + req.getRequestURI() + " " + req.getProtocol()); + + Enumeration headerIt = req.getHeaderNames(); + while (headerIt.hasMoreElements()) { + String name = headerIt.nextElement(); + String value = req.getHeader(name); + Debug.addRequestDebug("REQUEST:: > " + name + ":" + value); + + } + + final RequestContext ctx = RequestContext.getCurrentContext(); + if (!ctx.isChunkedRequestBody()) { + try { + InputStream inp = ctx.getRequest().getInputStream(); + if (inp != null) { + String body = IOUtils.toString(inp); + Debug.addRequestDebug("REQUEST:: > " + body); + + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return null; + } + +} diff --git a/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/filters/pre/PreDecorationFilter.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/filters/pre/PreDecorationFilter.java new file mode 100644 index 00000000..c2e1bdaf --- /dev/null +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/filters/pre/PreDecorationFilter.java @@ -0,0 +1,76 @@ +package org.springframework.platform.netflix.zuul.filters.pre; + +import com.google.common.base.Optional; +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; +import com.netflix.zuul.ZuulFilter; +import com.netflix.zuul.context.RequestContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.platform.netflix.zuul.Routes; +import org.springframework.platform.netflix.zuul.ZuulProxyProperties; + +import javax.annotation.Nullable; +import java.util.LinkedHashMap; + +public class PreDecorationFilter extends ZuulFilter { + private static Logger LOG = LoggerFactory.getLogger(PreDecorationFilter.class); + + @Autowired + private Routes routes; + + @Autowired + private ZuulProxyProperties properties; + + @Override + public int filterOrder() { + return 5; + } + + @Override + public String filterType() { + return "pre"; + } + + @Override + public boolean shouldFilter() { + return true; + } + + @Override + public Object run() { + RequestContext ctx = RequestContext.getCurrentContext(); + + String requestURI = ctx.getRequest().getRequestURI(); + + //remove proxy prefix TODO: only if embedded proxy + String proxyMapping = properties.getMapping(); + final String uriPart = requestURI.replace(proxyMapping, ""); //TODO: better strategy? + ctx.put("requestURI", uriPart); + + LinkedHashMap routesMap = routes.getRoutes(); + + Optional route = Iterables.tryFind(routesMap.keySet(), new Predicate() { + @Override + public boolean apply(@Nullable String path) { + return uriPart.startsWith(path); + } + }); + + if (route.isPresent()) { + String serviceId = routesMap.get(route.get()); + + if (serviceId != null) { + // set serviceId for use in filters.route.RibbonRequest + ctx.set("serviceId", serviceId); + ctx.setRouteHost(null); + ctx.addOriginResponseHeader("X-Zuul-ServiceId", serviceId); + } + } else { + LOG.warn("No route found for uri: "+requestURI); + //TODO: 404 + } + return null; + } +} diff --git a/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/filters/route/RibbonRoutingFilter.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/filters/route/RibbonRoutingFilter.java new file mode 100644 index 00000000..49067d65 --- /dev/null +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/filters/route/RibbonRoutingFilter.java @@ -0,0 +1,302 @@ +package org.springframework.platform.netflix.zuul.filters.route; + +import com.netflix.client.ClientException; +import com.netflix.client.ClientFactory; +import com.netflix.client.http.HttpResponse; +import com.netflix.hystrix.exception.HystrixRuntimeException; +import com.netflix.niws.client.http.RestClient; +import com.netflix.zuul.ZuulFilter; +import com.netflix.zuul.context.Debug; +import com.netflix.zuul.context.RequestContext; +import com.netflix.zuul.exception.ZuulException; +import com.netflix.zuul.util.HTTPRequestUtils; +import com.sun.jersey.core.util.MultivaluedMapImpl; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.platform.netflix.zuul.RibbonCommand; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.MultivaluedMap; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collection; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.zip.GZIPInputStream; + +import static com.netflix.client.http.HttpRequest.Verb; +import static org.springframework.platform.netflix.feign.FeignConfigurer.setServiceListClassAndVIP; + +public class RibbonRoutingFilter extends ZuulFilter { + + private static final Logger LOG = LoggerFactory.getLogger(RibbonRoutingFilter.class); + + public static final String CONTENT_ENCODING = "Content-Encoding"; + + @Override + public String filterType() { + return "route"; + } + + @Override + public int filterOrder() { + return 10; + } + + public boolean shouldFilter() { + RequestContext ctx = RequestContext.getCurrentContext(); + return (ctx.getRouteHost() == null && ctx.get("serviceId") != null && ctx.sendZuulResponse()); + } + + public Object run() { + RequestContext context = RequestContext.getCurrentContext(); + HttpServletRequest request = context.getRequest(); + + MultivaluedMap headers = buildZuulRequestHeaders(request); + MultivaluedMap params = buildZuulRequestQueryParams(request); + Verb verb = getVerb(request); + InputStream requestEntity = getRequestBody(request); + + String serviceId = (String) context.get("serviceId"); + + //TODO: can this be set be default? or an implementation of an interface? + setServiceListClassAndVIP(serviceId); + + RestClient restClient = (RestClient) ClientFactory.getNamedClient(serviceId); + + String uri = request.getRequestURI(); + if (context.get("requestURI") != null) { + uri = (String) context.get("requestURI"); + } + //remove double slashes + uri = uri.replace("//", "/"); + + try { + HttpResponse response = forward(restClient, verb, uri, headers, params, requestEntity); + setResponse(response); + return response; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void debug(RestClient restClient, Verb verb, String uri, MultivaluedMap headers, + MultivaluedMap params, InputStream requestEntity) throws IOException { + + if (Debug.debugRequest()) { + + for (String header: headers.keySet()) { + Debug.addRequestDebug(String.format("ZUUL:: > %s %s", header, headers.getFirst(header))); + } + StringBuilder query = new StringBuilder(); + for (String param : params.keySet()) { + for (String value : params.get(param)) { + query.append(param); + query.append("="); + query.append(value); + query.append("&"); + } + } + + Debug.addRequestDebug(String.format("ZUUL:: > %s %s?%s HTTP/1.1", verb.verb(), uri, query.toString())); + RequestContext ctx = RequestContext.getCurrentContext(); + if (!ctx.isChunkedRequestBody()) { + if (requestEntity != null) { + debugRequestEntity(ctx.getRequest().getInputStream()); + } + } + } + } + + private void debugRequestEntity(InputStream inputStream) throws IOException { + if (!Debug.debugRequestHeadersOnly()) { + String entity = IOUtils.toString(inputStream); + Debug.addRequestDebug("ZUUL:: > "+entity); + } + } + + + + private HttpResponse forward(RestClient restClient, Verb verb, String uri, MultivaluedMap headers, + MultivaluedMap params, InputStream requestEntity) throws Exception { + debug(restClient, verb, uri, headers, params, requestEntity); + + RibbonCommand command = new RibbonCommand(restClient, verb, uri, headers, params, requestEntity); + try { + HttpResponse response = command.execute(); + return response; + } catch (HystrixRuntimeException e) { + if (e.getFallbackException() != null && + e.getFallbackException().getCause() != null && + e.getFallbackException().getCause() instanceof ClientException) { + ClientException ex = (ClientException) e.getFallbackException().getCause(); + throw new ZuulException(ex, "Forwarding error", 500, ex.getErrorType().toString()); + } + throw new ZuulException(e, "Forwarding error", 500, e.getFailureType().toString()); + } + + } + + + private InputStream getRequestBody(HttpServletRequest request) { + InputStream requestEntity = null; + try { + requestEntity = (InputStream) RequestContext.getCurrentContext().get("requestEntity"); + if (requestEntity == null) { + requestEntity = request.getInputStream(); + } + } catch (IOException e) { + LOG.error("Error during getRequestBody", e); + } + + return requestEntity; + } + + private MultivaluedMap buildZuulRequestQueryParams(HttpServletRequest request) { + + Map> map = HTTPRequestUtils.getInstance().getQueryParams(); + + MultivaluedMap params = new MultivaluedMapImpl(); + if (map == null) return params; + + for (String key : params.keySet()) { + + for (String value : params.get(key)) { + params.add(key, value); + } + } + return params; + } + + private MultivaluedMap buildZuulRequestHeaders(HttpServletRequest request) { + + RequestContext context = RequestContext.getCurrentContext(); + + MultivaluedMap headers = new MultivaluedMapImpl(); + Enumeration headerNames = request.getHeaderNames(); + if (headerNames != null) { + while (headerNames.hasMoreElements()) { + String name = (String) headerNames.nextElement(); + String value = request.getHeader(name); + if (!name.toLowerCase().contains("content-length")) headers.putSingle(name, value); + } + } + Map zuulRequestHeaders = context.getZuulRequestHeaders(); + + for (String header : zuulRequestHeaders.keySet()) { + headers.putSingle(header, zuulRequestHeaders.get(header)); + } + + headers.putSingle("accept-encoding", "deflate, gzip"); + + if (headers.containsKey("transfer-encoding")) + headers.remove("transfer-encoding"); + + return headers; + } + + + + Verb getVerb(HttpServletRequest request) { + String sMethod = request.getMethod(); + return getVerb(sMethod); + } + + Verb getVerb(String sMethod) { + if (sMethod == null) return Verb.GET; + sMethod = sMethod.toLowerCase(); + if (sMethod.equals("post")) return Verb.POST; + if (sMethod.equals("put")) return Verb.PUT; + if (sMethod.equals("delete")) return Verb.DELETE; + if (sMethod.equals("options")) return Verb.OPTIONS; + if (sMethod.equals("head")) return Verb.HEAD; + return Verb.GET; + } + + void setResponse(HttpResponse resp) throws ClientException, IOException { + RequestContext context = RequestContext.getCurrentContext(); + + context.setResponseStatusCode(resp.getStatus()); + if (resp.hasEntity()) { + context.setResponseDataStream(resp.getInputStream()); + } + + String contentEncoding = null; + Collection contentEncodingHeader = resp.getHeaders().get(CONTENT_ENCODING); + if (contentEncodingHeader != null && !contentEncodingHeader.isEmpty()) { + contentEncoding = contentEncodingHeader.iterator().next(); + } + + if (contentEncoding != null && HTTPRequestUtils.getInstance().isGzipped(contentEncoding)) { + context.setResponseGZipped(true); + } else { + context.setResponseGZipped(false); + } + + if (Debug.debugRequest()) { + for (String key : resp.getHeaders().keySet()) { + boolean isValidHeader = isValidHeader(key); + + Collection list = resp.getHeaders().get(key); + for (String header : list) { + context.addOriginResponseHeader(key, header); + + if (key.equalsIgnoreCase("content-length")) + context.setOriginContentLength(header); + + if (isValidHeader) { + context.addZuulResponseHeader(key, header); + Debug.addRequestDebug(String.format("ORIGIN_RESPONSE:: < %s %s", key, header)); + } + } + } + + if (context.getResponseDataStream() != null) { + byte[] origBytes = IOUtils.toByteArray(context.getResponseDataStream()); + InputStream inStream = new ByteArrayInputStream(origBytes); + if (context.getResponseGZipped()) + inStream = new GZIPInputStream(inStream); + String responseEntity = IOUtils.toString(inStream); + Debug.addRequestDebug("ORIGIN_RESPONSE:: < "+responseEntity); + context.setResponseDataStream(new ByteArrayInputStream(origBytes)); + } + + } else { + for (String key : resp.getHeaders().keySet()) { + boolean isValidHeader = isValidHeader(key); + Collection list = resp.getHeaders().get(key); + for (String header : list) { + context.addOriginResponseHeader(key, header); + + if (key.equalsIgnoreCase("content-length")) + context.setOriginContentLength(header); + + if (isValidHeader) { + context.addZuulResponseHeader(key, header); + } + } + } + } + + + } + + boolean isValidHeader(String headerName) { + switch (headerName.toLowerCase()) { + case "connection": + case "content-length": + case "content-encoding": + case "server": + case "transfer-encoding": + return false; + default: + return true; + } + } + +} + + diff --git a/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/filters/route/SimpleHostRoutingFilter.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/filters/route/SimpleHostRoutingFilter.java new file mode 100644 index 00000000..e63e950a --- /dev/null +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/zuul/filters/route/SimpleHostRoutingFilter.java @@ -0,0 +1,456 @@ +package org.springframework.platform.netflix.zuul.filters.route; + +import com.google.common.base.Optional; +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; +import com.netflix.config.DynamicIntProperty; +import com.netflix.config.DynamicPropertyFactory; +import com.netflix.zuul.ZuulFilter; +import com.netflix.zuul.constants.ZuulConstants; +import com.netflix.zuul.context.Debug; +import com.netflix.zuul.context.RequestContext; +import com.netflix.zuul.util.HTTPRequestUtils; +import org.apache.commons.io.IOUtils; +import org.apache.http.Header; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +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.entity.InputStreamEntity; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; +import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; +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.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import javax.servlet.http.HttpServletRequest; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.Socket; +import java.net.URL; +import java.net.UnknownHostException; +import java.security.*; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; +import java.util.zip.GZIPInputStream; + +public class SimpleHostRoutingFilter extends ZuulFilter { + + public static final String CONTENT_ENCODING = "Content-Encoding"; + + private static final Logger LOG = LoggerFactory.getLogger(SimpleHostRoutingFilter.class); + 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); + private static final DynamicIntProperty CONNECTION_TIMEOUT = DynamicPropertyFactory.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(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 t) { + LOG.error("error closing expired connections", t); + } + } + }, 30000, 5000); + } + + public SimpleHostRoutingFilter() {} + + private static final 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)); + registry.register(new Scheme("https", sf, 8443)); + + 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; + } + + @Override + public String filterType() { + return "route"; + } + + @Override + public int filterOrder() { + return 100; + } + + public boolean shouldFilter() { + return RequestContext.getCurrentContext().getRouteHost() != null && + RequestContext.getCurrentContext().sendZuulResponse(); + } + + private static final 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 t) { + LOG.error("error shutting down old connection manager", t); + } + } + }, 30000); + } + + } + + private static final 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 e) { + throw new RuntimeException(e); + } + } + + public Object run() { + HttpServletRequest request = RequestContext.getCurrentContext().getRequest(); + Header[] headers = buildZuulRequestHeaders(request); + String verb = getVerb(request); + InputStream requestEntity = getRequestBody(request); + HttpClient httpclient = CLIENT.get(); + + String uri = request.getRequestURI(); + if (RequestContext.getCurrentContext().get("requestURI") != null) { + uri = (String) RequestContext.getCurrentContext().get("requestURI"); + } + + try { + HttpResponse response = forward(httpclient, verb, uri, request, headers, requestEntity); + setResponse(response); + } + catch (Exception e) { + if (Debug.debugRequest()) { + Debug.addRequestDebug("ZUUL:: ERROR " + e.getMessage()); + } + throw new RuntimeException(e); + } + return null; + } + + private InputStream debug(HttpClient httpclient, String verb, String uri, HttpServletRequest request, Header[] headers, InputStream requestEntity) throws Exception { + + if (Debug.debugRequest()) { + + Debug.addRequestDebug("ZUUL:: host="+RequestContext.getCurrentContext().getRouteHost()); + + for (Header header : headers) { + Debug.addRequestDebug(String.format("ZUUL::> %s %s", header.getName(), header.getValue())); + } + + Debug.addRequestDebug(String.format("ZUUL:: > ${verb} ${uri}?${query} HTTP/1.1", verb, uri, request.getQueryString())); + if (requestEntity != null) { + requestEntity = debugRequestEntity(requestEntity); + } + + } + return requestEntity; + } + + private InputStream debugRequestEntity(InputStream inputStream) throws Exception { + if (Debug.debugRequestHeadersOnly()) return inputStream; + if (inputStream == null) return null; + String entity = IOUtils.toString(inputStream); + Debug.addRequestDebug("ZUUL::> "+entity); + return new ByteArrayInputStream(entity.getBytes()); + } + + private HttpResponse forward(HttpClient httpclient, String verb, String uri, HttpServletRequest request, Header[] headers, InputStream requestEntity) throws Exception { + + requestEntity = debug(httpclient, verb, uri, request, headers, requestEntity); + + HttpHost httpHost = getHttpHost(); + + HttpRequest httpRequest; + + switch (verb) { + case "POST": + HttpPost httpPost = new HttpPost(uri + getQueryString()); + httpRequest = httpPost; + InputStreamEntity entity = new InputStreamEntity(requestEntity, request.getContentLength()); + httpPost.setEntity(entity); + break; + case "PUT": + HttpPut httpPut = new HttpPut(uri + getQueryString()); + httpRequest = httpPut; + InputStreamEntity entity2 = new InputStreamEntity(requestEntity, request.getContentLength()); + httpPut.setEntity(entity2); + break; + default: + httpRequest = new BasicHttpRequest(verb, uri + getQueryString()); + LOG.debug(uri + getQueryString()); + } + + try { + httpRequest.setHeaders(headers); + LOG.debug(httpHost.getHostName() + " " + httpHost.getPort() + " " + httpHost.getSchemeName()); + HttpResponse zuulResponse = forwardRequest(httpclient, httpHost, httpRequest); + return zuulResponse; + } finally { + // When HttpClient instance is no longer needed, + // shut down the connection manager to ensure + // immediate deallocation of all system resources +// httpclient.getConnectionManager().shutdown(); + } + + } + + private HttpResponse forwardRequest(HttpClient httpclient, HttpHost httpHost, HttpRequest httpRequest) throws IOException { + return httpclient.execute(httpHost, httpRequest); + } + + String getQueryString() { + HttpServletRequest request = RequestContext.getCurrentContext().getRequest(); + String query = request.getQueryString(); + return (query != null) ? "?"+query : ""; + } + + HttpHost getHttpHost() { + URL host = RequestContext.getCurrentContext().getRouteHost(); + + HttpHost httpHost = new HttpHost(host.getHost(), host.getPort(), host.getProtocol()); + + return httpHost; + } + + + private InputStream getRequestBody(HttpServletRequest request) { + InputStream requestEntity = null; + try { + requestEntity = request.getInputStream(); + } catch (IOException e) { + //no requestBody is ok. + } + return requestEntity; + } + + boolean isValidHeader(String name) { + if (name.toLowerCase().contains("content-length")) return false; + if (!RequestContext.getCurrentContext().getResponseGZipped()) { + if (name.toLowerCase().contains("accept-encoding")) return false; + } + return true; + } + + private Header[] buildZuulRequestHeaders(HttpServletRequest request) { + + ArrayList
headers = new ArrayList<>(); + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String name = (String) headerNames.nextElement(); + String value = request.getHeader(name); + if (isValidHeader(name)) headers.add(new BasicHeader(name, value)); + } + + Map zuulRequestHeaders = RequestContext.getCurrentContext().getZuulRequestHeaders(); + + for (String it : zuulRequestHeaders.keySet()) { + final String name = it.toLowerCase(); + Optional
h = Iterables.tryFind(headers, new Predicate
() { + @Override + public boolean apply(@Nullable Header input) { + return input.getName().equals(name); + } + }); + if (h.isPresent()) { + headers.remove(h); + } + headers.add(new BasicHeader(it, zuulRequestHeaders.get(it))); + } + + if (RequestContext.getCurrentContext().getResponseGZipped()) { + headers.add(new BasicHeader("accept-encoding", "deflate, gzip")); + } + return headers.toArray(new Header[0]); + } + + private String getVerb(HttpServletRequest request) { + String sMethod = request.getMethod(); + return sMethod.toUpperCase(); + } + + private String getVerb(String sMethod) { + if (sMethod == null) return "GET"; + sMethod = sMethod.toLowerCase(); + if (sMethod.equalsIgnoreCase("post")) return "POST"; + if (sMethod.equalsIgnoreCase("put")) return "PUT"; + if (sMethod.equalsIgnoreCase("delete")) return "DELETE"; + if (sMethod.equalsIgnoreCase("options")) return "OPTIONS"; + if (sMethod.equalsIgnoreCase("head")) return "HEAD"; + return "GET"; + } + + private void setResponse(HttpResponse response) throws IOException { + RequestContext context = RequestContext.getCurrentContext(); + + RequestContext.getCurrentContext().set("hostZuulResponse", response); + RequestContext.getCurrentContext().setResponseStatusCode(response.getStatusLine().getStatusCode()); + if (response.getEntity() != null) { + RequestContext.getCurrentContext().setResponseDataStream(response.getEntity().getContent()); + } + + boolean isOriginResponseGzipped = false; + + for (Header h : response.getHeaders(CONTENT_ENCODING)) { + if (HTTPRequestUtils.getInstance().isGzipped(h.getValue())) { + isOriginResponseGzipped = true; + break; + } + } + context.setResponseGZipped(isOriginResponseGzipped); + + if (Debug.debugRequest()) { + for (Header header : response.getAllHeaders()) { + if (isValidHeader(header)) { + RequestContext.getCurrentContext().addZuulResponseHeader(header.getName(), header.getValue()); + Debug.addRequestDebug("ORIGIN_RESPONSE:: < " + header.getName() +","+ header.getValue()); + } + } + + if (context.getResponseDataStream() != null) { + byte[] origBytes = IOUtils.toByteArray(context.getResponseDataStream()); + ByteArrayInputStream byteStream = new ByteArrayInputStream(origBytes); + InputStream inputStream = byteStream; + if (RequestContext.getCurrentContext().getResponseGZipped()) { + inputStream = new GZIPInputStream(byteStream); + } + + + context.setResponseDataStream(new ByteArrayInputStream(origBytes)); + } + + } else { + for (Header header : response.getAllHeaders()) { + RequestContext ctx = RequestContext.getCurrentContext(); + ctx.addOriginResponseHeader(header.getName(), header.getValue()); + + if (header.getName().equalsIgnoreCase("content-length")) + ctx.setOriginContentLength(header.getValue()); + + if (isValidHeader(header)) { + ctx.addZuulResponseHeader(header.getName(), header.getValue()); + } + } + } + + } + + boolean isValidHeader(Header header) { + switch (header.getName().toLowerCase()) { + case "connection": + case "content-length": + case "content-encoding": + case "server": + case "transfer-encoding": + return false; + default: + return true; + } + } + + public static class MySSLSocketFactory extends SSLSocketFactory { + SSLContext sslContext = SSLContext.getInstance("TLS"); + + public MySSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + super(truststore); + + TrustManager tm = new X509TrustManager() { + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public X509Certificate[] getAcceptedIssuers() { + return null; + } + }; + + TrustManager[] tms = new TrustManager[1]; + tms[0] = tm; + sslContext.init(null, tms, null); + } + + @Override + public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException, UnknownHostException { + return sslContext.getSocketFactory().createSocket(socket, host, port, autoClose); + } + + @Override + public Socket createSocket() throws IOException { + return sslContext.getSocketFactory().createSocket(); + } + } +} \ No newline at end of file diff --git a/spring-platform-netflix-core/src/test/java/org/springframework/platform/netflix/zuul/sample/Application.java b/spring-platform-netflix-core/src/test/java/org/springframework/platform/netflix/zuul/sample/Application.java new file mode 100644 index 00000000..322c1b6a --- /dev/null +++ b/spring-platform-netflix-core/src/test/java/org/springframework/platform/netflix/zuul/sample/Application.java @@ -0,0 +1,34 @@ +package org.springframework.platform.netflix.zuul.sample; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.platform.netflix.eureka.EnableEurekaClient; +import org.springframework.platform.netflix.zuul.EnableZuulProxy; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Configuration +@ComponentScan +@EnableAutoConfiguration +@RestController +@EnableZuulProxy +@EnableEurekaClient +public class Application { + + @RequestMapping("/testing123") + public String testing123() { + return "testing123"; + } + + @RequestMapping("/") + public String home() { + return "Hello world"; + } + + public static void main(String[] args) { + new SpringApplicationBuilder(Application.class).web(true).run(args); + } + +} diff --git a/spring-platform-netflix-core/src/test/java/org/springframework/platform/netflix/zuul/sample/ApplicationTests.java b/spring-platform-netflix-core/src/test/java/org/springframework/platform/netflix/zuul/sample/ApplicationTests.java new file mode 100644 index 00000000..b6302d97 --- /dev/null +++ b/spring-platform-netflix-core/src/test/java/org/springframework/platform/netflix/zuul/sample/ApplicationTests.java @@ -0,0 +1,18 @@ +package org.springframework.platform.netflix.zuul.sample; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = Application.class) +@WebAppConfiguration +public class ApplicationTests { + + @Test + public void contextLoads() { + } + +} diff --git a/spring-platform-netflix-core/src/test/resources/application.yml b/spring-platform-netflix-core/src/test/resources/application.yml index 4ddb02df..139ea8cf 100644 --- a/spring-platform-netflix-core/src/test/resources/application.yml +++ b/spring-platform-netflix-core/src/test/resources/application.yml @@ -1,12 +1,14 @@ server: - port: 9000 + port: 9999 spring: application: - name: client + name: testclient eureka: server: enabled: false - client: - serviceUrl: - defaultZone: http://localhost:8080/v2/ - default.defaultZone: http://localhost:8080/v2/ + +zuul: + proxy: + route: + testclient: /testing123 + stores: /stores diff --git a/spring-platform-netflix-zuul/src/main/java/io/spring/platform/netflix/zuul/Application.java b/spring-platform-netflix-zuul/src/main/java/io/spring/platform/netflix/zuul/Application.java index d7fd7838..46f5c6c7 100644 --- a/spring-platform-netflix-zuul/src/main/java/io/spring/platform/netflix/zuul/Application.java +++ b/spring-platform-netflix-zuul/src/main/java/io/spring/platform/netflix/zuul/Application.java @@ -8,6 +8,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.platform.netflix.endpoint.HystrixStreamEndpoint; +import org.springframework.platform.netflix.zuul.Routes; import org.springframework.scheduling.annotation.EnableScheduling; import java.util.ArrayList; diff --git a/spring-platform-netflix-zuul/src/main/java/io/spring/platform/netflix/zuul/FilterIntializer.java b/spring-platform-netflix-zuul/src/main/java/io/spring/platform/netflix/zuul/FilterIntializer.java index 1e772df3..7470fda2 100644 --- a/spring-platform-netflix-zuul/src/main/java/io/spring/platform/netflix/zuul/FilterIntializer.java +++ b/spring-platform-netflix-zuul/src/main/java/io/spring/platform/netflix/zuul/FilterIntializer.java @@ -11,7 +11,6 @@ import org.springframework.beans.factory.annotation.Autowired; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; -//import javax.servlet.http.HttpSessionEvent; /** * User: spencergibb @@ -19,7 +18,7 @@ import javax.servlet.ServletContextListener; * Time: 9:23 PM * TODO: .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) */ -public class FilterIntializer implements ServletContextListener/*, HttpSessionListener*/ { +public class FilterIntializer implements ServletContextListener { private static final Logger LOGGER = LoggerFactory.getLogger(FilterIntializer.class); @@ -27,9 +26,6 @@ public class FilterIntializer implements ServletContextListener/*, HttpSessionLi ZuulProperties props; @Override - /*public void sessionCreated(HttpSessionEvent se) { - contextInitialized(null); - }*/ public void contextInitialized(ServletContextEvent sce) { LOGGER.info("Starting filter initialzer context listener"); @@ -42,9 +38,6 @@ public class FilterIntializer implements ServletContextListener/*, HttpSessionLi } @Override - /*public void sessionDestroyed(HttpSessionEvent se) { - contextDestroyed(null); - }*/ public void contextDestroyed(ServletContextEvent sce) { LOGGER.info("Stopping filter initializer context listener"); } diff --git a/spring-platform-netflix-zuul/src/main/resources/filters/pre/PreDecorationFilter.groovy b/spring-platform-netflix-zuul/src/main/resources/filters/pre/PreDecorationFilter.groovy index 307b33b6..ed905eab 100644 --- a/spring-platform-netflix-zuul/src/main/resources/filters/pre/PreDecorationFilter.groovy +++ b/spring-platform-netflix-zuul/src/main/resources/filters/pre/PreDecorationFilter.groovy @@ -3,8 +3,8 @@ package filters.pre import com.netflix.zuul.context.RequestContext import org.slf4j.Logger import org.slf4j.LoggerFactory -import io.spring.platform.netflix.zuul.Routes -import io.spring.platform.netflix.zuul.SpringFilter +import org.springframework.platform.netflix.zuul.Routes +import org.springframework.platform.netflix.zuul.SpringFilter class PreDecorationFilter extends SpringFilter { private static Logger LOG = LoggerFactory.getLogger(PreDecorationFilter.class); diff --git a/spring-platform-netflix-zuul/src/main/resources/filters/route/RibbonRequest.groovy b/spring-platform-netflix-zuul/src/main/resources/filters/route/RibbonRequest.groovy index bddecebe..7509d029 100644 --- a/spring-platform-netflix-zuul/src/main/resources/filters/route/RibbonRequest.groovy +++ b/spring-platform-netflix-zuul/src/main/resources/filters/route/RibbonRequest.groovy @@ -15,14 +15,14 @@ import com.netflix.zuul.util.HTTPRequestUtils import com.sun.jersey.core.util.MultivaluedMapImpl import org.slf4j.Logger import org.slf4j.LoggerFactory -import io.spring.platform.netflix.zuul.RibbonCommand +import org.springframework.platform.netflix.zuul.RibbonCommand import javax.servlet.http.HttpServletRequest import javax.ws.rs.core.MultivaluedMap import java.util.zip.GZIPInputStream import static HttpRequest.Verb -import static org.springframework.platform.netflix.feign.FeignConfigurer.setServiceListClass +import static org.springframework.platform.netflix.feign.FeignConfigurer.setServiceListClassAndVIP class RibbonRequest extends ZuulFilter { @@ -57,7 +57,7 @@ class RibbonRequest extends ZuulFilter { def serviceId = context.get("serviceId") //TODO: can this be set be default? or an implementation of an interface? - setServiceListClass(serviceId) + setServiceListClassAndVIP(serviceId) IClient restClient = ClientFactory.getNamedClient(serviceId);