From 077089fbdea50e75fd478056665c81e8db07d6e9 Mon Sep 17 00:00:00 2001 From: Kai Moritz Date: Thu, 16 Jun 2016 11:10:24 +0200 Subject: [PATCH] WIP: proxy --- pom.xml | 33 +- .../ThymeproxyAutoConfiguration.java | 77 ---- .../autoconfigure/ThymeproxyProperties.java | 67 --- .../java/de/juplo/thymeproxy/Application.java | 45 ++ .../thymeproxy/ProxyHttpRequestHandler.java | 384 ++++++++++++++++++ .../thymeproxy/ProxyResourceResolver.java | 123 ------ .../thymeproxy/ProxyTemplateResolver.java | 297 -------------- .../RequestToProxyViewNameTranslator.java | 28 ++ .../thymeproxy/UrlProxyViewController.java | 28 ++ src/main/resources/META-INF/spring.factories | 1 - src/main/resources/application.properties | 1 + src/main/resources/log4j.xml | 28 -- src/{test => main}/resources/logback.xml | 6 +- .../ThymeproxyAutoConfigurationTest.java | 230 ----------- 14 files changed, 496 insertions(+), 852 deletions(-) delete mode 100644 src/main/java/de/juplo/autoconfigure/ThymeproxyAutoConfiguration.java delete mode 100644 src/main/java/de/juplo/autoconfigure/ThymeproxyProperties.java create mode 100644 src/main/java/de/juplo/thymeproxy/Application.java create mode 100644 src/main/java/de/juplo/thymeproxy/ProxyHttpRequestHandler.java delete mode 100644 src/main/java/de/juplo/thymeproxy/ProxyResourceResolver.java delete mode 100644 src/main/java/de/juplo/thymeproxy/ProxyTemplateResolver.java create mode 100644 src/main/java/de/juplo/thymeproxy/RequestToProxyViewNameTranslator.java create mode 100644 src/main/java/de/juplo/thymeproxy/UrlProxyViewController.java delete mode 100644 src/main/resources/META-INF/spring.factories delete mode 100644 src/main/resources/log4j.xml rename src/{test => main}/resources/logback.xml (71%) delete mode 100644 src/test/java/de/juplo/autoconfigure/ThymeproxyAutoConfigurationTest.java diff --git a/pom.xml b/pom.xml index 031167a..d58173c 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ de.juplo - thymeproxy + thymeproxy-starter 1.0-SNAPSHOT @@ -26,11 +26,12 @@ ${project.name} http://localhost:8080 - 80 + 8888 300000 1.0-SNAPSHOT + 1.0-SNAPSHOT @@ -44,6 +45,11 @@ org.springframework.boot spring-boot-starter-thymeleaf + + de.juplo + thymeproxy + ${thymeproxy.version} + de.juplo httpclient-spring-boot-starter @@ -63,31 +69,10 @@ ${nekohtml.version} - - - org.springframework - spring-test - test - - - org.springframework.boot - spring-boot-configuration-processor - test - - - junit - junit - test - - - org.slf4j - jcl-over-slf4j - test - ch.qos.logback logback-classic - test + runtime diff --git a/src/main/java/de/juplo/autoconfigure/ThymeproxyAutoConfiguration.java b/src/main/java/de/juplo/autoconfigure/ThymeproxyAutoConfiguration.java deleted file mode 100644 index d81b31d..0000000 --- a/src/main/java/de/juplo/autoconfigure/ThymeproxyAutoConfiguration.java +++ /dev/null @@ -1,77 +0,0 @@ -package de.juplo.autoconfigure; - - -import de.juplo.autoconfigure.ThymeproxyProperties.Origin; -import de.juplo.thymeproxy.ProxyTemplateResolver; -import org.apache.http.impl.client.CloseableHttpClient; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.boot.autoconfigure.AutoConfigureBefore; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - - -/** - * Automatic configuration - * - * @author Kai Moritz - */ -@Configuration -@EnableConfigurationProperties(ThymeproxyProperties.class) -@ConditionalOnClass(value = ThymeleafAutoConfiguration.class) -@AutoConfigureBefore(ThymeleafAutoConfiguration.class) -public class ThymeproxyAutoConfiguration -{ - private static final Logger LOG = - LoggerFactory.getLogger(ThymeproxyAutoConfiguration.class); - - - @Bean - @ConditionalOnProperty("thymeproxy.origins[0].uri") - public ProxyTemplateResolver defaultTemplateResolver( - CloseableHttpClient client, - ThymeproxyProperties properties, - ConfigurableApplicationContext context - ) - { - LOG.info("configuring {} proxies", properties.origins.size()); - - Origin origin = properties.origins.get(0); - String uri = origin.uri.toString(); - ProxyTemplateResolver defaultResolver = - new ProxyTemplateResolver( - "0: " + origin.uri.getHost(), - 0, - client, - uri, - origin.ttl - ); - LOG.info("registering defaultTemplateResolver for {}", uri); - - for (int i=1; i origins = new LinkedList<>(); - - - public void setName(String name) - { - this.name = name; - } - - public List getOrigins() - { - return origins; - } - - public void setOrigins(List origins) - { - this.origins = origins; - } - - - public static class Origin - { - @NotEmpty - URI uri; - List patterns = new LinkedList<>(); - Long ttl; - - - public void setUri(URI uri) - { - this.uri = uri; - } - - public List getPatterns() - { - return this.patterns; - } - - public void setPatterns(List patterns) - { - this.patterns = patterns; - } - - public void setTtl(Long ttl) - { - this.ttl = ttl; - } - } -} diff --git a/src/main/java/de/juplo/thymeproxy/Application.java b/src/main/java/de/juplo/thymeproxy/Application.java new file mode 100644 index 0000000..47a60c9 --- /dev/null +++ b/src/main/java/de/juplo/thymeproxy/Application.java @@ -0,0 +1,45 @@ +package de.juplo.thymeproxy; + +import java.util.Collections; +import org.apache.http.impl.client.CloseableHttpClient; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.core.Ordered; +import org.springframework.core.env.Environment; +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; + + +@SpringBootApplication +public class Application +{ + @Bean + public SimpleUrlHandlerMapping proxiedHandlerMapping( + ProxyHttpRequestHandler handler + ) + { + SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); + mapping.setOrder(Ordered.HIGHEST_PRECEDENCE); + mapping.setUrlMap(Collections.singletonMap("*.html", handler)); + return mapping; + } + + @Bean + public ProxyHttpRequestHandler proxiedRequestHandler( + CloseableHttpClient client, + Environment env + ) + { + ProxyHttpRequestHandler handler = new ProxyHttpRequestHandler(); + handler.setClient(client); + handler.setOrigin(env.getProperty("thymeproxy.origin")); + handler.setTtl(30000l); + return handler; + } + + + public static void main(String[] args) + { + SpringApplication.run(Application.class, args); + } +} \ No newline at end of file diff --git a/src/main/java/de/juplo/thymeproxy/ProxyHttpRequestHandler.java b/src/main/java/de/juplo/thymeproxy/ProxyHttpRequestHandler.java new file mode 100644 index 0000000..8320c7e --- /dev/null +++ b/src/main/java/de/juplo/thymeproxy/ProxyHttpRequestHandler.java @@ -0,0 +1,384 @@ +package de.juplo.thymeproxy; + + +import java.io.IOException; +import java.time.Clock; +import java.time.Duration; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.concurrent.TimeUnit; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.http.Header; +import org.apache.http.HeaderElement; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.CacheControl; +import org.springframework.util.Assert; +import org.springframework.util.StreamUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.HttpRequestHandler; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.support.WebContentGenerator; +import org.springframework.web.util.UrlPathHelper; + + + +/** + * + * @author kai + */ +public class ProxyHttpRequestHandler + extends + WebContentGenerator + implements + HttpRequestHandler +{ + private final static Logger LOG = + LoggerFactory.getLogger(ProxyHttpRequestHandler.class); + + + public final static Long DEFAULT_TTL = 300000l; /** 5 minutes */ + + + private UrlPathHelper urlPathHelper = new UrlPathHelper(); + + private CloseableHttpClient client; + private String origin; + private Long ttl; + + private final Clock clock; + + + public ProxyHttpRequestHandler() + { + clock = Clock.systemDefaultZone(); + } + + public ProxyHttpRequestHandler(Clock clock) + { + this.clock = clock; + } + + + @Override + public void handleRequest( + HttpServletRequest request, + HttpServletResponse response + ) + throws + ServletException, + IOException + { + String path = + (String) + request.getAttribute( + HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE + ); + + if (!StringUtils.hasText(path)) + path = urlPathHelper.getLookupPathForRequest(request); + + StringBuilder builder = new StringBuilder(); + builder.append(origin); + builder.append(path); + + String query = request.getQueryString(); + if (query != null) + { + builder.append('?'); + builder.append(query); + } + + String resource = builder.toString(); + + try + { + HttpGet p_request = new HttpGet(resource); + CloseableHttpResponse p_response = client.execute(p_request); + + setCacheControl(computeCacheControl(p_response)); + prepareResponse(response); + + Header length = p_response.getLastHeader("Content-Length"); + if (length != null) + response.addHeader(length.getName(), length.getValue()); + + int status = p_response.getStatusLine().getStatusCode(); + switch (status) + { + case HttpServletResponse.SC_FOUND: + case HttpServletResponse.SC_OK: + /** OK. Continue as normal... */ + response.setStatus(status); + + HttpEntity entity = p_response.getEntity(); + + StreamUtils.copy(entity.getContent(), response.getOutputStream()); + + /** Release the connection */ + EntityUtils.consume(entity); + p_response.close(); + + break; + + case HttpServletResponse.SC_NOT_FOUND: + case HttpServletResponse.SC_GONE: + /** The resource can not be resolved through this origin */ + response.sendError(status, p_response.getStatusLine().getReasonPhrase()); + return; + + case HttpServletResponse.SC_MOVED_PERMANENTLY: + // TODO + + case HttpServletResponse.SC_SEE_OTHER: + // TODO + + case HttpServletResponse.SC_TEMPORARY_REDIRECT: + // TODO + + default: + LOG.error("{} -- {}", p_response.getStatusLine(), resource); + // TODO: throw sensible exceptions, to communicate resolving-errors + throw new RuntimeException(p_response.getStatusLine().toString() + " -- " + resource); + } + } + catch (IOException e) + { + LOG.error("unexpected error while resolving {}: {}", resource, e.getMessage()); + // TODO: throw sensible exceptions, to communicate resolving-errors + throw new RuntimeException(e); + } + } + + public CacheControl computeCacheControl(HttpResponse response) + { + if (ttl == null) + /** Caching is disabled! */ + return CacheControl.noStore(); + + + boolean has_cache_control = false; + boolean is_public = false; + boolean is_private = false; + boolean no_cache = false; + boolean no_store = false; + boolean no_transform = false; + boolean must_revalidate = false; + boolean proxy_revalidate = false; + Long max_age = null; + Long s_maxage = null; + + for (Header header : response.getHeaders("Cache-Control")) + { + has_cache_control = true; + for (HeaderElement element : header.getElements()) + { + switch (element.getName()) + { + case "public": + is_public = true; + break; + case "private": + is_private = true; + break; + case "no-cache": + no_cache = true; + break; + case "no-store": + no_store = true; + break; + case "no_transform": + no_transform = true; + break; + case "must-revalidate": + must_revalidate = true; + break; + case "proxy-revalidate": + proxy_revalidate = true; + case "max-age": + try + { + max_age = Long.parseLong(element.getValue()); + } + catch (NumberFormatException e) + { + LOG.warn( + "invalid header \"Cache-Control: max-age={}\"", + element.getValue() + ); + } + break; + case "s-maxage": + try + { + s_maxage = Long.parseLong(element.getValue()); + } + catch (NumberFormatException e) + { + LOG.warn( + "invalid header \"Cache-Control: s-maxage={}\"", + element.getValue() + ); + } + break; + default: + LOG.warn( + "invalid header \"Cache-Control: {}{}\"", + element.getName(), + element.getValue() == null ? "" : "=" + element.getValue() + ); + } + } + } + + if (!has_cache_control) + { + Header header = response.getLastHeader("Expires"); + if (header == null) + /** No TTL specified in response-headers: use configured default */ + max_age = ttl; + else + try + { + OffsetDateTime expires + = OffsetDateTime.parse( + header.getValue(), + DateTimeFormatter.RFC_1123_DATE_TIME + ); + + Duration delta = Duration.between(OffsetDateTime.now(clock), expires); + if (delta.isNegative()) + no_store = true; + else + max_age = delta.getSeconds(); + } + catch (DateTimeParseException e) + { + LOG.warn("invalid header \"Expires: {}\"", header.getValue()); + /** + * No TTL specified in response-headers: assume expired + * see: https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.21 + */ + max_age = 0l; + } + } + + CacheControl cache_control; + + if (no_store) + cache_control = CacheControl.noStore(); + else if (no_cache) + cache_control = CacheControl.noCache(); + else if (max_age != null) + cache_control = CacheControl.maxAge(max_age, TimeUnit.SECONDS); + else + cache_control = CacheControl.empty(); + + if (is_private) + cache_control.cachePrivate(); + if (is_public) + cache_control.cachePublic(); + if (no_transform) + cache_control.noTransform(); + if (must_revalidate) + cache_control.mustRevalidate(); + if (proxy_revalidate) + cache_control.proxyRevalidate(); + if (s_maxage != null) + cache_control.sMaxAge(s_maxage, TimeUnit.SECONDS); + + return cache_control; + } + + + public ProxyHttpRequestHandler setClient(CloseableHttpClient client) + { + this.client = client; + return this; + } + + public ProxyHttpRequestHandler setOrigin(String origin) + { + this.origin = origin; + return this; + } + + public ProxyHttpRequestHandler setTtl(Long ttl) + { + this.ttl = ttl; + return this; + } + + /** + * Set if URL lookup should always use full path within current servlet + * context. Else, the path within the current servlet mapping is used + * if applicable (i.e. in the case of a ".../*" servlet mapping in web.xml). + * Default is "false". + * + * @see org.springframework.web.util.UrlPathHelper#setAlwaysUseFullPath + */ + public void setAlwaysUseFullPath(boolean alwaysUseFullPath) + { + this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath); + } + + /** + * Set if context path and request URI should be URL-decoded. + * Both are returned undecoded by the Servlet API, + * in contrast to the servlet path. + *

