Restored server push functionality. (#8760)

* Restored server push functionality.

* Moved Request.isPushSupported() to ConnectionMetaData.
* Removed HttpStream.isPushSupported().
* Implemented ee10 PushBuilder.
* Moved PushCacheFilterTest from core to ee10.
* Duplicated PushCacheFilterTest to ee9.

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
Co-authored-by: Greg Wilkins <gregw@webtide.com>
This commit is contained in:
Simone Bordet 2022-11-21 12:18:19 +01:00 committed by GitHub
parent 9cb6cc62d5
commit e7f6f6729a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1507 additions and 1152 deletions

View File

@ -84,7 +84,7 @@ public class HttpRequest implements Request
private List<HttpCookie> cookies; private List<HttpCookie> cookies;
private Map<String, Object> attributes; private Map<String, Object> attributes;
private List<RequestListener> requestListeners; private List<RequestListener> requestListeners;
private BiFunction<Request, Request, Response.CompleteListener> pushListener; private BiFunction<Request, Request, Response.CompleteListener> pushHandler;
private Supplier<HttpFields> trailers; private Supplier<HttpFields> trailers;
private String upgradeProtocol; private String upgradeProtocol;
private Object tag; private Object tag;
@ -607,6 +607,13 @@ public class HttpRequest implements Request
return this; return this;
} }
@Override
public Request onPush(BiFunction<Request, Request, Response.CompleteListener> pushHandler)
{
this.pushHandler = pushHandler;
return this;
}
@Override @Override
public Request onComplete(final Response.CompleteListener listener) public Request onComplete(final Response.CompleteListener listener)
{ {
@ -621,26 +628,6 @@ public class HttpRequest implements Request
return this; return this;
} }
/**
* <p>Sets a listener for pushed resources.</p>
* <p>When resources are pushed from the server, the given {@code listener}
* is invoked for every pushed resource.
* The parameters to the {@code BiFunction} are this request and the
* synthesized request for the pushed resource.
* The {@code BiFunction} should return a {@code CompleteListener} that
* may also implement other listener interfaces to be notified of various
* response events, or {@code null} to signal that the pushed resource
* should be canceled.</p>
*
* @param listener a listener for pushed resource events
* @return this request object
*/
public Request pushListener(BiFunction<Request, Request, Response.CompleteListener> listener)
{
this.pushListener = listener;
return this;
}
@Override @Override
public Request trailersSupplier(Supplier<HttpFields> trailers) public Request trailersSupplier(Supplier<HttpFields> trailers)
{ {
@ -800,9 +787,9 @@ public class HttpRequest implements Request
return responseListeners; return responseListeners;
} }
public BiFunction<Request, Request, Response.CompleteListener> getPushListener() public BiFunction<Request, Request, Response.CompleteListener> getPushHandler()
{ {
return pushListener; return pushHandler;
} }
@Override @Override

View File

