diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/CrossOriginFilter.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/CrossOriginFilter.java index bb57608c4b4..eb1b9859959 100644 --- a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/CrossOriginFilter.java +++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/CrossOriginFilter.java @@ -54,15 +54,18 @@ import org.eclipse.jetty.util.log.Logger; * and any 3 letter top-level domain (.com, .net, .org, etc.). *
  • allowedMethods, a comma separated list of HTTP methods that * are allowed to be used when accessing the resources. Default value is - * GET,POST
  • + * GET,POST,HEAD *
  • allowedHeaders, a comma separated list of HTTP headers that * are allowed to be specified when accessing the resources. Default value - * is X-Requested-With
  • + * is X-Requested-With,Content-Type,Accept,Origin *
  • preflightMaxAge, the number of seconds that preflight requests * can be cached by the client. Default value is 1800 seconds, or 30 * minutes
  • *
  • allowCredentials, a boolean indicating if the resource allows * requests with credentials. Default value is false
  • + *
  • exposeHeaders, a comma separated list of HTTP headers that + * are allowed to be exposed on the client. Default value is the + * empty list
  • *

    *

    A typical configuration could be: *

    @@ -79,8 +82,6 @@ import org.eclipse.jetty.util.log.Logger;
      *     ...
      * </web-app>
      * 

    - * - * @version $Revision$ $Date$ */ public class CrossOriginFilter implements Filter { @@ -96,12 +97,14 @@ public class CrossOriginFilter implements Filter public static final String ACCESS_CONTROL_ALLOW_HEADERS_HEADER = "Access-Control-Allow-Headers"; public static final String ACCESS_CONTROL_MAX_AGE_HEADER = "Access-Control-Max-Age"; public static final String ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER = "Access-Control-Allow-Credentials"; + public static final String ACCESS_CONTROL_EXPOSE_HEADERS_HEADER = "Access-Control-Expose-Headers"; // Implementation constants public static final String ALLOWED_ORIGINS_PARAM = "allowedOrigins"; public static final String ALLOWED_METHODS_PARAM = "allowedMethods"; public static final String ALLOWED_HEADERS_PARAM = "allowedHeaders"; public static final String PREFLIGHT_MAX_AGE_PARAM = "preflightMaxAge"; public static final String ALLOW_CREDENTIALS_PARAM = "allowCredentials"; + public static final String EXPOSED_HEADERS_PARAM = "exposedHeaders"; private static final String ANY_ORIGIN = "*"; private static final List SIMPLE_HTTP_METHODS = Arrays.asList("GET", "POST", "HEAD"); @@ -109,6 +112,7 @@ public class CrossOriginFilter implements Filter private List allowedOrigins = new ArrayList(); private List allowedMethods = new ArrayList(); private List allowedHeaders = new ArrayList(); + private List exposedHeaders = new ArrayList(); private int preflightMaxAge = 0; private boolean allowCredentials; @@ -163,6 +167,11 @@ public class CrossOriginFilter implements Filter allowedCredentialsConfig = "true"; allowCredentials = Boolean.parseBoolean(allowedCredentialsConfig); + String exposedHeadersConfig = config.getInitParameter(EXPOSED_HEADERS_PARAM); + if (exposedHeadersConfig == null) + exposedHeadersConfig = ""; + exposedHeaders.addAll(Arrays.asList(exposedHeadersConfig.split(","))); + if (LOG.isDebugEnabled()) { LOG.debug("Cross-origin filter configuration: " + @@ -170,7 +179,9 @@ public class CrossOriginFilter implements Filter ALLOWED_METHODS_PARAM + " = " + allowedMethodsConfig + ", " + ALLOWED_HEADERS_PARAM + " = " + allowedHeadersConfig + ", " + PREFLIGHT_MAX_AGE_PARAM + " = " + preflightMaxAgeConfig + ", " + - ALLOW_CREDENTIALS_PARAM + " = " + allowedCredentialsConfig); + ALLOW_CREDENTIALS_PARAM + " = " + allowedCredentialsConfig + "," + + EXPOSED_HEADERS_PARAM + " = " + exposedHeadersConfig + ); } } @@ -305,6 +316,8 @@ public class CrossOriginFilter implements Filter response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, origin); if (allowCredentials) response.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER, "true"); + if (!exposedHeaders.isEmpty()) + response.setHeader(ACCESS_CONTROL_EXPOSE_HEADERS_HEADER, commify(exposedHeaders)); } private void handlePreflightResponse(HttpServletRequest request, HttpServletResponse response, String origin) diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/CrossOriginFilterTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/CrossOriginFilterTest.java index fb8d6bba490..bd631ebf37c 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/CrossOriginFilterTest.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/CrossOriginFilterTest.java @@ -371,6 +371,27 @@ public class CrossOriginFilterTest Assert.assertTrue(latch.await(1, TimeUnit.SECONDS)); } + @Test + public void testSimpleRequestWithExposedHeaders() throws Exception + { + FilterHolder filterHolder = new FilterHolder(new CrossOriginFilter()); + filterHolder.setInitParameter("exposedHeaders", "Content-Length"); + tester.getContext().addFilter(filterHolder, "/*", FilterMapping.DEFAULT); + + CountDownLatch latch = new CountDownLatch(1); + tester.getContext().addServlet(new ServletHolder(new ResourceServlet(latch)), "/*"); + + String request = "" + + "GET / HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Origin: http://localhost\r\n" + + "\r\n"; + String response = tester.getResponses(request); + Assert.assertTrue(response.contains("HTTP/1.1 200")); + Assert.assertTrue(response.contains(CrossOriginFilter.ACCESS_CONTROL_EXPOSE_HEADERS_HEADER)); + Assert.assertTrue(latch.await(1, TimeUnit.SECONDS)); + } + public static class ResourceServlet extends HttpServlet { private static final long serialVersionUID = 1L; diff --git a/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/ReferrerPushStrategy.java b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/ReferrerPushStrategy.java index cf3fe06f1b6..52f1243d734 100644 --- a/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/ReferrerPushStrategy.java +++ b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/ReferrerPushStrategy.java @@ -18,7 +18,7 @@ package org.eclipse.jetty.spdy.http; import java.util.Arrays; import java.util.Collections; -import java.util.LinkedHashSet; +import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -39,58 +39,80 @@ import org.eclipse.jetty.util.log.Logger; *

    However, also following a hyperlink generates a HTTP request with a Referer * HTTP header that points to index.html; therefore main resources and associated * resources must be distinguishable.

    - *

    This class distinguishes associated resources by their URL path suffix. + *

    This class distinguishes associated 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.

    - *

    Note however, that CSS stylesheets may refer to images, and the CSS image request - * will have the CSS stylesheet as referrer, so there is some degree of recursion that - * needs to be handled.

    - * - * TODO: this class is kind-of leaking since the resources map is always adding entries - * TODO: although these entries will be limited by the number of application pages. - * TODO: however, there is no ConcurrentLinkedHashMap yet in JDK (there is in Guava though) - * TODO: so we cannot use the built-in LRU features of LinkedHashMap - * - * TODO: Wikipedia maps URLs like http://en.wikipedia.org/wiki/File:PNG-Gradient_hex.png - * TODO: to text/html, so perhaps we need to improve isPushResource() by looking at the - * TODO: response Content-Type header, and not only at the URL extension + *

    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.

    + *

    The push metadata built by this implementation is limited by the number of pages + * of the application itself, and by the + * {@link #getMaxAssociatedResources() 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 logger = Log.getLogger(ReferrerPushStrategy.class); private final ConcurrentMap> resources = new ConcurrentHashMap<>(); - private final Set pushRegexps = new LinkedHashSet<>(); - private final Set allowedPushOrigins = new LinkedHashSet<>(); + private final Set pushRegexps = new HashSet<>(); + private final Set pushContentTypes = new HashSet<>(); + private final Set allowedPushOrigins = new HashSet<>(); + private volatile int maxAssociatedResources = 32; public ReferrerPushStrategy() { - this(Arrays.asList(".*\\.css", ".*\\.js", ".*\\.png", ".*\\.jpg", ".*\\.gif")); + this(Arrays.asList(".*\\.css", ".*\\.js", ".*\\.png", ".*\\.jpeg", ".*\\.jpg", ".*\\.gif", ".*\\.ico")); } public ReferrerPushStrategy(List pushRegexps) { - this(pushRegexps, Collections.emptyList()); + this(pushRegexps, 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")); } - public ReferrerPushStrategy(List pushRegexps, List allowedPushOrigins) + public ReferrerPushStrategy(List pushRegexps, List pushContentTypes) + { + this(pushRegexps, pushContentTypes, Collections.emptyList()); + } + + public ReferrerPushStrategy(List pushRegexps, List pushContentTypes, List allowedPushOrigins) { for (String pushRegexp : pushRegexps) this.pushRegexps.add(Pattern.compile(pushRegexp)); + this.pushContentTypes.addAll(pushContentTypes); for (String allowedPushOrigin : allowedPushOrigins) this.allowedPushOrigins.add(Pattern.compile(allowedPushOrigin.replace(".", "\\.").replace("*", ".*"))); } + public int getMaxAssociatedResources() + { + return maxAssociatedResources; + } + + public void setMaxAssociatedResources(int maxAssociatedResources) + { + this.maxAssociatedResources = maxAssociatedResources; + } + @Override public Set apply(Stream stream, Headers requestHeaders, Headers responseHeaders) { Set result = Collections.emptySet(); - String scheme = requestHeaders.get("scheme").value(); - String host = requestHeaders.get("host").value(); + short version = stream.getSession().getVersion(); + String scheme = requestHeaders.get(HTTPSPDYHeader.SCHEME.name(version)).value(); + String host = requestHeaders.get(HTTPSPDYHeader.HOST.name(version)).value(); String origin = new StringBuilder(scheme).append("://").append(host).toString(); - String url = requestHeaders.get("url").value(); + String url = requestHeaders.get(HTTPSPDYHeader.URI.name(version)).value(); String absoluteURL = new StringBuilder(origin).append(url).toString(); logger.debug("Applying push strategy for {}", absoluteURL); - if (isValidMethod(requestHeaders.get("method").value())) + if (isValidMethod(requestHeaders.get(HTTPSPDYHeader.METHOD.name(version)).value())) { if (isMainResource(url, responseHeaders)) { @@ -129,7 +151,16 @@ public class ReferrerPushStrategy implements PushStrategy for (Pattern pushRegexp : pushRegexps) { if (pushRegexp.matcher(url).matches()) - return true; + { + Headers.Header header = responseHeaders.get("content-type"); + if (header == null) + return true; + + String contentType = header.value().toLowerCase(); + for (String pushContentType : pushContentTypes) + if (contentType.startsWith(pushContentType)) + return true; + } } return false; } @@ -154,8 +185,19 @@ public class ReferrerPushStrategy implements PushStrategy if (existing != null) pushResources = existing; } - pushResources.add(url); - logger.debug("Built push metadata for {}: {}", referrer, pushResources); + // 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 (pushResources.size() < getMaxAssociatedResources()) + { + pushResources.add(url); + logger.debug("Stored push metadata for {}: {}", referrer, pushResources); + } + else + { + logger.debug("Skipped store of push metadata {} for {}: max associated resources ({}) reached", + url, referrer, maxAssociatedResources); + } } } diff --git a/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/AbstractHTTPSPDYTest.java b/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/AbstractHTTPSPDYTest.java index b875379f3ba..f6f41776d24 100644 --- a/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/AbstractHTTPSPDYTest.java +++ b/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/AbstractHTTPSPDYTest.java @@ -113,4 +113,9 @@ public abstract class AbstractHTTPSPDYTest server.join(); } } + + protected short version() + { + return SPDY.V2; + } } diff --git a/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/ConcurrentStreamsTest.java b/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/ConcurrentStreamsTest.java index a26555004b9..0bbf0e68ff3 100644 --- a/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/ConcurrentStreamsTest.java +++ b/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/ConcurrentStreamsTest.java @@ -73,10 +73,10 @@ public class ConcurrentStreamsTest extends AbstractHTTPSPDYTest // Perform slow request. This will wait on server side until the fast request wakes it up Headers headers = new Headers(); - headers.put("method", "GET"); - headers.put("url", "/slow"); - headers.put("version", "HTTP/1.1"); - headers.put("host", "localhost:" + connector.getLocalPort()); + headers.put(HTTPSPDYHeader.METHOD.name(version()), "GET"); + headers.put(HTTPSPDYHeader.URI.name(version()), "/slow"); + headers.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1"); + headers.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort()); final CountDownLatch slowClientLatch = new CountDownLatch(1); session.syn(new SynInfo(headers, true), new StreamFrameListener.Adapter() { @@ -91,10 +91,10 @@ public class ConcurrentStreamsTest extends AbstractHTTPSPDYTest // Perform the fast request. This will wake up the slow request headers.clear(); - headers.put("method", "GET"); - headers.put("url", "/fast"); - headers.put("version", "HTTP/1.1"); - headers.put("host", "localhost:" + connector.getLocalPort()); + headers.put(HTTPSPDYHeader.METHOD.name(version()), "GET"); + headers.put(HTTPSPDYHeader.URI.name(version()), "/fast"); + headers.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1"); + headers.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort()); final CountDownLatch fastClientLatch = new CountDownLatch(1); session.syn(new SynInfo(headers, true), new StreamFrameListener.Adapter() { diff --git a/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/PushStrategyBenchmarkTest.java b/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/PushStrategyBenchmarkTest.java index 0ff4893bd09..29b5952d4e5 100644 --- a/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/PushStrategyBenchmarkTest.java +++ b/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/PushStrategyBenchmarkTest.java @@ -38,7 +38,6 @@ import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.spdy.AsyncConnectionFactory; import org.eclipse.jetty.spdy.api.DataInfo; import org.eclipse.jetty.spdy.api.Headers; -import org.eclipse.jetty.spdy.api.SPDY; import org.eclipse.jetty.spdy.api.Session; import org.eclipse.jetty.spdy.api.SessionFrameListener; import org.eclipse.jetty.spdy.api.Stream; @@ -62,11 +61,6 @@ public class PushStrategyBenchmarkTest extends AbstractHTTPSPDYTest private final long roundtrip = 100; private final int runs = 10; - protected short version() - { - return SPDY.V2; - } - @Test public void benchmarkPushStrategy() throws Exception { diff --git a/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/ReferrerPushStrategyTest.java b/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/ReferrerPushStrategyTest.java deleted file mode 100644 index 4215118d5a1..00000000000 --- a/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/ReferrerPushStrategyTest.java +++ /dev/null @@ -1,463 +0,0 @@ -package org.eclipse.jetty.spdy.http; - -import java.io.IOException; -import java.io.PrintWriter; -import java.net.InetSocketAddress; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.handler.AbstractHandler; -import org.eclipse.jetty.spdy.AsyncConnectionFactory; -import org.eclipse.jetty.spdy.SPDYServerConnector; -import org.eclipse.jetty.spdy.api.DataInfo; -import org.eclipse.jetty.spdy.api.Headers; -import org.eclipse.jetty.spdy.api.ReplyInfo; -import org.eclipse.jetty.spdy.api.Session; -import org.eclipse.jetty.spdy.api.SessionFrameListener; -import org.eclipse.jetty.spdy.api.Stream; -import org.eclipse.jetty.spdy.api.StreamFrameListener; -import org.eclipse.jetty.spdy.api.SynInfo; -import org.junit.Assert; -import org.junit.Test; - -public class ReferrerPushStrategyTest extends AbstractHTTPSPDYTest -{ - @Override - protected SPDYServerConnector newHTTPSPDYServerConnector(short version) - { - SPDYServerConnector connector = super.newHTTPSPDYServerConnector(version); - AsyncConnectionFactory defaultFactory = new ServerHTTPSPDYAsyncConnectionFactory(version, connector.getByteBufferPool(), connector.getExecutor(), connector.getScheduler(), connector, new ReferrerPushStrategy()); - connector.setDefaultAsyncConnectionFactory(defaultFactory); - return connector; - } - - @Test - public void testAssociatedResourceIsPushed() throws Exception - { - InetSocketAddress address = startHTTPServer(new AbstractHandler() - { - @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException - { - String url = request.getRequestURI(); - PrintWriter output = response.getWriter(); - if (url.endsWith(".html")) - output.print("HELLO"); - else if (url.endsWith(".css")) - output.print("body { background: #FFF; }"); - baseRequest.setHandled(true); - } - }); - Session session1 = startClient(address, null); - - final CountDownLatch mainResourceLatch = new CountDownLatch(1); - Headers mainRequestHeaders = new Headers(); - mainRequestHeaders.put("method", "GET"); - String mainResource = "/index.html"; - mainRequestHeaders.put("url", mainResource); - mainRequestHeaders.put("version", "HTTP/1.1"); - mainRequestHeaders.put("scheme", "http"); - mainRequestHeaders.put("host", "localhost:" + connector.getLocalPort()); - session1.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() - { - @Override - public void onData(Stream stream, DataInfo dataInfo) - { - dataInfo.consume(dataInfo.length()); - if (dataInfo.isClose()) - mainResourceLatch.countDown(); - } - }); - Assert.assertTrue(mainResourceLatch.await(5, TimeUnit.SECONDS)); - - final CountDownLatch associatedResourceLatch = new CountDownLatch(1); - Headers associatedRequestHeaders = new Headers(); - associatedRequestHeaders.put("method", "GET"); - associatedRequestHeaders.put("url", "/style.css"); - associatedRequestHeaders.put("version", "HTTP/1.1"); - associatedRequestHeaders.put("scheme", "http"); - associatedRequestHeaders.put("host", "localhost:" + connector.getLocalPort()); - associatedRequestHeaders.put("referer", "http://localhost:" + connector.getLocalPort() + mainResource); - session1.syn(new SynInfo(associatedRequestHeaders, true), new StreamFrameListener.Adapter() - { - @Override - public void onData(Stream stream, DataInfo dataInfo) - { - dataInfo.consume(dataInfo.length()); - if (dataInfo.isClose()) - associatedResourceLatch.countDown(); - } - }); - Assert.assertTrue(associatedResourceLatch.await(5, TimeUnit.SECONDS)); - - // Create another client, and perform the same request for the main resource, we expect the css being pushed - - final CountDownLatch mainStreamLatch = new CountDownLatch(2); - final CountDownLatch pushDataLatch = new CountDownLatch(1); - Session session2 = startClient(address, new SessionFrameListener.Adapter() - { - @Override - public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) - { - Assert.assertTrue(stream.isUnidirectional()); - return new StreamFrameListener.Adapter() - { - @Override - public void onData(Stream stream, DataInfo dataInfo) - { - dataInfo.consume(dataInfo.length()); - if (dataInfo.isClose()) - pushDataLatch.countDown(); - } - }; - } - }); - session2.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() - { - @Override - public void onReply(Stream stream, ReplyInfo replyInfo) - { - Assert.assertFalse(replyInfo.isClose()); - mainStreamLatch.countDown(); - } - - @Override - public void onData(Stream stream, DataInfo dataInfo) - { - dataInfo.consume(dataInfo.length()); - if (dataInfo.isClose()) - mainStreamLatch.countDown(); - } - }); - - Assert.assertTrue(mainStreamLatch.await(5, TimeUnit.SECONDS)); - Assert.assertTrue(pushDataLatch.await(5, TimeUnit.SECONDS)); - } - - @Test - public void testNestedAssociatedResourceIsPushed() throws Exception - { - InetSocketAddress address = startHTTPServer(new AbstractHandler() - { - @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException - { - String url = request.getRequestURI(); - PrintWriter output = response.getWriter(); - if (url.endsWith(".html")) - output.print("HELLO"); - else if (url.endsWith(".css")) - output.print("body { background: #FFF; }"); - else if (url.endsWith(".gif")) - output.print("\u0000"); - baseRequest.setHandled(true); - } - }); - Session session1 = startClient(address, null); - - final CountDownLatch mainResourceLatch = new CountDownLatch(1); - Headers mainRequestHeaders = new Headers(); - mainRequestHeaders.put("method", "GET"); - String mainResource = "/index.html"; - mainRequestHeaders.put("url", mainResource); - mainRequestHeaders.put("version", "HTTP/1.1"); - mainRequestHeaders.put("scheme", "http"); - mainRequestHeaders.put("host", "localhost:" + connector.getLocalPort()); - session1.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() - { - @Override - public void onData(Stream stream, DataInfo dataInfo) - { - dataInfo.consume(dataInfo.length()); - if (dataInfo.isClose()) - mainResourceLatch.countDown(); - } - }); - Assert.assertTrue(mainResourceLatch.await(5, TimeUnit.SECONDS)); - - final CountDownLatch associatedResourceLatch = new CountDownLatch(1); - Headers associatedRequestHeaders = new Headers(); - associatedRequestHeaders.put("method", "GET"); - String associatedResource = "/style.css"; - associatedRequestHeaders.put("url", associatedResource); - associatedRequestHeaders.put("version", "HTTP/1.1"); - associatedRequestHeaders.put("scheme", "http"); - associatedRequestHeaders.put("host", "localhost:" + connector.getLocalPort()); - associatedRequestHeaders.put("referer", "http://localhost:" + connector.getLocalPort() + mainResource); - session1.syn(new SynInfo(associatedRequestHeaders, true), new StreamFrameListener.Adapter() - { - @Override - public void onData(Stream stream, DataInfo dataInfo) - { - dataInfo.consume(dataInfo.length()); - if (dataInfo.isClose()) - associatedResourceLatch.countDown(); - } - }); - Assert.assertTrue(associatedResourceLatch.await(5, TimeUnit.SECONDS)); - - final CountDownLatch nestedResourceLatch = new CountDownLatch(1); - Headers nestedRequestHeaders = new Headers(); - nestedRequestHeaders.put("method", "GET"); - nestedRequestHeaders.put("url", "/image.gif"); - nestedRequestHeaders.put("version", "HTTP/1.1"); - nestedRequestHeaders.put("scheme", "http"); - nestedRequestHeaders.put("host", "localhost:" + connector.getLocalPort()); - nestedRequestHeaders.put("referer", "http://localhost:" + connector.getLocalPort() + associatedResource); - session1.syn(new SynInfo(nestedRequestHeaders, true), new StreamFrameListener.Adapter() - { - @Override - public void onData(Stream stream, DataInfo dataInfo) - { - dataInfo.consume(dataInfo.length()); - if (dataInfo.isClose()) - nestedResourceLatch.countDown(); - } - }); - Assert.assertTrue(nestedResourceLatch.await(5, TimeUnit.SECONDS)); - - // Create another client, and perform the same request for the main resource, we expect the css and the image being pushed - - final CountDownLatch mainStreamLatch = new CountDownLatch(2); - final CountDownLatch pushDataLatch = new CountDownLatch(2); - Session session2 = startClient(address, new SessionFrameListener.Adapter() - { - @Override - public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) - { - Assert.assertTrue(stream.isUnidirectional()); - return new StreamFrameListener.Adapter() - { - @Override - public void onData(Stream stream, DataInfo dataInfo) - { - dataInfo.consume(dataInfo.length()); - if (dataInfo.isClose()) - pushDataLatch.countDown(); - } - }; - } - }); - session2.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() - { - @Override - public void onReply(Stream stream, ReplyInfo replyInfo) - { - Assert.assertFalse(replyInfo.isClose()); - mainStreamLatch.countDown(); - } - - @Override - public void onData(Stream stream, DataInfo dataInfo) - { - dataInfo.consume(dataInfo.length()); - if (dataInfo.isClose()) - mainStreamLatch.countDown(); - } - }); - - Assert.assertTrue(mainStreamLatch.await(5, TimeUnit.SECONDS)); - Assert.assertTrue(pushDataLatch.await(5, TimeUnit.SECONDS)); - } - - @Test - public void testMainResourceWithReferrerIsNotPushed() throws Exception - { - InetSocketAddress address = startHTTPServer(new AbstractHandler() - { - @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException - { - String url = request.getRequestURI(); - PrintWriter output = response.getWriter(); - if (url.endsWith(".html")) - output.print("HELLO"); - baseRequest.setHandled(true); - } - }); - Session session1 = startClient(address, null); - - final CountDownLatch mainResourceLatch = new CountDownLatch(1); - Headers mainRequestHeaders = new Headers(); - mainRequestHeaders.put("method", "GET"); - String mainResource = "/index.html"; - mainRequestHeaders.put("url", mainResource); - mainRequestHeaders.put("version", "HTTP/1.1"); - mainRequestHeaders.put("scheme", "http"); - mainRequestHeaders.put("host", "localhost:" + connector.getLocalPort()); - session1.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() - { - @Override - public void onData(Stream stream, DataInfo dataInfo) - { - dataInfo.consume(dataInfo.length()); - if (dataInfo.isClose()) - mainResourceLatch.countDown(); - } - }); - Assert.assertTrue(mainResourceLatch.await(5, TimeUnit.SECONDS)); - - final CountDownLatch associatedResourceLatch = new CountDownLatch(1); - Headers associatedRequestHeaders = new Headers(); - associatedRequestHeaders.put("method", "GET"); - associatedRequestHeaders.put("url", "/home.html"); - associatedRequestHeaders.put("version", "HTTP/1.1"); - associatedRequestHeaders.put("scheme", "http"); - associatedRequestHeaders.put("host", "localhost:" + connector.getLocalPort()); - associatedRequestHeaders.put("referer", "http://localhost:" + connector.getLocalPort() + mainResource); - session1.syn(new SynInfo(associatedRequestHeaders, true), new StreamFrameListener.Adapter() - { - @Override - public void onData(Stream stream, DataInfo dataInfo) - { - dataInfo.consume(dataInfo.length()); - if (dataInfo.isClose()) - associatedResourceLatch.countDown(); - } - }); - Assert.assertTrue(associatedResourceLatch.await(5, TimeUnit.SECONDS)); - - // Create another client, and perform the same request for the main resource, we expect nothing being pushed - - final CountDownLatch mainStreamLatch = new CountDownLatch(2); - final CountDownLatch pushLatch = new CountDownLatch(1); - Session session2 = startClient(address, new SessionFrameListener.Adapter() - { - @Override - public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) - { - pushLatch.countDown(); - return null; - } - }); - session2.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() - { - @Override - public void onReply(Stream stream, ReplyInfo replyInfo) - { - Assert.assertFalse(replyInfo.isClose()); - mainStreamLatch.countDown(); - } - - @Override - public void onData(Stream stream, DataInfo dataInfo) - { - dataInfo.consume(dataInfo.length()); - if (dataInfo.isClose()) - mainStreamLatch.countDown(); - } - }); - - Assert.assertTrue(mainStreamLatch.await(5, TimeUnit.SECONDS)); - Assert.assertFalse(pushLatch.await(1, TimeUnit.SECONDS)); - } - - @Test - public void testRequestWithIfModifiedSinceHeaderPreventsPush() throws Exception - { - InetSocketAddress address = startHTTPServer(new AbstractHandler() - { - @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException - { - String url = request.getRequestURI(); - PrintWriter output = response.getWriter(); - if (url.endsWith(".html")) - output.print("HELLO"); - else if (url.endsWith(".css")) - output.print("body { background: #FFF; }"); - baseRequest.setHandled(true); - } - }); - Session session1 = startClient(address, null); - - final CountDownLatch mainResourceLatch = new CountDownLatch(1); - Headers mainRequestHeaders = new Headers(); - mainRequestHeaders.put("method", "GET"); - String mainResource = "/index.html"; - mainRequestHeaders.put("url", mainResource); - mainRequestHeaders.put("version", "HTTP/1.1"); - mainRequestHeaders.put("scheme", "http"); - mainRequestHeaders.put("host", "localhost:" + connector.getLocalPort()); - mainRequestHeaders.put("If-Modified-Since", "Tue, 27 Mar 2012 16:36:52 GMT"); - session1.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() - { - @Override - public void onData(Stream stream, DataInfo dataInfo) - { - dataInfo.consume(dataInfo.length()); - if (dataInfo.isClose()) - mainResourceLatch.countDown(); - } - }); - Assert.assertTrue(mainResourceLatch.await(5, TimeUnit.SECONDS)); - - final CountDownLatch associatedResourceLatch = new CountDownLatch(1); - Headers associatedRequestHeaders = new Headers(); - associatedRequestHeaders.put("method", "GET"); - associatedRequestHeaders.put("url", "/style.css"); - associatedRequestHeaders.put("version", "HTTP/1.1"); - associatedRequestHeaders.put("scheme", "http"); - associatedRequestHeaders.put("host", "localhost:" + connector.getLocalPort()); - associatedRequestHeaders.put("referer", "http://localhost:" + connector.getLocalPort() + mainResource); - session1.syn(new SynInfo(associatedRequestHeaders, true), new StreamFrameListener.Adapter() - { - @Override - public void onData(Stream stream, DataInfo dataInfo) - { - dataInfo.consume(dataInfo.length()); - if (dataInfo.isClose()) - associatedResourceLatch.countDown(); - } - }); - Assert.assertTrue(associatedResourceLatch.await(5, TimeUnit.SECONDS)); - - // Create another client, and perform the same request for the main resource, we expect the css NOT being pushed as the main request contains an - // if-modified-since header - - final CountDownLatch mainStreamLatch = new CountDownLatch(2); - final CountDownLatch pushDataLatch = new CountDownLatch(1); - Session session2 = startClient(address, new SessionFrameListener.Adapter() - { - @Override - public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) - { - Assert.assertTrue(stream.isUnidirectional()); - return new StreamFrameListener.Adapter() - { - @Override - public void onData(Stream stream, DataInfo dataInfo) - { - dataInfo.consume(dataInfo.length()); - if (dataInfo.isClose()) - pushDataLatch.countDown(); - } - }; - } - }); - session2.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() - { - @Override - public void onReply(Stream stream, ReplyInfo replyInfo) - { - Assert.assertFalse(replyInfo.isClose()); - mainStreamLatch.countDown(); - } - - @Override - public void onData(Stream stream, DataInfo dataInfo) - { - dataInfo.consume(dataInfo.length()); - if (dataInfo.isClose()) - mainStreamLatch.countDown(); - } - }); - - Assert.assertTrue(mainStreamLatch.await(5, TimeUnit.SECONDS)); - Assert.assertFalse("We don't expect data to be pushed as the main request contained an if-modified-since header",pushDataLatch.await(1, TimeUnit.SECONDS)); - } -} diff --git a/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/ReferrerPushStrategyV2Test.java b/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/ReferrerPushStrategyV2Test.java new file mode 100644 index 00000000000..ce88e712c74 --- /dev/null +++ b/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/ReferrerPushStrategyV2Test.java @@ -0,0 +1,748 @@ +/* + * Copyright (c) 2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.eclipse.jetty.spdy.http; + +import java.io.IOException; +import java.io.PrintWriter; +import java.net.InetSocketAddress; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.spdy.AsyncConnectionFactory; +import org.eclipse.jetty.spdy.SPDYServerConnector; +import org.eclipse.jetty.spdy.api.DataInfo; +import org.eclipse.jetty.spdy.api.Headers; +import org.eclipse.jetty.spdy.api.ReplyInfo; +import org.eclipse.jetty.spdy.api.Session; +import org.eclipse.jetty.spdy.api.SessionFrameListener; +import org.eclipse.jetty.spdy.api.Stream; +import org.eclipse.jetty.spdy.api.StreamFrameListener; +import org.eclipse.jetty.spdy.api.SynInfo; +import org.junit.Assert; +import org.junit.Test; + +public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest +{ + @Override + protected SPDYServerConnector newHTTPSPDYServerConnector(short version) + { + SPDYServerConnector connector = super.newHTTPSPDYServerConnector(version); + AsyncConnectionFactory defaultFactory = new ServerHTTPSPDYAsyncConnectionFactory(version, connector.getByteBufferPool(), connector.getExecutor(), connector.getScheduler(), connector, new ReferrerPushStrategy()); + connector.setDefaultAsyncConnectionFactory(defaultFactory); + return connector; + } + + @Test + public void testMaxAssociatedResources() throws Exception + { + InetSocketAddress address = startHTTPServer(version(), new AbstractHandler() + { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + String url = request.getRequestURI(); + PrintWriter output = response.getWriter(); + if (url.endsWith(".html")) + output.print("HELLO"); + else if (url.endsWith(".css")) + output.print("body { background: #FFF; }"); + else if (url.endsWith(".js")) + output.print("function(){}();"); + baseRequest.setHandled(true); + } + }); + ReferrerPushStrategy pushStrategy = new ReferrerPushStrategy(); + pushStrategy.setMaxAssociatedResources(1); + AsyncConnectionFactory defaultFactory = new ServerHTTPSPDYAsyncConnectionFactory(version(), connector.getByteBufferPool(), connector.getExecutor(), connector.getScheduler(), connector, pushStrategy); + connector.setDefaultAsyncConnectionFactory(defaultFactory); + + Session session1 = startClient(version(), address, null); + + final CountDownLatch mainResourceLatch = new CountDownLatch(1); + Headers mainRequestHeaders = new Headers(); + mainRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET"); + String mainResource = "/index.html"; + mainRequestHeaders.put(HTTPSPDYHeader.URI.name(version()), mainResource); + mainRequestHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1"); + mainRequestHeaders.put(HTTPSPDYHeader.SCHEME.name(version()), "http"); + mainRequestHeaders.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort()); + session1.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() + { + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + mainResourceLatch.countDown(); + } + }); + Assert.assertTrue(mainResourceLatch.await(5, TimeUnit.SECONDS)); + + final CountDownLatch associatedResourceLatch1 = new CountDownLatch(1); + Headers associatedRequestHeaders1 = new Headers(); + associatedRequestHeaders1.put(HTTPSPDYHeader.METHOD.name(version()), "GET"); + associatedRequestHeaders1.put(HTTPSPDYHeader.URI.name(version()), "/style.css"); + associatedRequestHeaders1.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1"); + associatedRequestHeaders1.put(HTTPSPDYHeader.SCHEME.name(version()), "http"); + associatedRequestHeaders1.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort()); + associatedRequestHeaders1.put("referer", "http://localhost:" + connector.getLocalPort() + mainResource); + session1.syn(new SynInfo(associatedRequestHeaders1, true), new StreamFrameListener.Adapter() + { + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + associatedResourceLatch1.countDown(); + } + }); + Assert.assertTrue(associatedResourceLatch1.await(5, TimeUnit.SECONDS)); + + final CountDownLatch associatedResourceLatch2 = new CountDownLatch(1); + Headers associatedRequestHeaders2 = new Headers(); + associatedRequestHeaders2.put(HTTPSPDYHeader.METHOD.name(version()), "GET"); + associatedRequestHeaders2.put(HTTPSPDYHeader.URI.name(version()), "/application.js"); + associatedRequestHeaders2.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1"); + associatedRequestHeaders2.put(HTTPSPDYHeader.SCHEME.name(version()), "http"); + associatedRequestHeaders2.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort()); + associatedRequestHeaders2.put("referer", "http://localhost:" + connector.getLocalPort() + mainResource); + session1.syn(new SynInfo(associatedRequestHeaders2, true), new StreamFrameListener.Adapter() + { + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + associatedResourceLatch2.countDown(); + } + }); + Assert.assertTrue(associatedResourceLatch2.await(5, TimeUnit.SECONDS)); + + // Create another client, and perform the same request for the main resource, + // we expect the css being pushed, but not the js + + final CountDownLatch mainStreamLatch = new CountDownLatch(2); + final CountDownLatch pushDataLatch = new CountDownLatch(1); + Session session2 = startClient(version(), address, new SessionFrameListener.Adapter() + { + @Override + public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) + { + Assert.assertTrue(stream.isUnidirectional()); + Assert.assertTrue(synInfo.getHeaders().get(HTTPSPDYHeader.URI.name(version())).value().endsWith(".css")); + return new StreamFrameListener.Adapter() + { + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + pushDataLatch.countDown(); + } + }; + } + }); + session2.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() + { + @Override + public void onReply(Stream stream, ReplyInfo replyInfo) + { + Assert.assertFalse(replyInfo.isClose()); + mainStreamLatch.countDown(); + } + + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + mainStreamLatch.countDown(); + } + }); + + Assert.assertTrue(mainStreamLatch.await(5, TimeUnit.SECONDS)); + Assert.assertTrue(pushDataLatch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void testAssociatedResourceIsPushed() throws Exception + { + InetSocketAddress address = startHTTPServer(version(), new AbstractHandler() + { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + String url = request.getRequestURI(); + PrintWriter output = response.getWriter(); + if (url.endsWith(".html")) + output.print("HELLO"); + else if (url.endsWith(".css")) + output.print("body { background: #FFF; }"); + baseRequest.setHandled(true); + } + }); + Session session1 = startClient(version(), address, null); + + final CountDownLatch mainResourceLatch = new CountDownLatch(1); + Headers mainRequestHeaders = new Headers(); + mainRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET"); + String mainResource = "/index.html"; + mainRequestHeaders.put(HTTPSPDYHeader.URI.name(version()), mainResource); + mainRequestHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1"); + mainRequestHeaders.put(HTTPSPDYHeader.SCHEME.name(version()), "http"); + mainRequestHeaders.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort()); + session1.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() + { + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + mainResourceLatch.countDown(); + } + }); + Assert.assertTrue(mainResourceLatch.await(5, TimeUnit.SECONDS)); + + final CountDownLatch associatedResourceLatch = new CountDownLatch(1); + Headers associatedRequestHeaders = new Headers(); + associatedRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET"); + associatedRequestHeaders.put(HTTPSPDYHeader.URI.name(version()), "/style.css"); + associatedRequestHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1"); + associatedRequestHeaders.put(HTTPSPDYHeader.SCHEME.name(version()), "http"); + associatedRequestHeaders.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort()); + associatedRequestHeaders.put("referer", "http://localhost:" + connector.getLocalPort() + mainResource); + session1.syn(new SynInfo(associatedRequestHeaders, true), new StreamFrameListener.Adapter() + { + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + associatedResourceLatch.countDown(); + } + }); + Assert.assertTrue(associatedResourceLatch.await(5, TimeUnit.SECONDS)); + + // Create another client, and perform the same request for the main resource, we expect the css being pushed + + final CountDownLatch mainStreamLatch = new CountDownLatch(2); + final CountDownLatch pushDataLatch = new CountDownLatch(1); + Session session2 = startClient(version(), address, new SessionFrameListener.Adapter() + { + @Override + public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) + { + Assert.assertTrue(stream.isUnidirectional()); + return new StreamFrameListener.Adapter() + { + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + pushDataLatch.countDown(); + } + }; + } + }); + session2.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() + { + @Override + public void onReply(Stream stream, ReplyInfo replyInfo) + { + Assert.assertFalse(replyInfo.isClose()); + mainStreamLatch.countDown(); + } + + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + mainStreamLatch.countDown(); + } + }); + + Assert.assertTrue(mainStreamLatch.await(5, TimeUnit.SECONDS)); + Assert.assertTrue(pushDataLatch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void testAssociatedResourceWithWrongContentTypeIsNotPushed() throws Exception + { + InetSocketAddress address = startHTTPServer(version(), new AbstractHandler() + { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + String url = request.getRequestURI(); + PrintWriter output = response.getWriter(); + if (url.endsWith(".html")) + { + response.setContentType("text/html"); + output.print("HELLO"); + } + else if (url.equals("/fake.png")) + { + response.setContentType("text/html"); + output.print("IMAGE"); + } + else if (url.endsWith(".css")) + { + response.setContentType("text/css"); + output.print("body { background: #FFF; }"); + } + baseRequest.setHandled(true); + } + }); + Session session1 = startClient(version(), address, null); + + final CountDownLatch mainResourceLatch = new CountDownLatch(1); + Headers mainRequestHeaders = new Headers(); + mainRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET"); + String mainResource = "/index.html"; + mainRequestHeaders.put(HTTPSPDYHeader.URI.name(version()), mainResource); + mainRequestHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1"); + mainRequestHeaders.put(HTTPSPDYHeader.SCHEME.name(version()), "http"); + mainRequestHeaders.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort()); + session1.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() + { + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + mainResourceLatch.countDown(); + } + }); + Assert.assertTrue(mainResourceLatch.await(5, TimeUnit.SECONDS)); + + final CountDownLatch associatedResourceLatch = new CountDownLatch(1); + Headers associatedRequestHeaders = new Headers(); + associatedRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET"); + associatedRequestHeaders.put(HTTPSPDYHeader.URI.name(version()), "/stylesheet.css"); + associatedRequestHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1"); + associatedRequestHeaders.put(HTTPSPDYHeader.SCHEME.name(version()), "http"); + associatedRequestHeaders.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort()); + associatedRequestHeaders.put("referer", "http://localhost:" + connector.getLocalPort() + mainResource); + session1.syn(new SynInfo(associatedRequestHeaders, true), new StreamFrameListener.Adapter() + { + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + associatedResourceLatch.countDown(); + } + }); + Assert.assertTrue(associatedResourceLatch.await(5, TimeUnit.SECONDS)); + + final CountDownLatch fakeAssociatedResourceLatch = new CountDownLatch(1); + Headers fakeAssociatedRequestHeaders = new Headers(); + fakeAssociatedRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET"); + fakeAssociatedRequestHeaders.put(HTTPSPDYHeader.URI.name(version()), "/fake.png"); + fakeAssociatedRequestHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1"); + fakeAssociatedRequestHeaders.put(HTTPSPDYHeader.SCHEME.name(version()), "http"); + fakeAssociatedRequestHeaders.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort()); + fakeAssociatedRequestHeaders.put("referer", "http://localhost:" + connector.getLocalPort() + mainResource); + session1.syn(new SynInfo(fakeAssociatedRequestHeaders, true), new StreamFrameListener.Adapter() + { + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + fakeAssociatedResourceLatch.countDown(); + } + }); + Assert.assertTrue(fakeAssociatedResourceLatch.await(5, TimeUnit.SECONDS)); + + // Create another client, and perform the same request for the main resource, + // we expect the css being pushed but not the fake PNG + + final CountDownLatch mainStreamLatch = new CountDownLatch(2); + final CountDownLatch pushDataLatch = new CountDownLatch(1); + Session session2 = startClient(version(), address, new SessionFrameListener.Adapter() + { + @Override + public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) + { + Assert.assertTrue(stream.isUnidirectional()); + Assert.assertTrue(synInfo.getHeaders().get(HTTPSPDYHeader.URI.name(version())).value().endsWith(".css")); + return new StreamFrameListener.Adapter() + { + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + pushDataLatch.countDown(); + } + }; + } + }); + session2.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() + { + @Override + public void onReply(Stream stream, ReplyInfo replyInfo) + { + Assert.assertFalse(replyInfo.isClose()); + mainStreamLatch.countDown(); + } + + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + mainStreamLatch.countDown(); + } + }); + + Assert.assertTrue(mainStreamLatch.await(5, TimeUnit.SECONDS)); + Assert.assertTrue(pushDataLatch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void testNestedAssociatedResourceIsPushed() throws Exception + { + InetSocketAddress address = startHTTPServer(version(), new AbstractHandler() + { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + String url = request.getRequestURI(); + PrintWriter output = response.getWriter(); + if (url.endsWith(".html")) + output.print("HELLO"); + else if (url.endsWith(".css")) + output.print("body { background: #FFF; }"); + else if (url.endsWith(".gif")) + output.print("\u0000"); + baseRequest.setHandled(true); + } + }); + Session session1 = startClient(version(), address, null); + + final CountDownLatch mainResourceLatch = new CountDownLatch(1); + Headers mainRequestHeaders = new Headers(); + mainRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET"); + String mainResource = "/index.html"; + mainRequestHeaders.put(HTTPSPDYHeader.URI.name(version()), mainResource); + mainRequestHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1"); + mainRequestHeaders.put(HTTPSPDYHeader.SCHEME.name(version()), "http"); + mainRequestHeaders.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort()); + session1.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() + { + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + mainResourceLatch.countDown(); + } + }); + Assert.assertTrue(mainResourceLatch.await(5, TimeUnit.SECONDS)); + + final CountDownLatch associatedResourceLatch = new CountDownLatch(1); + Headers associatedRequestHeaders = new Headers(); + associatedRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET"); + String associatedResource = "/style.css"; + associatedRequestHeaders.put(HTTPSPDYHeader.URI.name(version()), associatedResource); + associatedRequestHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1"); + associatedRequestHeaders.put(HTTPSPDYHeader.SCHEME.name(version()), "http"); + associatedRequestHeaders.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort()); + associatedRequestHeaders.put("referer", "http://localhost:" + connector.getLocalPort() + mainResource); + session1.syn(new SynInfo(associatedRequestHeaders, true), new StreamFrameListener.Adapter() + { + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + associatedResourceLatch.countDown(); + } + }); + Assert.assertTrue(associatedResourceLatch.await(5, TimeUnit.SECONDS)); + + final CountDownLatch nestedResourceLatch = new CountDownLatch(1); + Headers nestedRequestHeaders = new Headers(); + nestedRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET"); + nestedRequestHeaders.put(HTTPSPDYHeader.URI.name(version()), "/image.gif"); + nestedRequestHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1"); + nestedRequestHeaders.put(HTTPSPDYHeader.SCHEME.name(version()), "http"); + nestedRequestHeaders.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort()); + nestedRequestHeaders.put("referer", "http://localhost:" + connector.getLocalPort() + associatedResource); + session1.syn(new SynInfo(nestedRequestHeaders, true), new StreamFrameListener.Adapter() + { + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + nestedResourceLatch.countDown(); + } + }); + Assert.assertTrue(nestedResourceLatch.await(5, TimeUnit.SECONDS)); + + // Create another client, and perform the same request for the main resource, we expect the css and the image being pushed + + final CountDownLatch mainStreamLatch = new CountDownLatch(2); + final CountDownLatch pushDataLatch = new CountDownLatch(2); + Session session2 = startClient(version(), address, new SessionFrameListener.Adapter() + { + @Override + public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) + { + Assert.assertTrue(stream.isUnidirectional()); + return new StreamFrameListener.Adapter() + { + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + pushDataLatch.countDown(); + } + }; + } + }); + session2.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() + { + @Override + public void onReply(Stream stream, ReplyInfo replyInfo) + { + Assert.assertFalse(replyInfo.isClose()); + mainStreamLatch.countDown(); + } + + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + mainStreamLatch.countDown(); + } + }); + + Assert.assertTrue(mainStreamLatch.await(5, TimeUnit.SECONDS)); + Assert.assertTrue(pushDataLatch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void testMainResourceWithReferrerIsNotPushed() throws Exception + { + InetSocketAddress address = startHTTPServer(version(), new AbstractHandler() + { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + String url = request.getRequestURI(); + PrintWriter output = response.getWriter(); + if (url.endsWith(".html")) + output.print("HELLO"); + baseRequest.setHandled(true); + } + }); + Session session1 = startClient(version(), address, null); + + final CountDownLatch mainResourceLatch = new CountDownLatch(1); + Headers mainRequestHeaders = new Headers(); + mainRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET"); + String mainResource = "/index.html"; + mainRequestHeaders.put(HTTPSPDYHeader.URI.name(version()), mainResource); + mainRequestHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1"); + mainRequestHeaders.put(HTTPSPDYHeader.SCHEME.name(version()), "http"); + mainRequestHeaders.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort()); + session1.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() + { + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + mainResourceLatch.countDown(); + } + }); + Assert.assertTrue(mainResourceLatch.await(5, TimeUnit.SECONDS)); + + final CountDownLatch associatedResourceLatch = new CountDownLatch(1); + Headers associatedRequestHeaders = new Headers(); + associatedRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET"); + associatedRequestHeaders.put(HTTPSPDYHeader.URI.name(version()), "/home.html"); + associatedRequestHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1"); + associatedRequestHeaders.put(HTTPSPDYHeader.SCHEME.name(version()), "http"); + associatedRequestHeaders.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort()); + associatedRequestHeaders.put("referer", "http://localhost:" + connector.getLocalPort() + mainResource); + session1.syn(new SynInfo(associatedRequestHeaders, true), new StreamFrameListener.Adapter() + { + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + associatedResourceLatch.countDown(); + } + }); + Assert.assertTrue(associatedResourceLatch.await(5, TimeUnit.SECONDS)); + + // Create another client, and perform the same request for the main resource, we expect nothing being pushed + + final CountDownLatch mainStreamLatch = new CountDownLatch(2); + final CountDownLatch pushLatch = new CountDownLatch(1); + Session session2 = startClient(version(), address, new SessionFrameListener.Adapter() + { + @Override + public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) + { + pushLatch.countDown(); + return null; + } + }); + session2.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() + { + @Override + public void onReply(Stream stream, ReplyInfo replyInfo) + { + Assert.assertFalse(replyInfo.isClose()); + mainStreamLatch.countDown(); + } + + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + mainStreamLatch.countDown(); + } + }); + + Assert.assertTrue(mainStreamLatch.await(5, TimeUnit.SECONDS)); + Assert.assertFalse(pushLatch.await(1, TimeUnit.SECONDS)); + } + + @Test + public void testRequestWithIfModifiedSinceHeaderPreventsPush() throws Exception + { + InetSocketAddress address = startHTTPServer(version(), new AbstractHandler() + { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + String url = request.getRequestURI(); + PrintWriter output = response.getWriter(); + if (url.endsWith(".html")) + output.print("HELLO"); + else if (url.endsWith(".css")) + output.print("body { background: #FFF; }"); + baseRequest.setHandled(true); + } + }); + Session session1 = startClient(version(), address, null); + + final CountDownLatch mainResourceLatch = new CountDownLatch(1); + Headers mainRequestHeaders = new Headers(); + mainRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET"); + String mainResource = "/index.html"; + mainRequestHeaders.put(HTTPSPDYHeader.URI.name(version()), mainResource); + mainRequestHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1"); + mainRequestHeaders.put(HTTPSPDYHeader.SCHEME.name(version()), "http"); + mainRequestHeaders.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort()); + mainRequestHeaders.put("If-Modified-Since", "Tue, 27 Mar 2012 16:36:52 GMT"); + session1.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() + { + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + mainResourceLatch.countDown(); + } + }); + Assert.assertTrue(mainResourceLatch.await(5, TimeUnit.SECONDS)); + + final CountDownLatch associatedResourceLatch = new CountDownLatch(1); + Headers associatedRequestHeaders = new Headers(); + associatedRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET"); + associatedRequestHeaders.put(HTTPSPDYHeader.URI.name(version()), "/style.css"); + associatedRequestHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1"); + associatedRequestHeaders.put(HTTPSPDYHeader.SCHEME.name(version()), "http"); + associatedRequestHeaders.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort()); + associatedRequestHeaders.put("referer", "http://localhost:" + connector.getLocalPort() + mainResource); + session1.syn(new SynInfo(associatedRequestHeaders, true), new StreamFrameListener.Adapter() + { + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + associatedResourceLatch.countDown(); + } + }); + Assert.assertTrue(associatedResourceLatch.await(5, TimeUnit.SECONDS)); + + // Create another client, and perform the same request for the main resource, we expect the css NOT being pushed as the main request contains an + // if-modified-since header + + final CountDownLatch mainStreamLatch = new CountDownLatch(2); + final CountDownLatch pushDataLatch = new CountDownLatch(1); + Session session2 = startClient(version(), address, new SessionFrameListener.Adapter() + { + @Override + public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) + { + Assert.assertTrue(stream.isUnidirectional()); + return new StreamFrameListener.Adapter() + { + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + pushDataLatch.countDown(); + } + }; + } + }); + session2.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() + { + @Override + public void onReply(Stream stream, ReplyInfo replyInfo) + { + Assert.assertFalse(replyInfo.isClose()); + mainStreamLatch.countDown(); + } + + @Override + public void onData(Stream stream, DataInfo dataInfo) + { + dataInfo.consume(dataInfo.length()); + if (dataInfo.isClose()) + mainStreamLatch.countDown(); + } + }); + + Assert.assertTrue(mainStreamLatch.await(5, TimeUnit.SECONDS)); + Assert.assertFalse("We don't expect data to be pushed as the main request contained an if-modified-since header",pushDataLatch.await(1, TimeUnit.SECONDS)); + } +} diff --git a/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/ReferrerPushStrategyV3Test.java b/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/ReferrerPushStrategyV3Test.java new file mode 100644 index 00000000000..a722fde9f46 --- /dev/null +++ b/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/ReferrerPushStrategyV3Test.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.eclipse.jetty.spdy.http; + +import org.eclipse.jetty.spdy.api.SPDY; + +public class ReferrerPushStrategyV3Test extends ReferrerPushStrategyV2Test +{ + @Override + protected short version() + { + return SPDY.V3; + } +} diff --git a/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/ServerHTTPSPDYv2Test.java b/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/ServerHTTPSPDYv2Test.java index 3363d124a50..61ebfd127d9 100644 --- a/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/ServerHTTPSPDYv2Test.java +++ b/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/ServerHTTPSPDYv2Test.java @@ -41,7 +41,6 @@ import org.eclipse.jetty.spdy.api.BytesDataInfo; import org.eclipse.jetty.spdy.api.DataInfo; import org.eclipse.jetty.spdy.api.Headers; import org.eclipse.jetty.spdy.api.ReplyInfo; -import org.eclipse.jetty.spdy.api.SPDY; import org.eclipse.jetty.spdy.api.Session; import org.eclipse.jetty.spdy.api.Stream; import org.eclipse.jetty.spdy.api.StreamFrameListener; @@ -52,11 +51,6 @@ import org.junit.Test; public class ServerHTTPSPDYv2Test extends AbstractHTTPSPDYTest { - protected short version() - { - return SPDY.V2; - } - @Test public void testSimpleGET() throws Exception { diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/StandardDescriptorProcessor.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/StandardDescriptorProcessor.java index db5ec955afa..51800f4d7c6 100644 --- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/StandardDescriptorProcessor.java +++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/StandardDescriptorProcessor.java @@ -239,23 +239,26 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor String servlet_class = node.getString("servlet-class", false, true); // Handle JSP - String jspServletName=null; String jspServletClass=null;; - boolean hasJSP=false; + + //Handle the default jsp servlet instance if (id != null && id.equals("jsp")) { - jspServletName = servlet_name; jspServletClass = servlet_class; try { Loader.loadClass(this.getClass(), servlet_class); - hasJSP = true; } catch (ClassNotFoundException e) { LOG.info("NO JSP Support for {}, did not find {}", context.getContextPath(), servlet_class); jspServletClass = servlet_class = "org.eclipse.jetty.servlet.NoJspServlet"; } + } + + //could be more than 1 declaration of the jsp servlet, so configure them all + if (servlet_class != null && "org.apache.jasper.servlet.JspServlet".equals(servlet_class)) + { if (holder.getInitParameter("scratchdir") == null) { File tmp = context.getTempDirectory(); @@ -316,12 +319,12 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor } } - // Handler JSP file + // Handle JSP file String jsp_file = node.getString("jsp-file", false, true); if (jsp_file != null) { holder.setForcedPath(jsp_file); - holder.setClassName(jspServletClass); + holder.setClassName(jspServletClass); //only use our default instance //set the system classpath explicitly for the holder that will represent the JspServlet instance holder.setInitParameter("com.sun.appserv.jsp.classpath", getSystemClassPath(context)); }