added support for standalone zuul server with @EnableZuulServer.

Removed old zuul module.

fixes gh-14
This commit is contained in:
Spencer Gibb
2014-11-04 15:17:35 -07:00
parent eba7f0dac3
commit e2e193eaa7
34 changed files with 368 additions and 1547 deletions

View File

@@ -432,9 +432,20 @@ Zuul's rule engine allows rules and filters to be written in essentially any JVM
[[netflix-zuul-reverse-proxy]]
=== Embedded Zuul Reverse Proxy
Spring Cloud has created an embedded Zuul proxy to ease the development of a very common use case where a UI application wants to proxy calls to one or more back end services. To enable it, annotate a Spring Boot main class with `@EnableZuulProxy`. This forwards local calls to `/proxy/*` to the appropriate service. The proxy uses Ribbon to locate an instance to forward to via Eureka. Forwarding to the service is protected by a Hystrix circuit breaker. Rules are configured via the Spring environment. The Config Server is an ideal place for the Zuul configuration. Configuration rules look like the following:
Spring Cloud has created an embedded Zuul proxy to ease the development of a very common use case where a UI application wants to proxy calls to one or more back end services. To enable it, annotate a Spring Boot main class with `@EnableZuulProxy`. This forwards local calls to `/proxy/*` to the appropriate service. The proxy uses Ribbon to locate an instance to forward to via Eureka. Forwarding to the service is protected by a Hystrix circuit breaker. Rules are configured via the Spring environment. The Config Server is an ideal place for the Zuul configuration. Zuul Embedded Proxy configuration rules look like the following:
zuul.proxy.route.users: /users
This means that http calls to /proxy/users to the users service. This proxy configuration is useful for services that host a user interface to proxy to the backend services it requires.
This means that http calls to /proxy/users get forwarded to the users service. This proxy configuration is useful for services that host a user interface to proxy to the backend services it requires. By default, the proxy mapping gets stripped from the request before forwarding.
[[netflix-zuul-server]]
=== Standalone Zuul Server
Spring Cloud has created a standalone Zuul server. To enable it, annotate a Spring Boot main class with `@EnableZuulServer`. This routes all calls to the appropriate service. The server uses Ribbon to locate an instance to forward to via Eureka. Forwarding to the service is protected by a Hystrix circuit breaker. Rules are configured via the Spring environment. The Config Server is an ideal place for the Zuul configuration. Zuul Server configuration rules look like the following:
zuul.server.route.users: /users
This means that http calls to /users get forwarded to the users service.
Since zuul, by default, intercepts all requests (`/*`), to enable actuator, you should set the `management.port`.

View File

@@ -30,7 +30,7 @@
<module>spring-cloud-netflix-hystrix-dashboard</module>
<module>spring-cloud-netflix-eureka-server</module>
<module>spring-cloud-netflix-turbine</module>
<module>spring-cloud-netflix-zuul</module>
<module>spring-cloud-netflix-zuul-server</module>
<module>docs</module>
</modules>

View File

@@ -0,0 +1,81 @@
package org.springframework.cloud.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.actuate.trace.TraceRepository;
import org.springframework.boot.context.embedded.FilterRegistrationBean;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter;
import org.springframework.cloud.netflix.zuul.filters.pre.DebugFilter;
import org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter;
import org.springframework.context.annotation.Bean;
import java.util.ArrayList;
import java.util.Collection;
/**
* @author Spencer Gibb
*/
public abstract class AbstractZuulConfiguration {
@Autowired(required=false)
private TraceRepository traces;
protected abstract ZuulProperties getProperties();
@Bean
public Routes routes() {
return new Routes(getProperties().getRoutePrefix());
}
@Bean
public ServletRegistrationBean zuulServlet() {
return new ServletRegistrationBean(new ZuulServlet(), getProperties().getMapping()+"/*");
}
@Bean
public FilterRegistrationBean contextLifecycleFilter() {
Collection<String> urlPatterns = new ArrayList<>();
urlPatterns.add(getProperties().getMapping()+"/*");
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new ContextLifecycleFilter());
filterRegistrationBean.setUrlPatterns(urlPatterns);
return filterRegistrationBean;
}
@Bean
public FilterInitializer zuulFilterInitializer() {
return new FilterInitializer();
}
// pre filters
@Bean
public DebugFilter debugFilter() {
return new DebugFilter();
}
@Bean
public PreDecorationFilter preDecorationFilter() {
return new PreDecorationFilter();
}
// route filters
@Bean
public RibbonRoutingFilter ribbonRoutingFilter() {
RibbonRoutingFilter filter = new RibbonRoutingFilter();
if (traces!=null) {
filter.setTraces(traces);
}
return filter;
}
// post filters
@Bean
public SendResponseFilter sendResponseFilter() {
return new SendResponseFilter();
}
}

View File

