diff --git a/.gitignore b/.gitignore index 76ba2482..030de882 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ .settings/ .springBeans target/ - +.idea +*.iml diff --git a/pom.xml b/pom.xml index 2e53e736..e8b93057 100644 --- a/pom.xml +++ b/pom.xml @@ -1,74 +1,107 @@ - 4.0.0 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 - org.springframework.platform - spring-platform-netflix - 1.0.0.BUILD-SNAPSHOT + org.springframework.platform + spring-platform-netflix + 1.0.0.BUILD-SNAPSHOT + pom + spring-platform-netflix + Spring Platform Netflix - spring-platform-netflix - Eureka integration project + + org.springframework.boot + spring-boot-starter-parent + 1.1.5.BUILD-SNAPSHOT + + + - - org.springframework.boot - spring-boot-starter-parent - 1.1.5.BUILD-SNAPSHOT - - - - - - - com.netflix.eureka - eureka-client - 1.1.135 - - - com.netflix.eureka - eureka-client - 1.1.135 - - - org.projectlombok - lombok - 1.12.6 - - - + + spring-platform-netflix-core + spring-platform-netflix-zuul + - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-actuator - - - com.netflix.eureka - eureka-client - - - com.netflix.eureka - eureka-client - true - - - org.projectlombok - lombok - - - org.springframework.boot - spring-boot-starter-test - test - - + + + + com.netflix.archaius + archaius-core + ${archaius.version} + + + com.netflix.eureka + eureka-client + ${eureka.version} + + + com.netflix.feign + feign-core + ${feign.version} + + + com.netflix.feign + feign-ribbon + ${feign.version} + + + + com.netflix.hystrix + hystrix-core + ${hystrix.version} + + + com.netflix.hystrix + hystrix-metrics-event-stream + ${hystrix.version} + + + com.netflix.hystrix + hystrix-javanica + ${hystrix.version} + + + com.netflix.ribbon + ribbon-core + ${ribbon.version} + + + com.netflix.ribbon + ribbon-httpclient + ${ribbon.version} + + + com.netflix.ribbon + ribbon-eureka + ${ribbon.version} + + + com.netflix.zuul + zuul-core + ${zuul.version} + + + org.projectlombok + lombok + 1.12.6 + + + - - UTF-8 - 1.7 - + + UTF-8 + 1.7 + 0.6.0 + 1.1.135 + 6.1.2 + 1.4.0-RC4 + 0.3.12 + 1.0.24 + diff --git a/spring-platform-netflix-core/pom.xml b/spring-platform-netflix-core/pom.xml new file mode 100644 index 00000000..8d5e37e9 --- /dev/null +++ b/spring-platform-netflix-core/pom.xml @@ -0,0 +1,85 @@ + + + 4.0.0 + + org.springframework.platform + spring-platform-netflix-core + 1.0.0.BUILD-SNAPSHOT + jar + spring-platform-netflix-core + Spring Platform Netflix Core + + + org.springframework.platform + spring-platform-netflix + 1.0.0.BUILD-SNAPSHOT + .. + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + com.netflix.archaius + archaius-core + + + com.netflix.eureka + eureka-client + + + com.netflix.eureka + eureka-client + true + + + com.netflix.feign + feign-core + + + com.netflix.feign + feign-ribbon + + + + com.netflix.hystrix + hystrix-core + + + com.netflix.hystrix + hystrix-metrics-event-stream + + + com.netflix.hystrix + hystrix-javanica + + + com.netflix.ribbon + ribbon-core + + + com.netflix.ribbon + ribbon-eureka + + + org.projectlombok + lombok + + + org.springframework.boot + spring-boot-starter-test + test + + + + diff --git a/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/archaius/ArchaiusAutoConfiguration.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/archaius/ArchaiusAutoConfiguration.java new file mode 100644 index 00000000..39fb76be --- /dev/null +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/archaius/ArchaiusAutoConfiguration.java @@ -0,0 +1,78 @@ +package org.springframework.platform.netflix.archaius; + +import com.netflix.config.ConcurrentCompositeConfiguration; +import com.netflix.config.ConfigurationManager; +import com.netflix.config.DynamicURLConfiguration; +import org.apache.commons.configuration.EnvironmentConfiguration; +import org.apache.commons.configuration.SystemConfiguration; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.ConfigurableEnvironment; + +import static com.netflix.config.ConfigurationBasedDeploymentContext.DEPLOYMENT_APPLICATION_ID_PROPERTY; +import static com.netflix.config.ConfigurationManager.*; +import static com.netflix.config.ConfigurationManager.APPLICATION_PROPERTIES; +import static com.netflix.config.ConfigurationManager.ENV_CONFIG_NAME; + +/** + * Created by sgibb on 7/3/14. + */ +@Configuration +public class ArchaiusAutoConfiguration { + + @Autowired + ConfigurableEnvironment env; + + @Bean + ConfigurableEnvironmentConfiguration configurableEnvironmentConfiguration() { + ConfigurableEnvironmentConfiguration envConfig = new ConfigurableEnvironmentConfiguration(env); + configureArchaius(envConfig); + return envConfig; + } + + protected void configureArchaius(ConfigurableEnvironmentConfiguration envConfig) { + String appName = env.getProperty("spring.application.name"); + if (appName == null) { + throw new IllegalStateException("spring.application.name may not be null"); + } + //this is deprecated, but currently it seams the only way to set it initially + System.setProperty(DEPLOYMENT_APPLICATION_ID_PROPERTY, appName); + + //TODO: support for other DeploymentContexts + + ConcurrentCompositeConfiguration config = new ConcurrentCompositeConfiguration(); + + //support to add other Configurations (Jdbc, DynamoDb, Zookeeper, jclouds, etc...) + /*if (factories != null && !factories.isEmpty()) { + for (PropertiesSourceFactory factory: factories) { + config.addConfiguration(factory.getConfiguration(), factory.getName()); + } + }*/ + config.addConfiguration(envConfig, ConfigurableEnvironmentConfiguration.class.getSimpleName()); + + //below come from ConfigurationManager.createDefaultConfigInstance() + try { + DynamicURLConfiguration defaultURLConfig = new DynamicURLConfiguration(); + config.addConfiguration(defaultURLConfig, URL_CONFIG_NAME); + } catch (Throwable e) { + e.printStackTrace(); //TODO: log error + } + + //TODO: sys/env above urls? + if (!Boolean.getBoolean(DISABLE_DEFAULT_SYS_CONFIG)) { + SystemConfiguration sysConfig = new SystemConfiguration(); + config.addConfiguration(sysConfig, SYS_CONFIG_NAME); + } + if (!Boolean.getBoolean(DISABLE_DEFAULT_ENV_CONFIG)) { + EnvironmentConfiguration environmentConfiguration = new EnvironmentConfiguration(); + config.addConfiguration(environmentConfiguration, ENV_CONFIG_NAME); + } + + ConcurrentCompositeConfiguration appOverrideConfig = new ConcurrentCompositeConfiguration(); + config.addConfiguration(appOverrideConfig, APPLICATION_PROPERTIES); + config.setContainerConfigurationIndex(config.getIndexOfConfiguration(appOverrideConfig)); + + ConfigurationManager.install(config); + } +} diff --git a/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/archaius/ConfigurableEnvironmentConfiguration.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/archaius/ConfigurableEnvironmentConfiguration.java new file mode 100644 index 00000000..f958db13 --- /dev/null +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/archaius/ConfigurableEnvironmentConfiguration.java @@ -0,0 +1,97 @@ +package org.springframework.platform.netflix.archaius; + +import org.apache.commons.configuration.AbstractConfiguration; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.*; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Field; +import java.util.*; + +/** + * Created by sgibb on 6/27/14. + */ +public class ConfigurableEnvironmentConfiguration extends AbstractConfiguration { + ConfigurableEnvironment environment; + + public ConfigurableEnvironmentConfiguration(ConfigurableEnvironment environment) { + this.environment = environment; + } + + @Override + protected void addPropertyDirect(String key, Object value) { + + } + + @Override + public boolean isEmpty() { + return !getKeys().hasNext(); //TODO: find a better way to do this + } + + @Override + public boolean containsKey(String key) { + return environment.containsProperty(key); + } + + @Override + public Object getProperty(String key) { + return environment.getProperty(key); + } + + @Override + public Iterator getKeys() { + List result = new ArrayList<>(); + for (Map.Entry> entry : getPropertySources().entrySet()) { + PropertySource source = entry.getValue(); + if (source instanceof EnumerablePropertySource) { + EnumerablePropertySource enumerable = (EnumerablePropertySource) source; + for (String name : enumerable.getPropertyNames()) { + result.add(name); + } + } + } + return result.iterator(); + } + + private Map> getPropertySources() { + Map> map = new LinkedHashMap>(); + MutablePropertySources sources = null; + if (this.environment != null + && this.environment instanceof ConfigurableEnvironment) { + sources = ((ConfigurableEnvironment) this.environment).getPropertySources(); + } + else { + sources = new StandardEnvironment().getPropertySources(); + } + for (PropertySource source : sources) { + extract("", map, source); + } + return map; + } + + private void extract(String root, Map> map, + PropertySource source) { + if (source instanceof CompositePropertySource) { + Set> nested = getNestedPropertySources((CompositePropertySource) source); + for (PropertySource nest : nested) { + extract(source.getName() + ":", map, nest); + } + } + else { + map.put(root + source.getName(), source); + } + } + + @SuppressWarnings("unchecked") + private Set> getNestedPropertySources(CompositePropertySource source) { + try { + Field field = ReflectionUtils.findField(CompositePropertySource.class, + "propertySources"); + field.setAccessible(true); + return (Set>) field.get(source); + } + catch (Exception ex) { + return Collections.emptySet(); + } + } +} diff --git a/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/circuitbreaker/CircuitBreakerConfiguration.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/circuitbreaker/CircuitBreakerConfiguration.java new file mode 100644 index 00000000..4488f669 --- /dev/null +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/circuitbreaker/CircuitBreakerConfiguration.java @@ -0,0 +1,56 @@ +package org.springframework.platform.netflix.circuitbreaker; + +import com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportAware; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.platform.netflix.circuitbreaker.annotations.EnableCircuitBreaker; +import org.springframework.platform.netflix.endpoint.HystrixStreamEndpoint; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +import java.util.Collection; + +/** + * Created by sgibb on 6/19/14. + */ +@Configuration +public class CircuitBreakerConfiguration implements ImportAware { + + private AnnotationAttributes enableCircuitBreaker; + + @Bean + HystrixCommandAspect hystrixCommandAspect() { + return new HystrixCommandAspect(); + } + + @Bean + //TODO: add enable/disable + public HystrixStreamEndpoint hystrixStreamEndpoint() { + return new HystrixStreamEndpoint(); + } + + @Override + public void setImportMetadata(AnnotationMetadata importMetadata) { + this.enableCircuitBreaker = AnnotationAttributes.fromMap( + importMetadata.getAnnotationAttributes(EnableCircuitBreaker.class.getName(), false)); + Assert.notNull(this.enableCircuitBreaker, + "@EnableCircuitBreaker is not present on importing class " + importMetadata.getClassName()); + } + + @Autowired(required=false) + void setConfigurers(Collection configurers) { + if (CollectionUtils.isEmpty(configurers)) { + return; + } + if (configurers.size() > 1) { + throw new IllegalStateException("Only one TransactionManagementConfigurer may exist"); + } + //TODO: create CircuitBreakerConfigurer API + CircuitBreakerConfigurer configurer = configurers.iterator().next(); + //this.txManager = configurer.annotationDrivenTransactionManager(); + } +} diff --git a/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/circuitbreaker/CircuitBreakerConfigurationSelector.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/circuitbreaker/CircuitBreakerConfigurationSelector.java new file mode 100644 index 00000000..6de08edd --- /dev/null +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/circuitbreaker/CircuitBreakerConfigurationSelector.java @@ -0,0 +1,29 @@ +package org.springframework.platform.netflix.circuitbreaker; + +import org.springframework.context.annotation.AdviceMode; +import org.springframework.context.annotation.AdviceModeImportSelector; +import org.springframework.context.annotation.AutoProxyRegistrar; +import org.springframework.platform.netflix.circuitbreaker.annotations.EnableCircuitBreaker; + +/** + * Created by sgibb on 6/19/14. + */ +public class CircuitBreakerConfigurationSelector extends AdviceModeImportSelector { + /** + * The name of the AspectJ transaction management @{@code Configuration} class. + */ + public static final String TRANSACTION_ASPECT_CONFIGURATION_CLASS_NAME = + "org.springframework.transaction.aspectj.AspectJTransactionManagementConfiguration"; + + @Override + protected String[] selectImports(AdviceMode adviceMode) { + switch (adviceMode) { + case PROXY: + return new String[]{AutoProxyRegistrar.class.getName(), CircuitBreakerConfiguration.class.getName()}; + case ASPECTJ: + return new String[]{TRANSACTION_ASPECT_CONFIGURATION_CLASS_NAME}; + default: + return null; + } + } +} diff --git a/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/circuitbreaker/CircuitBreakerConfigurer.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/circuitbreaker/CircuitBreakerConfigurer.java new file mode 100644 index 00000000..602292a6 --- /dev/null +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/circuitbreaker/CircuitBreakerConfigurer.java @@ -0,0 +1,7 @@ +package org.springframework.platform.netflix.circuitbreaker; + +/** + * Created by sgibb on 6/19/14. + */ +public interface CircuitBreakerConfigurer { +} diff --git a/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/circuitbreaker/annotations/EnableCircuitBreaker.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/circuitbreaker/annotations/EnableCircuitBreaker.java new file mode 100644 index 00000000..1e40b50a --- /dev/null +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/circuitbreaker/annotations/EnableCircuitBreaker.java @@ -0,0 +1,47 @@ +package org.springframework.platform.netflix.circuitbreaker.annotations; + +import org.springframework.context.annotation.AdviceMode; +import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; +import org.springframework.platform.netflix.circuitbreaker.CircuitBreakerConfigurationSelector; + +import java.lang.annotation.*; + +/** + * Created by sgibb on 6/19/14. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Import(CircuitBreakerConfigurationSelector.class) +public @interface EnableCircuitBreaker { + + /** + * Indicate whether subclass-based (CGLIB) proxies are to be created ({@code true}) as + * opposed to standard Java interface-based proxies ({@code false}). The default is + * {@code false}. Applicable only if {@link #mode()} is set to + * {@link org.springframework.context.annotation.AdviceMode#PROXY}. + *

Note that setting this attribute to {@code true} will affect all + * Spring-managed beans requiring proxying, not just those marked with + * {@code @Transactional}. For example, other beans marked with Spring's + * {@code @Async} annotation will be upgraded to subclass proxying at the same + * time. This approach has no negative impact in practice unless one is explicitly + * expecting one type of proxy vs another, e.g. in tests. + */ + boolean proxyTargetClass() default false; + + /** + * Indicate how transactional advice should be applied. The default is + * {@link org.springframework.context.annotation.AdviceMode#PROXY}. + * @see org.springframework.context.annotation.AdviceMode + */ + AdviceMode mode() default AdviceMode.PROXY; + + /** + * Indicate the ordering of the execution of the transaction advisor + * when multiple advices are applied at a specific joinpoint. + * The default is {@link org.springframework.core.Ordered#LOWEST_PRECEDENCE}. + */ + int order() default Ordered.LOWEST_PRECEDENCE; + +} \ No newline at end of file diff --git a/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/endpoint/HystrixStreamEndpoint.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/endpoint/HystrixStreamEndpoint.java new file mode 100644 index 00000000..ffa57b09 --- /dev/null +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/endpoint/HystrixStreamEndpoint.java @@ -0,0 +1,15 @@ +package org.springframework.platform.netflix.endpoint; + +import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet; + +/** + * User: spencergibb + * Date: 4/22/14 + * Time: 3:16 PM + */ +public class HystrixStreamEndpoint extends ServletWrappingEndpoint { + + public HystrixStreamEndpoint() { + super(HystrixMetricsStreamServlet.class, "hystrixStream", "hystrix.stream", false, true); + } +} diff --git a/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/endpoint/ServletWrappingEndpoint.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/endpoint/ServletWrappingEndpoint.java new file mode 100644 index 00000000..0fff26af --- /dev/null +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/endpoint/ServletWrappingEndpoint.java @@ -0,0 +1,76 @@ +package org.springframework.platform.netflix.endpoint; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.actuate.endpoint.Endpoint; +import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.context.ServletContextAware; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ServletWrappingController; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * TODO: move to spring-boot? + * User: spencergibb + * Date: 4/24/14 + * Time: 9:13 PM + */ +public abstract class ServletWrappingEndpoint implements InitializingBean, + ApplicationContextAware, ServletContextAware, MvcEndpoint { + + protected String path; + protected boolean sensitive; + protected boolean enabled = true; + + protected final ServletWrappingController controller = new ServletWrappingController(); + + @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); + } + + protected ServletWrappingEndpoint(Class servletClass, String servletName, String path, + boolean sensitive, boolean enabled) { + controller.setServletClass(servletClass); + controller.setServletName(servletName); + this.path = path; + this.sensitive = sensitive; + this.enabled = enabled; + } + + @RequestMapping("**") + public ModelAndView handle(HttpServletRequest request, HttpServletResponse response) throws Exception { + return this.controller.handleRequest(request, response); + } + + @Override + public String getPath() { + return path; + } + + @Override + public boolean isSensitive() { + return sensitive; + } + + @Override + public Class getEndpointType() { + return null; + } +} diff --git a/src/main/java/org/springframework/platform/netflix/eureka/EurekaClientAutoConfiguration.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/eureka/EurekaClientAutoConfiguration.java similarity index 100% rename from src/main/java/org/springframework/platform/netflix/eureka/EurekaClientAutoConfiguration.java rename to spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/eureka/EurekaClientAutoConfiguration.java diff --git a/src/main/java/org/springframework/platform/netflix/eureka/EurekaClientConfigBean.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/eureka/EurekaClientConfigBean.java similarity index 96% rename from src/main/java/org/springframework/platform/netflix/eureka/EurekaClientConfigBean.java rename to spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/eureka/EurekaClientConfigBean.java index 09bf489a..58b71fe7 100644 --- a/src/main/java/org/springframework/platform/netflix/eureka/EurekaClientConfigBean.java +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/eureka/EurekaClientConfigBean.java @@ -147,16 +147,16 @@ public class EurekaClientConfigBean implements EurekaClientConfig { @Override public List getEurekaServerServiceUrls(String myZone) { - String serviceUrls = serviceUrl .get(myZone); + String serviceUrls = serviceUrl.get(myZone); if (serviceUrls == null || serviceUrls.isEmpty()) { - serviceUrls = serviceUrl.get("default" + myZone); + serviceUrls = serviceUrl.get("default"); } if (serviceUrls != null) { return Arrays.asList(serviceUrls.split(",")); } - return new ArrayList(); + return new ArrayList<>(); } @Override diff --git a/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 similarity index 96% rename from src/main/java/org/springframework/platform/netflix/eureka/EurekaInstanceConfigBean.java rename to spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/eureka/EurekaInstanceConfigBean.java index 611989a7..7f3925c0 100644 --- a/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 @@ -46,7 +46,7 @@ public class EurekaInstanceConfigBean implements EurekaInstanceConfig { @Getter(AccessLevel.PRIVATE) @Setter(AccessLevel.PRIVATE) private String[] hostInfo = initHostInfo(); - @Value("${spring.application.name:unkown}") + @Value("${spring.application.name:unknown}") private String appname = "unknown"; private String appGroupName; @@ -66,7 +66,7 @@ public class EurekaInstanceConfigBean implements EurekaInstanceConfig { private int leaseExpirationDurationInSeconds = 90; - @Value("${spring.application.name:unkown}.mydomain.net") + @Value("${spring.application.name:unknown}.mydomain.net") private String virtualHostName; private String secureVirtualHostName; diff --git a/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/feign/FeignAutoConfiguration.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/feign/FeignAutoConfiguration.java new file mode 100644 index 00000000..f7aa1496 --- /dev/null +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/feign/FeignAutoConfiguration.java @@ -0,0 +1,36 @@ +package org.springframework.platform.netflix.feign; + +import feign.Contract; +import feign.Logger; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.platform.netflix.archaius.ArchaiusAutoConfiguration; + +/** + * Created by sgibb on 7/3/14. + */ +@Configuration +@AutoConfigureAfter(ArchaiusAutoConfiguration.class) +public class FeignAutoConfiguration { + @Bean + SpringDecoder feignDecoder() { + return new SpringDecoder(); + } + + @Bean + SpringEncoder feignEncoder() { + return new SpringEncoder(); + } + + @Bean + public Logger feignLogger() { + //return new Slf4jLogger(); //TODO pass Client classname in + return new Logger.JavaLogger(); + } + + @Bean + public Contract feignContract() { + return new SpringMvcContract(); + } +} diff --git a/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/feign/FeignBase.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/feign/FeignBase.java new file mode 100644 index 00000000..b2876791 --- /dev/null +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/feign/FeignBase.java @@ -0,0 +1,101 @@ +package org.springframework.platform.netflix.feign; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.converter.ByteArrayHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.ResourceHttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter; +import org.springframework.http.converter.feed.RssChannelHttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; +import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; +import org.springframework.http.converter.xml.SourceHttpMessageConverter; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +import javax.xml.transform.Source; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * Created by sgibb on 6/27/14. + */ +public class FeignBase { + public static final boolean romePresent = + ClassUtils.isPresent("com.sun.syndication.feed.WireFeed", FeignBase.class.getClassLoader()); + + public static final boolean jaxb2Present = + ClassUtils.isPresent("javax.xml.bind.Binder", FeignBase.class.getClassLoader()); + + public static final boolean jackson2Present = + ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", FeignBase.class.getClassLoader()) && + ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", FeignBase.class.getClassLoader()); + + public static final boolean jacksonPresent = + ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", FeignBase.class.getClassLoader()) && + ClassUtils.isPresent("org.codehaus.jackson.JsonGenerator", FeignBase.class.getClassLoader()); + + protected final List> messageConverters = new ArrayList>(); + + public FeignBase() { + addDefaultConverters(messageConverters); + } + + /** + * Create a new instance of the {@link SpringDecoder} using the given list of + * {@link HttpMessageConverter} to use + * @param messageConverters the list of {@link HttpMessageConverter} to use + */ + public FeignBase(List> messageConverters) { + Assert.notEmpty(messageConverters, "'messageConverters' must not be empty"); + this.messageConverters.addAll(messageConverters); + } + + protected void addDefaultConverters(List> messageConverters) { + messageConverters.add(new ByteArrayHttpMessageConverter()); + messageConverters.add(new StringHttpMessageConverter()); + messageConverters.add(new ResourceHttpMessageConverter()); + messageConverters.add(new SourceHttpMessageConverter()); + messageConverters.add(new AllEncompassingFormHttpMessageConverter()); + + if (romePresent) { + messageConverters.add(new AtomFeedHttpMessageConverter()); + messageConverters.add(new RssChannelHttpMessageConverter()); + } + if (jaxb2Present) { + messageConverters.add(new Jaxb2RootElementHttpMessageConverter()); + } + if (jackson2Present) { + messageConverters.add(new MappingJackson2HttpMessageConverter()); + } + else if (jacksonPresent) { + messageConverters.add(new org.springframework.http.converter.json.MappingJacksonHttpMessageConverter()); + } + } + + /** + * Set the message body converters to use. + *

These converters are used to convert from and to HTTP requests and responses. + */ + public void setMessageConverters(List> messageConverters) { + Assert.notEmpty(messageConverters, "'messageConverters' must not be empty"); + this.messageConverters.clear(); + this.messageConverters.addAll(messageConverters); + } + + public List> getMessageConverters() { + return messageConverters; + } + + protected HttpHeaders getHttpHeaders(Map> headers) { + HttpHeaders httpHeaders = new HttpHeaders(); + for (Map.Entry> entry : headers.entrySet()) { + httpHeaders.put(entry.getKey(), new ArrayList<>(entry.getValue())); + } + return httpHeaders; + } + +} 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 new file mode 100644 index 00000000..98907b09 --- /dev/null +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/feign/FeignConfigurer.java @@ -0,0 +1,65 @@ +package org.springframework.platform.netflix.feign; + +import com.netflix.config.ConfigurationManager; +import com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList; +import feign.Contract; +import feign.Feign; +import feign.Logger; +import feign.codec.Decoder; +import feign.codec.Encoder; +import feign.ribbon.LoadBalancingTarget; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.platform.netflix.archaius.ConfigurableEnvironmentConfiguration; + +import java.net.URI; + +/** + * Created by sgibb on 7/3/14. + */ +@Configuration +public class FeignConfigurer { + @Autowired + ConfigurableEnvironmentConfiguration envConfig; //FIXME: howto enforce this? + + @Autowired + Decoder decoder; + + @Autowired + Encoder encoder; + + @Autowired + Logger logger; + + @Autowired + Contract contract; + + protected Feign.Builder feign() { + //ConfigurationManager.getConfigInstance().setProperty("exampleBackend.ribbon.listOfServers", "localhost:7080"); + //exampleBackend.ribbon.NIWSServerListClassName=my.package.MyServerList + return Feign.builder() + .logger(logger) + .encoder(encoder) + .decoder(decoder) + .contract(contract); + } + + protected T loadBalance(Class type, String schemeName) { + return loadBalance(feign(), type, schemeName); + } + + protected T loadBalance(Feign.Builder builder, Class type, String schemeName) { + String name = URI.create(schemeName).getHost(); + setServiceListClass(name); + return builder.target(LoadBalancingTarget.create(type, schemeName)); + } + + public static void setServiceListClass(String serviceId) { + setProp(serviceId, "NIWSServerListClassName", DiscoveryEnabledNIWSServerList.class.getName()); + setProp(serviceId, "DeploymentContextBasedVipAddresses", serviceId); //FIXME: what should this be? + } + + private static void setProp(String serviceId, String suffix, String value) { + ConfigurationManager.getConfigInstance().setProperty(serviceId + ".ribbon." + suffix, value); + } +} diff --git a/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/feign/SpringDecoder.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/feign/SpringDecoder.java new file mode 100644 index 00000000..1fdff195 --- /dev/null +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/feign/SpringDecoder.java @@ -0,0 +1,87 @@ +package org.springframework.platform.netflix.feign; + +import feign.FeignException; +import feign.Response; +import feign.codec.DecodeException; +import feign.codec.Decoder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.web.client.HttpMessageConverterExtractor; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Type; +import java.util.List; + +/** + * Created by sgibb on 6/26/14. + */ +public class SpringDecoder extends FeignBase implements Decoder { + private static final Logger logger = LoggerFactory.getLogger(SpringDecoder.class); + + public SpringDecoder() { + } + + public SpringDecoder(List> messageConverters) { + super(messageConverters); + } + + @Override + public Object decode(final Response response, Type type) throws IOException, DecodeException, FeignException { + if (type instanceof Class) { + HttpMessageConverterExtractor extractor = + new HttpMessageConverterExtractor((Class) type, getMessageConverters()); + + Object data = extractor.extractData(new FeignResponseAdapter(response)); + return data; + } + throw new DecodeException("type is not an instance of Class: "+type); + } + + private class FeignResponseAdapter implements ClientHttpResponse { + private final Response response; + + private FeignResponseAdapter(Response response) { + this.response = response; + } + + @Override + public HttpStatus getStatusCode() throws IOException { + return HttpStatus.valueOf(response.status()); + } + + @Override + public int getRawStatusCode() throws IOException { + return response.status(); + } + + @Override + public String getStatusText() throws IOException { + return response.reason(); + } + + @Override + public void close() { + try { + response.body().close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public InputStream getBody() throws IOException { + return response.body().asInputStream(); + } + + @Override + public HttpHeaders getHeaders() { + return getHttpHeaders(response.headers()); + } + + } +} diff --git a/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/feign/SpringEncoder.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/feign/SpringEncoder.java new file mode 100644 index 00000000..2e0654ec --- /dev/null +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/feign/SpringEncoder.java @@ -0,0 +1,101 @@ +package org.springframework.platform.netflix.feign; + +import com.google.common.base.Charsets; +import feign.RequestTemplate; +import feign.codec.EncodeException; +import feign.codec.Encoder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Collection; +import java.util.List; + +/** + * Created by sgibb on 6/26/14. + */ +public class SpringEncoder extends FeignBase implements Encoder { + private static final Logger logger = LoggerFactory.getLogger(SpringEncoder.class); + + public SpringEncoder() { + } + + public SpringEncoder(List> messageConverters) { + super(messageConverters); + } + + @Override + public void encode(Object requestBody, RequestTemplate request) throws EncodeException { + //template.body(conversionService.convert(object, String.class)); + if (requestBody != null) { + Class requestType = requestBody.getClass(); + Collection contentTypes = request.headers().get("Content-Type"); + + MediaType requestContentType = null; + if (contentTypes != null && !contentTypes.isEmpty()) { + String type = contentTypes.iterator().next(); + requestContentType = MediaType.valueOf(type); + } + + for (HttpMessageConverter messageConverter : getMessageConverters()) { + if (messageConverter.canWrite(requestType, requestContentType)) { + if (logger.isDebugEnabled()) { + if (requestContentType != null) { + logger.debug("Writing [" + requestBody + "] as \"" + requestContentType + + "\" using [" + messageConverter + "]"); + } + else { + logger.debug("Writing [" + requestBody + "] using [" + messageConverter + "]"); + } + + } + + FeignOutputMessage outputMessage = new FeignOutputMessage(request); + try { + ((HttpMessageConverter) messageConverter).write( + requestBody, requestContentType, outputMessage); + } catch (IOException e) { + throw new EncodeException("Error converting request body", e); + } + request.body(outputMessage.getOutputStream().toByteArray(), Charsets.UTF_8); //TODO: set charset + return; + } + } + String message = "Could not write request: no suitable HttpMessageConverter found for request type [" + + requestType.getName() + "]"; + if (requestContentType != null) { + message += " and content type [" + requestContentType + "]"; + } + throw new EncodeException(message); + } + } + + private class FeignOutputMessage implements HttpOutputMessage { + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + RequestTemplate request; + + private FeignOutputMessage(RequestTemplate request) { + this.request = request; + } + + @Override + public OutputStream getBody() throws IOException { + return outputStream; + } + + @Override + public HttpHeaders getHeaders() { + return getHttpHeaders(request.headers()); + } + + public ByteArrayOutputStream getOutputStream() { + return outputStream; + } + } +} diff --git a/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/feign/SpringMvcContract.java b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/feign/SpringMvcContract.java new file mode 100644 index 00000000..7fedbb56 --- /dev/null +++ b/spring-platform-netflix-core/src/main/java/org/springframework/platform/netflix/feign/SpringMvcContract.java @@ -0,0 +1,84 @@ +package org.springframework.platform.netflix.feign; + +import feign.Contract; +import feign.MethodMetadata; +import org.springframework.web.bind.annotation.RequestMapping; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Arrays; + +import static feign.Util.checkState; +import static feign.Util.emptyToNull; + +/** + * Created by sgibb on 6/27/14. + */ +public class SpringMvcContract extends Contract.BaseContract { + static final String ACCEPT = "Accept"; + static final String CONTENT_TYPE = "Content-Type"; + + @Override + protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) { + Class annotationType = methodAnnotation.annotationType(); + RequestMapping mapping = RequestMapping.class.cast(methodAnnotation); + if (mapping != null) { + //HTTP Method + checkOne(method, mapping.method(), "method"); + data.template().method(mapping.method()[0].name()); + + //path + checkOne(method, mapping.value(), "value"); + + String methodAnnotationValue = mapping.value()[0]; + String pathValue = emptyToNull(methodAnnotationValue); + checkState(pathValue != null, "value was empty on method %s", method.getName()); + if (!methodAnnotationValue.startsWith("/") && !data.template().toString().endsWith("/")) { + methodAnnotationValue = "/" + methodAnnotationValue; + } + data.template().append(methodAnnotationValue); + + //produces + checkAtMostOne(method, mapping.produces(), "produces"); + String[] serverProduces = mapping.produces(); + String clientAccepts = serverProduces.length == 0 ? null: emptyToNull(serverProduces[0]); + if (clientAccepts != null) { + data.template().header(ACCEPT, clientAccepts); + } + + //consumes + checkAtMostOne(method, mapping.consumes(), "consumes"); + String[] serverConsumes = mapping.consumes(); + String clientProduces = serverConsumes.length == 0 ? null: emptyToNull(serverConsumes[0]); + if (clientProduces != null) { + data.template().header(CONTENT_TYPE, clientProduces); + } + + //headers + //TODO: only supports one header value per key + if (mapping.headers() != null && mapping.headers().length > 0) + for (String header : mapping.headers()) { + int colon = header.indexOf(':'); + data.template().header(header.substring(0, colon), header.substring(colon + 2)); + } + } + } + + private void checkAtMostOne(Method method, Object[] values, String fieldName) { + checkState(values != null && (values.length == 0 || values.length == 1), + "Method %s can only contain at most 1 %s field. Found: %s", method.getName(), fieldName, + values == null ? null : Arrays.asList(values)); + } + + private void checkOne(Method method, Object[] values, String fieldName) { + checkState(values != null && values.length == 1, + "Method %s can only contain 1 %s field. Found: %s", method.getName(), fieldName, + values == null ? null : Arrays.asList(values)); + } + + @Override + protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) { + //TODO: support spring parameter annotations? + return false; + } +} diff --git a/spring-platform-netflix-core/src/main/resources/META-INF/spring.factories b/spring-platform-netflix-core/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..ef517201 --- /dev/null +++ b/spring-platform-netflix-core/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.platform.netflix.archaius.ArchaiusAutoConfiguration,\ +org.springframework.platform.netflix.eureka.EurekaClientAutoConfiguration,\ +org.springframework.platform.netflix.feign.FeignAutoConfiguration \ No newline at end of file diff --git a/src/test/java/org/springframework/platform/netflix/eureka/sample/Application.java b/spring-platform-netflix-core/src/test/java/org/springframework/platform/netflix/eureka/sample/Application.java similarity index 100% rename from src/test/java/org/springframework/platform/netflix/eureka/sample/Application.java rename to spring-platform-netflix-core/src/test/java/org/springframework/platform/netflix/eureka/sample/Application.java diff --git a/src/test/java/org/springframework/platform/netflix/eureka/sample/ApplicationTests.java b/spring-platform-netflix-core/src/test/java/org/springframework/platform/netflix/eureka/sample/ApplicationTests.java similarity index 100% rename from src/test/java/org/springframework/platform/netflix/eureka/sample/ApplicationTests.java rename to spring-platform-netflix-core/src/test/java/org/springframework/platform/netflix/eureka/sample/ApplicationTests.java diff --git a/src/test/resources/application.yml b/spring-platform-netflix-core/src/test/resources/application.yml similarity index 100% rename from src/test/resources/application.yml rename to spring-platform-netflix-core/src/test/resources/application.yml diff --git a/spring-platform-netflix-zuul/pom.xml b/spring-platform-netflix-zuul/pom.xml new file mode 100644 index 00000000..307ebe5d --- /dev/null +++ b/spring-platform-netflix-zuul/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + org.springframework.platform + spring-platform-netflix-zuul + 1.0.0.BUILD-SNAPSHOT + jar + Spring Platform Netflix Zuul + http://projects.spring.io/spring-platform/ + + org.springframework.platform + spring-platform-netflix + 1.0.0.BUILD-SNAPSHOT + .. + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.7 + 1.7 + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + 1.0.0.BUILD-SNAPSHOT + + + + + + org.springframework.platform + spring-platform-config + ${spring-platform-config.version} + pom + import + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.platform + spring-platform-netflix-core + ${project.version} + + + org.springframework.platform + spring-platform-config-client + + + com.netflix.hystrix + hystrix-core + + + com.netflix.ribbon + ribbon-core + + + com.netflix.ribbon + ribbon-httpclient + + + com.netflix.zuul + zuul-core + + + diff --git a/spring-platform-netflix-zuul/src/main/java/org/springframework/platform/netflix/zuul/Application.java b/spring-platform-netflix-zuul/src/main/java/org/springframework/platform/netflix/zuul/Application.java new file mode 100644 index 00000000..c9696292 --- /dev/null +++ b/spring-platform-netflix-zuul/src/main/java/org/springframework/platform/netflix/zuul/Application.java @@ -0,0 +1,67 @@ +package org.springframework.platform.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.platform.netflix.endpoint.HystrixStreamEndpoint; +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 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); + } +} diff --git a/spring-platform-netflix-zuul/src/main/java/org/springframework/platform/netflix/zuul/FilterIntializer.java b/spring-platform-netflix-zuul/src/main/java/org/springframework/platform/netflix/zuul/FilterIntializer.java new file mode 100644 index 00000000..5abc5319 --- /dev/null +++ b/spring-platform-netflix-zuul/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.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 javax.servlet.http.HttpSessionEvent; + +/** + * User: spencergibb + * Date: 4/24/14 + * Time: 9:23 PM + * TODO: .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + */ +public class FilterIntializer implements ServletContextListener/*, HttpSessionListener*/ { + + private static final Logger LOGGER = LoggerFactory.getLogger(FilterIntializer.class); + + @Autowired + ZuulProperties props; + + @Override + /*public void sessionCreated(HttpSessionEvent se) { + contextInitialized(null); + }*/ + 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 sessionDestroyed(HttpSessionEvent se) { + contextDestroyed(null); + }*/ + 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); + } + } +} diff --git a/spring-platform-netflix-zuul/src/main/java/org/springframework/platform/netflix/zuul/RibbonCommand.java b/spring-platform-netflix-zuul/src/main/java/org/springframework/platform/netflix/zuul/RibbonCommand.java new file mode 100644 index 00000000..1d30d43a --- /dev/null +++ b/spring-platform-netflix-zuul/src/main/java/org/springframework/platform/netflix/zuul/RibbonCommand.java @@ -0,0 +1,111 @@ +package org.springframework.platform.netflix.zuul; + +import com.netflix.client.http.HttpRequest; +import com.netflix.client.http.HttpResponse; +import com.netflix.config.DynamicPropertyFactory; +import com.netflix.hystrix.HystrixCommand; +import com.netflix.hystrix.HystrixCommandGroupKey; +import com.netflix.hystrix.HystrixCommandProperties; +import com.netflix.niws.client.http.RestClient; +import com.netflix.zuul.constants.ZuulConstants; +import com.netflix.zuul.context.RequestContext; + +import javax.ws.rs.core.MultivaluedMap; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; + +import static com.netflix.client.http.HttpRequest.Builder; +import static com.netflix.client.http.HttpRequest.Verb; + +/** + * Hystrix wrapper around Eureka Ribbon command + * + * see original https://github.com/Netflix/zuul/blob/master/zuul-netflix/src/main/java/com/netflix/zuul/dependency/ribbon/hystrix/RibbonCommand.java + */ +public class RibbonCommand extends HystrixCommand { + + RestClient restClient; + Verb verb; + URI uri; + MultivaluedMap headers; + MultivaluedMap params; + InputStream requestEntity; + + + public RibbonCommand(RestClient restClient, + Verb verb, + String uri, + MultivaluedMap headers, + MultivaluedMap params, + InputStream requestEntity) throws URISyntaxException { + this("default", restClient, verb, uri, headers, params, requestEntity); + } + + + public RibbonCommand(String commandKey, + RestClient restClient, + Verb verb, + String uri, + MultivaluedMap headers, + MultivaluedMap params, + InputStream requestEntity) throws URISyntaxException { + + super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(commandKey)).andCommandPropertiesDefaults( + // we want to default to semaphore-isolation since this wraps + // 2 others commands that are already thread isolated + HystrixCommandProperties.Setter().withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE) + .withExecutionIsolationSemaphoreMaxConcurrentRequests(DynamicPropertyFactory.getInstance(). + getIntProperty(ZuulConstants.ZUUL_EUREKA + commandKey + ".semaphore.maxSemaphores", 100).get()))); + + this.restClient = restClient; + this.verb = verb; + this.uri = new URI(uri); + this.headers = headers; + this.params = params; + this.requestEntity = requestEntity; + } + + @Override + protected HttpResponse run() throws Exception { + try { + return forward(); + } catch (Exception e) { + throw e; + } + } + + HttpResponse forward() throws Exception { + + RequestContext context = RequestContext.getCurrentContext(); + + + Builder builder = HttpRequest.newBuilder(). + verb(verb). + uri(uri). + entity(requestEntity); + + for (String name : headers.keySet()) { + List values = headers.get(name); + for (String value : values) { + builder.header(name, value); + } + } + + for (String name : params.keySet()) { + List values = params.get(name); + for (String value : values) { + builder.queryParams(name, value); + } + } + + HttpRequest httpClientRequest = builder.build(); + + HttpResponse response = restClient.executeWithLoadBalancer(httpClientRequest); + context.set("ribbonResponse", response); + return response; + } + + +} \ No newline at end of file diff --git a/spring-platform-netflix-zuul/src/main/java/org/springframework/platform/netflix/zuul/Routes.java b/spring-platform-netflix-zuul/src/main/java/org/springframework/platform/netflix/zuul/Routes.java new file mode 100644 index 00000000..48b48b63 --- /dev/null +++ b/spring-platform-netflix-zuul/src/main/java/org/springframework/platform/netflix/zuul/Routes.java @@ -0,0 +1,78 @@ +package org.springframework.platform.netflix.zuul; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.bind.PropertySourceUtils; +import org.springframework.core.env.*; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Field; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +/** + * Created by sgibb on 7/10/14. + */ +public class Routes { + private static final Logger logger = LoggerFactory.getLogger(Routes.class); + + public static final String DEFAULT_ROUTE = "/"; + + @Autowired + ConfigurableEnvironment env; + private final Field propertySourcesField; + + public Routes() { + propertySourcesField = ReflectionUtils.findField(CompositePropertySource.class, "propertySources"); + propertySourcesField.setAccessible(true); + } + + //TODO: cache routes or respond to environment event and refresh all routes + public LinkedHashMap getRoutes() { + LinkedHashMap routes = new LinkedHashMap<>(); + MutablePropertySources propertySources = env.getPropertySources(); + for (PropertySource propertySource : propertySources) { + getRoutes(propertySource, routes); + } + + String defaultServiceId = routes.get(DEFAULT_ROUTE); + + if (defaultServiceId != null) { + //move the defaultServiceId to the end + routes.remove(DEFAULT_ROUTE); + routes.put(DEFAULT_ROUTE, defaultServiceId); + } + + return routes; + } + + public void getRoutes(PropertySource propertySource, LinkedHashMap routes) { + if (propertySource instanceof CompositePropertySource) { + try { + @SuppressWarnings("unchecked") + Set> sources = (Set>) propertySourcesField.get(propertySource); + for (PropertySource source : sources) { + getRoutes(source, routes); + } + } catch (IllegalAccessException e) { + return; + } + } else { + //EnumerablePropertySource enumerable = (EnumerablePropertySource) propertySource; + MutablePropertySources propertySources = new MutablePropertySources(); + propertySources.addLast(propertySource); + Map routeEntries = PropertySourceUtils.getSubProperties(propertySources, "zuul.route."); + for (Map.Entry entry : routeEntries.entrySet()) { + String serviceId = entry.getKey(); + String route = entry.getValue().toString(); + + if (routes.containsKey(route)) { + logger.warn("Overwriting route {}: already defined by {}", route, routes.get(route)); + } + routes.put(route, serviceId); + } + } + } +} diff --git a/spring-platform-netflix-zuul/src/main/java/org/springframework/platform/netflix/zuul/SpringFilter.java b/spring-platform-netflix-zuul/src/main/java/org/springframework/platform/netflix/zuul/SpringFilter.java new file mode 100644 index 00000000..bba3a258 --- /dev/null +++ b/spring-platform-netflix-zuul/src/main/java/org/springframework/platform/netflix/zuul/SpringFilter.java @@ -0,0 +1,31 @@ +package org.springframework.platform.netflix.zuul; + +import com.netflix.zuul.ZuulFilter; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.context.support.WebApplicationContextUtils; + +import javax.servlet.http.HttpServletRequest; + +/** + * User: spencergibb + * Date: 5/1/14 + * Time: 10:59 PM + */ +public abstract class SpringFilter extends ZuulFilter { + + protected T getBean(Class beanClass) { + //FIXME: hack because zuul uses servlet 2.5? + RequestAttributes requestAttr = RequestContextHolder.currentRequestAttributes(); + if (!(requestAttr instanceof ServletRequestAttributes)) { + throw new IllegalStateException("Current request is not a servlet request"); + } + ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttr; + HttpServletRequest request = attributes.getRequest(); + + WebApplicationContext context = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext()); + return context.getBean(beanClass); + } +} diff --git a/spring-platform-netflix-zuul/src/main/java/org/springframework/platform/netflix/zuul/ZuulController.java b/spring-platform-netflix-zuul/src/main/java/org/springframework/platform/netflix/zuul/ZuulController.java new file mode 100644 index 00000000..ffb8aa72 --- /dev/null +++ b/spring-platform-netflix-zuul/src/main/java/org/springframework/platform/netflix/zuul/ZuulController.java @@ -0,0 +1,28 @@ +package org.springframework.platform.netflix.zuul; + +import com.netflix.zuul.http.ZuulServlet; +import org.springframework.platform.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); + } +} diff --git a/spring-platform-netflix-zuul/src/main/java/org/springframework/platform/netflix/zuul/ZuulProperties.java b/spring-platform-netflix-zuul/src/main/java/org/springframework/platform/netflix/zuul/ZuulProperties.java new file mode 100644 index 00000000..ddbd1d83 --- /dev/null +++ b/spring-platform-netflix-zuul/src/main/java/org/springframework/platform/netflix/zuul/ZuulProperties.java @@ -0,0 +1,32 @@ +package org.springframework.platform.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; + } +} diff --git a/spring-platform-netflix-zuul/src/main/java/org/springframework/platform/util/AbstractServletWrappingController.java b/spring-platform-netflix-zuul/src/main/java/org/springframework/platform/util/AbstractServletWrappingController.java new file mode 100644 index 00000000..f4641c14 --- /dev/null +++ b/spring-platform-netflix-zuul/src/main/java/org/springframework/platform/util/AbstractServletWrappingController.java @@ -0,0 +1,42 @@ +package org.springframework.platform.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); + } + +} diff --git a/spring-platform-netflix-zuul/src/main/resources/META-INF/spring.factories b/spring-platform-netflix-zuul/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..87096b50 --- /dev/null +++ b/spring-platform-netflix-zuul/src/main/resources/META-INF/spring.factories @@ -0,0 +1,8 @@ +#org.springframework.context.ApplicationListener=\ +#org.springframework.platform.netflix.eureka.EurekaStartingListener,\ +#org.springframework.platform.netflix.eureka.EurekaUpListener,\ +#org.springframework.platform.netflix.eureka.EurekaOutOfServiceListener + +#org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +#org.springframework.platform.netflix.archaius.ArchaiusAutoConfiguration,\ +#org.springframework.platform.netflix.feign.FeignAutoConfiguration diff --git a/spring-platform-netflix-zuul/src/main/resources/application.yml b/spring-platform-netflix-zuul/src/main/resources/application.yml new file mode 100644 index 00000000..6e880ac1 --- /dev/null +++ b/spring-platform-netflix-zuul/src/main/resources/application.yml @@ -0,0 +1,48 @@ +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:8080/v2/ + defaultZone: http://localhost:8080/v2/ + + instance: + #Virtual host name by which the clients identifies this service + virtualHostName: ${spring.application.name} diff --git a/spring-platform-netflix-zuul/src/main/resources/bootstrap.yml b/spring-platform-netflix-zuul/src/main/resources/bootstrap.yml new file mode 100644 index 00000000..2041e99a --- /dev/null +++ b/spring-platform-netflix-zuul/src/main/resources/bootstrap.yml @@ -0,0 +1,6 @@ +spring: + application: + name: zuul + platform: + config: + uri: http://localhost:${config.port:8888} \ No newline at end of file diff --git a/spring-platform-netflix-zuul/src/main/resources/filters/post/SendResponse.groovy b/spring-platform-netflix-zuul/src/main/resources/filters/post/SendResponse.groovy new file mode 100644 index 00000000..0018a40d --- /dev/null +++ b/spring-platform-netflix-zuul/src/main/resources/filters/post/SendResponse.groovy @@ -0,0 +1,170 @@ +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> zuulResponseHeaders = context.getZuulResponseHeaders(); + String debugHeader = "" + + List rd + + rd = (List) RequestContext.getCurrentContext().get("routingDebug"); + rd?.each { + 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?.each { Pair it -> + servletResponse.addHeader(it.first(), it.second()) + Debug.addRequestDebug("OUTBOUND: < " + it.first() + ":" + it.second()) + } + } else { + zuulResponseHeaders?.each { Pair 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) + } + } + +} \ No newline at end of file diff --git a/spring-platform-netflix-zuul/src/main/resources/filters/post/Stats.groovy b/spring-platform-netflix-zuul/src/main/resources/filters/post/Stats.groovy new file mode 100755 index 00000000..46fa9a56 --- /dev/null +++ b/spring-platform-netflix-zuul/src/main/resources/filters/post/Stats.groovy @@ -0,0 +1,43 @@ +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 rd = (List) RequestContext.getCurrentContext().get("requestDebug"); + rd?.each { + println("REQUEST_DEBUG::${it}"); + } + } + + public void dumpRoutingDebug() { + List rd = (List) RequestContext.getCurrentContext().get("routingDebug"); + rd?.each { + println("ZUUL_DEBUG::${it}"); + } + } + +} diff --git a/spring-platform-netflix-zuul/src/main/resources/filters/pre/Debug.groovy b/spring-platform-netflix-zuul/src/main/resources/filters/pre/Debug.groovy new file mode 100644 index 00000000..2a499102 --- /dev/null +++ b/spring-platform-netflix-zuul/src/main/resources/filters/pre/Debug.groovy @@ -0,0 +1,42 @@ +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; + } + + +} + + + diff --git a/spring-platform-netflix-zuul/src/main/resources/filters/pre/DebugRequest.groovy b/spring-platform-netflix-zuul/src/main/resources/filters/pre/DebugRequest.groovy new file mode 100644 index 00000000..1f028624 --- /dev/null +++ b/spring-platform-netflix-zuul/src/main/resources/filters/pre/DebugRequest.groovy @@ -0,0 +1,55 @@ +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; + } + +} 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 new file mode 100644 index 00000000..ed905eab --- /dev/null +++ b/spring-platform-netflix-zuul/src/main/resources/filters/pre/PreDecorationFilter.groovy @@ -0,0 +1,52 @@ +package filters.pre + +import com.netflix.zuul.context.RequestContext +import org.slf4j.Logger +import org.slf4j.LoggerFactory +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); + + @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); + } + } +} 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 new file mode 100644 index 00000000..c7f12cd0 --- /dev/null +++ b/spring-platform-netflix-zuul/src/main/resources/filters/route/RibbonRequest.groovy @@ -0,0 +1,284 @@ +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.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 + +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 headers = buildZuulRequestHeaders(request) + MultivaluedMap 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? + setServiceListClass(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 headers, MultivaluedMap 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 headers, + MultivaluedMap 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 buildZuulRequestQueryParams(HttpServletRequest request) { + + Map> map = HTTPRequestUtils.getInstance().getQueryParams() + + MultivaluedMap params = new MultivaluedMapImpl(); + if (map == null) return params; + map.entrySet().each { + it.value.each { v -> + params.add(it.key, v) + } + + } + return params + } + + + def MultivaluedMap buildZuulRequestHeaders(HttpServletRequest request) { + + RequestContext context = RequestContext.currentContext + + MultivaluedMap headers = new MultivaluedMapImpl(); + 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 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 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 + } + } + +} + + diff --git a/spring-platform-netflix-zuul/src/main/resources/filters/route/SimpleHostRequest.groovy b/spring-platform-netflix-zuul/src/main/resources/filters/route/SimpleHostRequest.groovy new file mode 100644 index 00000000..cd2e9627 --- /dev/null +++ b/spring-platform-netflix-zuul/src/main/resources/filters/route/SimpleHostRequest.groovy @@ -0,0 +1,434 @@ +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 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 + 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(); + } + } +} \ No newline at end of file diff --git a/src/main/resources/META-INF/spring.factories b/src/main/resources/META-INF/spring.factories deleted file mode 100644 index b14604cb..00000000 --- a/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,2 +0,0 @@ -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -org.springframework.platform.netflix.eureka.EurekaClientAutoConfiguration \ No newline at end of file