@ -26,6 +26,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.function.BiFunction;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
@ -420,6 +421,22 @@ public interface Request
*/ */
Request onResponseFailure(Response.FailureListener listener); Request onResponseFailure(Response.FailureListener listener);
/**
* <p>Sets a handler for pushed resources.</p>
* <p>When resources are pushed from the server, the given {@code pushHandler}
* is invoked for every pushed resource.
* The parameters to the {@code BiFunction} are this request and the
* synthesized request for the pushed resource.
* The {@code BiFunction} should return a {@code CompleteListener} that
* may also implement other listener interfaces to be notified of various
* response events, or {@code null} to signal that the pushed resource
* should be canceled.</p>
*
* @param pushHandler a handler for pushed resource events
* @return this request object
*/
Request onPush(BiFunction<Request, Request, Response.CompleteListener> pushHandler);
/** /**
* @param listener a listener for complete event * @param listener a listener for complete event
* @return this request object * @return this request object

View File

@ -292,18 +292,6 @@ public class HttpStreamOverFCGI implements HttpStream
return _generator.generateResponseContent(_id, buffer, last, _aborted, callback); return _generator.generateResponseContent(_id, buffer, last, _aborted, callback);
} }
@Override
public boolean isPushSupported()
{
return false;
}
@Override
public void push(MetaData.Request request)
{
throw new UnsupportedOperationException();
}
@Override @Override
public boolean isCommitted() public boolean isCommitted()
{ {

View File

@ -168,7 +168,7 @@ public class HttpReceiverOverHTTP2 extends HttpReceiver implements HTTP2Channel.
HttpRequest pushRequest = (HttpRequest)getHttpDestination().getHttpClient().newRequest(metaData.getURIString()); HttpRequest pushRequest = (HttpRequest)getHttpDestination().getHttpClient().newRequest(metaData.getURIString());
// TODO: copy PUSH_PROMISE headers into pushRequest. // TODO: copy PUSH_PROMISE headers into pushRequest.
BiFunction<Request, Request, Response.CompleteListener> pushListener = request.getPushListener(); BiFunction<Request, Request, Response.CompleteListener> pushListener = request.getPushHandler();
if (pushListener != null) if (pushListener != null)
{ {
Response.CompleteListener listener = pushListener.apply(request, pushRequest); Response.CompleteListener listener = pushListener.apply(request, pushRequest);

View File

@ -389,6 +389,12 @@ public class HTTP2ServerConnection extends HTTP2Connection implements Connection
return getEndPoint() instanceof SslConnection.DecryptedEndPoint; return getEndPoint() instanceof SslConnection.DecryptedEndPoint;
} }
@Override
public boolean isPushSupported()
{
return getSession().isPushEnabled();
}
@Override @Override
public SocketAddress getRemoteSocketAddress() public SocketAddress getRemoteSocketAddress()
{ {

View File

@ -431,37 +431,31 @@ public class HttpStreamOverHTTP2 implements HttpStream, HTTP2Channel.Server
} }
@Override @Override
public boolean isPushSupported() public void push(MetaData.Request resource)
{
return _stream.getSession().isPushEnabled();
}
@Override
public void push(MetaData.Request request)
{ {
if (!_stream.getSession().isPushEnabled()) if (!_stream.getSession().isPushEnabled())
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("HTTP/2 push disabled for {}", request); LOG.debug("HTTP/2 push disabled for {}", resource);
return; return;
} }
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("HTTP/2 push {}", request); LOG.debug("HTTP/2 push {}", resource);
_stream.push(new PushPromiseFrame(_stream.getId(), request), new Promise<>() _stream.push(new PushPromiseFrame(_stream.getId(), resource), new Promise<>()
{ {
@Override @Override
public void succeeded(Stream pushStream) public void succeeded(Stream pushStream)
{ {
_connection.push((HTTP2Stream)pushStream, request); _connection.push((HTTP2Stream)pushStream, resource);
} }
@Override @Override
public void failed(Throwable x) public void failed(Throwable x)
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("Could not HTTP/2 push {}", request, x); LOG.debug("Could not HTTP/2 push {}", resource, x);
} }
}, null); // TODO: handle reset from the client ? }, null); // TODO: handle reset from the client ?
} }
@ -470,6 +464,7 @@ public class HttpStreamOverHTTP2 implements HttpStream, HTTP2Channel.Server
{ {
try try
{ {
_requestMetaData = request;
Runnable task = _httpChannel.onRequest(request); Runnable task = _httpChannel.onRequest(request);
_httpChannel.getRequest().setAttribute("org.eclipse.jetty.pushed", Boolean.TRUE); _httpChannel.getRequest().setAttribute("org.eclipse.jetty.pushed", Boolean.TRUE);

View File

@ -1,981 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.http2.tests;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.fail;
// TODO
@Disabled("move to ee9 or provide a PushCacheHandler")
public class PushCacheFilterTest extends AbstractTest
{
@Test
public void test()
{
fail();
}
// private String contextPath = "/push";
//
// @Override
// protected void customizeContext(ServletContextHandler context)
// {
// context.setContextPath(contextPath);
// context.addFilter(PushCacheFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
// }
//
// @Override
// protected MetaData.Request newRequest(String method, String path, HttpFields fields)
// {
// return new MetaData.Request(method, HttpScheme.HTTP.asString(), new HostPortHttpField("localhost:" + connector.getLocalPort()), contextPath + servletPath + path, HttpVersion.HTTP_2, fields, -1);
// }
//
// private String newURI(String pathInfo)
// {
// return "http://localhost:" + connector.getLocalPort() + contextPath + servletPath + pathInfo;
// }
//
// @Test
// public void testPush() throws Exception
// {
// final String primaryResource = "/primary.html";
// final String secondaryResource = "/secondary.png";
// final byte[] secondaryData = "SECONDARY".getBytes(StandardCharsets.UTF_8);
// start(new HttpServlet()
// {
// @Override
// protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
// {
// String requestURI = req.getRequestURI();
// ServletOutputStream output = resp.getOutputStream();
// if (requestURI.endsWith(primaryResource))
// output.print("<html><head></head><body>PRIMARY</body></html>");
// else if (requestURI.endsWith(secondaryResource))
// output.write(secondaryData);
// }
// });
//
// final Session session = newClient(new Session.Listener() {});
//
// // Request for the primary and secondary resource to build the cache.
// final String referrerURI = newURI(primaryResource);
// MetaData.Request primaryRequest = newRequest("GET", primaryResource, HttpFields.EMPTY);
// final CountDownLatch warmupLatch = new CountDownLatch(1);
// session.newStream(new HeadersFrame(primaryRequest, null, true), new Promise.Adapter<>(), new Stream.Listener()
// {
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// if (frame.isEndStream())
// {
// // Request for the secondary resource.
// HttpFields.Mutable secondaryFields = HttpFields.build()
// .put(HttpHeader.REFERER, referrerURI);
// MetaData.Request secondaryRequest = newRequest("GET", secondaryResource, secondaryFields);
// session.newStream(new HeadersFrame(secondaryRequest, null, true), new Promise.Adapter<>(), new Stream.Listener()
// {
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// warmupLatch.countDown();
// }
// });
// }
// }
// });
// assertTrue(warmupLatch.await(5, TimeUnit.SECONDS));
//
// // Request again the primary resource, we should get the secondary resource pushed.
// primaryRequest = newRequest("GET", primaryResource, HttpFields.EMPTY);
// final CountDownLatch primaryResponseLatch = new CountDownLatch(2);
// final CountDownLatch pushLatch = new CountDownLatch(2);
// session.newStream(new HeadersFrame(primaryRequest, null, true), new Promise.Adapter<>(), new Stream.Listener()
// {
// @Override
// public void onHeaders(Stream stream, HeadersFrame frame)
// {
// MetaData.Response response = (MetaData.Response)frame.getMetaData();
// if (response.getStatus() == HttpStatus.OK_200)
// primaryResponseLatch.countDown();
// }
//
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// if (frame.isEndStream())
// primaryResponseLatch.countDown();
// }
//
// @Override
// public Stream.Listener onPush(Stream stream, PushPromiseFrame frame)
// {
// return new Adapter()
// {
// @Override
// public void onHeaders(Stream stream, HeadersFrame frame)
// {
// MetaData.Response response = (MetaData.Response)frame.getMetaData();
// if (response.getStatus() == HttpStatus.OK_200)
// pushLatch.countDown();
// }
//
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// if (frame.isEndStream())
// pushLatch.countDown();
// }
// };
// }
// });
// assertTrue(pushLatch.await(5, TimeUnit.SECONDS));
// assertTrue(primaryResponseLatch.await(5, TimeUnit.SECONDS));
// }
//
// @Test
// public void testPushReferrerNoPath() throws Exception
// {
// final String primaryResource = "/primary.html";
// final String secondaryResource = "/secondary.png";
// final byte[] secondaryData = "SECONDARY".getBytes(StandardCharsets.UTF_8);
// start(new HttpServlet()
// {
// @Override
// protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
// {
// String requestURI = req.getRequestURI();
// ServletOutputStream output = resp.getOutputStream();
// if (requestURI.endsWith(primaryResource))
// output.print("<html><head></head><body>PRIMARY</body></html>");
// else if (requestURI.endsWith(secondaryResource))
// output.write(secondaryData);
// }
// });
//
// final Session session = newClient(new Session.Listener() {});
//
// // Request for the primary and secondary resource to build the cache.
// // The referrerURI does not point to the primary resource, so there will be no
// // resource association with the primary resource and therefore won't be pushed.
// final String referrerURI = "http://localhost:" + connector.getLocalPort();
// HttpFields.Mutable primaryFields = HttpFields.build();
// MetaData.Request primaryRequest = newRequest("GET", primaryResource, primaryFields);
// final CountDownLatch warmupLatch = new CountDownLatch(1);
// session.newStream(new HeadersFrame(primaryRequest, null, true), new Promise.Adapter<>(), new Stream.Listener()
// {
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// if (frame.isEndStream())
// {
// // Request for the secondary resource.
// HttpFields.Mutable secondaryFields = HttpFields.build()
// .put(HttpHeader.REFERER, referrerURI);
// MetaData.Request secondaryRequest = newRequest("GET", secondaryResource, secondaryFields);
// session.newStream(new HeadersFrame(secondaryRequest, null, true), new Promise.Adapter<>(), new Stream.Listener()
// {
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// warmupLatch.countDown();
// }
// });
// }
// }
// });
// assertTrue(warmupLatch.await(5, TimeUnit.SECONDS));
//
// // Request again the primary resource, we should not get the secondary resource pushed.
// primaryRequest = newRequest("GET", primaryResource, primaryFields);
// final CountDownLatch primaryResponseLatch = new CountDownLatch(1);
// final CountDownLatch pushLatch = new CountDownLatch(1);
// session.newStream(new HeadersFrame(primaryRequest, null, true), new Promise.Adapter<>(), new Stream.Listener()
// {
// @Override
// public Stream.Listener onPush(Stream stream, PushPromiseFrame frame)
// {
// return new Adapter()
// {
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// if (frame.isEndStream())
// pushLatch.countDown();
// }
// };
// }
//
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// if (frame.isEndStream())
// primaryResponseLatch.countDown();
// }
// });
// assertFalse(pushLatch.await(1, TimeUnit.SECONDS));
// assertTrue(primaryResponseLatch.await(5, TimeUnit.SECONDS));
// }
//
// @Test
// public void testPushIsReset() throws Exception
// {
// final String primaryResource = "/primary.html";
// final String secondaryResource = "/secondary.png";
// final byte[] secondaryData = "SECONDARY".getBytes(StandardCharsets.UTF_8);
// start(new HttpServlet()
// {
// @Override
// protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
// {
// String requestURI = req.getRequestURI();
// ServletOutputStream output = resp.getOutputStream();
// if (requestURI.endsWith(primaryResource))
// output.print("<html><head></head><body>PRIMARY</body></html>");
// else if (requestURI.endsWith(secondaryResource))
// output.write(secondaryData);
// }
// });
//
// final Session session = newClient(new Session.Listener() {});
//
// // Request for the primary and secondary resource to build the cache.
// final String primaryURI = newURI(primaryResource);
// HttpFields.Mutable primaryFields = HttpFields.build();
// MetaData.Request primaryRequest = newRequest("GET", primaryResource, primaryFields);
// final CountDownLatch warmupLatch = new CountDownLatch(1);
// session.newStream(new HeadersFrame(primaryRequest, null, true), new Promise.Adapter<>(), new Stream.Listener()
// {
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// if (frame.isEndStream())
// {
// // Request for the secondary resource.
// HttpFields.Mutable secondaryFields = HttpFields.build()
// .put(HttpHeader.REFERER, primaryURI);
// MetaData.Request secondaryRequest = newRequest("GET", secondaryResource, secondaryFields);
// session.newStream(new HeadersFrame(secondaryRequest, null, true), new Promise.Adapter<>(), new Stream.Listener()
// {
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// warmupLatch.countDown();
// }
// });
// }
// }
// });
// assertTrue(warmupLatch.await(5, TimeUnit.SECONDS));
//
// // Request again the primary resource, we should get the secondary resource pushed.
// primaryRequest = newRequest("GET", primaryResource, primaryFields);
// final CountDownLatch primaryResponseLatch = new CountDownLatch(1);
// final CountDownLatch pushLatch = new CountDownLatch(1);
// session.newStream(new HeadersFrame(primaryRequest, null, true), new Promise.Adapter<>(), new Stream.Listener()
// {
// @Override
// public Stream.Listener onPush(Stream stream, PushPromiseFrame frame)
// {
// // Reset the stream as soon as we see the push.
// ResetFrame resetFrame = new ResetFrame(stream.getId(), ErrorCode.REFUSED_STREAM_ERROR.code);
// stream.reset(resetFrame, Callback.NOOP);
// return new Adapter()
// {
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// pushLatch.countDown();
// }
// };
// }
//
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// if (frame.isEndStream())
// primaryResponseLatch.countDown();
// }
// });
// // We should not receive pushed data that we reset.
// assertFalse(pushLatch.await(1, TimeUnit.SECONDS));
// assertTrue(primaryResponseLatch.await(5, TimeUnit.SECONDS));
//
// // Make sure the session is sane by requesting the secondary resource.
// HttpFields.Mutable secondaryFields = HttpFields.build();
// secondaryFields.put(HttpHeader.REFERER, primaryURI);
// MetaData.Request secondaryRequest = newRequest("GET", secondaryResource, secondaryFields);
// final CountDownLatch secondaryResponseLatch = new CountDownLatch(1);
// session.newStream(new HeadersFrame(secondaryRequest, null, true), new Promise.Adapter<>(), new Stream.Listener()
// {
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// if (frame.isEndStream())
// secondaryResponseLatch.countDown();
// }
// });
// assertTrue(secondaryResponseLatch.await(5, TimeUnit.SECONDS));
// }
//
// @Test
// public void testPushWithoutPrimaryResponseContent() throws Exception
// {
// final String primaryResource = "/primary.html";
// final String secondaryResource = "/secondary.png";
// start(new HttpServlet()
// {
// @Override
// protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
// {
// String requestURI = request.getRequestURI();
// final ServletOutputStream output = response.getOutputStream();
// if (requestURI.endsWith(secondaryResource))
// output.write("SECONDARY".getBytes(StandardCharsets.UTF_8));
// }
// });
//
// final Session session = newClient(new Session.Listener() {});
//
// // Request for the primary and secondary resource to build the cache.
// final String primaryURI = newURI(primaryResource);
// HttpFields.Mutable primaryFields = HttpFields.build();
// MetaData.Request primaryRequest = newRequest("GET", primaryResource, primaryFields);
// final CountDownLatch warmupLatch = new CountDownLatch(1);
// session.newStream(new HeadersFrame(primaryRequest, null, true), new Promise.Adapter<>(), new Stream.Listener()
// {
// @Override
// public void onHeaders(Stream stream, HeadersFrame frame)
// {
// if (frame.isEndStream())
// {
// // Request for the secondary resource.
// HttpFields.Mutable secondaryFields = HttpFields.build();
// secondaryFields.put(HttpHeader.REFERER, primaryURI);
// MetaData.Request secondaryRequest = newRequest("GET", secondaryResource, secondaryFields);
// session.newStream(new HeadersFrame(secondaryRequest, null, true), new Promise.Adapter<>(), new Stream.Listener()
// {
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// warmupLatch.countDown();
// }
// });
// }
// }
// });
// assertTrue(warmupLatch.await(5, TimeUnit.SECONDS));
//
// Thread.sleep(1000);
//
// // Request again the primary resource, we should get the secondary resource pushed.
// primaryRequest = newRequest("GET", primaryResource, primaryFields);
// final CountDownLatch primaryResponseLatch = new CountDownLatch(1);
// final CountDownLatch pushLatch = new CountDownLatch(1);
// session.newStream(new HeadersFrame(primaryRequest, null, true), new Promise.Adapter<>(), new Stream.Listener()
// {
// @Override
// public void onHeaders(Stream stream, HeadersFrame frame)
// {
// if (frame.isEndStream())
// primaryResponseLatch.countDown();
// }
//
// @Override
// public Stream.Listener onPush(Stream stream, PushPromiseFrame frame)
// {
// return new Adapter()
// {
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// if (frame.isEndStream())
// pushLatch.countDown();
// }
// };
// }
// });
// assertTrue(pushLatch.await(5, TimeUnit.SECONDS));
// assertTrue(primaryResponseLatch.await(5, TimeUnit.SECONDS));
// }
//
// @Test
// public void testRecursivePush() throws Exception
// {
// final String primaryResource = "/primary.html";
// final String secondaryResource1 = "/secondary1.css";
// final String secondaryResource2 = "/secondary2.js";
// final String tertiaryResource = "/tertiary.png";
// start(new HttpServlet()
// {
// @Override
// protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
// {
// String requestURI = request.getRequestURI();
// final ServletOutputStream output = response.getOutputStream();
// if (requestURI.endsWith(primaryResource))
// output.print("<html><head></head><body>PRIMARY</body></html>");
// else if (requestURI.endsWith(secondaryResource1))
// output.print("body { background-image: url(\"" + tertiaryResource + "\"); }");
// else if (requestURI.endsWith(secondaryResource2))
// output.print("(function() { window.alert('HTTP/2'); })()");
// if (requestURI.endsWith(tertiaryResource))
// output.write("TERTIARY".getBytes(StandardCharsets.UTF_8));
// }
// });
//
// final Session session = newClient(new Session.Listener() {});
//
// // Request for the primary, secondary and tertiary resource to build the cache.
// final String primaryURI = newURI(primaryResource);
// HttpFields.Mutable primaryFields = HttpFields.build();
// MetaData.Request primaryRequest = newRequest("GET", primaryResource, primaryFields);
// final CountDownLatch warmupLatch = new CountDownLatch(2);
// session.newStream(new HeadersFrame(primaryRequest, null, true), new Promise.Adapter<>(), new Stream.Listener()
// {
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// if (frame.isEndStream())
// {
// // Request for the secondary resources.
// String secondaryURI1 = newURI(secondaryResource1);
// HttpFields.Mutable secondaryFields1 = HttpFields.build()
// .put(HttpHeader.REFERER, primaryURI);
// MetaData.Request secondaryRequest1 = newRequest("GET", secondaryResource1, secondaryFields1);
// session.newStream(new HeadersFrame(secondaryRequest1, null, true), new Promise.Adapter<>(), new Stream.Listener()
// {
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// if (frame.isEndStream())
// {
// // Request for the tertiary resource.
// HttpFields.Mutable tertiaryFields = HttpFields.build()
// .put(HttpHeader.REFERER, secondaryURI1);
// MetaData.Request tertiaryRequest = newRequest("GET", tertiaryResource, tertiaryFields);
// session.newStream(new HeadersFrame(tertiaryRequest, null, true), new Promise.Adapter<>(), new Adapter()
// {
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// if (frame.isEndStream())
// warmupLatch.countDown();
// }
// });
// }
// }
// });
//
// HttpFields.Mutable secondaryFields2 = HttpFields.build()
// .put(HttpHeader.REFERER, primaryURI);
// MetaData.Request secondaryRequest2 = newRequest("GET", secondaryResource2, secondaryFields2);
// session.newStream(new HeadersFrame(secondaryRequest2, null, true), new Promise.Adapter<>(), new Stream.Listener()
// {
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// if (frame.isEndStream())
// warmupLatch.countDown();
// }
// });
// }
// }
// });
// assertTrue(warmupLatch.await(5, TimeUnit.SECONDS));
//
// Thread.sleep(1000);
//
// // Request again the primary resource, we should get the secondary and tertiary resources pushed.
// primaryRequest = newRequest("GET", primaryResource, primaryFields);
// final CountDownLatch primaryResponseLatch = new CountDownLatch(1);
// final CountDownLatch primaryPushesLatch = new CountDownLatch(3);
// final CountDownLatch recursiveLatch = new CountDownLatch(1);
// session.newStream(new HeadersFrame(primaryRequest, null, true), new Promise.Adapter<>(), new Stream.Listener()
// {
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// if (frame.isEndStream())
// primaryResponseLatch.countDown();
// }
//
// @Override
// public Stream.Listener onPush(Stream stream, PushPromiseFrame frame)
// {
// // The stream id of the PUSH_PROMISE must
// // always be a client stream and therefore odd.
// assertEquals(1, frame.getStreamId() & 1);
// return new Adapter()
// {
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// if (frame.isEndStream())
// primaryPushesLatch.countDown();
// }
//
// @Override
// public Stream.Listener onPush(Stream stream, PushPromiseFrame frame)
// {
// return new Adapter()
// {
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// if (frame.isEndStream())
// recursiveLatch.countDown();
// }
// };
// }
// };
// }
// });
//
// assertTrue(primaryPushesLatch.await(5, TimeUnit.SECONDS));
// assertFalse(recursiveLatch.await(1, TimeUnit.SECONDS));
// assertTrue(primaryResponseLatch.await(5, TimeUnit.SECONDS));
//
// // Make sure that explicitly requesting a secondary resource, we get the tertiary pushed.
// CountDownLatch secondaryResponseLatch = new CountDownLatch(1);
// CountDownLatch secondaryPushLatch = new CountDownLatch(1);
// MetaData.Request secondaryRequest = newRequest("GET", secondaryResource1, HttpFields.EMPTY);
// session.newStream(new HeadersFrame(secondaryRequest, null, true), new Promise.Adapter<>(), new Stream.Listener()
// {
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// if (frame.isEndStream())
// secondaryResponseLatch.countDown();
// }
//
// @Override
// public Stream.Listener onPush(Stream stream, PushPromiseFrame frame)
// {
// return new Adapter()
// {
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// if (frame.isEndStream())
// secondaryPushLatch.countDown();
// }
// };
// }
// });
//
// assertTrue(secondaryPushLatch.await(5, TimeUnit.SECONDS));
// assertTrue(secondaryResponseLatch.await(5, TimeUnit.SECONDS));
// }
//
// @Test
// public void testSelfPush() throws Exception
// {
// // The test case is that of a login page, for example.
// // When the user sends the credentials to the login page,
// // the login may fail and redirect to the same login page,
// // perhaps with different query parameters.
// // In this case a request for the login page will push
// // the login page itself, which will generate the pushed
// // request for the login page, which will push the login
// // page itself, etc. which is not the desired behavior.
//
// final String primaryResource = "/login.html";
// start(new HttpServlet()
// {
// @Override
// protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
// {
// ServletOutputStream output = response.getOutputStream();
// String credentials = request.getParameter("credentials");
// if (credentials == null)
// {
// output.print("<html><head></head><body>LOGIN</body></html>");
// }
// else if ("secret".equals(credentials))
// {
// output.print("<html><head></head><body>OK</body></html>");
// }
// else
// {
// response.setStatus(HttpStatus.TEMPORARY_REDIRECT_307);
// response.getHeaders().put(HttpHeader.LOCATION, primaryResource);
// }
// }
// });
// final String primaryURI = newURI(primaryResource);
//
// final Session session = newClient(new Session.Listener() {});
//
// // Login with the wrong credentials, causing a redirect to self.
// HttpFields.Mutable primaryFields = HttpFields.build();
// MetaData.Request primaryRequest = newRequest("GET", primaryResource + "?credentials=wrong", primaryFields);
// final CountDownLatch warmupLatch = new CountDownLatch(1);
// session.newStream(new HeadersFrame(primaryRequest, null, true), new Promise.Adapter<>(), new Stream.Listener()
// {
// @Override
// public void onHeaders(Stream stream, HeadersFrame frame)
// {
// if (frame.isEndStream())
// {
// MetaData.Response response = (MetaData.Response)frame.getMetaData();
// if (response.getStatus() == HttpStatus.TEMPORARY_REDIRECT_307)
// {
// // Follow the redirect.
// String location = response.getFields().get(HttpHeader.LOCATION);
// HttpFields.Mutable redirectFields = HttpFields.build();
// redirectFields.put(HttpHeader.REFERER, primaryURI);
// MetaData.Request redirectRequest = newRequest("GET", location, redirectFields);
// session.newStream(new HeadersFrame(redirectRequest, null, true), new Promise.Adapter<>(), new Adapter()
// {
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// if (frame.isEndStream())
// warmupLatch.countDown();
// }
// });
// }
// }
// }
// });
// assertTrue(warmupLatch.await(5, TimeUnit.SECONDS));
//
// Thread.sleep(1000);
//
// // Login with the right credentials, there must be no push.
// primaryRequest = newRequest("GET", primaryResource + "?credentials=secret", primaryFields);
// final CountDownLatch primaryResponseLatch = new CountDownLatch(1);
// final CountDownLatch pushLatch = new CountDownLatch(1);
// session.newStream(new HeadersFrame(primaryRequest, null, true), new Promise.Adapter<>(), new Stream.Listener()
// {
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// if (frame.isEndStream())
// primaryResponseLatch.countDown();
// }
//
// @Override
// public Stream.Listener onPush(Stream stream, PushPromiseFrame frame)
// {
// pushLatch.countDown();
// return null;
// }
// });
// assertFalse(pushLatch.await(1, TimeUnit.SECONDS));
// assertTrue(primaryResponseLatch.await(5, TimeUnit.SECONDS));
// }
//
// @Test
// public void testPushWithQueryParameters() throws Exception
// {
// String name = "foo";
// String value = "bar";
// final String primaryResource = "/primary.html?" + name + "=" + value;
// final String secondaryResource = "/secondary.html?" + name + "=" + value;
// start(new HttpServlet()
// {
// @Override
// protected void doGet(HttpServletRequest request, HttpServletResponse response)
// {
// String requestURI = request.getRequestURI();
// if (requestURI.endsWith(primaryResource))
// {
// response.setStatus(HttpStatus.OK_200);
// }
// else if (requestURI.endsWith(secondaryResource))
// {
// String param = request.getParameter(name);
// if (param == null)
// response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500);
// else
// response.setStatus(HttpStatus.OK_200);
// }
// }
// });
//
// final Session session = newClient(new Session.Listener() {});
//
// // Request for the primary and secondary resource to build the cache.
// final String primaryURI = newURI(primaryResource);
// HttpFields.Mutable primaryFields = HttpFields.build();
// MetaData.Request primaryRequest = newRequest("GET", primaryResource, primaryFields);
// final CountDownLatch warmupLatch = new CountDownLatch(1);
// session.newStream(new HeadersFrame(primaryRequest, null, true), new Promise.Adapter<>(), new Stream.Listener()
// {
// @Override
// public void onHeaders(Stream stream, HeadersFrame frame)
// {
// if (frame.isEndStream())
// {
// // Request for the secondary resource.
// HttpFields.Mutable secondaryFields = HttpFields.build();
// secondaryFields.put(HttpHeader.REFERER, primaryURI);
// MetaData.Request secondaryRequest = newRequest("GET", secondaryResource, secondaryFields);
// session.newStream(new HeadersFrame(secondaryRequest, null, true), new Promise.Adapter<>(), new Stream.Listener()
// {
// @Override
// public void onHeaders(Stream stream, HeadersFrame frame)
// {
// if (frame.isEndStream())
// warmupLatch.countDown();
// }
// });
// }
// }
// });
// assertTrue(warmupLatch.await(5, TimeUnit.SECONDS));
//
// Thread.sleep(1000);
//
// // Request again the primary resource, we should get the secondary resource pushed.
// primaryRequest = newRequest("GET", primaryResource, primaryFields);
// final CountDownLatch primaryResponseLatch = new CountDownLatch(1);
// final CountDownLatch pushLatch = new CountDownLatch(1);
// session.newStream(new HeadersFrame(primaryRequest, null, true), new Promise.Adapter<>(), new Stream.Listener()
// {
// @Override
// public Stream.Listener onPush(Stream stream, PushPromiseFrame frame)
// {
// MetaData metaData = frame.getMetaData();
// assertTrue(metaData instanceof MetaData.Request);
// MetaData.Request pushedRequest = (MetaData.Request)metaData;
// assertEquals(contextPath + servletPath + secondaryResource, pushedRequest.getURI().getPathQuery());
// return new Adapter()
// {
// @Override
// public void onHeaders(Stream stream, HeadersFrame frame)
// {
// if (frame.isEndStream())
// {
// MetaData.Response response = (MetaData.Response)frame.getMetaData();
// if (response.getStatus() == HttpStatus.OK_200)
// pushLatch.countDown();
// }
// }
// };
// }
//
// @Override
// public void onHeaders(Stream stream, HeadersFrame frame)
// {
// if (frame.isEndStream())
// primaryResponseLatch.countDown();
// }
// });
// assertTrue(pushLatch.await(5, TimeUnit.SECONDS));
// assertTrue(primaryResponseLatch.await(5, TimeUnit.SECONDS));
// }
//
// @Test
// public void testPOSTRequestIsNotPushed() throws Exception
// {
// final String primaryResource = "/primary.html";
// final String secondaryResource = "/secondary.png";
// final byte[] secondaryData = "SECONDARY".getBytes(StandardCharsets.UTF_8);
// start(new HttpServlet()
// {
// @Override
// protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException
// {
// String requestURI = req.getRequestURI();
// ServletOutputStream output = resp.getOutputStream();
// if (requestURI.endsWith(primaryResource))
// output.print("<html><head></head><body>PRIMARY</body></html>");
// else if (requestURI.endsWith(secondaryResource))
// output.write(secondaryData);
// }
// });
//
// final Session session = newClient(new Session.Listener() {});
//
// // Request for the primary and secondary resource to build the cache.
// final String referrerURI = newURI(primaryResource);
// HttpFields.Mutable primaryFields = HttpFields.build();
// MetaData.Request primaryRequest = newRequest("GET", primaryResource, primaryFields);
// final CountDownLatch warmupLatch = new CountDownLatch(1);
// session.newStream(new HeadersFrame(primaryRequest, null, true), new Promise.Adapter<>(), new Stream.Listener()
// {
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// if (frame.isEndStream())
// {
// // Request for the secondary resource.
// HttpFields.Mutable secondaryFields = HttpFields.build();
// secondaryFields.put(HttpHeader.REFERER, referrerURI);
// MetaData.Request secondaryRequest = newRequest("GET", secondaryResource, secondaryFields);
// session.newStream(new HeadersFrame(secondaryRequest, null, true), new Promise.Adapter<>(), new Stream.Listener()
// {
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// warmupLatch.countDown();
// }
// });
// }
// }
// });
// assertTrue(warmupLatch.await(5, TimeUnit.SECONDS));
//
// // Request again the primary resource with POST, we should not get the secondary resource pushed.
// primaryRequest = newRequest("POST", primaryResource, primaryFields);
// final CountDownLatch primaryResponseLatch = new CountDownLatch(1);
// final CountDownLatch pushLatch = new CountDownLatch(1);
// session.newStream(new HeadersFrame(primaryRequest, null, true), new Promise.Adapter<>(), new Stream.Listener()
// {
// @Override
// public Stream.Listener onPush(Stream stream, PushPromiseFrame frame)
// {
// return new Adapter()
// {
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// if (frame.isEndStream())
// pushLatch.countDown();
// }
// };
// }
//
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// if (frame.isEndStream())
// primaryResponseLatch.countDown();
// }
// });
// assertTrue(primaryResponseLatch.await(5, TimeUnit.SECONDS));
// assertFalse(pushLatch.await(1, TimeUnit.SECONDS));
// }
//
// @Test
// public void testPushDisabled() throws Exception
// {
// final String primaryResource = "/primary.html";
// final String secondaryResource = "/secondary.png";
// final byte[] secondaryData = "SECONDARY".getBytes(StandardCharsets.UTF_8);
// start(new HttpServlet()
// {
// @Override
// protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
// {
// String requestURI = req.getRequestURI();
// ServletOutputStream output = resp.getOutputStream();
// if (requestURI.endsWith(primaryResource))
// output.print("<html><head></head><body>PRIMARY</body></html>");
// else if (requestURI.endsWith(secondaryResource))
// output.write(secondaryData);
// }
// });
//
// final Session session = newClient(new Session.Listener()
// {
// @Override
// public Map<Integer, Integer> onPreface(Session session)
// {
// Map<Integer, Integer> settings = new HashMap<>();
// settings.put(SettingsFrame.ENABLE_PUSH, 0);
// return settings;
// }
// });
//
// // Request for the primary and secondary resource to build the cache.
// final String referrerURI = newURI(primaryResource);
// HttpFields.Mutable primaryFields = HttpFields.build();
// MetaData.Request primaryRequest = newRequest("GET", primaryResource, primaryFields);
// final CountDownLatch warmupLatch = new CountDownLatch(1);
// session.newStream(new HeadersFrame(primaryRequest, null, true), new Promise.Adapter<>(), new Stream.Listener()
// {
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// if (frame.isEndStream())
// {
// // Request for the secondary resource.
// HttpFields.Mutable secondaryFields = HttpFields.build();
// secondaryFields.put(HttpHeader.REFERER, referrerURI);
// MetaData.Request secondaryRequest = newRequest("GET", secondaryResource, secondaryFields);
// session.newStream(new HeadersFrame(secondaryRequest, null, true), new Promise.Adapter<>(), new Stream.Listener()
// {
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// warmupLatch.countDown();
// }
// });
// }
// }
// });
// assertTrue(warmupLatch.await(5, TimeUnit.SECONDS));
//
// // Request again the primary resource, we should not get the secondary resource pushed.
// primaryRequest = newRequest("GET", primaryResource, primaryFields);
// final CountDownLatch primaryResponseLatch = new CountDownLatch(1);
// final CountDownLatch pushLatch = new CountDownLatch(1);
// session.newStream(new HeadersFrame(primaryRequest, null, true), new Promise.Adapter<>(), new Stream.Listener()
// {
// @Override
// public Stream.Listener onPush(Stream stream, PushPromiseFrame frame)
// {
// pushLatch.countDown();
// return null;
// }
//
// @Override
// public void onData(Stream stream, DataFrame frame, Callback callback)
// {
// callback.succeeded();
// if (frame.isEndStream())
// primaryResponseLatch.countDown();
// }
// });
// assertFalse(pushLatch.await(1, TimeUnit.SECONDS));
// assertTrue(primaryResponseLatch.await(5, TimeUnit.SECONDS));
// }
}

View File

@ -14,25 +14,30 @@
package org.eclipse.jetty.http2.tests; package org.eclipse.jetty.http2.tests;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.Random; import java.util.Random;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.client.HttpRequest;
import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Result; import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.BufferingResponseListener; import org.eclipse.jetty.client.util.BufferingResponseListener;
import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http2.api.Session;
import org.eclipse.jetty.http2.api.Stream; import org.eclipse.jetty.http2.api.Stream;
import org.eclipse.jetty.http2.api.server.ServerSessionListener; import org.eclipse.jetty.http2.api.server.ServerSessionListener;
import org.eclipse.jetty.http2.frames.HeadersFrame; import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.frames.PushPromiseFrame; import org.eclipse.jetty.http2.frames.PushPromiseFrame;
import org.eclipse.jetty.http2.frames.ResetFrame; import org.eclipse.jetty.http2.frames.ResetFrame;
import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Response;
@ -42,6 +47,7 @@ import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
public class PushedResourcesTest extends AbstractTest public class PushedResourcesTest extends AbstractTest
@ -80,9 +86,8 @@ public class PushedResourcesTest extends AbstractTest
} }
}); });
HttpRequest request = (HttpRequest)httpClient.newRequest("localhost", connector.getLocalPort()); ContentResponse response = httpClient.newRequest("localhost", connector.getLocalPort())
ContentResponse response = request .onPush((mainRequest, pushedRequest) -> null)
.pushListener((mainRequest, pushedRequest) -> null)
.timeout(5, TimeUnit.SECONDS) .timeout(5, TimeUnit.SECONDS)
.send(); .send();
@ -130,9 +135,8 @@ public class PushedResourcesTest extends AbstractTest
CountDownLatch latch1 = new CountDownLatch(1); CountDownLatch latch1 = new CountDownLatch(1);
CountDownLatch latch2 = new CountDownLatch(1); CountDownLatch latch2 = new CountDownLatch(1);
HttpRequest request = (HttpRequest)httpClient.newRequest("localhost", connector.getLocalPort()); ContentResponse response = httpClient.newRequest("localhost", connector.getLocalPort())
ContentResponse response = request .onPush((mainRequest, pushedRequest) -> new BufferingResponseListener()
.pushListener((mainRequest, pushedRequest) -> new BufferingResponseListener()
{ {
@Override @Override
public void onComplete(Result result) public void onComplete(Result result)
@ -191,9 +195,8 @@ public class PushedResourcesTest extends AbstractTest
}); });
CountDownLatch latch = new CountDownLatch(1); CountDownLatch latch = new CountDownLatch(1);
HttpRequest request = (HttpRequest)httpClient.newRequest("localhost", connector.getLocalPort()); ContentResponse response = httpClient.newRequest("localhost", connector.getLocalPort())
ContentResponse response = request .onPush((mainRequest, pushedRequest) -> new BufferingResponseListener()
.pushListener((mainRequest, pushedRequest) -> new BufferingResponseListener()
{ {
@Override @Override
public void onComplete(Result result) public void onComplete(Result result)
@ -211,4 +214,118 @@ public class PushedResourcesTest extends AbstractTest
assertEquals(HttpStatus.OK_200, response.getStatus()); assertEquals(HttpStatus.OK_200, response.getStatus());
assertTrue(latch.await(5, TimeUnit.SECONDS)); assertTrue(latch.await(5, TimeUnit.SECONDS));
} }
@Test
public void testPushDisabled() throws Exception
{
String primaryResource = "/primary.html";
String secondaryResource = "/secondary.png";
String secondaryData = "SECONDARY";
start(new Handler.Processor()
{
@Override
public void process(Request request, Response response, Callback callback)
{
String requestURI = Request.getPathInContext(request);
if (requestURI.endsWith(primaryResource))
{
assertFalse(request.getConnectionMetaData().isPushSupported());
Content.Sink.write(response, true, "<html><head></head><body>PRIMARY</body></html>", callback);
}
else if (requestURI.endsWith(secondaryResource))
{
Content.Sink.write(response, true, secondaryData, callback);
}
else
{
callback.succeeded();
}
}
});
Session session = newClientSession(new Session.Listener()
{
@Override
public Map<Integer, Integer> onPreface(Session session)
{
Map<Integer, Integer> settings = new HashMap<>();
settings.put(SettingsFrame.ENABLE_PUSH, 0);
return settings;
}
});
// Request for the primary and secondary resource to build the cache.
HttpFields.Mutable primaryFields = HttpFields.build();
MetaData.Request primaryRequest = newRequest("GET", primaryResource, primaryFields);
String referrerURI = primaryRequest.getURIString();
CountDownLatch warmupLatch = new CountDownLatch(1);
session.newStream(new HeadersFrame(primaryRequest, null, true), new Stream.Listener()
{
@Override
public void onDataAvailable(Stream stream)
{
Stream.Data data = stream.readData();
if (data == null)
{
stream.demand();
return;
}
data.release();
if (data.frame().isEndStream())
{
// Request for the secondary resource.
HttpFields.Mutable secondaryFields = HttpFields.build();
secondaryFields.put(HttpHeader.REFERER, referrerURI);
MetaData.Request secondaryRequest = newRequest("GET", secondaryResource, secondaryFields);
session.newStream(new HeadersFrame(secondaryRequest, null, true), new Stream.Listener()
{
@Override
public void onDataAvailable(Stream stream)
{
Stream.Data data = stream.readData();
if (data == null)
{
stream.demand();
return;
}
data.release();
if (data.frame().isEndStream())
warmupLatch.countDown();
}
});
}
}
});
assertTrue(warmupLatch.await(5, TimeUnit.SECONDS));
// Request again the primary resource, we should not get the secondary resource pushed.
primaryRequest = newRequest("GET", primaryResource, primaryFields);
CountDownLatch primaryResponseLatch = new CountDownLatch(1);
CountDownLatch pushLatch = new CountDownLatch(1);
session.newStream(new HeadersFrame(primaryRequest, null, true), new Promise.Adapter<>(), new Stream.Listener()
{
@Override
public Stream.Listener onPush(Stream stream, PushPromiseFrame frame)
{
pushLatch.countDown();
return null;
}
@Override
public void onDataAvailable(Stream stream)
{
Stream.Data data = stream.readData();
if (data == null)
{
stream.demand();
return;
}
data.release();
if (data.frame().isEndStream())
primaryResponseLatch.countDown();
}
});
assertTrue(primaryResponseLatch.await(5, TimeUnit.SECONDS));
assertFalse(pushLatch.await(1, TimeUnit.SECONDS));
}
} }

View File

@ -456,18 +456,6 @@ public class HttpStreamOverHTTP3 implements HttpStream
return stream.trailer(frame); return stream.trailer(frame);
} }
@Override
public boolean isPushSupported()
{
return false;
}
@Override
public void push(MetaData.Request request)
{
throw new UnsupportedOperationException();
}
@Override @Override
public boolean isCommitted() public boolean isCommitted()
{ {

View File

@ -45,6 +45,14 @@ public interface ConnectionMetaData extends Attributes
boolean isSecure(); boolean isSecure();
/**
* @return whether the functionality of pushing resources is supported
*/
default boolean isPushSupported()
{
return false;
}
/** /**
* @return The address of the remote end of this connection. By default, this is the first hop of the underlying * @return The address of the remote end of this connection. By default, this is the first hop of the underlying
* network connection, but it may be wrapped to represent a more remote end point. * network connection, but it may be wrapped to represent a more remote end point.
@ -137,6 +145,12 @@ public interface ConnectionMetaData extends Attributes
return getWrapped().isSecure(); return getWrapped().isSecure();
} }
@Override
public boolean isPushSupported()
{
return getWrapped().isPushSupported();
}
@Override @Override
public SocketAddress getRemoteSocketAddress() public SocketAddress getRemoteSocketAddress()
{ {

View File

@ -85,9 +85,17 @@ public interface HttpStream extends Callback
*/ */
void send(MetaData.Request request, MetaData.Response response, boolean last, ByteBuffer content, Callback callback); void send(MetaData.Request request, MetaData.Response response, boolean last, ByteBuffer content, Callback callback);
boolean isPushSupported(); /**
* <p>Pushes the given {@code resource} to the client.</p>
void push(MetaData.Request request); *
* @param resource the resource to push
* @throws UnsupportedOperationException if the push functionality is not supported
* @see ConnectionMetaData#isPushSupported()
*/
default void push(MetaData.Request resource)
{
throw new UnsupportedOperationException();
}
boolean isCommitted(); boolean isCommitted();
@ -170,15 +178,9 @@ public interface HttpStream extends Callback
} }
@Override @Override
public final boolean isPushSupported() public void push(MetaData.Request resource)
{ {
return getWrapped().isPushSupported(); getWrapped().push(resource);
}
@Override
public void push(MetaData.Request request)
{
getWrapped().push(request);
} }
@Override @Override

