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();
105 LOG.debug("requesting: {}", resource);
107 CloseableHttpResponse p_response = null;
110 HttpGet p_request = new HttpGet(resource);
112 p_response = client.execute(p_request);
114 StatusLine status = p_response.getStatusLine();
115 int code = status.getStatusCode();
116 LOG.debug("response: {} - {}", code, status.getReasonPhrase());
118 setCacheControl(computeCacheControl(p_response));
119 prepareResponse(response);
121 /** Copy some headers, if present.. */
122 ProxyHttpRequestHandler.copyHeader(p_response, response, "Date");
123 ProxyHttpRequestHandler.copyHeader(p_response, response, "ETag");
124 ProxyHttpRequestHandler.copyHeader(p_response, response, "Last-Modified");
125 ProxyHttpRequestHandler.copyHeader(p_response, response, "Content-Length");
126 ProxyHttpRequestHandler.copyHeader(p_response, response, "Content-Type");
130 case HttpServletResponse.SC_FOUND:
131 case HttpServletResponse.SC_OK:
132 /** OK. Continue as normal... */
133 response.setStatus(code);
135 HttpEntity entity = p_response.getEntity();
137 StreamUtils.copy(entity.getContent(), response.getOutputStream());
139 /** Release the connection */
140 EntityUtils.consume(entity);
145 case HttpServletResponse.SC_NOT_FOUND:
146 case HttpServletResponse.SC_GONE:
147 /** The resource can not be resolved through this origin */
148 response.sendError(code, status.getReasonPhrase());
151 case HttpServletResponse.SC_MOVED_PERMANENTLY:
154 case HttpServletResponse.SC_SEE_OTHER:
157 case HttpServletResponse.SC_TEMPORARY_REDIRECT:
161 LOG.error("{} -- {}", p_response.getStatusLine(), resource);
162 // TODO: throw sensible exceptions, to communicate resolving-errors
163 throw new RuntimeException(p_response.getStatusLine().toString() + " -- " + resource);
166 catch (IOException e)
168 LOG.error("unexpected error while resolving {}: {}", resource, e.getMessage());
169 // TODO: throw sensible exceptions, to communicate resolving-errors
170 throw new RuntimeException(e);
174 if (p_response != null)
176 EntityUtils.consumeQuietly(p_response.getEntity());
182 public static void copyHeader(
184 HttpServletResponse target,
188 Header header = source.getLastHeader(name);
191 LOG.trace("copy header {}: {}", header.getName(), header.getValue());
192 target.addHeader(header.getName(), header.getValue());
196 public CacheControl computeCacheControl(HttpResponse response)
199 /** Caching is disabled! */
200 return CacheControl.noStore();
203 boolean has_cache_control = false;
204 boolean is_public = false;
205 boolean is_private = false;
206 boolean no_cache = false;
207 boolean no_store = false;
208 boolean no_transform = false;
209 boolean must_revalidate = false;
210 boolean proxy_revalidate = false;
212 Long s_maxage = null;
214 for (Header header : response.getHeaders("Cache-Control"))
216 has_cache_control = true;
217 for (HeaderElement element : header.getElements())
219 switch (element.getName())
236 case "must-revalidate":
237 must_revalidate = true;
239 case "proxy-revalidate":
240 proxy_revalidate = true;
244 max_age = Long.parseLong(element.getValue());
246 catch (NumberFormatException e)
249 "invalid header \"Cache-Control: max-age={}\"",
257 s_maxage = Long.parseLong(element.getValue());
259 catch (NumberFormatException e)
262 "invalid header \"Cache-Control: s-maxage={}\"",
269 "invalid header \"Cache-Control: {}{}\"",
271 element.getValue() == null ? "" : "=" + element.getValue()
277 if (!has_cache_control)
279 Header header = response.getLastHeader("Expires");
281 /** No TTL specified in response-headers: use configured default */
286 OffsetDateTime expires
287 = OffsetDateTime.parse(
289 DateTimeFormatter.RFC_1123_DATE_TIME
292 Duration delta = Duration.between(OffsetDateTime.now(clock), expires);
293 if (delta.isNegative())
296 max_age = delta.getSeconds();
298 catch (DateTimeParseException e)
300 LOG.warn("invalid header \"Expires: {}\"", header.getValue());
302 * No TTL specified in response-headers: assume expired
303 * see: https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.21
309 CacheControl cache_control;
312 cache_control = CacheControl.noStore();
314 cache_control = CacheControl.noCache();
315 else if (max_age != null)
316 cache_control = CacheControl.maxAge(max_age, TimeUnit.SECONDS);
318 cache_control = CacheControl.empty();
321 cache_control.cachePrivate();
323 cache_control.cachePublic();
325 cache_control.noTransform();
327 cache_control.mustRevalidate();
328 if (proxy_revalidate)
329 cache_control.proxyRevalidate();
330 if (s_maxage != null)
331 cache_control.sMaxAge(s_maxage, TimeUnit.SECONDS);
333 return cache_control;
337 public ProxyHttpRequestHandler setClient(CloseableHttpClient client)
339 this.client = client;
343 public ProxyHttpRequestHandler setOrigin(String origin)
345 if (origin.endsWith("/"))
346 this.origin = origin;
348 this.origin = origin + "/";
352 public ProxyHttpRequestHandler setTtl(Long ttl)
359 * Set if URL lookup should always use full path within current servlet
360 * context. Else, the path within the current servlet mapping is used
361 * if applicable (i.e. in the case of a ".../*" servlet mapping in web.xml).
362 * Default is "false".
364 * @see org.springframework.web.util.UrlPathHelper#setAlwaysUseFullPath
366 public ProxyHttpRequestHandler setAlwaysUseFullPath(boolean alwaysUseFullPath)
368 this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath);
373 * Set if context path and request URI should be URL-decoded.
374 * Both are returned <i>undecoded</i> by the Servlet API,
375 * in contrast to the servlet path.
377 * Uses either the request encoding or the default encoding according
378 * to the Servlet spec (ISO-8859-1).
380 * @see org.springframework.web.util.UrlPathHelper#setUrlDecode
382 public ProxyHttpRequestHandler setUrlDecode(boolean urlDecode)
384 this.urlPathHelper.setUrlDecode(urlDecode);
389 * Set if ";" (semicolon) content should be stripped from the request URI.
392 * org.springframework.web.util.UrlPathHelper#setRemoveSemicolonContent(boolean)
394 public ProxyHttpRequestHandler setRemoveSemicolonContent(boolean removeSemicolonContent)
396 this.urlPathHelper.setRemoveSemicolonContent(removeSemicolonContent);
401 * Set the UrlPathHelper to use for the resolution of lookup paths.
403 * Use this to override the default UrlPathHelper with a custom subclass,
404 * or to share common UrlPathHelper settings across multiple
405 * MethodNameResolvers
406 * and HandlerMappings.
409 * org.springframework.web.servlet.handler.AbstractUrlHandlerMapping#setUrlPathHelper
411 public ProxyHttpRequestHandler setUrlPathHelper(UrlPathHelper urlPathHelper)
413 Assert.notNull(urlPathHelper, "UrlPathHelper must not be null");
414 this.urlPathHelper = urlPathHelper;