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