@@ -25,11 +25,6 @@ public class Routes {
private Field propertySourcesField;
private String keyPrefix;
public Routes() {
keyPrefix = "zuul.proxy.route.";
initField();
}
public Routes(String keyPrefix) {
this.keyPrefix = keyPrefix;
initField();

View File

@@ -0,0 +1,11 @@
package org.springframework.cloud.netflix.zuul;
/**
* @author Spencer Gibb
*/
public interface ZuulProperties {
public String getMapping();
public boolean isStripMapping();
public String getRoutePrefix();
public boolean isAddProxyHeaders();
}

View File

@@ -1,23 +1,11 @@
package org.springframework.cloud.netflix.zuul;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.trace.TraceRepository;
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.cloud.netflix.zuul.filters.post.SendResponseFilter;
import org.springframework.cloud.netflix.zuul.filters.pre.DebugFilter;
import org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter;
import com.netflix.zuul.context.ContextLifecycleFilter;
import com.netflix.zuul.http.ZuulServlet;
/**
@@ -27,65 +15,41 @@ import com.netflix.zuul.http.ZuulServlet;
@EnableConfigurationProperties(ZuulProxyProperties.class)
@ConditionalOnClass(ZuulServlet.class)
@ConditionalOnExpression("${zuul.proxy.enabled:true}")
public class ZuulProxyConfiguration {
@Autowired
private ZuulProxyProperties props;
@Autowired(required=false)
private TraceRepository traces;
public class ZuulProxyConfiguration extends AbstractZuulConfiguration {
@Bean
public FilterRegistrationBean contextLifecycleFilter() {
Collection<String> urlPatterns = new ArrayList<>();
urlPatterns.add(props.getMapping()+"/*");
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new ContextLifecycleFilter());
filterRegistrationBean.setUrlPatterns(urlPatterns);
return filterRegistrationBean;
public ZuulProxyProperties zuulProxyProperties() {
return new ZuulProxyProperties();
}
@Bean
public ServletRegistrationBean zuulServlet() {
return new ServletRegistrationBean(new ZuulServlet(), props.getMapping()+"/*");
//ZuulProxyProperties doesn't implement ZuulProperties so there are not 2 implementations (see ZuulServerConfiguration
public ZuulProperties zuulProperties() {
return new ZuulProperties() {
@Override
public String getMapping() {
return zuulProxyProperties().getMapping();
}
@Override
public boolean isStripMapping() {
return zuulProxyProperties().isStripMapping();
}
@Override
public String getRoutePrefix() {
return zuulProxyProperties().getRoutePrefix();
}
@Override
public boolean isAddProxyHeaders() {
return zuulProxyProperties().isAddProxyHeaders();
}
};
}
@Bean
Routes routes() {
return new Routes();
@Override
protected ZuulProperties getProperties() {
return zuulProperties();
}
@Bean
FilterInitializer zuulFilterInitializer() {
return new FilterInitializer();
}
// pre filters
@Bean
public DebugFilter debugFilter() {
return new DebugFilter();
}
@Bean
public PreDecorationFilter preDecorationFilter() {
return new PreDecorationFilter();
}
// route filters
@Bean
public RibbonRoutingFilter ribbonRoutingFilter() {
RibbonRoutingFilter filter = new RibbonRoutingFilter();
if (traces!=null) {
filter.setTraces(traces);
}
return filter;
}
// post filters
@Bean
public SendResponseFilter sendResponseFilter() {
return new SendResponseFilter();
}
}

View File

@@ -11,4 +11,6 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
public class ZuulProxyProperties {
private String mapping = "/proxy";
private boolean stripMapping = true; // this is currently the default behaviour
private String routePrefix = "zuul.proxy.route.";
private boolean addProxyHeaders = false; // TODO: current CF demo's rely on this
}

View File

@@ -9,7 +9,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.Routes;
import org.springframework.cloud.netflix.zuul.ZuulProxyProperties;
import org.springframework.cloud.netflix.zuul.ZuulProperties;
import javax.annotation.Nullable;
import java.util.LinkedHashMap;
@@ -21,7 +21,7 @@ public class PreDecorationFilter extends ZuulFilter {
private Routes routes;
@Autowired
private ZuulProxyProperties properties;
private ZuulProperties properties;
@Override
public int filterOrder() {
@@ -71,6 +71,10 @@ public class PreDecorationFilter extends ZuulFilter {
ctx.set("serviceId", serviceId);
ctx.setRouteHost(null);
ctx.addOriginResponseHeader("X-Zuul-ServiceId", serviceId);
if (properties.isAddProxyHeaders()) {
ctx.addZuulRequestHeader("X-Forwarded-Host", ctx.getRequest().getServerName() + ":" + String.valueOf(ctx.getRequest().getServerPort()));
}
}
} else {
LOG.warn("No route found for uri: "+requestURI);

View File

@@ -4,7 +4,6 @@ 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.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -14,8 +13,7 @@ import org.springframework.web.bind.annotation.RestController;
@EnableAutoConfiguration
@RestController
@EnableZuulProxy
@EnableEurekaClient
public class Application {
public class ZuulProxyApplication {
@RequestMapping("/testing123")
public String testing123() {
@@ -28,7 +26,7 @@ public class Application {
}
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(true).run(args);
new SpringApplicationBuilder(ZuulProxyApplication.class).web(true).run(args);
}
}

View File

@@ -8,10 +8,10 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@SpringApplicationConfiguration(classes = ZuulProxyApplication.class)
@WebAppConfiguration
@IntegrationTest("server.port=0")
public class ApplicationTests {
public class ZuulProxyApplicationTests {
@Test
public void contextLoads() {

View File

@@ -10,8 +10,8 @@ eureka:
zuul:
proxy:
mapping: /api
stripMapping: false
stripMapping: true
route:
testclient: /api/testing123
stores: /api/stores
customers: /api/customers
testclient: /testing123
stores: /stores
customers: /customers

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-netflix-zuul-server</artifactId>
<packaging>jar</packaging>
<name>Spring Cloud Netflix Zuul Server</name>
<url>http://projects.spring.io/spring-cloud/</url>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<build>
<plugins>
</plugins>
</build>
<properties>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-core</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.eureka</groupId>
<artifactId>eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-core</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-metrics-event-stream</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-core</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-eureka</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-httpclient</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.zuul</groupId>
<artifactId>zuul-core</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<!-- Only needed at compile time -->
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,19 @@
package org.springframework.cloud.netflix.zuul;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
/**
* @author Spencer Gibb
*/
@EnableHystrix
@EnableEurekaClient
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ZuulServerConfiguration.class)
public @interface EnableZuulServer {
}

View File

@@ -0,0 +1,54 @@
package org.springframework.cloud.netflix.zuul;
import com.netflix.zuul.http.ZuulServlet;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author Spencer Gibb
*/
@Configuration
@ConditionalOnClass(ZuulServlet.class)
@EnableConfigurationProperties(ZuulServerProperties.class)
@ConditionalOnExpression("${zuul.server.enabled:true}")
public class ZuulServerConfiguration extends AbstractZuulConfiguration {
@Bean
public ZuulServerProperties zuulServerProperties() {
return new ZuulServerProperties();
}
@Bean
//ZuulServerProperties doesn't implement ZuulProperties so there are not 2 implementations (see ZuulProxyConfiguration
public ZuulProperties zuulProperties() {
return new ZuulProperties() {
@Override
public String getMapping() {
return zuulServerProperties().getMapping();
}
@Override
public boolean isStripMapping() {
return zuulServerProperties().isStripMapping();
}
@Override
public String getRoutePrefix() {
return zuulServerProperties().getRoutePrefix();
}
@Override
public boolean isAddProxyHeaders() {
return zuulServerProperties().isAddProxyHeaders();
}
};
}
@Override
protected ZuulProperties getProperties() {
return zuulProperties();
}
}

View File

@@ -0,0 +1,16 @@
package org.springframework.cloud.netflix.zuul;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author Spencer Gibb
*/
@Data
@ConfigurationProperties("zuul.server")
public class ZuulServerProperties {
private String mapping = "";
private boolean stripMapping = false;
private String routePrefix = "zuul.server.route.";
private boolean addProxyHeaders = true;
}

View File

@@ -0,0 +1,16 @@
package org.springframework.cloud.netflix.zuul;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableAutoConfiguration
@EnableZuulServer
public class ZuulServerApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(ZuulServerApplication.class).web(true).run(args);
}
}

