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