1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package org.eclipse.jetty.servlets;
21
22 import java.io.IOException;
23 import java.util.Collections;
24 import java.util.HashMap;
25 import java.util.HashSet;
26 import java.util.Map;
27 import java.util.Set;
28 import java.util.TreeSet;
29 import java.util.concurrent.ConcurrentHashMap;
30 import java.util.concurrent.ConcurrentMap;
31 import java.util.concurrent.TimeUnit;
32 import java.util.concurrent.atomic.AtomicLong;
33
34 import javax.servlet.Filter;
35 import javax.servlet.FilterChain;
36 import javax.servlet.FilterConfig;
37 import javax.servlet.RequestDispatcher;
38 import javax.servlet.ServletException;
39 import javax.servlet.ServletRequest;
40 import javax.servlet.ServletResponse;
41 import javax.servlet.http.HttpServletRequest;
42
43 import org.eclipse.jetty.http.HttpField;
44 import org.eclipse.jetty.http.HttpFields;
45 import org.eclipse.jetty.http.HttpHeader;
46 import org.eclipse.jetty.http.HttpURI;
47 import org.eclipse.jetty.http.HttpVersion;
48 import org.eclipse.jetty.server.Dispatcher;
49 import org.eclipse.jetty.server.Request;
50 import org.eclipse.jetty.util.StringUtil;
51 import org.eclipse.jetty.util.URIUtil;
52 import org.eclipse.jetty.util.annotation.ManagedAttribute;
53 import org.eclipse.jetty.util.annotation.ManagedObject;
54 import org.eclipse.jetty.util.annotation.ManagedOperation;
55 import org.eclipse.jetty.util.log.Log;
56 import org.eclipse.jetty.util.log.Logger;
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75 @ManagedObject("Push cache based on the HTTP 'Referer' header")
76 public class PushCacheFilter implements Filter
77 {
78 private static final Logger LOG = Log.getLogger(PushCacheFilter.class);
79
80 private final Set<Integer> _ports = new HashSet<>();
81 private final Set<String> _hosts = new HashSet<>();
82 private final ConcurrentMap<String, PrimaryResource> _cache = new ConcurrentHashMap<>();
83 private long _associatePeriod = 4000L;
84 private int _maxAssociations = 16;
85 private long _renew = System.nanoTime();
86
87 @Override
88 public void init(FilterConfig config) throws ServletException
89 {
90 String associatePeriod = config.getInitParameter("associatePeriod");
91 if (associatePeriod != null)
92 _associatePeriod = Long.parseLong(associatePeriod);
93
94 String maxAssociations = config.getInitParameter("maxAssociations");
95 if (maxAssociations != null)
96 _maxAssociations = Integer.parseInt(maxAssociations);
97
98 String hosts = config.getInitParameter("hosts");
99 if (hosts != null)
100 Collections.addAll(_hosts, StringUtil.csvSplit(hosts));
101
102 String ports = config.getInitParameter("ports");
103 if (ports != null)
104 for (String p : StringUtil.csvSplit(ports))
105 _ports.add(Integer.parseInt(p));
106
107
108 config.getServletContext().setAttribute(config.getFilterName(), this);
109
110 if (LOG.isDebugEnabled())
111 LOG.debug("period={} max={} hosts={} ports={}", _associatePeriod, _maxAssociations, _hosts, _ports);
112 }
113
114 @Override
115 public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException
116 {
117 if (HttpVersion.fromString(req.getProtocol()).getVersion() < 20)
118 {
119 chain.doFilter(req, resp);
120 return;
121 }
122
123 long now = System.nanoTime();
124 HttpServletRequest request = (HttpServletRequest)req;
125
126
127 HttpFields fields = Request.getBaseRequest(request).getHttpFields();
128 boolean conditional = false;
129 String referrer = null;
130 loop:
131 for (int i = 0; i < fields.size(); i++)
132 {
133 HttpField field = fields.getField(i);
134 HttpHeader header = field.getHeader();
135 if (header == null)
136 continue;
137
138 switch (header)
139 {
140 case IF_MATCH:
141 case IF_MODIFIED_SINCE:
142 case IF_NONE_MATCH:
143 case IF_UNMODIFIED_SINCE:
144 conditional = true;
145 break loop;
146
147 case REFERER:
148 referrer = field.getValue();
149 break;
150
151 default:
152 break;
153 }
154 }
155
156 if (LOG.isDebugEnabled())
157 LOG.debug("{} {} referrer={} conditional={}", request.getMethod(), request.getRequestURI(), referrer, conditional);
158
159 String path = URIUtil.addPaths(request.getServletPath(), request.getPathInfo());
160 String query = request.getQueryString();
161 if (query != null)
162 path += "?" + query;
163 if (referrer != null)
164 {
165 HttpURI referrerURI = new HttpURI(referrer);
166 String host = referrerURI.getHost();
167 int port = referrerURI.getPort();
168 if (port <= 0)
169 port = request.isSecure() ? 443 : 80;
170
171 boolean referredFromHere = _hosts.size() > 0 ? _hosts.contains(host) : host.equals(request.getServerName());
172 referredFromHere &= _ports.size() > 0 ? _ports.contains(port) : port == request.getServerPort();
173
174 if (referredFromHere)
175 {
176 if ("GET".equalsIgnoreCase(request.getMethod()))
177 {
178 String referrerPath = referrerURI.getPath();
179 if (referrerPath == null)
180 referrerPath = "/";
181 if (referrerPath.startsWith(request.getContextPath()))
182 {
183 String referrerPathNoContext = referrerPath.substring(request.getContextPath().length());
184 if (!referrerPathNoContext.equals(path))
185 {
186 PrimaryResource primaryResource = _cache.get(referrerPathNoContext);
187 if (primaryResource != null)
188 {
189 long primaryTimestamp = primaryResource._timestamp.get();
190 if (primaryTimestamp != 0)
191 {
192 RequestDispatcher dispatcher = request.getServletContext().getRequestDispatcher(path);
193 if (now - primaryTimestamp < TimeUnit.MILLISECONDS.toNanos(_associatePeriod))
194 {
195 ConcurrentMap<String, RequestDispatcher> associated = primaryResource._associated;
196
197 if (associated.size() <= _maxAssociations)
198 {
199 if (associated.putIfAbsent(path, dispatcher) == null)
200 {
201 if (LOG.isDebugEnabled())
202 LOG.debug("Associated {} to {}", path, referrerPathNoContext);
203 }
204 }
205 else
206 {
207 if (LOG.isDebugEnabled())
208 LOG.debug("Not associated {} to {}, exceeded max associations of {}", path, referrerPathNoContext, _maxAssociations);
209 }
210 }
211 else
212 {
213 if (LOG.isDebugEnabled())
214 LOG.debug("Not associated {} to {}, outside associate period of {}ms", path, referrerPathNoContext, _associatePeriod);
215 }
216 }
217 }
218 }
219 else
220 {
221 if (LOG.isDebugEnabled())
222 LOG.debug("Not associated {} to {}, referring to self", path, referrerPathNoContext);
223 }
224 }
225 }
226 }
227 else
228 {
229 if (LOG.isDebugEnabled())
230 LOG.debug("External referrer {}", referrer);
231 }
232 }
233
234
235 PrimaryResource primaryResource = _cache.get(path);
236 if (primaryResource == null)
237 {
238 PrimaryResource t = new PrimaryResource();
239 primaryResource = _cache.putIfAbsent(path, t);
240 primaryResource = primaryResource == null ? t : primaryResource;
241 primaryResource._timestamp.compareAndSet(0, now);
242 if (LOG.isDebugEnabled())
243 LOG.debug("Cached primary resource {}", path);
244 }
245 else
246 {
247 long last = primaryResource._timestamp.get();
248 if (last < _renew && primaryResource._timestamp.compareAndSet(last, now))
249 {
250 primaryResource._associated.clear();
251 if (LOG.isDebugEnabled())
252 LOG.debug("Clear associated resources for {}", path);
253 }
254 }
255
256
257 if (!conditional && !primaryResource._associated.isEmpty())
258 {
259 for (RequestDispatcher dispatcher : primaryResource._associated.values())
260 {
261 if (LOG.isDebugEnabled())
262 LOG.debug("Pushing {} for {}", dispatcher, path);
263 ((Dispatcher)dispatcher).push(request);
264 }
265 }
266
267 chain.doFilter(request, resp);
268 }
269
270 @Override
271 public void destroy()
272 {
273 clearPushCache();
274 }
275
276 @ManagedAttribute("The push cache contents")
277 public Map<String, String> getPushCache()
278 {
279 Map<String, String> result = new HashMap<>();
280 for (Map.Entry<String, PrimaryResource> entry : _cache.entrySet())
281 {
282 PrimaryResource resource = entry.getValue();
283 String value = String.format("size=%d: %s", resource._associated.size(), new TreeSet<>(resource._associated.keySet()));
284 result.put(entry.getKey(), value);
285 }
286 return result;
287 }
288
289 @ManagedOperation(value = "Renews the push cache contents", impact = "ACTION")
290 public void renewPushCache()
291 {
292 _renew = System.nanoTime();
293 }
294
295 @ManagedOperation(value = "Clears the push cache contents", impact = "ACTION")
296 public void clearPushCache()
297 {
298 _cache.clear();
299 }
300
301 private static class PrimaryResource
302 {
303 private final ConcurrentMap<String, RequestDispatcher> _associated = new ConcurrentHashMap<>();
304 private final AtomicLong _timestamp = new AtomicLong();
305 }
306 }