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)).value()) && !isUserAgentBlacklisted(requestHeaders))
164 {
165 String scheme = requestHeaders.get(HTTPSPDYHeader.SCHEME.name(version)).value();
166 String host = requestHeaders.get(HTTPSPDYHeader.HOST.name(version)).value();
167 String origin = scheme + "://" + host;
168 String url = requestHeaders.get(HTTPSPDYHeader.URI.name(version)).value();
169 String absoluteURL = origin + url;
170 LOG.debug("Applying push strategy for {}", absoluteURL);
171 if (isMainResource(url, responseHeaders))
172 {
173 MainResource mainResource = getOrCreateMainResource(absoluteURL);
174 result = mainResource.getResources();
175 }
176 else if (isPushResource(url, responseHeaders))
177 {
178 Fields.Field referrerHeader = requestHeaders.get("referer");
179 if (referrerHeader != null)
180 {
181 String referrer = referrerHeader.value();
182 MainResource mainResource = mainResources.get(referrer);
183 if (mainResource == null)
184 mainResource = getOrCreateMainResource(referrer);
185
186 Set<String> pushResources = mainResource.getResources();
187 if (!pushResources.contains(url))
188 mainResource.addResource(url, origin, referrer);
189 else
190 result = getPushResources(absoluteURL);
191 }
192 }
193 LOG.debug("Pushing {} resources for {}: {}", result.size(), absoluteURL, result);
194 }
195 return result;
196 }
197
198 private Set<String> getPushResources(String absoluteURL)
199 {
200 Set<String> result = Collections.emptySet();
201 if (mainResources.get(absoluteURL) != null)
202 result = mainResources.get(absoluteURL).getResources();
203 return result;
204 }
205
206 private MainResource getOrCreateMainResource(String absoluteURL)
207 {
208 MainResource mainResource = mainResources.get(absoluteURL);
209 if (mainResource == null)
210 {
211 LOG.debug("Creating new main resource for {}", absoluteURL);
212 MainResource value = new MainResource(absoluteURL);
213 mainResource = mainResources.putIfAbsent(absoluteURL, value);
214 if (mainResource == null)
215 mainResource = value;
216 }
217 return mainResource;
218 }
219
220 private boolean isIfModifiedSinceHeaderPresent(Fields headers)
221 {
222 return headers.get("if-modified-since") != null;
223 }
224
225 private boolean isValidMethod(String method)
226 {
227 return "GET".equalsIgnoreCase(method);
228 }
229
230 private boolean isMainResource(String url, Fields responseHeaders)
231 {
232 return !isPushResource(url, responseHeaders);
233 }
234
235 public boolean isUserAgentBlacklisted(Fields headers)
236 {
237 Fields.Field userAgentHeader = headers.get("user-agent");
238 if (userAgentHeader != null)
239 for (Pattern userAgentPattern : userAgentBlacklist)
240 if (userAgentPattern.matcher(userAgentHeader.value()).matches())
241 return true;
242 return false;
243 }
244
245 private boolean isPushResource(String url, Fields responseHeaders)
246 {
247 for (Pattern pushRegexp : pushRegexps)
248 {
249 if (pushRegexp.matcher(url).matches())
250 {
251 Fields.Field header = responseHeaders.get("content-type");
252 if (header == null)
253 return true;
254
255 String contentType = header.value().toLowerCase(Locale.ENGLISH);
256 for (String pushContentType : pushContentTypes)
257 if (contentType.startsWith(pushContentType))
258 return true;
259 }
260 }
261 return false;
262 }
263
264 private class MainResource
265 {
266 private final String name;
267 private final CopyOnWriteArraySet<String> resources = new CopyOnWriteArraySet<>();
268 private final AtomicLong firstResourceAdded = new AtomicLong(-1);
269
270 private MainResource(String name)
271 {
272 this.name = name;
273 }
274
275 public boolean addResource(String url, String origin, String referrer)
276 {
277
278
279
280
281 firstResourceAdded.compareAndSet(-1, System.nanoTime());
282
283 long delay = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - firstResourceAdded.get());
284 if (!referrer.startsWith(origin) && !isPushOriginAllowed(origin))
285 {
286 LOG.debug("Skipped store of push metadata {} for {}: Origin: {} doesn't match or origin not allowed",
287 url, name, origin);
288 return false;
289 }
290
291
292
293
294 if (resources.size() >= maxAssociatedResources)
295 {
296 LOG.debug("Skipped store of push metadata {} for {}: max associated resources ({}) reached",
297 url, name, maxAssociatedResources);
298 return false;
299 }
300 if (delay > referrerPushPeriod)
301 {
302 LOG.debug("Delay: {}ms longer than referrerPushPeriod ({}ms). Not adding resource: {} for: {}", delay,
303 referrerPushPeriod, url, name);
304 return false;
305 }
306
307 LOG.debug("Adding: {} to: {} with delay: {}ms.", url, this, delay);
308 resources.add(url);
309 return true;
310 }
311
312 public Set<String> getResources()
313 {
314 return Collections.unmodifiableSet(resources);
315 }
316
317 public String toString()
318 {
319 return String.format("%s@%x{name=%s,resources=%s}",
320 getClass().getSimpleName(),
321 hashCode(),
322 name,
323 resources
324 );
325 }
326
327 private boolean isPushOriginAllowed(String origin)
328 {
329 for (Pattern allowedPushOrigin : allowedPushOrigins)
330 if (allowedPushOrigin.matcher(origin).matches())
331 return true;
332 return false;
333 }
334 }
335 }