View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
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   * <p>A filter that builds a cache of secondary resources associated
60   * to primary resources.</p>
61   * <p>A typical request for a primary resource such as {@code index.html}
62   * is immediately followed by a number of requests for secondary resources.
63   * Secondary resource requests will have a {@code Referer} HTTP header
64   * that points to {@code index.html}, which is used to associate the secondary
65   * resource to the primary resource.</p>
66   * <p>Only secondary resources that are requested within a (small) time period
67   * from the request of the primary resource are associated with the primary
68   * resource.</p>
69   * <p>This allows to build a cache of secondary resources associated with
70   * primary resources. When a request for a primary resource arrives, associated
71   * secondary resources are pushed to the client, unless the request carries
72   * {@code If-xxx} header that hint that the client has the resources in its
73   * cache.</p>
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         // Expose for JMX.
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         // Iterating over fields is more efficient than multiple gets
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                                         // Not strictly concurrent-safe, just best effort to limit associations.
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         // Push some resources?
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         // Push associated for non conditional
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 }