package com.dbg.cloud.acheron.adminendpoints; import com.dbg.cloud.acheron.autoconfigure.admin.AdminServerProperties; import org.apache.catalina.Valve; import org.apache.catalina.valves.AccessLogValve; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.web.DefaultErrorAttributes; import org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.ErrorAttributes; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer; import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer; import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.undertow.UndertowEmbeddedServletContainerFactory; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.servlet.ErrorPage; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.util.CollectionUtils; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.servlet.*; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.*; @Configuration @EnableWebMvc @EnableConfigurationProperties({AdminEndpointCorsProperties.class}) public class AdminChildContextConfiguration { @Autowired private AdminEndpointCorsProperties corsProperties; @Bean(name = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME) public DispatcherServlet dispatcherServlet() { final DispatcherServlet dispatcherServlet = new DispatcherServlet(); // Ensure the parent configuration does not leak down to us dispatcherServlet.setDetectAllHandlerAdapters(false); dispatcherServlet.setDetectAllHandlerExceptionResolvers(false); dispatcherServlet.setDetectAllHandlerMappings(false); dispatcherServlet.setDetectAllViewResolvers(false); return dispatcherServlet; } @Bean public ServerCustomization serverCustomization() { return new ServerCustomization(); } @Configuration protected static class AdminEndpointHandlerMappingConfiguration { @Autowired public void handlerMapping(AdminEndpointHandlerMapping mapping) { // In a child context we definitely want to see the parent endpoints mapping.setDetectHandlerMethodsInAncestorContexts(true); } } @Bean @ConditionalOnBean({ErrorAttributes.class, ServerProperties.class}) public AdminErrorEndpoint errorEndpoint(ErrorAttributes errorAttributes, ServerProperties serverProperties) { return new AdminErrorEndpoint(errorAttributes, serverProperties.getError()); } @Bean(name = DispatcherServlet.HANDLER_MAPPING_BEAN_NAME) public CompositeHandlerMapping compositeHandlerMapping() { return new CompositeHandlerMapping(); } @Bean(name = DispatcherServlet.HANDLER_ADAPTER_BEAN_NAME) public CompositeHandlerAdapter compositeHandlerAdapter() { return new CompositeHandlerAdapter(); } @Bean(name = DispatcherServlet.HANDLER_EXCEPTION_RESOLVER_BEAN_NAME) public CompositeHandlerExceptionResolver compositeHandlerExceptionResolver() { return new CompositeHandlerExceptionResolver(); } @Bean public DefaultErrorAttributes errorAttributes() { final Collection<String> allowedErrorAttributes = Arrays.asList( "timestamp", "status", "error", "message", "path" ); return new DefaultErrorAttributes() { @Override public Map<String, Object> getErrorAttributes( RequestAttributes requestAttributes, boolean includeStackTrace) { Map<String, Object> errorAttributes = super.getErrorAttributes(requestAttributes, includeStackTrace); errorAttributes.entrySet().removeIf(mapEntry -> !allowedErrorAttributes.contains(mapEntry.getKey())); return errorAttributes; } }; } @Bean public ResponseStatusExceptionResolver responseStatusExceptionResolver() { return new ResponseStatusExceptionResolver(); } static class ServerCustomization implements EmbeddedServletContainerCustomizer, Ordered { @Autowired private ListableBeanFactory beanFactory; private ServerProperties server; // This needs to be lazily initialized because EmbeddedServletContainerCustomizer // instances get their callback very early in the context lifecycle. private AdminServerProperties adminServerProperties; @Override public int getOrder() { return 0; } @Override public void customize(final ConfigurableEmbeddedServletContainer container) { if (adminServerProperties == null) { adminServerProperties = BeanFactoryUtils.beanOfTypeIncludingAncestors( beanFactory, AdminServerProperties.class); server = BeanFactoryUtils.beanOfTypeIncludingAncestors(beanFactory, ServerProperties.class); } // Customize as per the parent context first (so e.g. the access logs go to the same place) server.customize(container); // Then reset the error pages container.setErrorPages(Collections.<ErrorPage>emptySet()); // and the context path container.setContextPath(""); // and add the admin-specific bits if (adminServerProperties.getPort() != null) { container.setPort(adminServerProperties.getPort()); } if (adminServerProperties.getSsl() != null) { container.setSsl(this.adminServerProperties.getSsl()); } if (adminServerProperties.getContextPath() != null) { container.setContextPath(adminServerProperties.getContextPath()); } container.setServerHeader(server.getServerHeader()); if (adminServerProperties.getAddress() != null) { container.setAddress(adminServerProperties.getAddress()); } container.addErrorPages(new ErrorPage(server.getError().getPath())); } } @Bean public UndertowAccessLogCustomizer undertowAccessLogCustomizer() { return new UndertowAccessLogCustomizer(); } @Bean @ConditionalOnClass(name = "org.apache.catalina.valves.AccessLogValve") public TomcatAccessLogCustomizer tomcatAccessLogCustomizer() { return new TomcatAccessLogCustomizer(); } static class CompositeHandlerMapping implements HandlerMapping { @Autowired private ListableBeanFactory beanFactory; private List<HandlerMapping> mappings; @Override public HandlerExecutionChain getHandler(final HttpServletRequest request) throws Exception { if (this.mappings == null) { this.mappings = extractMappings(); } for (HandlerMapping mapping : this.mappings) { final HandlerExecutionChain handler = mapping.getHandler(request); if (handler != null) { return handler; } } return null; } private List<HandlerMapping> extractMappings() { final List<HandlerMapping> list = new ArrayList<HandlerMapping>(); list.addAll(this.beanFactory.getBeansOfType(HandlerMapping.class).values()); list.remove(this); AnnotationAwareOrderComparator.sort(list); return list; } } static class CompositeHandlerAdapter implements HandlerAdapter { @Autowired private ListableBeanFactory beanFactory; private List<HandlerAdapter> adapters; private List<HandlerAdapter> extractAdapters() { List<HandlerAdapter> list = new ArrayList<HandlerAdapter>(); list.addAll(this.beanFactory.getBeansOfType(HandlerAdapter.class).values()); list.remove(this); AnnotationAwareOrderComparator.sort(list); return list; } @Override public boolean supports(final Object handler) { if (this.adapters == null) { this.adapters = extractAdapters(); } for (HandlerAdapter mapping : this.adapters) { if (mapping.supports(handler)) { return true; } } return false; } @Override public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception { if (this.adapters == null) { this.adapters = extractAdapters(); } for (HandlerAdapter mapping : this.adapters) { if (mapping.supports(handler)) { return mapping.handle(request, response, handler); } } return null; } @Override public long getLastModified(final HttpServletRequest request, final Object handler) { if (this.adapters == null) { this.adapters = extractAdapters(); } for (HandlerAdapter mapping : this.adapters) { if (mapping.supports(handler)) { return mapping.getLastModified(request, handler); } } return 0; } } static class CompositeHandlerExceptionResolver implements HandlerExceptionResolver { @Autowired private ListableBeanFactory beanFactory; private List<HandlerExceptionResolver> resolvers; private List<HandlerExceptionResolver> extractResolvers() { final List<HandlerExceptionResolver> list = new ArrayList<HandlerExceptionResolver>(); list.addAll(this.beanFactory.getBeansOfType(HandlerExceptionResolver.class).values()); list.remove(this); AnnotationAwareOrderComparator.sort(list); return list; } @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { if (this.resolvers == null) { this.resolvers = extractResolvers(); } for (HandlerExceptionResolver mapping : this.resolvers) { final ModelAndView mav = mapping.resolveException(request, response, handler, ex); if (mav != null) { return mav; } } return null; } } static abstract class AccessLogCustomizer<T extends EmbeddedServletContainerFactory> implements EmbeddedServletContainerCustomizer, Ordered { private final Class<T> factoryClass; AccessLogCustomizer(Class<T> factoryClass) { this.factoryClass = factoryClass; } protected String customizePrefix(String prefix) { return "admin_" + prefix; } @Override public int getOrder() { return 1; } @Override public void customize(ConfigurableEmbeddedServletContainer container) { if (this.factoryClass.isInstance(container)) { customize(this.factoryClass.cast(container)); } } abstract void customize(T container); } static class TomcatAccessLogCustomizer extends AccessLogCustomizer<TomcatEmbeddedServletContainerFactory> { TomcatAccessLogCustomizer() { super(TomcatEmbeddedServletContainerFactory.class); } @Override public void customize(final TomcatEmbeddedServletContainerFactory container) { final AccessLogValve accessLogValve = findAccessLogValve(container); if (accessLogValve == null) { return; } accessLogValve.setPrefix(customizePrefix(accessLogValve.getPrefix())); } private AccessLogValve findAccessLogValve(final TomcatEmbeddedServletContainerFactory container) { for (Valve engineValve : container.getEngineValves()) { if (engineValve instanceof AccessLogValve) { return (AccessLogValve) engineValve; } } return null; } } static class UndertowAccessLogCustomizer extends AccessLogCustomizer<UndertowEmbeddedServletContainerFactory> { UndertowAccessLogCustomizer() { super(UndertowEmbeddedServletContainerFactory.class); } @Override public void customize(final UndertowEmbeddedServletContainerFactory container) { container.setAccessLogPrefix(customizePrefix(container.getAccessLogPrefix())); } } @Bean @ConditionalOnMissingBean public AdminEndpointHandlerMapping endpointHandlerMapping() { final Set<? extends AdminEndpoint> endpoints = adminEndpoints().getEndpoints(); final CorsConfiguration corsConfiguration = getCorsConfiguration(this.corsProperties); final AdminEndpointHandlerMapping mapping = new AdminEndpointHandlerMapping(endpoints, corsConfiguration); return mapping; } @Bean @ConditionalOnMissingBean public AdminEndpoints adminEndpoints() { return new AdminEndpoints(); } private CorsConfiguration getCorsConfiguration(final AdminEndpointCorsProperties properties) { if (CollectionUtils.isEmpty(properties.getAllowedOrigins())) { return null; } final CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(properties.getAllowedOrigins()); if (!CollectionUtils.isEmpty(properties.getAllowedHeaders())) { configuration.setAllowedHeaders(properties.getAllowedHeaders()); } if (!CollectionUtils.isEmpty(properties.getAllowedMethods())) { configuration.setAllowedMethods(properties.getAllowedMethods()); } if (!CollectionUtils.isEmpty(properties.getExposedHeaders())) { configuration.setExposedHeaders(properties.getExposedHeaders()); } if (properties.getMaxAge() != null) { configuration.setMaxAge(properties.getMaxAge()); } if (properties.getAllowCredentials() != null) { configuration.setAllowCredentials(properties.getAllowCredentials()); } return configuration; } }