View File

@@ -0,0 +1,20 @@
package org.springframework.cloud.netflix.zuul;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = ZuulServerApplication.class)
@WebAppConfiguration
@IntegrationTest("server.port=0")
public class ZuulServerApplicationTests {
@Test
public void contextLoads() {
}
}

View File

@@ -0,0 +1,15 @@
server:
port: 9876
management:
port: 9877
spring:
application:
name: testzuulserver
zuul:
server:
route:
testclient: /testing123
stores: /stores
customers: /customers

View File

@@ -1,84 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-netflix-zuul</artifactId>
<packaging>jar</packaging>
<name>Spring Cloud Netflix Zuul</name>
<url>http://projects.spring.io/spring-cloud/</url>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<!--skip deploy (this is just a test module) -->
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<spring-cloud-config.version>1.0.0.BUILD-SNAPSHOT</spring-cloud-config.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config</artifactId>
<version>${spring-cloud-config.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-core</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-metrics-event-stream</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-core</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-eureka</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-httpclient</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.zuul</groupId>
<artifactId>zuul-core</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -1,69 +0,0 @@
package io.spring.cloud.netflix.zuul;
import com.netflix.zuul.context.ContextLifecycleFilter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.embedded.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.cloud.netflix.hystrix.HystrixStreamEndpoint;
import org.springframework.cloud.netflix.zuul.Routes;
import org.springframework.scheduling.annotation.EnableScheduling;
import java.util.ArrayList;
import java.util.Collection;
/**
* User: spencergibb
* Date: 4/24/14
* Time: 8:57 PM
*/
@Configuration
@EnableAutoConfiguration
@EnableScheduling
@ComponentScan(basePackageClasses = Application.class)
public class Application {
@Bean
ZuulProperties routerProperties() {
return new ZuulProperties();
}
@Bean
public FilterRegistrationBean contextLifecycleFilter() {
Collection<String> urlPatterns = new ArrayList<>();
urlPatterns.add("/*");
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new ContextLifecycleFilter());
filterRegistrationBean.setUrlPatterns(urlPatterns);
return filterRegistrationBean;
}
//TODO: replace with auto config
@Bean
HystrixStreamEndpoint hystrixStreamEndpoint() {
return new HystrixStreamEndpoint();
}
@Bean
Routes routes() {
return new Routes();
}
@Bean
FilterIntializer filterIntializer() {
return new FilterIntializer();
}
@Bean
ZuulController zuulController() {
return new ZuulController();
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

View File

@@ -1,65 +0,0 @@
package io.spring.cloud.netflix.zuul;
import com.netflix.zuul.FilterFileManager;
import com.netflix.zuul.FilterLoader;
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;
/**
* 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
ZuulProperties props;
@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();
// initializes groovy filesystem poller
initGroovyFilterManager();
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
LOGGER.info("Stopping filter initializer context listener");
}
private void initGroovyFilterManager() {
//TODO: pluggable filter initialzer
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);
}
}
}

View File

@@ -1,28 +0,0 @@
package io.spring.cloud.netflix.zuul;
import com.netflix.zuul.http.ZuulServlet;
import io.spring.cloud.util.AbstractServletWrappingController;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* User: spencergibb
* Date: 4/24/14
* Time: 9:12 PM
*/
@Controller
public class ZuulController extends AbstractServletWrappingController {
public ZuulController() {
super(ZuulServlet.class, "zuulServlet");
}
@RequestMapping("/**")
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response) throws Exception {
return this.controller.handleRequest(request, response);
}
}

View File

@@ -1,32 +0,0 @@
package io.spring.cloud.netflix.zuul;
import org.springframework.boot.context.properties.ConfigurationProperties;
import static java.util.concurrent.TimeUnit.MINUTES;
/**
* User: spencergibb
* Date: 5/2/14
* Time: 9:20 PM
*/
@ConfigurationProperties("zuul")
public class ZuulProperties {
private String filterRoot;
private long cacheRefresh = MINUTES.toMillis(5L);
public String getFilterRoot() {
return filterRoot;
}
public void setFilterRoot(String filterRoot) {
this.filterRoot = filterRoot;
}
public long getCacheRefresh() {
return cacheRefresh;
}
public void setCacheRefresh(long cacheRefresh) {
this.cacheRefresh = cacheRefresh;
}
}

View File

@@ -1,42 +0,0 @@
package io.spring.cloud.util;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.web.context.ServletContextAware;
import org.springframework.web.servlet.mvc.ServletWrappingController;
import javax.servlet.ServletContext;
/**
* User: spencergibb
* Date: 5/1/14
* Time: 10:03 AM
*/
public abstract class AbstractServletWrappingController implements InitializingBean,
ApplicationContextAware, ServletContextAware {
protected final ServletWrappingController controller = new ServletWrappingController();
public AbstractServletWrappingController(Class<?> servletClass, String servletName) {
controller.setServletClass(servletClass);
controller.setServletName(servletName);
}
@Override
public void afterPropertiesSet() throws Exception {
this.controller.afterPropertiesSet();
}
@Override
public void setServletContext(ServletContext servletContext) {
this.controller.setServletContext(servletContext);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.controller.setApplicationContext(applicationContext);
}
}

View File

@@ -1,8 +0,0 @@
#org.springframework.context.ApplicationListener=\
#org.springframework.cloud.netflix.eureka.EurekaStartingListener,\
#org.springframework.cloud.netflix.eureka.EurekaUpListener,\
#org.springframework.cloud.netflix.eureka.EurekaOutOfServiceListener
#org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
#org.springframework.cloud.netflix.archaius.ArchaiusAutoConfiguration,\
#org.springframework.cloud.netflix.feign.FeignAutoConfiguration

View File

@@ -1,48 +0,0 @@
info:
component: Platform Router (Zuul)
endpoints:
restart:
enabled: true
shutdown:
enabled: true
server:
port: 6080
management:
port: 6081
logging:
level: INFO
logging:
level:
com:
netflix: WARN
eureka:
client:
#Region where eureka is deployed -For AWS specify one of the AWS regions, for other datacenters specify a arbitrary string
#indicating the region.This is normally specified as a -D option (eg) -Deureka.region=us-east-1
region: default
#For eureka clients running in eureka server, it needs to connect to servers in other zones
preferSameZone: false
#Change this if you want to use a DNS based lookup for determining other eureka servers. For example
#of specifying the DNS entries, check the eureka-client-test.properties, eureka-client-prod.properties
#shouldUseDns: false
us-east-1:
availabilityZones: default
#serviceUrl:
#default: http://localhost:8761/eureka/
#defaultZone: http://localhost:8761/eureka/
instance:
#Virtual host name by which the clients identifies this service
virtualHostName: ${spring.application.name}

View File

@@ -1,6 +0,0 @@
spring:
application:
name: zuul
platform:
config:
uri: http://localhost:${config.port:8888}

View File

@@ -1,170 +0,0 @@
package filters.post
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.util.zip.GZIPInputStream
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
String filterType() {
return 'post'
}
@Override
int filterOrder() {
return 1000
}
boolean shouldFilter() {
return !RequestContext.currentContext.getZuulResponseHeaders().isEmpty() ||
RequestContext.currentContext.getResponseDataStream() != null ||
RequestContext.currentContext.responseBody != null
}
Object run() {
addResponseHeaders()
writeResponse()
}
void writeResponse() {
RequestContext context = RequestContext.currentContext
// 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.currentContext.responseBody != null) {
String body = RequestContext.currentContext.responseBody
writeResponse(new ByteArrayInputStream(body.bytes), 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) {
println("gzip expected but not received assuming unencoded response" + RequestContext.currentContext.getRequest().getRequestURL().toString())
inputStream = is
}
else if (context.getResponseGZipped() && isGzipRequested)
servletResponse.setHeader(ZuulHeaders.CONTENT_ENCODING, "gzip")
writeResponse(inputStream, outStream)
}
}
} finally {
try {
is?.close();
outStream.flush()
outStream.close()
} catch (IOException e) {
}
}
}
def writeResponse(InputStream zin, OutputStream out) {
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<Pair<String, String>> zuulResponseHeaders = context.getZuulResponseHeaders();
String debugHeader = ""
List<String> rd
rd = (List<String>) RequestContext.getCurrentContext().get("routingDebug");
rd?.each {
debugHeader += "[[[${it}]]]";
}
/*
rd = (List<String>) 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?.each { Pair<String, String> it ->
servletResponse.addHeader(it.first(), it.second())
Debug.addRequestDebug("OUTBOUND: < " + it.first() + ":" + it.second())
}
} else {
zuulResponseHeaders?.each { Pair<String, String> it ->
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)
}
}
}

View File

@@ -1,43 +0,0 @@
package filters.post
import com.netflix.zuul.ZuulFilter
import com.netflix.zuul.context.RequestContext
class Stats extends ZuulFilter {
@Override
String filterType() {
return "post"
}
@Override
int filterOrder() {
return 2000
}
@Override
boolean shouldFilter() {
return true
}
@Override
Object run() {
dumpRoutingDebug()
dumpRequestDebug()
}
public void dumpRequestDebug() {
List<String> rd = (List<String>) RequestContext.getCurrentContext().get("requestDebug");
rd?.each {
println("REQUEST_DEBUG::${it}");
}
}
public void dumpRoutingDebug() {
List<String> rd = (List<String>) RequestContext.getCurrentContext().get("routingDebug");
rd?.each {
println("ZUUL_DEBUG::${it}");
}
}
}

View File

@@ -1,42 +0,0 @@
package 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
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
String filterType() {
return 'pre'
}
@Override
int filterOrder() {
return 1
}
boolean shouldFilter() {
if ("true".equals(RequestContext.getCurrentContext().getRequest().getParameter(debugParameter.get()))) return true;
return routingDebug.get();
}
Object run() {
RequestContext ctx = RequestContext.getCurrentContext()
ctx.setDebugRouting(true)
ctx.setDebugRequest(true)
return null;
}
}

View File

@@ -1,55 +0,0 @@
package filters.pre
import com.netflix.zuul.ZuulFilter
import com.netflix.zuul.context.Debug
import com.netflix.zuul.context.RequestContext
import javax.servlet.http.HttpServletRequest
class DebugRequest extends ZuulFilter {
@Override
String filterType() {
return 'pre'
}
@Override
int filterOrder() {
return 10000
}
@Override
boolean shouldFilter() {
return Debug.debugRequest()
}
@Override
Object run() {
HttpServletRequest req = RequestContext.currentContext.request as HttpServletRequest
Debug.addRequestDebug("REQUEST:: " + req.getScheme() + " " + req.getRemoteAddr() + ":" + req.getRemotePort())
Debug.addRequestDebug("REQUEST:: > " + req.getMethod() + " " + req.getRequestURI() + " " + req.getProtocol())
Iterator headerIt = req.getHeaderNames().iterator()
while (headerIt.hasNext()) {
String name = (String) headerIt.next()
String value = req.getHeader(name)
Debug.addRequestDebug("REQUEST:: > " + name + ":" + value)
}
final RequestContext ctx = RequestContext.getCurrentContext()
if (!ctx.isChunkedRequestBody()) {
InputStream inp = ctx.request.getInputStream()
String body = null
if (inp != null) {
body = inp.getText()
Debug.addRequestDebug("REQUEST:: > " + body)
}
}
return null;
}
}

View File

@@ -1,52 +0,0 @@
package filters.pre
import com.netflix.zuul.context.RequestContext
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.cloud.netflix.zuul.Routes
import org.springframework.cloud.netflix.zuul.SpringFilter
class PreDecorationFilter extends SpringFilter {
private static Logger LOG = LoggerFactory.getLogger(PreDecorationFilter.class);
@Override
int filterOrder() {
return 5
}
@Override
String filterType() {
return "pre"
}
@Override
boolean shouldFilter() {
return true;
}
@Override
Object run() {
RequestContext ctx = RequestContext.getCurrentContext()
def requestURI = ctx.getRequest().getRequestURI()
Routes routes = getBean(Routes.class)
def routesMap = routes.getRoutes()
def route = routesMap.keySet().find { path ->
//TODO: use ant matchers?
if (requestURI.startsWith(path)) {
return true
}
return false
}
def serviceId = routesMap.get(route)
if (serviceId != null) {
// set serviceId for use in filters.route.RibbonRequest
ctx.set("serviceId", serviceId)
ctx.setRouteHost(null)
ctx.addOriginResponseHeader("X-Zuul-ServiceId", serviceId);
}
}
}

View File

@@ -1,284 +0,0 @@
package filters.route
import com.netflix.client.ClientException
import com.netflix.client.ClientFactory
import com.netflix.client.IClient
import com.netflix.client.http.HttpRequest
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.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.cloud.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.cloud.netflix.feign.FeignConfigurer.setServiceListClassAndVIP
class RibbonRequest extends ZuulFilter {
private static final Logger LOG = LoggerFactory.getLogger(RibbonRequest.class);
public static final String CONTENT_ENCODING = "Content-Encoding";
@Override
String filterType() {
return 'route'
}
@Override
int filterOrder() {
return 10
}
boolean shouldFilter() {
def ctx = RequestContext.currentContext
return (ctx.getRouteHost() == null && ctx.get("serviceId") != null && ctx.sendZuulResponse())
}
Object run() {
RequestContext context = RequestContext.currentContext
HttpServletRequest request = context.getRequest();
MultivaluedMap<String, String> headers = buildZuulRequestHeaders(request)
MultivaluedMap<String, String> params = buildZuulRequestQueryParams(request)
Verb verb = getVerb(request);
Object requestEntity = getRequestBody(request)
def serviceId = context.get("serviceId")
//TODO: can this be set be default? or an implementation of an interface?
setServiceListClassAndVIP(serviceId)
IClient restClient = ClientFactory.getNamedClient(serviceId);
String uri = request.getRequestURI()
if (context.requestURI != null) {
uri = context.requestURI
}
//remove double slashes
uri = uri.replace("//", "/")
HttpResponse response = forward(restClient, verb, uri, headers, params, requestEntity)
setResponse(response)
return response
}
void debug(RestClient restClient, Verb verb, uri, MultivaluedMap<String, String> headers, MultivaluedMap<String, String> params, InputStream requestEntity) {
if (Debug.debugRequest()) {
headers.each {
Debug.addRequestDebug("ZUUL:: > ${it.key} ${it.value[0]}")
}
String query = ""
params.each {
it.value.each { v ->
query += it.key + "=" + v + "&"
}
}
Debug.addRequestDebug("ZUUL:: > ${verb.verb()} ${uri}?${query} HTTP/1.1")
RequestContext ctx = RequestContext.getCurrentContext()
if (!ctx.isChunkedRequestBody()) {
if (requestEntity != null) {
debugRequestEntity(ctx.request.getInputStream())
}
}
}
}
void debugRequestEntity(InputStream inputStream) {
if (!Debug.debugRequestHeadersOnly()) {
String entity = inputStream.getText()
Debug.addRequestDebug("ZUUL:: > ${entity}")
}
}
def HttpResponse forward(RestClient restClient, Verb verb, uri, MultivaluedMap<String, String> headers,
MultivaluedMap<String, String> params, InputStream requestEntity) {
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?.fallbackException?.cause instanceof ClientException) {
ClientException ex = e.fallbackException.cause as ClientException
throw new ZuulException(ex, "Forwarding error", 500, ex.getErrorType().toString())
}
throw new ZuulException(e, "Forwarding error", 500, e.failureType.toString())
}
}
def getRequestBody(HttpServletRequest request) {
Object requestEntity = null;
try {
requestEntity = RequestContext.currentContext.requestEntity
if (requestEntity == null) {
requestEntity = request.getInputStream();
}
} catch (IOException e) {
LOG.error(e);
}
return requestEntity
}
def MultivaluedMap<String, String> buildZuulRequestQueryParams(HttpServletRequest request) {
Map<String, List<String>> map = HTTPRequestUtils.getInstance().getQueryParams()
MultivaluedMap<String, String> params = new MultivaluedMapImpl<String, String>();
if (map == null) return params;
map.entrySet().each {
it.value.each { v ->
params.add(it.key, v)
}
}
return params
}
def MultivaluedMap<String, String> buildZuulRequestHeaders(HttpServletRequest request) {
RequestContext context = RequestContext.currentContext
MultivaluedMap<String, String> headers = new MultivaluedMapImpl<String, String>();
Enumeration headerNames = request.getHeaderNames();
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();
zuulRequestHeaders.keySet().each {
headers.putSingle((String) it, (String) zuulRequestHeaders[it])
}
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) {
RequestContext context = RequestContext.getCurrentContext()
context.setResponseStatusCode(resp.getStatus());
if (resp.hasEntity()) {
context.responseDataStream = resp.inputStream;
}
String contentEncoding = resp.getHeaders().get(CONTENT_ENCODING)?.first();
if (contentEncoding != null && HTTPRequestUtils.getInstance().isGzipped(contentEncoding)) {
context.setResponseGZipped(true);
} else {
context.setResponseGZipped(false);
}
if (Debug.debugRequest()) {
resp.getHeaders().keySet().each { key ->
boolean isValidHeader = isValidHeader(key)
Collection<String> list = resp.getHeaders().get(key)
list.each { header ->
context.addOriginResponseHeader(key, header)
if (key.equalsIgnoreCase("content-length"))
context.setOriginContentLength(header);
if (isValidHeader) {
context.addZuulResponseHeader(key, header);
Debug.addRequestDebug("ORIGIN_RESPONSE:: < ${key} ${header}")
}
}
}
if (context.responseDataStream) {
byte[] origBytes = context.getResponseDataStream().bytes
InputStream inStream = new ByteArrayInputStream(origBytes);
if (context.getResponseGZipped())
inStream = new GZIPInputStream(inStream);
String responseEntity = inStream.getText()
Debug.addRequestDebug("ORIGIN_RESPONSE:: < ${responseEntity}")
context.setResponseDataStream(new ByteArrayInputStream(origBytes))
}
} else {
resp.getHeaders().keySet().each { key ->
boolean isValidHeader = isValidHeader(key)
Collection<java.lang.String> list = resp.getHeaders().get(key)
list.each { header ->
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
}
}
}

View File

@@ -1,434 +0,0 @@
package filters.route
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.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.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.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
import javax.servlet.http.HttpServletRequest
import java.security.*
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import java.util.concurrent.atomic.AtomicReference
import java.util.zip.GZIPInputStream
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
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<HttpClient> CLIENT = new AtomicReference<HttpClient>(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
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() {
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));
ClientConnectionManager 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
String filterType() {
return 'route'
}
@Override
int filterOrder() {
return 100
}
boolean shouldFilter() {
return RequestContext.currentContext.getRouteHost() != null && RequestContext.currentContext.sendZuulResponse()
}
private static final void loadClient() {
final HttpClient oldClient = CLIENT.get();
CLIENT.set(newClient())
if (oldClient != null) {
CONNECTION_MANAGER_TIMER.schedule(new TimerTask() {
@Override
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
HttpClient 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
boolean isRedirected(HttpRequest httpRequest, HttpResponse httpResponse, HttpContext httpContext) {
return false
}
@Override
org.apache.http.client.methods.HttpUriRequest getRedirect(HttpRequest httpRequest, HttpResponse httpResponse, HttpContext httpContext) {
return null
}
})
return httpclient
}
Object run() {
HttpServletRequest request = RequestContext.currentContext.getRequest();
Header[] headers = buildZuulRequestHeaders(request)
String verb = getVerb(request);
InputStream requestEntity = getRequestBody(request)
HttpClient httpclient = CLIENT.get()
String uri = request.getRequestURI()
if (RequestContext.currentContext.requestURI != null) {
uri = RequestContext.currentContext.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 e;
}
return null
}
def InputStream debug(HttpClient httpclient, String verb, String uri, HttpServletRequest request, Header[] headers, InputStream requestEntity) {
if (Debug.debugRequest()) {
Debug.addRequestDebug("ZUUL:: host=${RequestContext.currentContext.getRouteHost()}")
headers.each {
Debug.addRequestDebug("ZUUL::> ${it.name} ${it.value}")
}
String query = request.queryString
Debug.addRequestDebug("ZUUL:: > ${verb} ${uri}?${query} HTTP/1.1")
if (requestEntity != null) {
requestEntity = debugRequestEntity(requestEntity)
}
}
return requestEntity
}
InputStream debugRequestEntity(InputStream inputStream) {
if (Debug.debugRequestHeadersOnly()) return inputStream
if (inputStream == null) return null
String entity = inputStream.getText()
Debug.addRequestDebug("ZUUL::> ${entity}")
return new ByteArrayInputStream(entity.bytes)
}
def HttpResponse forward(HttpClient httpclient, String verb, String uri, HttpServletRequest request, Header[] headers, InputStream requestEntity) {
requestEntity = debug(httpclient, verb, uri, request, headers, requestEntity)
org.apache.http.HttpHost httpHost
httpHost = getHttpHost()
org.apache.http.HttpRequest httpRequest;
switch (verb) {
case 'POST':
httpRequest = new HttpPost(uri + getQueryString())
InputStreamEntity entity = new InputStreamEntity(requestEntity, request.getContentLength())
httpRequest.setEntity(entity)
break
case 'PUT':
httpRequest = new HttpPut(uri + getQueryString())
InputStreamEntity entity = new InputStreamEntity(requestEntity, request.getContentLength())
httpRequest.setEntity(entity)
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();
}
}
HttpResponse forwardRequest(HttpClient httpclient, HttpHost httpHost, HttpRequest httpRequest) {
return httpclient.execute(httpHost, httpRequest);
}
String getQueryString() {
HttpServletRequest request = RequestContext.currentContext.getRequest();
String query = request.getQueryString()
return (query != null) ? "?${query}" : "";
}
HttpHost getHttpHost() {
HttpHost httpHost
URL host = RequestContext.currentContext.getRouteHost()
httpHost = new HttpHost(host.getHost(), host.getPort(), host.getProtocol())
return httpHost
}
def getRequestBody(HttpServletRequest request) {
Object 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.currentContext.responseGZipped) {
if (name.toLowerCase().contains("accept-encoding")) return false;
}
return true;
}
def Header[] buildZuulRequestHeaders(HttpServletRequest request) {
def 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();
zuulRequestHeaders.keySet().each {
String name = it.toLowerCase()
BasicHeader h = headers.find { BasicHeader he -> he.name == name }
if (h != null) {
headers.remove(h)
}
headers.add(new BasicHeader((String) it, (String) zuulRequestHeaders[it]))
}
if (RequestContext.currentContext.responseGZipped) {
headers.add(new BasicHeader("accept-encoding", "deflate, gzip"))
}
return headers
}
String getVerb(HttpServletRequest request) {
String sMethod = request.getMethod();
return sMethod.toUpperCase();
}
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"
}
void setResponse(HttpResponse response) {
RequestContext context = RequestContext.getCurrentContext()
RequestContext.currentContext.set("hostZuulResponse", response)
RequestContext.getCurrentContext().setResponseStatusCode(response.getStatusLine().statusCode)
RequestContext.getCurrentContext().responseDataStream = response?.entity?.content
boolean isOriginResponseGzipped = false
for (Header h : response.getHeaders(CONTENT_ENCODING)) {
if (HTTPRequestUtils.getInstance().isGzipped(h.value)) {
isOriginResponseGzipped = true;
break;
}
}
context.setResponseGZipped(isOriginResponseGzipped);
if (Debug.debugRequest()) {
response.getAllHeaders()?.each { Header header ->
if (isValidHeader(header)) {
RequestContext.getCurrentContext().addZuulResponseHeader(header.name, header.value);
Debug.addRequestDebug("ORIGIN_RESPONSE:: < ${header.name}, ${header.value}")
}
}
if (context.responseDataStream) {
byte[] origBytes = context.getResponseDataStream().bytes
ByteArrayInputStream byteStream = new ByteArrayInputStream(origBytes)
InputStream inputStream = byteStream
if (RequestContext.currentContext.responseGZipped) {
inputStream = new GZIPInputStream(byteStream);
}
context.setResponseDataStream(new ByteArrayInputStream(origBytes))
}
} else {
response.getAllHeaders()?.each { Header header ->
RequestContext ctx = RequestContext.getCurrentContext()
ctx.addOriginResponseHeader(header.name, header.value)
if (header.name.equalsIgnoreCase("content-length"))
ctx.setOriginContentLength(header.value)
if (isValidHeader(header)) {
ctx.addZuulResponseHeader(header.name, header.value);
}
}
}
}
boolean isValidHeader(Header header) {
switch (header.name.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();
}
}
}