| // |
| // ======================================================================== |
| // Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd. |
| // ------------------------------------------------------------------------ |
| // All rights reserved. This program and the accompanying materials |
| // are made available under the terms of the Eclipse Public License v1.0 |
| // and Apache License v2.0 which accompanies this distribution. |
| // |
| // The Eclipse Public License is available at |
| // http://www.eclipse.org/legal/epl-v10.html |
| // |
| // The Apache License v2.0 is available at |
| // http://www.opensource.org/licenses/apache2.0.php |
| // |
| // You may elect to redistribute this code under either of these licenses. |
| // ======================================================================== |
| // |
| |
| |
| package org.eclipse.jetty.spdy.server.http; |
| |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Set; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.ConcurrentMap; |
| import java.util.concurrent.CopyOnWriteArraySet; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.atomic.AtomicLong; |
| import java.util.regex.Pattern; |
| |
| import org.eclipse.jetty.spdy.api.Stream; |
| import org.eclipse.jetty.spdy.http.HTTPSPDYHeader; |
| import org.eclipse.jetty.util.Fields; |
| import org.eclipse.jetty.util.log.Log; |
| import org.eclipse.jetty.util.log.Logger; |
| |
| /** |
| * <p>A SPDY push strategy that auto-populates push metadata based on referrer URLs.<p>A typical request for a main |
| * resource such as {@code index.html} is immediately followed by a number of requests for associated resources. |
| * Associated resource requests will have a {@code Referer} HTTP header that points to {@code index.html}, which is used |
| * to link the associated resource to the main resource.<p>However, also following a hyperlink generates a HTTP request |
| * with a {@code Referer} HTTP header that points to {@code index.html}; therefore a proper value for {@link |
| * #setReferrerPushPeriod(int)} has to be set. If the referrerPushPeriod for a main resource has elapsed, no more |
| * associated resources will be added for that main resource.<p>This class distinguishes associated main resources by |
| * their URL path suffix and content type. CSS stylesheets, images and JavaScript files have recognizable URL path |
| * suffixes that are classified as associated resources. The suffix regexs can be configured by constructor argument</p> |
| * <p>When CSS stylesheets refer to images, the CSS image request will have the CSS stylesheet as referrer. This |
| * implementation will push also the CSS image.<p>The push metadata built by this implementation is limited by the |
| * number of pages of the application itself, and by the {@link #setMaxAssociatedResources(int)} max associated |
| * resources} parameter. This parameter limits the number of associated resources per each main resource, so that if a |
| * main resource has hundreds of associated resources, only up to the number specified by this parameter will be |
| * pushed. |
| */ |
| public class ReferrerPushStrategy implements PushStrategy |
| { |
| private static final Logger LOG = Log.getLogger(ReferrerPushStrategy.class); |
| private final ConcurrentMap<String, MainResource> mainResources = new ConcurrentHashMap<>(); |
| private final Set<Pattern> pushRegexps = new HashSet<>(); |
| private final Set<String> pushContentTypes = new HashSet<>(); |
| private final Set<Pattern> allowedPushOrigins = new HashSet<>(); |
| private final Set<Pattern> userAgentBlacklist = new HashSet<>(); |
| private volatile int maxAssociatedResources = 32; |
| private volatile int referrerPushPeriod = 5000; |
| |
| public ReferrerPushStrategy() |
| { |
| List<String> defaultPushRegexps = Arrays.asList(".*\\.css", ".*\\.js", ".*\\.png", ".*\\.jpeg", ".*\\.jpg", |
| ".*\\.gif", ".*\\.ico"); |
| addPushRegexps(defaultPushRegexps); |
| |
| List<String> defaultPushContentTypes = Arrays.asList( |
| "text/css", |
| "text/javascript", "application/javascript", "application/x-javascript", |
| "image/png", "image/x-png", |
| "image/jpeg", |
| "image/gif", |
| "image/x-icon", "image/vnd.microsoft.icon"); |
| this.pushContentTypes.addAll(defaultPushContentTypes); |
| } |
| |
| public void setPushRegexps(List<String> pushRegexps) |
| { |
| pushRegexps.clear(); |
| addPushRegexps(pushRegexps); |
| } |
| |
| private void addPushRegexps(List<String> pushRegexps) |
| { |
| for (String pushRegexp : pushRegexps) |
| this.pushRegexps.add(Pattern.compile(pushRegexp)); |
| } |
| |
| public void setPushContentTypes(List<String> pushContentTypes) |
| { |
| pushContentTypes.clear(); |
| pushContentTypes.addAll(pushContentTypes); |
| } |
| |
| public void setAllowedPushOrigins(List<String> allowedPushOrigins) |
| { |
| allowedPushOrigins.clear(); |
| for (String allowedPushOrigin : allowedPushOrigins) |
| this.allowedPushOrigins.add(Pattern.compile(allowedPushOrigin.replace(".", "\\.").replace("*", ".*"))); |
| } |
| |
| public void setUserAgentBlacklist(List<String> userAgentPatterns) |
| { |
| userAgentBlacklist.clear(); |
| for (String userAgentPattern : userAgentPatterns) |
| userAgentBlacklist.add(Pattern.compile(userAgentPattern)); |
| } |
| |
| public void setMaxAssociatedResources(int maxAssociatedResources) |
| { |
| this.maxAssociatedResources = maxAssociatedResources; |
| } |
| |
| public void setReferrerPushPeriod(int referrerPushPeriod) |
| { |
| this.referrerPushPeriod = referrerPushPeriod; |
| } |
| |
| public Set<Pattern> getPushRegexps() |
| { |
| return pushRegexps; |
| } |
| |
| public Set<String> getPushContentTypes() |
| { |
| return pushContentTypes; |
| } |
| |
| public Set<Pattern> getAllowedPushOrigins() |
| { |
| return allowedPushOrigins; |
| } |
| |
| public Set<Pattern> getUserAgentBlacklist() |
| { |
| return userAgentBlacklist; |
| } |
| |
| public int getMaxAssociatedResources() |
| { |
| return maxAssociatedResources; |
| } |
| |
| public int getReferrerPushPeriod() |
| { |
| return referrerPushPeriod; |
| } |
| |
| @Override |
| public Set<String> apply(Stream stream, Fields requestHeaders, Fields responseHeaders) |
| { |
| Set<String> result = Collections.<String>emptySet(); |
| short version = stream.getSession().getVersion(); |
| if (!isIfModifiedSinceHeaderPresent(requestHeaders) && isValidMethod(requestHeaders.get(HTTPSPDYHeader.METHOD |
| .name(version)).getValue()) && !isUserAgentBlacklisted(requestHeaders)) |
| { |
| String scheme = requestHeaders.get(HTTPSPDYHeader.SCHEME.name(version)).getValue(); |
| String host = requestHeaders.get(HTTPSPDYHeader.HOST.name(version)).getValue(); |
| String origin = scheme + "://" + host; |
| String url = requestHeaders.get(HTTPSPDYHeader.URI.name(version)).getValue(); |
| String absoluteURL = origin + url; |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Applying push strategy for {}", absoluteURL); |
| if (isMainResource(url, responseHeaders)) |
| { |
| MainResource mainResource = getOrCreateMainResource(absoluteURL); |
| result = mainResource.getResources(); |
| } |
| else if (isPushResource(url, responseHeaders)) |
| { |
| Fields.Field referrerHeader = requestHeaders.get("referer"); |
| if (referrerHeader != null) |
| { |
| String referrer = referrerHeader.getValue(); |
| MainResource mainResource = mainResources.get(referrer); |
| if (mainResource == null) |
| mainResource = getOrCreateMainResource(referrer); |
| |
| Set<String> pushResources = mainResource.getResources(); |
| if (!pushResources.contains(url)) |
| mainResource.addResource(url, origin, referrer); |
| else |
| result = getPushResources(absoluteURL); |
| } |
| } |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Pushing {} resources for {}: {}", result.size(), absoluteURL, result); |
| } |
| return result; |
| } |
| |
| private Set<String> getPushResources(String absoluteURL) |
| { |
| Set<String> result = Collections.emptySet(); |
| if (mainResources.get(absoluteURL) != null) |
| result = mainResources.get(absoluteURL).getResources(); |
| return result; |
| } |
| |
| private MainResource getOrCreateMainResource(String absoluteURL) |
| { |
| MainResource mainResource = mainResources.get(absoluteURL); |
| if (mainResource == null) |
| { |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Creating new main resource for {}", absoluteURL); |
| MainResource value = new MainResource(absoluteURL); |
| mainResource = mainResources.putIfAbsent(absoluteURL, value); |
| if (mainResource == null) |
| mainResource = value; |
| } |
| return mainResource; |
| } |
| |
| private boolean isIfModifiedSinceHeaderPresent(Fields headers) |
| { |
| return headers.get("if-modified-since") != null; |
| } |
| |
| private boolean isValidMethod(String method) |
| { |
| return "GET".equalsIgnoreCase(method); |
| } |
| |
| private boolean isMainResource(String url, Fields responseHeaders) |
| { |
| return !isPushResource(url, responseHeaders); |
| } |
| |
| public boolean isUserAgentBlacklisted(Fields headers) |
| { |
| Fields.Field userAgentHeader = headers.get("user-agent"); |
| if (userAgentHeader != null) |
| for (Pattern userAgentPattern : userAgentBlacklist) |
| if (userAgentPattern.matcher(userAgentHeader.getValue()).matches()) |
| return true; |
| return false; |
| } |
| |
| private boolean isPushResource(String url, Fields responseHeaders) |
| { |
| for (Pattern pushRegexp : pushRegexps) |
| { |
| if (pushRegexp.matcher(url).matches()) |
| { |
| Fields.Field header = responseHeaders.get("content-type"); |
| if (header == null) |
| return true; |
| |
| String contentType = header.getValue().toLowerCase(Locale.ENGLISH); |
| for (String pushContentType : pushContentTypes) |
| if (contentType.startsWith(pushContentType)) |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private class MainResource |
| { |
| private final String name; |
| private final CopyOnWriteArraySet<String> resources = new CopyOnWriteArraySet<>(); |
| private final AtomicLong firstResourceAdded = new AtomicLong(-1); |
| |
| private MainResource(String name) |
| { |
| this.name = name; |
| } |
| |
| public boolean addResource(String url, String origin, String referrer) |
| { |
| // We start the push period here and not when initializing the main resource, because a browser with a |
| // prefilled cache won't request the subresources. If the browser with warmed up cache now hits the main |
| // resource after a server restart, the push period shouldn't start until the first subresource is |
| // being requested. |
| firstResourceAdded.compareAndSet(-1, System.nanoTime()); |
| |
| long delay = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - firstResourceAdded.get()); |
| if (!referrer.startsWith(origin) && !isPushOriginAllowed(origin)) |
| { |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Skipped store of push metadata {} for {}: Origin: {} doesn't match or origin not allowed", |
| url, name, origin); |
| return false; |
| } |
| |
| // This check is not strictly concurrent-safe, but limiting |
| // the number of associated resources is achieved anyway |
| // although in rare cases few more resources will be stored |
| if (resources.size() >= maxAssociatedResources) |
| { |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Skipped store of push metadata {} for {}: max associated resources ({}) reached", |
| url, name, maxAssociatedResources); |
| return false; |
| } |
| if (delay > referrerPushPeriod) |
| { |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Delay: {}ms longer than referrerPushPeriod ({}ms). Not adding resource: {} for: {}", |
| delay, referrerPushPeriod, url, name); |
| return false; |
| } |
| |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Adding: {} to: {} with delay: {}ms.", url, this, delay); |
| resources.add(url); |
| return true; |
| } |
| |
| public Set<String> getResources() |
| { |
| return Collections.unmodifiableSet(resources); |
| } |
| |
| public String toString() |
| { |
| return String.format("%s@%x{name=%s,resources=%s}", |
| getClass().getSimpleName(), |
| hashCode(), |
| name, |
| resources |
| ); |
| } |
| |
| private boolean isPushOriginAllowed(String origin) |
| { |
| for (Pattern allowedPushOrigin : allowedPushOrigins) |
| if (allowedPushOrigin.matcher(origin).matches()) |
| return true; |
| return false; |
| } |
| } |
| } |