View File

@ -203,14 +203,18 @@ public interface Request extends Attributes, Content.Source
@Override @Override
Content.Chunk read(); Content.Chunk read();
// TODO should this be on the connectionMetaData? /**
default boolean isPushSupported() * <p>Pushes the given {@code resource} to the client.</p>
*
* @param resource the resource to push
* @throws UnsupportedOperationException if the push functionality is not supported
* @see ConnectionMetaData#isPushSupported()
*/
default void push(MetaData.Request resource)
{ {
return false; // TODO throw new UnsupportedOperationException();
} }
void push(MetaData.Request request); // TODO
/** /**
* <p>Adds a listener for asynchronous errors.</p> * <p>Adds a listener for asynchronous errors.</p>
* <p>The listener is a predicate function that should return {@code true} to indicate * <p>The listener is a predicate function that should return {@code true} to indicate
@ -582,15 +586,9 @@ public interface Request extends Attributes, Content.Source
} }
@Override @Override
public boolean isPushSupported() public void push(MetaData.Request resource)
{ {
return getWrapped().isPushSupported(); getWrapped().push(resource);
}
@Override
public void push(MetaData.Request request)
{
getWrapped().push(request);
} }
@Override @Override

View File

@ -999,15 +999,9 @@ public class HttpChannelState implements HttpChannel, Components
} }
@Override @Override
public boolean isPushSupported() public void push(MetaData.Request resource)
{ {
return true; getHttpStream().push(resource);
}
@Override
public void push(MetaData.Request request)
{
getHttpStream().push(request);
} }
@Override @Override

View File

@ -1441,18 +1441,6 @@ public class HttpConnection extends AbstractConnection implements Runnable, Writ
_sendCallback.iterate(); _sendCallback.iterate();
} }
@Override
public boolean isPushSupported()
{
return false;
}
@Override
public void push(MetaData.Request request)
{
throw new UnsupportedOperationException();
}
@Override @Override
public boolean isCommitted() public boolean isCommitted()
{ {

View File

@ -931,10 +931,10 @@ public class HttpChannelTest
} }
@Override @Override
public void push(MetaData.Request request) public void push(MetaData.Request resource)
{ {
history.add("push"); history.add("push");
super.push(request); super.push(resource);
} }
@Override @Override

View File

@ -204,18 +204,6 @@ public class MockHttpStream implements HttpStream
callback.succeeded(); callback.succeeded();
} }
@Override
public boolean isPushSupported()
{
return false;
}
@Override
public void push(MetaData.Request request)
{
throw new UnsupportedOperationException();
}
@Override @Override
public boolean isCommitted() public boolean isCommitted()
{ {

View File

@ -153,17 +153,6 @@ public class TestableRequest implements Request
return false; return false;
} }
@Override
public boolean isPushSupported()
{
return false;
}
@Override
public void push(org.eclipse.jetty.http.MetaData.Request request)
{
}
@Override @Override
public TunnelSupport getTunnelSupport() public TunnelSupport getTunnelSupport()
{ {

View File

@ -0,0 +1,165 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.ee10.servlet;
import java.util.Set;
import jakarta.servlet.http.PushBuilder;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.util.URIUtil;
class PushBuilderImpl implements PushBuilder
{
private final ServletContextRequest _request;
private final HttpFields.Mutable _headers;
private String _method;
private String _query;
private String _sessionId;
private String _path;
public PushBuilderImpl(ServletContextRequest request, HttpFields.Mutable headers, String sessionId)
{
_request = request;
_headers = headers;
_method = request.getMethod();
_query = request.getHttpURI().getQuery();
_sessionId = sessionId;
}
@Override
public PushBuilder method(String method)
{
HttpMethod httpMethod = HttpMethod.fromString(method);
if (httpMethod == null || !httpMethod.isSafe())
throw new IllegalArgumentException("method not allowed for push: " + method);
_method = httpMethod.asString();
return this;
}
@Override
public PushBuilder queryString(String queryString)
{
_query = queryString;
return this;
}
@Override
public PushBuilder sessionId(String sessionId)
{
_sessionId = sessionId;
return this;
}
@Override
public PushBuilder setHeader(String name, String value)
{
_headers.put(name, value);
return this;
}
@Override
public PushBuilder addHeader(String name, String value)
{
_headers.add(name, value);
return this;
}
@Override
public PushBuilder removeHeader(String name)
{
_headers.remove(name);
return this;
}
@Override
public PushBuilder path(String path)
{
_path = path;
return this;
}
@Override
public void push()
{
String pushPath = getPath();
if (pushPath == null || pushPath.isBlank())
throw new IllegalArgumentException("invalid push path: " + pushPath);
String query = getQueryString();
String pushQuery = query;
int q = pushPath.indexOf('?');
if (q > 0)
{
pushQuery = pushPath.substring(q + 1);
if (query != null)
pushQuery += "&" + query;
pushPath = pushPath.substring(0, q);
}
if (!pushPath.startsWith("/"))
pushPath = URIUtil.addPaths(_request.getContext().getContextPath(), pushPath);
String pushParam = null;
if (_sessionId != null)
{
if (_request.getServletApiRequest().isRequestedSessionIdFromURL())
pushParam = "jsessionid=" + _sessionId;
}
HttpURI pushURI = HttpURI.build(_request.getHttpURI(), pushPath, pushParam, pushQuery).normalize();
MetaData.Request push = new MetaData.Request(_method, pushURI, _request.getConnectionMetaData().getHttpVersion(), _headers);
_request.push(push);
_path = null;
}
@Override
public String getMethod()
{
return _method;
}
@Override
public String getQueryString()
{
return _query;
}
@Override
public String getSessionId()
{
return _sessionId;
}
@Override
public Set<String> getHeaderNames()
{
return _headers.getFieldNamesCollection();
}
@Override
public String getHeader(String name)
{
return _headers.get(name);
}
@Override
public String getPath()
{
return _path;
}
}

View File

@ -23,6 +23,7 @@ import java.security.Principal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.EventListener; import java.util.EventListener;
import java.util.HashMap; import java.util.HashMap;
@ -811,8 +812,75 @@ public class ServletContextRequest extends ContextRequest implements Runnable
@Override @Override
public PushBuilder newPushBuilder() public PushBuilder newPushBuilder()
{ {
// TODO NYI if (!getConnectionMetaData().isPushSupported())
return null; return null;
HttpFields.Mutable pushHeaders = HttpFields.build(ServletContextRequest.this.getHeaders(), EnumSet.of(
HttpHeader.IF_MATCH,
HttpHeader.IF_RANGE,
HttpHeader.IF_UNMODIFIED_SINCE,
HttpHeader.RANGE,
HttpHeader.EXPECT,
HttpHeader.IF_NONE_MATCH,
HttpHeader.IF_MODIFIED_SINCE)
);
String referrer = getRequestURL().toString();
String query = getQueryString();
if (query != null)
referrer += "?" + query;
pushHeaders.put(HttpHeader.REFERER, referrer);
// Any Set-Cookie in the response should be present in the push.
HttpFields.Mutable responseHeaders = getResponse().getHeaders();
List<String> setCookies = new ArrayList<>(responseHeaders.getValuesList(HttpHeader.SET_COOKIE));
setCookies.addAll(responseHeaders.getValuesList(HttpHeader.SET_COOKIE2));
String cookies = pushHeaders.get(HttpHeader.COOKIE);
if (!setCookies.isEmpty())
{
StringBuilder pushCookies = new StringBuilder();
if (cookies != null)
pushCookies.append(cookies);
for (String setCookie : setCookies)
{
Map<String, String> cookieFields = HttpCookie.extractBasics(setCookie);
String cookieName = cookieFields.get("name");
String cookieValue = cookieFields.get("value");
String cookieMaxAge = cookieFields.get("max-age");
long maxAge = cookieMaxAge != null ? Long.parseLong(cookieMaxAge) : -1;
if (maxAge > 0)
{
if (pushCookies.length() > 0)
pushCookies.append("; ");
pushCookies.append(cookieName).append("=").append(cookieValue);
}
}
pushHeaders.put(HttpHeader.COOKIE, pushCookies.toString());
}
String sessionId;
HttpSession httpSession = getSession(false);
if (httpSession != null)
{
try
{
// Check that the session is valid;
httpSession.getLastAccessedTime();
sessionId = httpSession.getId();
}
catch (Throwable x)
{
if (LOG.isTraceEnabled())
LOG.trace("invalid HTTP session", x);
sessionId = getRequestedSessionId();
}
}
else
{
sessionId = getRequestedSessionId();
}
return new PushBuilderImpl(ServletContextRequest.this, pushHeaders, sessionId);
} }
@Override @Override

View File

@ -91,6 +91,11 @@
<artifactId>jetty-ee10-servlet</artifactId> <artifactId>jetty-ee10-servlet</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.eclipse.jetty.ee10</groupId>
<artifactId>jetty-ee10-servlets</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.eclipse.jetty</groupId> <groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-client</artifactId> <artifactId>jetty-client</artifactId>

View File

@ -85,6 +85,13 @@ public class AbstractTest
return transports; return transports;
} }
public static Collection<Transport> transportsWithPushSupport()
{
Collection<Transport> transports = transports();
transports.retainAll(List.of(Transport.H2C, Transport.H2));
return transports;
}
@AfterEach @AfterEach
public void dispose() public void dispose()
{ {

View File

@ -0,0 +1,515 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.ee10.test.client.transport;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.EnumSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.client.HttpDestination;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.BufferingResponseListener;
import org.eclipse.jetty.ee10.servlets.PushCacheFilter;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class PushCacheFilterTest extends AbstractTest
{
@ParameterizedTest
@MethodSource("transportsWithPushSupport")
public void testPush(Transport transport) throws Exception
{
String primaryResource = "/primary.html";
String secondaryResource = "/secondary.png";
byte[] secondaryData = "SECONDARY".getBytes(StandardCharsets.UTF_8);
start(transport, new HttpServlet()
{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
{
String requestURI = req.getRequestURI();
ServletOutputStream output = resp.getOutputStream();
if (requestURI.endsWith(primaryResource))
output.print("<html><head></head><body>PRIMARY</body></html>");
else if (requestURI.endsWith(secondaryResource))
output.write(secondaryData);
}
});
servletContextHandler.addFilter(PushCacheFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
// Request for the primary and secondary resources to build the cache.
URI uri = newURI(transport);
ContentResponse response = client.newRequest(uri)
.path(primaryResource)
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
response = client.newRequest(uri)
.path(secondaryResource)
.headers(headers -> headers.put(HttpHeader.REFERER, uri.resolve(primaryResource).toString()))
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
// Request again the primary resource, we should get the secondary resource pushed.
CountDownLatch pushLatch = new CountDownLatch(2);
response = client.newRequest(uri)
.path(primaryResource)
.onPush((request, pushed) ->
{
pushLatch.countDown();
return new BufferingResponseListener()
{
@Override
public void onComplete(Result result)
{
pushLatch.countDown();
}
};
})
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
assertTrue(pushLatch.await(5, TimeUnit.SECONDS));
}
@ParameterizedTest
@MethodSource("transportsWithPushSupport")
public void testPushReferrerNoPath(Transport transport) throws Exception
{
String primaryResource = "/primary.html";
String secondaryResource = "/secondary.png";
byte[] secondaryData = "SECONDARY".getBytes(StandardCharsets.UTF_8);
start(transport, new HttpServlet()
{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
{
String requestURI = req.getRequestURI();
ServletOutputStream output = resp.getOutputStream();
if (requestURI.endsWith(primaryResource))
output.print("<html><head></head><body>PRIMARY</body></html>");
else if (requestURI.endsWith(secondaryResource))
output.write(secondaryData);
}
});
servletContextHandler.addFilter(PushCacheFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
// Request for the primary and secondary resources to build the cache.
// The referrerURI does not point to the primary resource, so there will be no
// resource association with the primary resource and therefore won't be pushed.
URI uri = newURI(transport);
ContentResponse response = client.newRequest(uri)
.path(primaryResource)
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
response = client.newRequest(uri)
.path(secondaryResource)
.headers(headers -> headers.put(HttpHeader.REFERER, uri.toString()))
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
// Request again the primary resource, we should not get the secondary resource pushed.
CountDownLatch pushLatch = new CountDownLatch(1);
response = client.newRequest(uri)
.path(primaryResource)
.onPush((request, pushed) ->
{
pushLatch.countDown();
return null;
})
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
assertFalse(pushLatch.await(1, TimeUnit.SECONDS));
}
@ParameterizedTest
@MethodSource("transportsWithPushSupport")
public void testPushIsReset(Transport transport) throws Exception
{
String primaryResource = "/primary.html";
String secondaryResource = "/secondary.png";
byte[] secondaryData = "SECONDARY".getBytes(StandardCharsets.UTF_8);
start(transport, new HttpServlet()
{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
{
String requestURI = req.getRequestURI();
ServletOutputStream output = resp.getOutputStream();
if (requestURI.endsWith(primaryResource))
output.print("<html><head></head><body>PRIMARY</body></html>");
else if (requestURI.endsWith(secondaryResource))
output.write(secondaryData);
}
});
servletContextHandler.addFilter(PushCacheFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
// Request for the primary and secondary resources to build the cache.
URI uri = newURI(transport);
ContentResponse response = client.newRequest(uri)
.path(primaryResource)
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
response = client.newRequest(uri)
.path(secondaryResource)
.headers(headers -> headers.put(HttpHeader.REFERER, uri.resolve(primaryResource).toString()))
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
// Request again the primary resource, we should get the secondary resource pushed.
CountDownLatch pushLatch = new CountDownLatch(1);
response = client.newRequest(uri)
.path(primaryResource)
.onPush((request, pushed) ->
{
pushLatch.countDown();
// Cancel the push.
return null;
})
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
assertTrue(pushLatch.await(5, TimeUnit.SECONDS));
// Make sure the connection is sane.
HttpDestination destination = (HttpDestination)client.getDestinations().get(0);
assertFalse(destination.getConnectionPool().isEmpty());
}
@ParameterizedTest
@MethodSource("transportsWithPushSupport")
public void testPushWithoutPrimaryResponseContent(Transport transport) throws Exception
{
String primaryResource = "/primary.html";
String secondaryResource = "/secondary.png";
start(transport, new HttpServlet()
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
String requestURI = request.getRequestURI();
ServletOutputStream output = response.getOutputStream();
if (requestURI.endsWith(secondaryResource))
output.write("SECONDARY".getBytes(StandardCharsets.UTF_8));
}
});
servletContextHandler.addFilter(PushCacheFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
// Request for the primary and secondary resources to build the cache.
URI uri = newURI(transport);
ContentResponse response = client.newRequest(uri)
.path(primaryResource)
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
response = client.newRequest(uri)
.path(secondaryResource)
.headers(headers -> headers.put(HttpHeader.REFERER, uri.resolve(primaryResource).toString()))
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
// Request again the primary resource, we should get the secondary resource pushed.
CountDownLatch pushLatch = new CountDownLatch(2);
response = client.newRequest(uri)
.path(primaryResource)
.onPush((request, pushed) ->
{
pushLatch.countDown();
return new BufferingResponseListener()
{
@Override
public void onComplete(Result result)
{
pushLatch.countDown();
}
};
})
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
assertTrue(pushLatch.await(5, TimeUnit.SECONDS));
}
@ParameterizedTest
@MethodSource("transportsWithPushSupport")
public void testRecursivePush(Transport transport) throws Exception
{
String primaryResource = "/primary.html";
String secondaryResource1 = "/secondary1.css";
String secondaryResource2 = "/secondary2.js";
String tertiaryResource = "/tertiary.png";
start(transport, new HttpServlet()
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
String requestURI = request.getRequestURI();
ServletOutputStream output = response.getOutputStream();
if (requestURI.endsWith(primaryResource))
output.print("<html><head></head><body>PRIMARY</body></html>");
else if (requestURI.endsWith(secondaryResource1))
output.print("body { background-image: url(\"" + tertiaryResource + "\"); }");
else if (requestURI.endsWith(secondaryResource2))
output.print("(function() { window.alert('HTTP/2'); })()");
if (requestURI.endsWith(tertiaryResource))
output.write("TERTIARY".getBytes(StandardCharsets.UTF_8));
}
});
servletContextHandler.addFilter(PushCacheFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
// Request for the primary, secondary and tertiary resources to build the cache.
URI uri = newURI(transport);
ContentResponse response = client.newRequest(uri)
.path(primaryResource)
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
response = client.newRequest(uri)
.path(secondaryResource1)
.headers(headers -> headers.put(HttpHeader.REFERER, uri.resolve(primaryResource).toString()))
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
response = client.newRequest(uri)
.path(secondaryResource2)
.headers(headers -> headers.put(HttpHeader.REFERER, uri.resolve(primaryResource).toString()))
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
response = client.newRequest(uri)
.path(tertiaryResource)
.headers(headers -> headers.put(HttpHeader.REFERER, uri.resolve(secondaryResource1).toString()))
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
// Request again the primary resource, we should get the secondary and tertiary resources pushed.
CountDownLatch primaryPushLatch = new CountDownLatch(3);
response = client.newRequest(uri)
.path(primaryResource)
.onPush((request, pushed) -> new BufferingResponseListener()
{
@Override
public void onComplete(Result result)
{
primaryPushLatch.countDown();
}
})
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
assertTrue(primaryPushLatch.await(5, TimeUnit.SECONDS));
// Make sure that explicitly requesting a secondary resource, we get the tertiary pushed.
CountDownLatch secondaryPushLatch = new CountDownLatch(1);
response = client.newRequest(uri)
.path(primaryResource)
.onPush((request, pushed) -> new BufferingResponseListener()
{
@Override
public void onComplete(Result result)
{
secondaryPushLatch.countDown();
}
})
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
assertTrue(secondaryPushLatch.await(5, TimeUnit.SECONDS));
}
@ParameterizedTest
@MethodSource("transportsWithPushSupport")
public void testSelfPush(Transport transport) throws Exception
{
// The test case is that of a login page, for example.
// When the user sends the credentials to the login page,
// the login may fail and redirect to the same login page,
// perhaps with different query parameters.
// In this case a request for the login page will push
// the login page itself, which will generate the pushed
// request for the login page, which will push the login
// page itself, etc. which is not the desired behavior.
String primaryResource = "/login.html";
start(transport, new HttpServlet()
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
ServletOutputStream output = response.getOutputStream();
String credentials = request.getParameter("credentials");
if (credentials == null)
{
output.print("<html><head></head><body>LOGIN</body></html>");
}
else if ("secret".equals(credentials))
{
output.print("<html><head></head><body>OK</body></html>");
}
else
{
response.setStatus(HttpStatus.TEMPORARY_REDIRECT_307);
response.setHeader(HttpHeader.LOCATION.asString(), primaryResource);
}
}
});
servletContextHandler.addFilter(PushCacheFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
// Login with the wrong credentials, causing a redirect to self.
URI uri = newURI(transport);
ContentResponse response = client.newRequest(uri)
.path(primaryResource + "?credentials=wrong")
.followRedirects(false)
.send();
assertEquals(HttpStatus.TEMPORARY_REDIRECT_307, response.getStatus());
String location = response.getHeaders().get(HttpHeader.LOCATION);
response = client.newRequest(uri)
.path(location)
.headers(headers -> headers.put(HttpHeader.REFERER, uri.resolve(primaryResource).toString()))
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
// Login with the right credentials, there must be no push.
CountDownLatch pushLatch = new CountDownLatch(1);
response = client.newRequest(uri)
.path(primaryResource + "?credentials=secret")
.onPush((request, pushed) ->
{
pushLatch.countDown();
return null;
})
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
assertFalse(pushLatch.await(1, TimeUnit.SECONDS));
}
@ParameterizedTest
@MethodSource("transportsWithPushSupport")
public void testPushWithQueryParameters(Transport transport) throws Exception
{
String name = "foo";
String value = "bar";
String query = name + "=" + value;
String primaryResource = "/primary.html?" + query;
String secondaryResource = "/secondary.html?" + query;
start(transport, new HttpServlet()
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
{
String requestURI = request.getRequestURI();
if (requestURI.endsWith(primaryResource))
{
response.setStatus(HttpStatus.OK_200);
}
else if (requestURI.endsWith(secondaryResource))
{
String param = request.getParameter(name);
if (param == null)
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500);
else
response.setStatus(HttpStatus.OK_200);
}
}
});
servletContextHandler.addFilter(PushCacheFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
// Request for the primary and secondary resources to build the cache.
URI uri = newURI(transport);
ContentResponse response = client.newRequest(uri)
.path(primaryResource)
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
response = client.newRequest(uri)
.path(secondaryResource)
.headers(headers -> headers.put(HttpHeader.REFERER, uri.resolve(primaryResource).toString()))
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
// Request again the primary resource, we should get the secondary resource pushed.
CountDownLatch pushLatch = new CountDownLatch(1);
response = client.newRequest(uri)
.path(primaryResource)
.onPush((request, pushed) ->
{
assertEquals(query, pushed.getURI().getQuery());
return new BufferingResponseListener()
{
@Override
public void onComplete(Result result)
{
pushLatch.countDown();
}
};
})
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
assertTrue(pushLatch.await(5, TimeUnit.SECONDS));
}
@ParameterizedTest
@MethodSource("transportsWithPushSupport")
public void testPOSTRequestIsNotPushed(Transport transport) throws Exception
{
String primaryResource = "/primary.html";
String secondaryResource = "/secondary.png";
byte[] secondaryData = "SECONDARY".getBytes(StandardCharsets.UTF_8);
start(transport, new HttpServlet()
{
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException
{
String requestURI = req.getRequestURI();
ServletOutputStream output = resp.getOutputStream();
if (requestURI.endsWith(primaryResource))
output.print("<html><head></head><body>PRIMARY</body></html>");
else if (requestURI.endsWith(secondaryResource))
output.write(secondaryData);
}
});
servletContextHandler.addFilter(PushCacheFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
// Request for the primary and secondary resource to build the cache.
URI uri = newURI(transport);
ContentResponse response = client.newRequest(uri)
.path(primaryResource)
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
response = client.newRequest(uri)
.path(secondaryResource)
.headers(headers -> headers.put(HttpHeader.REFERER, uri.resolve(primaryResource).toString()))
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
// Request again the primary resource with POST, we should not get the secondary resource pushed.
CountDownLatch pushLatch = new CountDownLatch(1);
response = client.newRequest(uri)
.method(HttpMethod.POST)
.path(primaryResource)
.onPush((request, pushed) ->
{
pushLatch.countDown();
return null;
})
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
assertFalse(pushLatch.await(1, TimeUnit.SECONDS));
}
}

View File

@ -29,9 +29,6 @@ import org.eclipse.jetty.util.URIUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
/**
*
*/
public class PushBuilderImpl implements PushBuilder public class PushBuilderImpl implements PushBuilder
{ {
private static final Logger LOG = LoggerFactory.getLogger(PushBuilderImpl.class); private static final Logger LOG = LoggerFactory.getLogger(PushBuilderImpl.class);

View File

@ -243,7 +243,7 @@ public class Request implements HttpServletRequest
public boolean isPushSupported() public boolean isPushSupported()
{ {
return !isPush() && getCoreRequest().isPushSupported(); return !isPush() && getCoreRequest().getConnectionMetaData().isPushSupported();
} }
private static final EnumSet<HttpHeader> NOT_PUSHED_HEADERS = EnumSet.of( private static final EnumSet<HttpHeader> NOT_PUSHED_HEADERS = EnumSet.of(
@ -274,7 +274,7 @@ public class Request implements HttpServletRequest
String id; String id;
try try
{ {
HttpSession session = getSession(); HttpSession session = getSession(false);
if (session != null) if (session != null)
{ {
session.getLastAccessedTime(); // checks if session is valid session.getLastAccessedTime(); // checks if session is valid
@ -343,8 +343,12 @@ public class Request implements HttpServletRequest
fields.add(new HttpField(HttpHeader.COOKIE, buff.toString())); fields.add(new HttpField(HttpHeader.COOKIE, buff.toString()));
} }
PushBuilder builder = new PushBuilderImpl(this, fields, getMethod(), getQueryString(), id); String query = getQueryString();
builder.addHeader("referer", getRequestURL().toString()); PushBuilder builder = new PushBuilderImpl(this, fields, getMethod(), query, id);
String referrer = getRequestURL().toString();
if (query != null)
referrer += "?" + query;
builder.addHeader("referer", referrer);
return builder; return builder;
} }

View File

@ -61,7 +61,6 @@ import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.UriCompliance; import org.eclipse.jetty.http.UriCompliance;
import org.eclipse.jetty.http.pathmap.MatchedPath; import org.eclipse.jetty.http.pathmap.MatchedPath;
@ -2350,17 +2349,6 @@ public class RequestTest
{ {
} }
@Override
public boolean isPushSupported()
{
return false;
}
@Override
public void push(MetaData.Request request)
{
}
@Override @Override
public boolean addErrorListener(Predicate<Throwable> onError) public boolean addErrorListener(Predicate<Throwable> onError)
{ {

View File

@ -2352,17 +2352,6 @@ public class ResponseTest
{ {
} }
@Override
public boolean isPushSupported()
{
return false;
}
@Override
public void push(MetaData.Request request)
{
}
@Override @Override
public boolean addErrorListener(Predicate<Throwable> onError) public boolean addErrorListener(Predicate<Throwable> onError)
{ {

View File

@ -91,6 +91,11 @@
<artifactId>jetty-ee9-servlet</artifactId> <artifactId>jetty-ee9-servlet</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.eclipse.jetty.ee9</groupId>
<artifactId>jetty-ee9-servlets</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.eclipse.jetty</groupId> <groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-client</artifactId> <artifactId>jetty-client</artifactId>

View File

@ -85,6 +85,13 @@ public class AbstractTest
return transports; return transports;
} }
public static Collection<Transport> transportsWithPushSupport()
{
Collection<Transport> transports = transports();
transports.retainAll(List.of(Transport.H2C, Transport.H2));
return transports;
}
@AfterEach @AfterEach
public void dispose() public void dispose()
{ {

View File

@ -0,0 +1,515 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.ee9.test.client.transport;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.EnumSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.client.HttpDestination;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.BufferingResponseListener;
import org.eclipse.jetty.ee9.servlets.PushCacheFilter;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class PushCacheFilterTest extends AbstractTest
{
@ParameterizedTest
@MethodSource("transportsWithPushSupport")
public void testPush(Transport transport) throws Exception
{
String primaryResource = "/primary.html";
String secondaryResource = "/secondary.png";
byte[] secondaryData = "SECONDARY".getBytes(StandardCharsets.UTF_8);
start(transport, new HttpServlet()
{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
{
String requestURI = req.getRequestURI();
ServletOutputStream output = resp.getOutputStream();
if (requestURI.endsWith(primaryResource))
output.print("<html><head></head><body>PRIMARY</body></html>");
else if (requestURI.endsWith(secondaryResource))
output.write(secondaryData);
}
});
servletContextHandler.addFilter(PushCacheFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
// Request for the primary and secondary resources to build the cache.
URI uri = newURI(transport);
ContentResponse response = client.newRequest(uri)
.path(primaryResource)
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
response = client.newRequest(uri)
.path(secondaryResource)
.headers(headers -> headers.put(HttpHeader.REFERER, uri.resolve(primaryResource).toString()))
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
// Request again the primary resource, we should get the secondary resource pushed.
CountDownLatch pushLatch = new CountDownLatch(2);
response = client.newRequest(uri)
.path(primaryResource)
.onPush((request, pushed) ->
{
pushLatch.countDown();
return new BufferingResponseListener()
{
@Override
public void onComplete(Result result)
{
pushLatch.countDown();
}
};
})
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
assertTrue(pushLatch.await(5, TimeUnit.SECONDS));
}
@ParameterizedTest
@MethodSource("transportsWithPushSupport")
public void testPushReferrerNoPath(Transport transport) throws Exception
{
String primaryResource = "/primary.html";
String secondaryResource = "/secondary.png";
byte[] secondaryData = "SECONDARY".getBytes(StandardCharsets.UTF_8);
start(transport, new HttpServlet()
{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
{
String requestURI = req.getRequestURI();
ServletOutputStream output = resp.getOutputStream();
if (requestURI.endsWith(primaryResource))
output.print("<html><head></head><body>PRIMARY</body></html>");
else if (requestURI.endsWith(secondaryResource))
output.write(secondaryData);
}
});
servletContextHandler.addFilter(PushCacheFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
// Request for the primary and secondary resources to build the cache.
// The referrerURI does not point to the primary resource, so there will be no
// resource association with the primary resource and therefore won't be pushed.
URI uri = newURI(transport);
ContentResponse response = client.newRequest(uri)
.path(primaryResource)
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
response = client.newRequest(uri)
.path(secondaryResource)
.headers(headers -> headers.put(HttpHeader.REFERER, uri.toString()))
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
// Request again the primary resource, we should not get the secondary resource pushed.
CountDownLatch pushLatch = new CountDownLatch(1);
response = client.newRequest(uri)
.path(primaryResource)
.onPush((request, pushed) ->
{
pushLatch.countDown();
return null;
})
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
assertFalse(pushLatch.await(1, TimeUnit.SECONDS));
}
@ParameterizedTest
@MethodSource("transportsWithPushSupport")
public void testPushIsReset(Transport transport) throws Exception
{
String primaryResource = "/primary.html";
String secondaryResource = "/secondary.png";
byte[] secondaryData = "SECONDARY".getBytes(StandardCharsets.UTF_8);
start(transport, new HttpServlet()
{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException
{
String requestURI = req.getRequestURI();
ServletOutputStream output = resp.getOutputStream();
if (requestURI.endsWith(primaryResource))
output.print("<html><head></head><body>PRIMARY</body></html>");
else if (requestURI.endsWith(secondaryResource))
output.write(secondaryData);
}
});
servletContextHandler.addFilter(PushCacheFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
// Request for the primary and secondary resources to build the cache.
URI uri = newURI(transport);
ContentResponse response = client.newRequest(uri)
.path(primaryResource)
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
response = client.newRequest(uri)
.path(secondaryResource)
.headers(headers -> headers.put(HttpHeader.REFERER, uri.resolve(primaryResource).toString()))
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
// Request again the primary resource, we should get the secondary resource pushed.
CountDownLatch pushLatch = new CountDownLatch(1);
response = client.newRequest(uri)
.path(primaryResource)
.onPush((request, pushed) ->
{
pushLatch.countDown();
// Cancel the push.
return null;
})
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
assertTrue(pushLatch.await(5, TimeUnit.SECONDS));
// Make sure the connection is sane.
HttpDestination destination = (HttpDestination)client.getDestinations().get(0);
assertFalse(destination.getConnectionPool().isEmpty());
}
@ParameterizedTest
@MethodSource("transportsWithPushSupport")
public void testPushWithoutPrimaryResponseContent(Transport transport) throws Exception
{
String primaryResource = "/primary.html";
String secondaryResource = "/secondary.png";
start(transport, new HttpServlet()
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
String requestURI = request.getRequestURI();
ServletOutputStream output = response.getOutputStream();
if (requestURI.endsWith(secondaryResource))
output.write("SECONDARY".getBytes(StandardCharsets.UTF_8));
}
});
servletContextHandler.addFilter(PushCacheFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
// Request for the primary and secondary resources to build the cache.
URI uri = newURI(transport);
ContentResponse response = client.newRequest(uri)
.path(primaryResource)
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
response = client.newRequest(uri)
.path(secondaryResource)
.headers(headers -> headers.put(HttpHeader.REFERER, uri.resolve(primaryResource).toString()))
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
// Request again the primary resource, we should get the secondary resource pushed.
CountDownLatch pushLatch = new CountDownLatch(2);
response = client.newRequest(uri)
.path(primaryResource)
.onPush((request, pushed) ->
{
pushLatch.countDown();
return new BufferingResponseListener()
{
@Override
public void onComplete(Result result)
{
pushLatch.countDown();
}
};
})
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
assertTrue(pushLatch.await(5, TimeUnit.SECONDS));
}
@ParameterizedTest
@MethodSource("transportsWithPushSupport")
public void testRecursivePush(Transport transport) throws Exception
{
String primaryResource = "/primary.html";
String secondaryResource1 = "/secondary1.css";
String secondaryResource2 = "/secondary2.js";
String tertiaryResource = "/tertiary.png";
start(transport, new HttpServlet()
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
String requestURI = request.getRequestURI();
ServletOutputStream output = response.getOutputStream();
if (requestURI.endsWith(primaryResource))
output.print("<html><head></head><body>PRIMARY</body></html>");
else if (requestURI.endsWith(secondaryResource1))
output.print("body { background-image: url(\"" + tertiaryResource + "\"); }");
else if (requestURI.endsWith(secondaryResource2))
output.print("(function() { window.alert('HTTP/2'); })()");
if (requestURI.endsWith(tertiaryResource))
output.write("TERTIARY".getBytes(StandardCharsets.UTF_8));
}
});
servletContextHandler.addFilter(PushCacheFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
// Request for the primary, secondary and tertiary resources to build the cache.
URI uri = newURI(transport);
ContentResponse response = client.newRequest(uri)
.path(primaryResource)
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
response = client.newRequest(uri)
.path(secondaryResource1)
.headers(headers -> headers.put(HttpHeader.REFERER, uri.resolve(primaryResource).toString()))
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
response = client.newRequest(uri)
.path(secondaryResource2)
.headers(headers -> headers.put(HttpHeader.REFERER, uri.resolve(primaryResource).toString()))
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
response = client.newRequest(uri)
.path(tertiaryResource)
.headers(headers -> headers.put(HttpHeader.REFERER, uri.resolve(secondaryResource1).toString()))
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
// Request again the primary resource, we should get the secondary and tertiary resources pushed.
CountDownLatch primaryPushLatch = new CountDownLatch(3);
response = client.newRequest(uri)
.path(primaryResource)
.onPush((request, pushed) -> new BufferingResponseListener()
{
@Override
public void onComplete(Result result)
{
primaryPushLatch.countDown();
}
})
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
assertTrue(primaryPushLatch.await(5, TimeUnit.SECONDS));
// Make sure that explicitly requesting a secondary resource, we get the tertiary pushed.
CountDownLatch secondaryPushLatch = new CountDownLatch(1);
response = client.newRequest(uri)
.path(primaryResource)
.onPush((request, pushed) -> new BufferingResponseListener()
{
@Override
public void onComplete(Result result)
{
secondaryPushLatch.countDown();
}
})
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
assertTrue(secondaryPushLatch.await(5, TimeUnit.SECONDS));
}
@ParameterizedTest
@MethodSource("transportsWithPushSupport")
public void testSelfPush(Transport transport) throws Exception
{
// The test case is that of a login page, for example.
// When the user sends the credentials to the login page,
// the login may fail and redirect to the same login page,
// perhaps with different query parameters.
// In this case a request for the login page will push
// the login page itself, which will generate the pushed
// request for the login page, which will push the login
// page itself, etc. which is not the desired behavior.
String primaryResource = "/login.html";
start(transport, new HttpServlet()
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
{
ServletOutputStream output = response.getOutputStream();
String credentials = request.getParameter("credentials");
if (credentials == null)
{
output.print("<html><head></head><body>LOGIN</body></html>");
}
else if ("secret".equals(credentials))
{
output.print("<html><head></head><body>OK</body></html>");
}
else
{
response.setStatus(HttpStatus.TEMPORARY_REDIRECT_307);
response.setHeader(HttpHeader.LOCATION.asString(), primaryResource);
}
}
});
servletContextHandler.addFilter(PushCacheFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
// Login with the wrong credentials, causing a redirect to self.
URI uri = newURI(transport);
ContentResponse response = client.newRequest(uri)
.path(primaryResource + "?credentials=wrong")
.followRedirects(false)
.send();
assertEquals(HttpStatus.TEMPORARY_REDIRECT_307, response.getStatus());
String location = response.getHeaders().get(HttpHeader.LOCATION);
response = client.newRequest(uri)
.path(location)
.headers(headers -> headers.put(HttpHeader.REFERER, uri.resolve(primaryResource).toString()))
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
// Login with the right credentials, there must be no push.
CountDownLatch pushLatch = new CountDownLatch(1);
response = client.newRequest(uri)
.path(primaryResource + "?credentials=secret")
.onPush((request, pushed) ->
{
pushLatch.countDown();
return null;
})
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
assertFalse(pushLatch.await(1, TimeUnit.SECONDS));
}
@ParameterizedTest
@MethodSource("transportsWithPushSupport")
public void testPushWithQueryParameters(Transport transport) throws Exception
{
String name = "foo";
String value = "bar";
String query = name + "=" + value;
String primaryResource = "/primary.html?" + query;
String secondaryResource = "/secondary.html?" + query;
start(transport, new HttpServlet()
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
{
String requestURI = request.getRequestURI();
if (requestURI.endsWith(primaryResource))
{
response.setStatus(HttpStatus.OK_200);
}
else if (requestURI.endsWith(secondaryResource))
{
String param = request.getParameter(name);
if (param == null)
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500);
else
response.setStatus(HttpStatus.OK_200);
}
}
});
servletContextHandler.addFilter(PushCacheFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
// Request for the primary and secondary resources to build the cache.
URI uri = newURI(transport);
ContentResponse response = client.newRequest(uri)
.path(primaryResource)
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
response = client.newRequest(uri)
.path(secondaryResource)
.headers(headers -> headers.put(HttpHeader.REFERER, uri.resolve(primaryResource).toString()))
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
// Request again the primary resource, we should get the secondary resource pushed.
CountDownLatch pushLatch = new CountDownLatch(1);
response = client.newRequest(uri)
.path(primaryResource)
.onPush((request, pushed) ->
{
assertEquals(query, pushed.getURI().getQuery());
return new BufferingResponseListener()
{
@Override
public void onComplete(Result result)
{
pushLatch.countDown();
}
};
})
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
assertTrue(pushLatch.await(5, TimeUnit.SECONDS));
}
@ParameterizedTest
@MethodSource("transportsWithPushSupport")
public void testPOSTRequestIsNotPushed(Transport transport) throws Exception
{
String primaryResource = "/primary.html";
String secondaryResource = "/secondary.png";
byte[] secondaryData = "SECONDARY".getBytes(StandardCharsets.UTF_8);
start(transport, new HttpServlet()
{
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException
{
String requestURI = req.getRequestURI();
ServletOutputStream output = resp.getOutputStream();
if (requestURI.endsWith(primaryResource))
output.print("<html><head></head><body>PRIMARY</body></html>");
else if (requestURI.endsWith(secondaryResource))
output.write(secondaryData);
}
});
servletContextHandler.addFilter(PushCacheFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
// Request for the primary and secondary resource to build the cache.
URI uri = newURI(transport);
ContentResponse response = client.newRequest(uri)
.path(primaryResource)
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
response = client.newRequest(uri)
.path(secondaryResource)
.headers(headers -> headers.put(HttpHeader.REFERER, uri.resolve(primaryResource).toString()))
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
// Request again the primary resource with POST, we should not get the secondary resource pushed.
CountDownLatch pushLatch = new CountDownLatch(1);
response = client.newRequest(uri)
.method(HttpMethod.POST)
.path(primaryResource)
.onPush((request, pushed) ->
{
pushLatch.countDown();
return null;
})
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
assertFalse(pushLatch.await(1, TimeUnit.SECONDS));
}
}