1 package de.juplo.thymeproxy;
4 import java.io.IOException;
5 import java.time.Clock;
6 import java.time.Duration;
7 import java.time.OffsetDateTime;
8 import java.time.format.DateTimeFormatter;
9 import java.time.format.DateTimeParseException;
10 import java.util.concurrent.TimeUnit;
11 import javax.servlet.ServletException;
12 import javax.servlet.http.HttpServletRequest;
13 import javax.servlet.http.HttpServletResponse;
14 import org.apache.http.Header;
15 import org.apache.http.HeaderElement;
16 import org.apache.http.HttpEntity;
17 import org.apache.http.HttpResponse;
18 import org.apache.http.client.methods.CloseableHttpResponse;
19 import org.apache.http.client.methods.HttpGet;
20 import org.apache.http.impl.client.CloseableHttpClient;
21 import org.apache.http.util.EntityUtils;
22 import org.slf4j.Logger;
23 import org.slf4j.LoggerFactory;
24 import org.springframework.http.CacheControl;
25 import org.springframework.util.Assert;
26 import org.springframework.util.StreamUtils;
27 import org.springframework.util.StringUtils;
28 import org.springframework.web.HttpRequestHandler;
29 import org.springframework.web.servlet.HandlerMapping;
30 import org.springframework.web.servlet.support.WebContentGenerator;
31 import org.springframework.web.util.UrlPathHelper;
39 public class ProxyHttpRequestHandler
45 private final static Logger LOG =
46 LoggerFactory.getLogger(ProxyHttpRequestHandler.class);
49 public final static Long DEFAULT_TTL = 300000l; /** 5 minutes */
52 private UrlPathHelper urlPathHelper = new UrlPathHelper();
54 private CloseableHttpClient client;
55 private String origin;
58 private final Clock clock;
61 public ProxyHttpRequestHandler()
63 clock = Clock.systemDefaultZone();
66 public ProxyHttpRequestHandler(Clock clock)
73 public void handleRequest(
74 HttpServletRequest request,
75 HttpServletResponse response
84 HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE
87 if (!StringUtils.hasText(path))
88 path = urlPathHelper.getLookupPathForRequest(request);
90 StringBuilder builder = new StringBuilder();
91 builder.append(origin);
94 String query = request.getQueryString();
98 builder.append(query);
101 String resource = builder.toString();
105 HttpGet p_request = new HttpGet(resource);
106 CloseableHttpResponse p_response = client.execute(p_request);
108 setCacheControl(computeCacheControl(p_response));
109 prepareResponse(response);
111 Header length = p_response.getLastHeader("Content-Length");
113 response.addHeader(length.getName(), length.getValue());
115 int status = p_response.getStatusLine().getStatusCode();
118 case HttpServletResponse.SC_FOUND:
119 case HttpServletResponse.SC_OK:
120 /** OK. Continue as normal... */
121 response.setStatus(status);
123 HttpEntity entity = p_response.getEntity();
125 StreamUtils.copy(entity.getContent(), response.getOutputStream());
127 /** Release the connection */
128 EntityUtils.consume(entity);
133 case HttpServletResponse.SC_NOT_FOUND:
134 case HttpServletResponse.SC_GONE:
135 /** The resource can not be resolved through this origin */
136 response.sendError(status, p_response.getStatusLine().getReasonPhrase());
139 case HttpServletResponse.SC_MOVED_PERMANENTLY:
142 case HttpServletResponse.SC_SEE_OTHER:
145 case HttpServletResponse.SC_TEMPORARY_REDIRECT:
149 LOG.error("{} -- {}", p_response.getStatusLine(), resource);
150 // TODO: throw sensible exceptions, to communicate resolving-errors
151 throw new RuntimeException(p_response.getStatusLine().toString() + " -- " + resource);
154 catch (IOException e)
156 LOG.error("unexpected error while resolving {}: {}", resource, e.getMessage());
157 // TODO: throw sensible exceptions, to communicate resolving-errors
158 throw new RuntimeException(e);
162 public CacheControl computeCacheControl(HttpResponse response)
165 /** Caching is disabled! */
166 return CacheControl.noStore();
169 boolean has_cache_control = false;
170 boolean is_public = false;
171 boolean is_private = false;
172 boolean no_cache = false;
173 boolean no_store = false;
174 boolean no_transform = false;
175 boolean must_revalidate = false;
176 boolean proxy_revalidate = false;
178 Long s_maxage = null;
180 for (Header header : response.getHeaders("Cache-Control"))
182 has_cache_control = true;
183 for (HeaderElement element : header.getElements())
185 switch (element.getName())
202 case "must-revalidate":
203 must_revalidate = true;
205 case "proxy-revalidate":
206 proxy_revalidate = true;
210 max_age = Long.parseLong(element.getValue());
212 catch (NumberFormatException e)
215 "invalid header \"Cache-Control: max-age={}\"",
223 s_maxage = Long.parseLong(element.getValue());
225 catch (NumberFormatException e)
228 "invalid header \"Cache-Control: s-maxage={}\"",
235 "invalid header \"Cache-Control: {}{}\"",
237 element.getValue() == null ? "" : "=" + element.getValue()
243 if (!has_cache_control)
245 Header header = response.getLastHeader("Expires");
247 /** No TTL specified in response-headers: use configured default */
252 OffsetDateTime expires
253 = OffsetDateTime.parse(
255 DateTimeFormatter.RFC_1123_DATE_TIME
258 Duration delta = Duration.between(OffsetDateTime.now(clock), expires);
259 if (delta.isNegative())
262 max_age = delta.getSeconds();
264 catch (DateTimeParseException e)
266 LOG.warn("invalid header \"Expires: {}\"", header.getValue());
268 * No TTL specified in response-headers: assume expired
269 * see: https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.21
275 CacheControl cache_control;
278 cache_control = CacheControl.noStore();
280 cache_control = CacheControl.noCache();
281 else if (max_age != null)
282 cache_control = CacheControl.maxAge(max_age, TimeUnit.SECONDS);
284 cache_control = CacheControl.empty();
287 cache_control.cachePrivate();
289 cache_control.cachePublic();
291 cache_control.noTransform();
293 cache_control.mustRevalidate();
294 if (proxy_revalidate)
295 cache_control.proxyRevalidate();
296 if (s_maxage != null)
297 cache_control.sMaxAge(s_maxage, TimeUnit.SECONDS);
299 return cache_control;
303 public ProxyHttpRequestHandler setClient(CloseableHttpClient client)
305 this.client = client;
309 public ProxyHttpRequestHandler setOrigin(String origin)
311 this.origin = origin;
315 public ProxyHttpRequestHandler setTtl(Long ttl)
322 * Set if URL lookup should always use full path within current servlet
323 * context. Else, the path within the current servlet mapping is used
324 * if applicable (i.e. in the case of a ".../*" servlet mapping in web.xml).
325 * Default is "false".
327 * @see org.springframework.web.util.UrlPathHelper#setAlwaysUseFullPath
329 public void setAlwaysUseFullPath(boolean alwaysUseFullPath)
331 this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath);
335 * Set if context path and request URI should be URL-decoded.
336 * Both are returned <i>undecoded</i> by the Servlet API,
337 * in contrast to the servlet path.
339 * Uses either the request encoding or the default encoding according
340 * to the Servlet spec (ISO-8859-1).
342 * @see org.springframework.web.util.UrlPathHelper#setUrlDecode
344 public void setUrlDecode(boolean urlDecode)
346 this.urlPathHelper.setUrlDecode(urlDecode);
350 * Set if ";" (semicolon) content should be stripped from the request URI.
353 * org.springframework.web.util.UrlPathHelper#setRemoveSemicolonContent(boolean)
355 public void setRemoveSemicolonContent(boolean removeSemicolonContent)
357 this.urlPathHelper.setRemoveSemicolonContent(removeSemicolonContent);
361 * Set the UrlPathHelper to use for the resolution of lookup paths.
363 * Use this to override the default UrlPathHelper with a custom subclass,
364 * or to share common UrlPathHelper settings across multiple
365 * MethodNameResolvers
366 * and HandlerMappings.
369 * org.springframework.web.servlet.handler.AbstractUrlHandlerMapping#setUrlPathHelper
371 public void setUrlPathHelper(UrlPathHelper urlPathHelper)
373 Assert.notNull(urlPathHelper, "UrlPathHelper must not be null");
374 this.urlPathHelper = urlPathHelper;
378 * Return the UrlPathHelper to use for the resolution of lookup paths.
380 protected UrlPathHelper getUrlPathHelper()
382 return this.urlPathHelper;