1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package org.eclipse.jetty.spdy.http;
21
22 import java.util.Arrays;
23 import java.util.Collections;
24 import java.util.HashSet;
25 import java.util.List;
26 import java.util.Locale;
27 import java.util.Set;
28 import java.util.concurrent.ConcurrentHashMap;
29 import java.util.concurrent.ConcurrentMap;
30 import java.util.concurrent.TimeUnit;
31 import java.util.concurrent.atomic.AtomicLong;
32 import java.util.regex.Pattern;
33
34 import org.eclipse.jetty.spdy.api.Headers;
35 import org.eclipse.jetty.spdy.api.Stream;
36 import org.eclipse.jetty.util.log.Log;
37 import org.eclipse.jetty.util.log.Logger;
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62 public class ReferrerPushStrategy implements PushStrategy
63 {
64 private static final Logger logger = Log.getLogger(ReferrerPushStrategy.class);
65 private final ConcurrentMap<String, MainResource> mainResources = new ConcurrentHashMap<>();
66 private final Set<Pattern> pushRegexps = new HashSet<>();
67 private final Set<String> pushContentTypes = new HashSet<>();
68 private final Set<Pattern> allowedPushOrigins = new HashSet<>();
69 private volatile int maxAssociatedResources = 32;
70 private volatile int referrerPushPeriod = 5000;
71
72 public ReferrerPushStrategy()
73 {
74 this(Arrays.asList(".*\\.css", ".*\\.js", ".*\\.png", ".*\\.jpeg", ".*\\.jpg", ".*\\.gif", ".*\\.ico"));
75 }
76
77 public ReferrerPushStrategy(List<String> pushRegexps)
78 {
79 this(pushRegexps, Arrays.asList(
80 "text/css",
81 "text/javascript", "application/javascript", "application/x-javascript",
82 "image/png", "image/x-png",
83 "image/jpeg",
84 "image/gif",
85 "image/x-icon", "image/vnd.microsoft.icon"));
86 }
87
88 public ReferrerPushStrategy(List<String> pushRegexps, List<String> pushContentTypes)
89 {
90 this(pushRegexps, pushContentTypes, Collections.<String>emptyList());
91 }
92
93 public ReferrerPushStrategy(List<String> pushRegexps, List<String> pushContentTypes, List<String> allowedPushOrigins)
94 {
95 for (String pushRegexp : pushRegexps)
96 this.pushRegexps.add(Pattern.compile(pushRegexp));
97 this.pushContentTypes.addAll(pushContentTypes);
98 for (String allowedPushOrigin : allowedPushOrigins)
99 this.allowedPushOrigins.add(Pattern.compile(allowedPushOrigin.replace(".", "\\.").replace("*", ".*")));
100 }
101
102 public int getMaxAssociatedResources()
103 {
104 return maxAssociatedResources;
105 }
106
107 public void setMaxAssociatedResources(int maxAssociatedResources)
108 {
109 this.maxAssociatedResources = maxAssociatedResources;
110 }
111
112 public int getReferrerPushPeriod()
113 {
114 return referrerPushPeriod;
115 }
116
117 public void setReferrerPushPeriod(int referrerPushPeriod)
118 {
119 this.referrerPushPeriod = referrerPushPeriod;
120 }
121
122 @Override
123 public Set<String> apply(Stream stream, Headers requestHeaders, Headers responseHeaders)
124 {
125 Set<String> result = Collections.<String>emptySet();
126 short version = stream.getSession().getVersion();
127 if (!isIfModifiedSinceHeaderPresent(requestHeaders) && isValidMethod(requestHeaders.get(HTTPSPDYHeader.METHOD.name(version)).value()))
128 {
129 String scheme = requestHeaders.get(HTTPSPDYHeader.SCHEME.name(version)).value();
130 String host = requestHeaders.get(HTTPSPDYHeader.HOST.name(version)).value();
131 String origin = scheme + "://" + host;
132 String url = requestHeaders.get(HTTPSPDYHeader.URI.name(version)).value();
133 String absoluteURL = origin + url;
134 logger.debug("Applying push strategy for {}", absoluteURL);
135 if (isMainResource(url, responseHeaders))
136 {
137 MainResource mainResource = getOrCreateMainResource(absoluteURL);
138 result = mainResource.getResources();
139 }
140 else if (isPushResource(url, responseHeaders))
141 {
142 Headers.Header referrerHeader = requestHeaders.get("referer");
143 if (referrerHeader != null)
144 {
145 String referrer = referrerHeader.value();
146 MainResource mainResource = mainResources.get(referrer);
147 if (mainResource == null)
148 mainResource = getOrCreateMainResource(referrer);
149
150 Set<String> pushResources = mainResource.getResources();
151 if (!pushResources.contains(url))
152 mainResource.addResource(url, origin, referrer);
153 else
154 result = getPushResources(absoluteURL);
155 }
156 }
157 logger.debug("Pushing {} resources for {}: {}", result.size(), absoluteURL, result);
158 }
159 return result;
160 }
161
162 private Set<String> getPushResources(String absoluteURL)
163 {
164 Set<String> result = Collections.emptySet();
165 if (mainResources.get(absoluteURL) != null)
166 result = mainResources.get(absoluteURL).getResources();
167 return result;
168 }
169
170 private MainResource getOrCreateMainResource(String absoluteURL)
171 {
172 MainResource mainResource = mainResources.get(absoluteURL);
173 if (mainResource == null)
174 {
175 logger.debug("Creating new main resource for {}", absoluteURL);
176 MainResource value = new MainResource(absoluteURL);
177 mainResource = mainResources.putIfAbsent(absoluteURL, value);
178 if (mainResource == null)
179 mainResource = value;
180 }
181 return mainResource;
182 }
183
184 private boolean isIfModifiedSinceHeaderPresent(Headers headers)
185 {
186 return headers.get("if-modified-since") != null;
187 }
188
189 private boolean isValidMethod(String method)
190 {
191 return "GET".equalsIgnoreCase(method);
192 }
193
194 private boolean isMainResource(String url, Headers responseHeaders)
195 {
196 return !isPushResource(url, responseHeaders);
197 }
198
199 private boolean isPushResource(String url, Headers responseHeaders)
200 {
201 for (Pattern pushRegexp : pushRegexps)
202 {
203 if (pushRegexp.matcher(url).matches())
204 {
205 Headers.Header header = responseHeaders.get("content-type");
206 if (header == null)
207 return true;
208
209 String contentType = header.value().toLowerCase(Locale.ENGLISH);
210 for (String pushContentType : pushContentTypes)
211 if (contentType.startsWith(pushContentType))
212 return true;
213 }
214 }
215 return false;
216 }
217
218 private class MainResource
219 {
220 private final String name;
221 private final Set<String> resources = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
222 private final AtomicLong firstResourceAdded = new AtomicLong(-1);
223
224 private MainResource(String name)
225 {
226 this.name = name;
227 }
228
229 public boolean addResource(String url, String origin, String referrer)
230 {
231
232
233
234
235 firstResourceAdded.compareAndSet(-1, System.nanoTime());
236
237 long delay = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - firstResourceAdded.get());
238 if (!referrer.startsWith(origin) && !isPushOriginAllowed(origin))
239 {
240 logger.debug("Skipped store of push metadata {} for {}: Origin: {} doesn't match or origin not allowed",
241 url, name, origin);
242 return false;
243 }
244
245
246
247
248 if (resources.size() >= maxAssociatedResources)
249 {
250 logger.debug("Skipped store of push metadata {} for {}: max associated resources ({}) reached",
251 url, name, maxAssociatedResources);
252 return false;
253 }
254 if (delay > referrerPushPeriod)
255 {
256 logger.debug("Delay: {}ms longer than referrerPushPeriod: {}ms. Not adding resource: {} for: {}", delay, referrerPushPeriod, url, name);
257 return false;
258 }
259
260 logger.debug("Adding resource: {} for: {} with delay: {}ms.", url, name, delay);
261 resources.add(url);
262 return true;
263 }
264
265 public Set<String> getResources()
266 {
267 return Collections.unmodifiableSet(resources);
268 }
269
270 public String toString()
271 {
272 return "MainResource: " + name + " associated resources:" + resources.size();
273 }
274
275 private boolean isPushOriginAllowed(String origin)
276 {
277 for (Pattern allowedPushOrigin : allowedPushOrigins)
278 {
279 if (allowedPushOrigin.matcher(origin).matches())
280 return true;
281 }
282 return false;
283 }
284 }
285 }