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