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.StatusLine;
19 import org.apache.http.client.methods.CloseableHttpResponse;
20 import org.apache.http.client.methods.HttpGet;
21 import org.apache.http.impl.client.CloseableHttpClient;
22 import org.apache.http.util.EntityUtils;
23 import org.slf4j.Logger;
24 import org.slf4j.LoggerFactory;
25 import org.springframework.http.CacheControl;
26 import org.springframework.util.Assert;
27 import org.springframework.util.StreamUtils;
28 import org.springframework.util.StringUtils;
29 import org.springframework.web.HttpRequestHandler;
30 import org.springframework.web.servlet.HandlerMapping;
31 import org.springframework.web.servlet.support.WebContentGenerator;
32 import org.springframework.web.util.UrlPathHelper;
40 public class ProxyHttpRequestHandler
46 private final static Logger LOG =
47 LoggerFactory.getLogger(ProxyHttpRequestHandler.class);
50 public final static Long DEFAULT_TTL = 300000l; /** 5 minutes */
53 private UrlPathHelper urlPathHelper = new UrlPathHelper();
55 private CloseableHttpClient client;
56 private String origin;
59 private final Clock clock;
62 public ProxyHttpRequestHandler()
64 clock = Clock.systemDefaultZone();
67 public ProxyHttpRequestHandler(Clock clock)
74 public void handleRequest(
75 HttpServletRequest request,
76 HttpServletResponse response
82 LOG.debug("handling: {}", request.getRequestURI());
87 HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE
90 if (!StringUtils.hasText(path))
91 path = urlPathHelper.getLookupPathForRequest(request);
93 StringBuilder builder = new StringBuilder();
94 builder.append(origin);
97 String query = request.getQueryString();
101 builder.append(query);
104 String resource = builder.toString();
106 CloseableHttpResponse p_response = null;
109 HttpGet p_request = new HttpGet(resource);
111 p_response = client.execute(p_request);
113 StatusLine status = p_response.getStatusLine();
114 int code = status.getStatusCode();
115 LOG.info("{} - {}: {}", code, status.getReasonPhrase(), resource);
117 setCacheControl(computeCacheControl(p_response));
118 prepareResponse(response);
120 /** Copy some headers, if present.. */
121 ProxyHttpRequestHandler.copyHeader(p_response, response, "Date");
122 ProxyHttpRequestHandler.copyHeader(p_response, response, "ETag");
123 ProxyHttpRequestHandler.copyHeader(p_response, response, "Last-Modified");
124 ProxyHttpRequestHandler.copyHeader(p_response, response, "Content-Length");
125 ProxyHttpRequestHandler.copyHeader(p_response, response, "Content-Type");
129 case HttpServletResponse.SC_FOUND:
130 case HttpServletResponse.SC_OK:
131 /** OK. Continue as normal... */
132 response.setStatus(code);
134 HttpEntity entity = p_response.getEntity();
136 StreamUtils.copy(entity.getContent(), response.getOutputStream());
138 /** Release the connection */
139 EntityUtils.consume(entity);
144 case HttpServletResponse.SC_NOT_FOUND:
145 case HttpServletResponse.SC_GONE:
146 /** The resource can not be resolved through this origin */
147 response.sendError(code, status.getReasonPhrase());
150 case HttpServletResponse.SC_MOVED_PERMANENTLY:
153 case HttpServletResponse.SC_SEE_OTHER:
156 case HttpServletResponse.SC_TEMPORARY_REDIRECT:
160 LOG.error("{} -- {}", p_response.getStatusLine(), resource);
161 // TODO: throw sensible exceptions, to communicate resolving-errors
162 throw new RuntimeException(p_response.getStatusLine().toString() + " -- " + resource);
165 catch (IOException e)
167 LOG.error("unexpected error while resolving {}: {}", resource, e.getMessage());
168 // TODO: throw sensible exceptions, to communicate resolving-errors
169 throw new RuntimeException(e);
173 if (p_response != null)
175 EntityUtils.consumeQuietly(p_response.getEntity());
181 public static void copyHeader(
183 HttpServletResponse target,
187 Header header = source.getLastHeader(name);
190 LOG.trace("copy header {}: {}", header.getName(), header.getValue());
191 target.addHeader(header.getName(), header.getValue());
195 public CacheControl computeCacheControl(HttpResponse response)
198 /** Caching is disabled! */
199 return CacheControl.noStore();
202 boolean has_cache_control = false;
203 boolean is_public = false;
204 boolean is_private = false;
205 boolean no_cache = false;
206 boolean no_store = false;
207 boolean no_transform = false;
208 boolean must_revalidate = false;
209 boolean proxy_revalidate = false;
211 Long s_maxage = null;
213 for (Header header : response.getHeaders("Cache-Control"))
215 has_cache_control = true;
216 for (HeaderElement element : header.getElements())
218 switch (element.getName())
235 case "must-revalidate":
236 must_revalidate = true;
238 case "proxy-revalidate":
239 proxy_revalidate = true;
243 max_age = Long.parseLong(element.getValue());
245 catch (NumberFormatException e)
248 "invalid header \"Cache-Control: max-age={}\"",
256 s_maxage = Long.parseLong(element.getValue());
258 catch (NumberFormatException e)
261 "invalid header \"Cache-Control: s-maxage={}\"",
268 "invalid header \"Cache-Control: {}{}\"",
270 element.getValue() == null ? "" : "=" + element.getValue()
276 if (!has_cache_control)
278 Header header = response.getLastHeader("Expires");
280 /** No TTL specified in response-headers: use configured default */
285 OffsetDateTime expires
286 = OffsetDateTime.parse(
288 DateTimeFormatter.RFC_1123_DATE_TIME
291 Duration delta = Duration.between(OffsetDateTime.now(clock), expires);
292 if (delta.isNegative())
295 max_age = delta.getSeconds();
297 catch (DateTimeParseException e)
299 LOG.warn("invalid header \"Expires: {}\"", header.getValue());
301 * No TTL specified in response-headers: assume expired
302 * see: https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.21
308 CacheControl cache_control;
311 cache_control = CacheControl.noStore();
313 cache_control = CacheControl.noCache();
314 else if (max_age != null)
315 cache_control = CacheControl.maxAge(max_age, TimeUnit.SECONDS);
317 cache_control = CacheControl.empty();
320 cache_control.cachePrivate();
322 cache_control.cachePublic();
324 cache_control.noTransform();
326 cache_control.mustRevalidate();
327 if (proxy_revalidate)
328 cache_control.proxyRevalidate();
329 if (s_maxage != null)
330 cache_control.sMaxAge(s_maxage, TimeUnit.SECONDS);
332 return cache_control;
336 public ProxyHttpRequestHandler setClient(CloseableHttpClient client)
338 this.client = client;
342 public ProxyHttpRequestHandler setOrigin(String origin)
344 this.origin = origin;
348 public ProxyHttpRequestHandler setTtl(Long ttl)
355 * Set if URL lookup should always use full path within current servlet
356 * context. Else, the path within the current servlet mapping is used
357 * if applicable (i.e. in the case of a ".../*" servlet mapping in web.xml).
358 * Default is "false".
360 * @see org.springframework.web.util.UrlPathHelper#setAlwaysUseFullPath
362 public ProxyHttpRequestHandler setAlwaysUseFullPath(boolean alwaysUseFullPath)
364 this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath);
369 * Set if context path and request URI should be URL-decoded.
370 * Both are returned <i>undecoded</i> by the Servlet API,
371 * in contrast to the servlet path.
373 * Uses either the request encoding or the default encoding according
374 * to the Servlet spec (ISO-8859-1).
376 * @see org.springframework.web.util.UrlPathHelper#setUrlDecode
378 public ProxyHttpRequestHandler setUrlDecode(boolean urlDecode)
380 this.urlPathHelper.setUrlDecode(urlDecode);
385 * Set if ";" (semicolon) content should be stripped from the request URI.
388 * org.springframework.web.util.UrlPathHelper#setRemoveSemicolonContent(boolean)
390 public ProxyHttpRequestHandler setRemoveSemicolonContent(boolean removeSemicolonContent)
392 this.urlPathHelper.setRemoveSemicolonContent(removeSemicolonContent);
397 * Set the UrlPathHelper to use for the resolution of lookup paths.
399 * Use this to override the default UrlPathHelper with a custom subclass,
400 * or to share common UrlPathHelper settings across multiple
401 * MethodNameResolvers
402 * and HandlerMappings.
405 * org.springframework.web.servlet.handler.AbstractUrlHandlerMapping#setUrlPathHelper
407 public ProxyHttpRequestHandler setUrlPathHelper(UrlPathHelper urlPathHelper)
409 Assert.notNull(urlPathHelper, "UrlPathHelper must not be null");
410 this.urlPathHelper = urlPathHelper;