View Javadoc

1   /*
2    * Copyright (c) 2012 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.eclipse.jetty.spdy.http;
18  
19  import java.util.Arrays;
20  import java.util.Collections;
21  import java.util.LinkedHashSet;
22  import java.util.List;
23  import java.util.Set;
24  import java.util.concurrent.ConcurrentHashMap;
25  import java.util.concurrent.ConcurrentMap;
26  import java.util.regex.Pattern;
27  
28  import org.eclipse.jetty.spdy.api.Headers;
29  import org.eclipse.jetty.spdy.api.Stream;
30  import org.eclipse.jetty.util.log.Log;
31  import org.eclipse.jetty.util.log.Logger;
32  
33  /**
34   * <p>A SPDY push strategy that auto-populates push metadata based on referrer URLs.</p>
35   * <p>A typical request for a main resource such as <tt>index.html</tt> is immediately
36   * followed by a number of requests for associated resources. Associated resource requests
37   * will have a <tt>Referer</tt> HTTP header that points to <tt>index.html</tt>, which we
38   * use to link the associated resource to the main resource.</p>
39   * <p>However, also following a hyperlink generates a HTTP request with a <tt>Referer</tt>
40   * HTTP header that points to <tt>index.html</tt>; therefore main resources and associated
41   * resources must be distinguishable.</p>
42   * <p>This class distinguishes associated resources by their URL path suffix.
43   * CSS stylesheets, images and JavaScript files have recognizable URL path suffixes that
44   * are classified as associated resources.</p>
45   * <p>Note however, that CSS stylesheets may refer to images, and the CSS image request
46   * will have the CSS stylesheet as referrer, so there is some degree of recursion that
47   * needs to be handled.</p>
48   *
49   * TODO: this class is kind-of leaking since the resources map is always adding entries
50   * TODO: although these entries will be limited by the number of application pages.
51   * TODO: however, there is no ConcurrentLinkedHashMap yet in JDK (there is in Guava though)
52   * TODO: so we cannot use the built-in LRU features of LinkedHashMap
53   *
54   * TODO: Wikipedia maps URLs like http://en.wikipedia.org/wiki/File:PNG-Gradient_hex.png
55   * TODO: to text/html, so perhaps we need to improve isPushResource() by looking at the
56   * TODO: response Content-Type header, and not only at the URL extension
57   */
58  public class ReferrerPushStrategy implements PushStrategy
59  {
60      private static final Logger logger = Log.getLogger(ReferrerPushStrategy.class);
61      private final ConcurrentMap<String, Set<String>> resources = new ConcurrentHashMap<>();
62      private final Set<Pattern> pushRegexps = new LinkedHashSet<>();
63      private final Set<Pattern> allowedPushOrigins = new LinkedHashSet<>();
64  
65      public ReferrerPushStrategy()
66      {
67          this(Arrays.asList(".*\\.css", ".*\\.js", ".*\\.png", ".*\\.jpg", ".*\\.gif"));
68      }
69  
70      public ReferrerPushStrategy(List<String> pushRegexps)
71      {
72          this(pushRegexps, Collections.<String>emptyList());
73      }
74  
75      public ReferrerPushStrategy(List<String> pushRegexps, List<String> allowedPushOrigins)
76      {
77          for (String pushRegexp : pushRegexps)
78              this.pushRegexps.add(Pattern.compile(pushRegexp));
79          for (String allowedPushOrigin : allowedPushOrigins)
80              this.allowedPushOrigins.add(Pattern.compile(allowedPushOrigin.replace(".", "\\.").replace("*", ".*")));
81      }
82  
83      @Override
84      public Set<String> apply(Stream stream, Headers requestHeaders, Headers responseHeaders)
85      {
86          Set<String> result = Collections.emptySet();
87          String scheme = requestHeaders.get("scheme").value();
88          String host = requestHeaders.get("host").value();
89          String origin = new StringBuilder(scheme).append("://").append(host).toString();
90          String url = requestHeaders.get("url").value();
91          String absoluteURL = new StringBuilder(origin).append(url).toString();
92          logger.debug("Applying push strategy for {}", absoluteURL);
93          if (isValidMethod(requestHeaders.get("method").value()))
94          {
95              if (isMainResource(url, responseHeaders))
96              {
97                  result = pushResources(absoluteURL);
98              }
99              else if (isPushResource(url, responseHeaders))
100             {
101                 Headers.Header referrerHeader = requestHeaders.get("referer");
102                 if (referrerHeader != null)
103                 {
104                     String referrer = referrerHeader.value();
105                     Set<String> pushResources = resources.get(referrer);
106                     if (pushResources == null || !pushResources.contains(url))
107                         buildMetadata(origin, url, referrer);
108                     else
109                         result = pushResources(absoluteURL);
110                 }
111             }
112         }
113         logger.debug("Push resources for {}: {}", absoluteURL, result);
114         return result;
115     }
116 
117     private boolean isValidMethod(String method)
118     {
119         return "GET".equalsIgnoreCase(method);
120     }
121 
122     private boolean isMainResource(String url, Headers responseHeaders)
123     {
124         return !isPushResource(url, responseHeaders);
125     }
126 
127     private boolean isPushResource(String url, Headers responseHeaders)
128     {
129         for (Pattern pushRegexp : pushRegexps)
130         {
131             if (pushRegexp.matcher(url).matches())
132                 return true;
133         }
134         return false;
135     }
136 
137     private Set<String> pushResources(String absoluteURL)
138     {
139         Set<String> pushResources = resources.get(absoluteURL);
140         if (pushResources == null)
141             return Collections.emptySet();
142         return Collections.unmodifiableSet(pushResources);
143     }
144 
145     private void buildMetadata(String origin, String url, String referrer)
146     {
147         if (referrer.startsWith(origin) || isPushOriginAllowed(origin))
148         {
149             Set<String> pushResources = resources.get(referrer);
150             if (pushResources == null)
151             {
152                 pushResources = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
153                 Set<String> existing = resources.putIfAbsent(referrer, pushResources);
154                 if (existing != null)
155                     pushResources = existing;
156             }
157             pushResources.add(url);
158             logger.debug("Built push metadata for {}: {}", referrer, pushResources);
159         }
160     }
161 
162     private boolean isPushOriginAllowed(String origin)
163     {
164         for (Pattern allowedPushOrigin : allowedPushOrigins)
165         {
166             if (allowedPushOrigin.matcher(origin).matches())
167                 return true;
168         }
169         return false;
170     }
171 }