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