+ * Uses either the request encoding or the default encoding according + * to the Servlet spec (ISO-8859-1). + * + * @see org.springframework.web.util.UrlPathHelper#setUrlDecode + */ + public void setUrlDecode(boolean urlDecode) + { + this.urlPathHelper.setUrlDecode(urlDecode); + } + + /** + * Set if ";" (semicolon) content should be stripped from the request URI. + * + * @see + * org.springframework.web.util.UrlPathHelper#setRemoveSemicolonContent(boolean) + */ + public void setRemoveSemicolonContent(boolean removeSemicolonContent) + { + this.urlPathHelper.setRemoveSemicolonContent(removeSemicolonContent); + } + + /** + * Set the UrlPathHelper to use for the resolution of lookup paths. + *

+ * Use this to override the default UrlPathHelper with a custom subclass, + * or to share common UrlPathHelper settings across multiple + * MethodNameResolvers + * and HandlerMappings. + * + * @see + * org.springframework.web.servlet.handler.AbstractUrlHandlerMapping#setUrlPathHelper + */ + public void setUrlPathHelper(UrlPathHelper urlPathHelper) + { + Assert.notNull(urlPathHelper, "UrlPathHelper must not be null"); + this.urlPathHelper = urlPathHelper; + } + + /** + * Return the UrlPathHelper to use for the resolution of lookup paths. + */ + protected UrlPathHelper getUrlPathHelper() + { + return this.urlPathHelper; + } +} diff --git a/src/main/java/de/juplo/thymeproxy/ProxyResourceResolver.java b/src/main/java/de/juplo/thymeproxy/ProxyResourceResolver.java deleted file mode 100644 index 5e24034..0000000 --- a/src/main/java/de/juplo/thymeproxy/ProxyResourceResolver.java +++ /dev/null @@ -1,123 +0,0 @@ -package de.juplo.thymeproxy; - - -import java.io.IOException; -import java.io.InputStream; -import org.apache.http.HttpEntity; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.util.EntityUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.thymeleaf.TemplateProcessingParameters; -import org.thymeleaf.resourceresolver.IResourceResolver; - - - -/** - * - * @author kai - */ -public class ProxyResourceResolver implements IResourceResolver -{ - private final static Logger LOG = - LoggerFactory.getLogger(ProxyResourceResolver.class); - - - private final String resource; - - private final CloseableHttpResponse response; - private final HttpEntity entity; - - - public ProxyResourceResolver( - String resource, - CloseableHttpResponse response, - HttpEntity entity - ) - { - this.resource = resource; - this.response = response; - this.entity = entity; - } - - - @Override - public String getName() - { - return resource; - } - - @Override - public InputStream getResourceAsStream(TemplateProcessingParameters templateProcessingParameters, String resourceName) - { - InputStream is; - try - { - is = entity.getContent(); - } - catch (IOException e) - { - LOG.error("unexpected error while retriving the response-body", e); - return null; - } - - return new InputStream() - { - @Override - public boolean markSupported() - { - return is.markSupported(); - } - - @Override - public synchronized void reset() throws IOException - { - is.reset(); - } - - @Override - public synchronized void mark(int readlimit) - { - is.mark(readlimit); - } - - @Override - public void close() throws IOException - { - is.close(); - EntityUtils.consume(entity); - response.close(); - } - - @Override - public int available() throws IOException - { - return is.available(); - } - - @Override - public long skip(long n) throws IOException - { - return is.skip(n); - } - - @Override - public int read() throws IOException - { - return is.read(); - } - - @Override - public int read(byte[] b, int off, int len) throws IOException - { - return is.read(b, off, len); - } - - @Override - public int read(byte[] b) throws IOException - { - return is.read(b); - } - }; - } -} diff --git a/src/main/java/de/juplo/thymeproxy/ProxyTemplateResolver.java b/src/main/java/de/juplo/thymeproxy/ProxyTemplateResolver.java deleted file mode 100644 index c6601a6..0000000 --- a/src/main/java/de/juplo/thymeproxy/ProxyTemplateResolver.java +++ /dev/null @@ -1,297 +0,0 @@ -package de.juplo.thymeproxy; - - -import java.io.IOException; -import java.time.Clock; -import java.time.Duration; -import java.time.OffsetDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.util.regex.Pattern; -import javax.servlet.http.HttpServletResponse; -import org.apache.http.Header; -import org.apache.http.HeaderElement; -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.entity.ContentType; -import org.apache.http.impl.client.CloseableHttpClient; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.thymeleaf.TemplateProcessingParameters; -import org.thymeleaf.templatemode.StandardTemplateModeHandlers; -import org.thymeleaf.templateresolver.ITemplateResolutionValidity; -import org.thymeleaf.templateresolver.ITemplateResolver; -import org.thymeleaf.templateresolver.NonCacheableTemplateResolutionValidity; -import org.thymeleaf.templateresolver.TTLTemplateResolutionValidity; -import org.thymeleaf.templateresolver.TemplateResolution; - - - -/** - * - * @author Kai Moritz - */ -public class ProxyTemplateResolver implements ITemplateResolver -{ - private final static Logger LOG = - LoggerFactory.getLogger(ProxyTemplateResolver.class); - - private final static Pattern HTML = - Pattern.compile("text/html", Pattern.CASE_INSENSITIVE); - private final static Pattern XHTML = - Pattern.compile("application/xhtml+xml", Pattern.CASE_INSENSITIVE); - private final static Pattern XML = - Pattern.compile("(?:/|\\+)xml$", Pattern.CASE_INSENSITIVE); - - public final static Long DEFAULT_TTL = 300000l; /** 5 minutes */ - - private final String name; - private final Integer order; - - private final CloseableHttpClient client; - private final String origin; - private final Long ttl; - private final Clock clock; - - - public ProxyTemplateResolver( - String name, - Integer order, - CloseableHttpClient client, - String origin, - Long ttl, - Clock clock - ) - { - super(); - this.name = name; - this.order = order; - this.client = client; - this.origin = origin; - this.ttl = ttl; - this.clock = clock; - } - - public ProxyTemplateResolver( - String name, - Integer order, - CloseableHttpClient client, - String origin, - Long ttl - ) - { - this(name, order, client, origin, ttl, Clock.systemDefaultZone()); - } - - public ProxyTemplateResolver( - String name, - Integer order, - CloseableHttpClient client, - String origin - ) - { - this(name, order, client, origin, DEFAULT_TTL, Clock.systemDefaultZone()); - } - - - @Override - public String getName() - { - return name; - } - - @Override - public Integer getOrder() - { - return order; - } - - public String getOrigin() - { - return origin; - } - - public Long getDefaultTTL() - { - return ttl; - } - - @Override - public TemplateResolution resolveTemplate(TemplateProcessingParameters params) - { - StringBuilder builder = new StringBuilder(); - builder.append(origin); - builder.append(params.getTemplateName()); - - String resource = builder.toString(); - - try - { - HttpGet request = new HttpGet(resource); - CloseableHttpResponse response = client.execute(request); - - switch (response.getStatusLine().getStatusCode()) - { - case HttpServletResponse.SC_FOUND: - case HttpServletResponse.SC_OK: - /** OK. Continue as normal... */ - break; - - case HttpServletResponse.SC_NOT_FOUND: - case HttpServletResponse.SC_GONE: - /** The resource can not be resolved through this origin */ - return null; - - case HttpServletResponse.SC_MOVED_PERMANENTLY: - // TODO - - case HttpServletResponse.SC_SEE_OTHER: - // TODO - - case HttpServletResponse.SC_TEMPORARY_REDIRECT: - // TODO - - default: - LOG.error("{} -- {}", response.getStatusLine(), resource); - // TODO: throw sensible exceptions, to communicate resolving-errors - throw new RuntimeException(response.getStatusLine().toString() + " -- " + resource); - } - - HttpEntity entity = response.getEntity(); - ContentType content = ContentType.getOrDefault(entity); - - return new TemplateResolution( - params.getTemplateName(), - resource, - new ProxyResourceResolver(resource, response, entity), - content.getCharset().displayName(), - ProxyTemplateResolver.computeTemplateMode(content), - computeValidity(response) - ); - } - catch (IOException e) - { - LOG.error("unexpected error while resolving {}: {}", resource, e.getMessage()); - // TODO: throw sensible exceptions, to communicate resolving-errors - throw new RuntimeException(e); - } - } - - @Override - public void initialize() - { - } - - - public static String computeTemplateMode(ContentType content) - { - String type = content.getMimeType(); - - if (HTML.matcher(type).matches()) - return StandardTemplateModeHandlers.LEGACYHTML5.getTemplateModeName(); - - if (XML.matcher(type).find()) - { - if (XHTML.matcher(type).matches()) - return StandardTemplateModeHandlers.XHTML.getTemplateModeName(); - else - return StandardTemplateModeHandlers.XML.getTemplateModeName(); - } - - throw new RuntimeException("Cannot handle mime-type " + type); - } - - public ITemplateResolutionValidity computeValidity(HttpResponse response) - { - if (ttl == null) - /** Caching is disabled! */ - return NonCacheableTemplateResolutionValidity.INSTANCE; - - boolean cacheable = true; - Integer max_age = null; - - for (Header header : response.getHeaders("Cache-Control")) - { - for (HeaderElement element : header.getElements()) - { - switch (element.getName()) - { - case "no-cache": - case "no-store": - case "must-revalidate": - cacheable = false; - break; - - case "max-age": - try - { - max_age = Integer.parseInt(element.getValue()); - } - catch (NumberFormatException e) - { - LOG.warn( - "invalid header \"Cache-Control: max-age={}\"", - element.getValue() - ); - } - break; - } - } - } - - if (max_age != null && max_age < 1) - cacheable = false; - - if (!cacheable) - return NonCacheableTemplateResolutionValidity.INSTANCE; - - if (max_age != null) - { - long millis = max_age; - if (millis >= Long.MAX_VALUE / 1000l ) - millis = Long.MAX_VALUE; - else - millis = millis * 1000l; - return new TTLTemplateResolutionValidity(millis); - } - - Header header = response.getLastHeader("Expires"); - if (header == null) - /** No TTL specified in response-headers: use configured default */ - return new TTLTemplateResolutionValidity(ttl); - - try - { - OffsetDateTime expires = - OffsetDateTime.parse( - header.getValue(), - DateTimeFormatter.RFC_1123_DATE_TIME - ); - - Duration delta = Duration.between(OffsetDateTime.now(clock), expires); - if (delta.isNegative() || delta.isZero()) - return NonCacheableTemplateResolutionValidity.INSTANCE; - - long millis = delta.getSeconds(); - if (millis >= Long.MAX_VALUE / 1000l) - millis = Long.MAX_VALUE; - else - { - millis = millis * 1000; - millis = millis + (long)(delta.getNano() / 1000000); - } - return new TTLTemplateResolutionValidity(millis); - } - catch (DateTimeParseException e) - { - LOG.warn("invalid header \"Expires: {}\"", header.getValue()); - /** - * No TTL specified in response-headers: assume expired - * (see: https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.21) - */ - return NonCacheableTemplateResolutionValidity.INSTANCE; - } - } -} diff --git a/src/main/java/de/juplo/thymeproxy/RequestToProxyViewNameTranslator.java b/src/main/java/de/juplo/thymeproxy/RequestToProxyViewNameTranslator.java new file mode 100644 index 0000000..53c83fb --- /dev/null +++ b/src/main/java/de/juplo/thymeproxy/RequestToProxyViewNameTranslator.java @@ -0,0 +1,28 @@ +package de.juplo.thymeproxy; + + +import javax.servlet.http.HttpServletRequest; +import org.springframework.web.servlet.RequestToViewNameTranslator; + + + +/** + * + * @author kai + */ +public class RequestToProxyViewNameTranslator implements RequestToViewNameTranslator +{ + @Override + public String getViewName(HttpServletRequest request) throws Exception + { + StringBuilder builder = new StringBuilder(); + builder.append(request.getRequestURI()); + String query = request.getQueryString(); + if (query != null) + { + builder.append('?'); + builder.append(query); + } + return builder.toString(); + } +} diff --git a/src/main/java/de/juplo/thymeproxy/UrlProxyViewController.java b/src/main/java/de/juplo/thymeproxy/UrlProxyViewController.java new file mode 100644 index 0000000..23ef9e3 --- /dev/null +++ b/src/main/java/de/juplo/thymeproxy/UrlProxyViewController.java @@ -0,0 +1,28 @@ +package de.juplo.thymeproxy; + + +import javax.servlet.http.HttpServletRequest; +import org.springframework.web.servlet.mvc.AbstractUrlViewController; + + + +/** + * + * @author kai + */ +public class UrlProxyViewController extends AbstractUrlViewController +{ + @Override + protected String getViewNameForRequest(HttpServletRequest request) + { + StringBuilder builder = new StringBuilder(); + builder.append(request.getRequestURI()); + String query = request.getQueryString(); + if (query != null) + { + builder.append('?'); + builder.append(query); + } + return builder.toString(); + } +} diff --git a/src/main/resources/META-INF/spring.factories b/src/main/resources/META-INF/spring.factories deleted file mode 100644 index f6c92b4..0000000 --- a/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1 +0,0 @@ -org.springframework.boot.autoconfigure.EnableAutoConfiguration=de.juplo.autoconfiguration.ThymeproxyAutoConfiguration diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index dd442f2..1ada05c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -2,3 +2,4 @@ thymeproxy.name=@thymeproxy.name@ thymeproxy.origin=@thymeproxy.origin@ server.port=@thymeproxy.port@ thymeproxy.ttl=@thymeproxy.ttl@ +thymeproxy.origins[0].uri=http://localhost:8080/thymeleaf/ diff --git a/src/main/resources/log4j.xml b/src/main/resources/log4j.xml deleted file mode 100644 index a3d7d44..0000000 --- a/src/main/resources/log4j.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/test/resources/logback.xml b/src/main/resources/logback.xml similarity index 71% rename from src/test/resources/logback.xml rename to src/main/resources/logback.xml index e6f6c58..793eaa1 100644 --- a/src/test/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -7,14 +7,10 @@ - + - - - - diff --git a/src/test/java/de/juplo/autoconfigure/ThymeproxyAutoConfigurationTest.java b/src/test/java/de/juplo/autoconfigure/ThymeproxyAutoConfigurationTest.java deleted file mode 100644 index 9360b7c..0000000 --- a/src/test/java/de/juplo/autoconfigure/ThymeproxyAutoConfigurationTest.java +++ /dev/null @@ -1,230 +0,0 @@ -package de.juplo.autoconfigure; - - -import de.juplo.thymeproxy.ProxyTemplateResolver; -import java.net.URI; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import org.junit.Test; -import org.springframework.context.annotation.Configuration; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.BeansException; -import org.springframework.boot.autoconfigure.logging.AutoConfigurationReportLoggingInitializer; -import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration; -import org.springframework.boot.test.EnvironmentTestUtils; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.thymeleaf.templateresolver.ITemplateResolver; -import org.thymeleaf.templateresolver.TemplateResolver; - - - -public class ThymeproxyAutoConfigurationTest -{ - private final Logger LOG = - LoggerFactory.getLogger(ThymeproxyAutoConfigurationTest.class); - - - @Test - public void propertyBinding() throws Exception - { - LOG.info("<-- Start Of New Test-Case!"); - - ConfigurableApplicationContext context; - ThymeproxyProperties properties; - - context = load( - EmptyConfiguration.class, - "thymeproxy.name=Thymeproxy", - "thymeproxy.origins[0].uri=http://localhost:8080/test/", - "thymeproxy.origins[0].patterns[0]=^/css/", - "thymeproxy.origins[0].patterns[1]=^/img/", - "thymeproxy.origins[0].patterns[2]=*\\.xml$", - "thymeproxy.origins[1].uri=http://127.0.0.1:8081", - "thymeproxy.origins[1].ttl=30000" - ); - properties = context.getBean(ThymeproxyProperties.class); - assertNotNull(properties); - assertEquals("Thymeproxy", properties.name); - assertNotNull(properties.origins); - assertEquals(2, properties.origins.size()); - assertEquals(new URI("http://localhost:8080/test/"), properties.origins.get(0).uri); - assertNotNull(properties.origins.get(0).patterns); - assertEquals(3, properties.origins.get(0).patterns.size()); - assertEquals("^/css/", properties.origins.get(0).patterns.get(0)); - assertEquals("^/img/", properties.origins.get(0).patterns.get(1)); - assertEquals("*\\.xml$", properties.origins.get(0).patterns.get(2)); - assertNull(properties.origins.get(0).ttl); - assertEquals(new URI("http://127.0.0.1:8081"), properties.origins.get(1).uri); - assertNotNull(properties.origins.get(1).patterns); - assertEquals(0, properties.origins.get(1).patterns.size()); - assertEquals(new Long(30000l), properties.origins.get(1).ttl); - context.close(); - } - - @Test - public void defaultConfiguration() - { - LOG.info("<-- Start Of New Test-Case!"); - - ConfigurableApplicationContext context = load(EmptyConfiguration.class); - - ITemplateResolver resolver = - (ITemplateResolver)context.getBean("defaultTemplateResolver"); - assertNotNull(resolver); - assertTrue( - "Expected an instance of type TemplateResolver", - resolver instanceof TemplateResolver - ); - assertNotEquals(DefaultTemplateResolverConfiguration.RESOLVER, resolver); - - context.close(); - } - - @Test - public void defaultTemplateResolverConfigured() - { - LOG.info("<-- Start Of New Test-Case!"); - - ConfigurableApplicationContext context = - load(DefaultTemplateResolverConfiguration.class); - - ITemplateResolver resolver = - (ITemplateResolver)context.getBean("defaultTemplateResolver"); - assertNotNull(resolver); - assertTrue( - "Expected an instance of type TemplateResolver", - resolver instanceof TemplateResolver - ); - assertEquals(DefaultTemplateResolverConfiguration.RESOLVER, resolver); - - context.close(); - } - - @Test - public void proxiesConfigured() - { - LOG.info("<-- Start Of New Test-Case!"); - - ConfigurableApplicationContext context = load( - EmptyConfiguration.class, - "thymeproxy.name=Thymeproxy", - "thymeproxy.origins[0].uri=http://localhost:8080/test/", - "thymeproxy.origins[0].patterns[0]=^/css/", - "thymeproxy.origins[0].patterns[1]=^/img/", - "thymeproxy.origins[0].patterns[2]=*\\.xml$", - "thymeproxy.origins[1].uri=http://127.0.0.1:8081", - "thymeproxy.origins[1].ttl=30000" - ); - - ITemplateResolver resolver; - ProxyTemplateResolver proxy; - - resolver = (ITemplateResolver)context.getBean("defaultTemplateResolver"); - assertNotNull(resolver); - assertTrue( - "Expected an instance of type ProxyTemplateResolver", - resolver instanceof ProxyTemplateResolver - ); - proxy = (ProxyTemplateResolver)resolver; - assertEquals("0: localhost", proxy.getName()); - assertEquals(new Integer(0), proxy.getOrder()); - assertEquals("http://localhost:8080/test/", proxy.getOrigin()); - assertNull(proxy.getDefaultTTL()); - - resolver = (ITemplateResolver)context.getBean("proxy1"); - assertNotNull(resolver); - assertTrue( - "Expected an instance of type ProxyTemplateResolver", - resolver instanceof ProxyTemplateResolver - ); - proxy = (ProxyTemplateResolver)resolver; - assertEquals("1: 127.0.0.1", proxy.getName()); - assertEquals(new Integer(1), proxy.getOrder()); - assertEquals("http://127.0.0.1:8081", proxy.getOrigin()); - assertEquals(new Long(30000l), proxy.getDefaultTTL()); - - context.close(); - } - - @Test - public void proxiesAndDefaultTemplateResolverConfigured() - { - LOG.info("<-- Start Of New Test-Case!"); - - ConfigurableApplicationContext context = load( - DefaultTemplateResolverConfiguration.class, - "thymeproxy.name=Thymeproxy", - "thymeproxy.origins[0].uri=http://localhost:8080/test/", - "thymeproxy.origins[0].patterns[0]=^/css/", - "thymeproxy.origins[0].patterns[1]=^/img/", - "thymeproxy.origins[0].patterns[2]=*\\.xml$", - "thymeproxy.origins[1].uri=http://127.0.0.1:8081", - "thymeproxy.origins[1].ttl=30000" - ); - - ITemplateResolver resolver; - ProxyTemplateResolver proxy; - - resolver = (ITemplateResolver)context.getBean("defaultTemplateResolver"); - assertNotNull(resolver); - assertTrue( - "Expected an instance of type TemplateResolver", - resolver instanceof TemplateResolver - ); - assertEquals(DefaultTemplateResolverConfiguration.RESOLVER, resolver); - - try - { - resolver = (ITemplateResolver)context.getBean("proxy1"); - fail("Found bean for name proxy1: " + resolver); - } - catch (BeansException e) - { - LOG.info(e.toString()); - } - - context.close(); - } - - - @Configuration - static class EmptyConfiguration - { - } - - @Configuration - static class DefaultTemplateResolverConfiguration - { - public static TemplateResolver RESOLVER = new TemplateResolver(); - - @Bean - public TemplateResolver defaultTemplateResolver() - { - return RESOLVER; - } - } - - - private ConfigurableApplicationContext load(Class config, String... pairs) - { - AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(); - EnvironmentTestUtils.addEnvironment(ctx, pairs); - ctx.register(HttpClientAutoConfiguration.class); - ctx.register(ThymeleafAutoConfiguration.class); - ctx.register(ThymeproxyAutoConfiguration.class); - ctx.register(config); - AutoConfigurationReportLoggingInitializer report = - new AutoConfigurationReportLoggingInitializer(); - report.initialize(ctx); - ctx.refresh(); - return ctx; - } -} -- 2.20.1