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 if (referrer != null)
161 {
162 HttpURI referrerURI = new HttpURI(referrer);
163 String host = referrerURI.getHost();
164 int port = referrerURI.getPort();
165 if (port <= 0)
166 port = request.isSecure() ? 443 : 80;
167
168 boolean referredFromHere = _hosts.size() > 0 ? _hosts.contains(host) : host.equals(request.getServerName());
169 referredFromHere &= _ports.size() > 0 ? _ports.contains(port) : port == request.getServerPort();
170
171 if (referredFromHere)
172 {
173 if ("GET".equalsIgnoreCase(request.getMethod()))
174 {
175 String referrerPath = referrerURI.getPath();
176 if (referrerPath == null)
177 referrerPath = "/";
178 if (referrerPath.startsWith(request.getContextPath()))
179 {
180 String referrerPathNoContext = referrerPath.substring(request.getContextPath().length());
181 if (!referrerPathNoContext.equals(path))
182 {
183 PrimaryResource primaryResource = _cache.get(referrerPathNoContext);
184 if (primaryResource != null)
185 {
186 long primaryTimestamp = primaryResource._timestamp.get();
187 if (primaryTimestamp != 0)
188 {
189 RequestDispatcher dispatcher = request.getServletContext().getRequestDispatcher(path);
190 if (now - primaryTimestamp < TimeUnit.MILLISECONDS.toNanos(_associatePeriod))
191 {
192 ConcurrentMap<String, RequestDispatcher> associated = primaryResource._associated;
193
194 if (associated.size() <= _maxAssociations)
195 {
196 if (associated.putIfAbsent(path, dispatcher) == null)
197 {
198 if (LOG.isDebugEnabled())
199 LOG.debug("Associated {} to {}", path, referrerPathNoContext);
200 }
201 }
202 else
203 {
204 if (LOG.isDebugEnabled())
205 LOG.debug("Not associated {} to {}, exceeded max associations of {}", path, referrerPathNoContext, _maxAssociations);
206 }
207 }
208 else
209 {
210 if (LOG.isDebugEnabled())
211 LOG.debug("Not associated {} to {}, outside associate period of {}ms", path, referrerPathNoContext, _associatePeriod);
212 }
213 }
214 }
215 }
216 else
217 {
218 if (LOG.isDebugEnabled())
219 LOG.debug("Not associated {} to {}, referring to self", path, referrerPathNoContext);
220 }
221 }
222 }
223 }
224 else
225 {
226 if (LOG.isDebugEnabled())
227 LOG.debug("External referrer {}", referrer);
228 }
229 }
230
231
232 PrimaryResource primaryResource = _cache.get(path);
233 if (primaryResource == null)
234 {
235 PrimaryResource t = new PrimaryResource();
236 primaryResource = _cache.putIfAbsent(path, t);
237 primaryResource = primaryResource == null ? t : primaryResource;
238 primaryResource._timestamp.compareAndSet(0, now);
239 if (LOG.isDebugEnabled())
240 LOG.debug("Cached primary resource {}", path);
241 }
242 else
243 {
244 long last = primaryResource._timestamp.get();
245 if (last < _renew && primaryResource._timestamp.compareAndSet(last, now))
246 {
247 primaryResource._associated.clear();
248 if (LOG.isDebugEnabled())
249 LOG.debug("Clear associated resources for {}", path);
250 }
251 }
252
253
254 if (!conditional && !primaryResource._associated.isEmpty())
255 {
256 for (RequestDispatcher dispatcher : primaryResource._associated.values())
257 {
258 if (LOG.isDebugEnabled())
259 LOG.debug("Pushing {} for {}", dispatcher, path);
260 ((Dispatcher)dispatcher).push(request);
261 }
262 }
263
264 chain.doFilter(request, resp);
265 }
266
267 @Override
268 public void destroy()
269 {
270 clearPushCache();
271 }
272
273 @ManagedAttribute("The push cache contents")
274 public Map<String, String> getPushCache()
275 {
276 Map<String, String> result = new HashMap<>();
277 for (Map.Entry<String, PrimaryResource> entry : _cache.entrySet())
278 {
279 PrimaryResource resource = entry.getValue();
280 String value = String.format("size=%d: %s", resource._associated.size(), new TreeSet<>(resource._associated.keySet()));
281 result.put(entry.getKey(), value);
282 }
283 return result;
284 }
285
286 @ManagedOperation(value = "Renews the push cache contents", impact = "ACTION")
287 public void renewPushCache()
288 {
289 _renew = System.nanoTime();
290 }
291
292 @ManagedOperation(value = "Clears the push cache contents", impact = "ACTION")
293 public void clearPushCache()
294 {
295 _cache.clear();
296 }
297
298 private static class PrimaryResource
299 {
300 private final ConcurrentMap<String, RequestDispatcher> _associated = new ConcurrentHashMap<>();
301 private final AtomicLong _timestamp = new AtomicLong();
302 }
303 }