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