PushPromisesTestCase.java
/*
* JBoss, Home of Professional Open Source.
* Copyright 2021 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.undertow.servlet.test.push;
import io.undertow.UndertowOptions;
import io.undertow.client.ClientCallback;
import io.undertow.client.ClientConnection;
import io.undertow.client.ClientExchange;
import io.undertow.client.ClientRequest;
import io.undertow.client.ClientResponse;
import io.undertow.client.PushCallback;
import io.undertow.client.UndertowClient;
import io.undertow.protocols.ssl.UndertowXnioSsl;
import io.undertow.server.OpenListener;
import io.undertow.server.handlers.PathHandler;
import io.undertow.server.protocol.http.AlpnOpenListener;
import io.undertow.server.protocol.http2.Http2OpenListener;
import io.undertow.servlet.api.DeploymentInfo;
import io.undertow.servlet.api.DeploymentManager;
import io.undertow.servlet.api.ServletContainer;
import io.undertow.servlet.api.ServletInfo;
import io.undertow.servlet.test.util.TestClassIntrospector;
import io.undertow.testutils.DefaultServer;
import io.undertow.testutils.ProxyIgnore;
import io.undertow.util.AttachmentKey;
import io.undertow.util.Headers;
import io.undertow.util.Methods;
import io.undertow.util.SingleByteStreamSinkConduit;
import io.undertow.util.SingleByteStreamSourceConduit;
import io.undertow.util.StatusCodes;
import io.undertow.util.StringReadChannelListener;
import java.io.IOException;
import java.net.URI;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.xnio.ChannelListener;
import org.xnio.ChannelListeners;
import org.xnio.IoUtils;
import org.xnio.OptionMap;
import org.xnio.Options;
import org.xnio.StreamConnection;
import org.xnio.Xnio;
import org.xnio.XnioWorker;
/**
* <p>Test that checks that push promises are returned and double promises
* are avoid (a resource sent as a promise trigers another promise).</p>
*
* @author rmartinc
*/
@RunWith(DefaultServer.class)
@ProxyIgnore
public class PushPromisesTestCase {
private static final AttachmentKey<String> RESPONSE_BODY = AttachmentKey.create(String.class);
private static OpenListener openListener;
private static ChannelListener acceptListener;
private static XnioWorker worker;
private static ChannelListener<StreamConnection> wrapOpenListener(final ChannelListener<StreamConnection> listener) {
return (StreamConnection channel) -> {
channel.getSinkChannel().setConduit(new SingleByteStreamSinkConduit(channel.getSinkChannel().getConduit(), 10000));
channel.getSourceChannel().setConduit(new SingleByteStreamSourceConduit(channel.getSourceChannel().getConduit(), 10000));
listener.handleEvent(channel);
};
}
@BeforeClass
public static void setup() throws Exception {
final PathHandler root = new PathHandler();
final ServletContainer container = ServletContainer.Factory.newInstance();
ServletInfo s = new ServletInfo("servlet", PushServlet.class)
.addMappings("/index.html", "/resources/*");
DeploymentInfo info = new DeploymentInfo()
.setClassLoader(PushPromisesTestCase.class.getClassLoader())
.setContextPath("/push-example")
.setClassIntrospecter(TestClassIntrospector.INSTANCE)
.setDeploymentName("push-example.war")
.addServlet(s);
DeploymentManager manager = container.addDeployment(info);
manager.deploy();
root.addPrefixPath(info.getContextPath(), manager.start());
openListener = new Http2OpenListener(DefaultServer.getBufferPool(), OptionMap.create(UndertowOptions.ENABLE_HTTP2, true, UndertowOptions.HTTP2_PADDING_SIZE, 10));
acceptListener = ChannelListeners.openListenerAdapter(wrapOpenListener(new AlpnOpenListener(DefaultServer.getBufferPool()).addProtocol(Http2OpenListener.HTTP2, (io.undertow.server.DelegateOpenListener) openListener, 10)));
openListener.setRootHandler(root);
DefaultServer.startSSLServer(OptionMap.EMPTY, acceptListener);
final Xnio xnio = Xnio.getInstance();
final XnioWorker xnioWorker = xnio.createWorker(null,
OptionMap.builder()
.set(Options.WORKER_IO_THREADS, 8)
.set(Options.TCP_NODELAY, true)
.set(Options.KEEP_ALIVE, true)
.set(Options.WORKER_NAME, "Client").getMap());
worker = xnioWorker;
}
@AfterClass
public static void cleanUp() throws Exception {
openListener.closeConnections();
DefaultServer.stopSSLServer();
}
private PushCallback createPushCallback(final Map<String, ClientResponse> responses, final CountDownLatch latch) {
return new PushCallback() {
@Override
public boolean handlePush(ClientExchange originalRequest, ClientExchange pushedRequest) {
pushedRequest.setResponseListener(new ResponseListener(responses, latch));
return true;
}
};
}
private ClientCallback<ClientExchange> createClientCallback(final Map<String, ClientResponse> responses, final CountDownLatch latch) {
return new ClientCallback<ClientExchange>() {
@Override
public void completed(final ClientExchange result) {
result.setResponseListener(new ResponseListener(responses, latch));
result.setPushHandler(createPushCallback(responses, latch));
}
@Override
public void failed(IOException e) {
e.printStackTrace();
latch.countDown();
}
};
}
private static class ResponseListener implements ClientCallback<ClientExchange> {
private final Map<String, ClientResponse> responses;
private final CountDownLatch latch;
ResponseListener(Map<String, ClientResponse> responses, CountDownLatch latch) {
this.responses = responses;
this.latch = latch;
}
@Override
public void completed(final ClientExchange result) {
responses.put(result.getRequest().getPath(), result.getResponse());
new StringReadChannelListener(result.getConnection().getBufferPool()) {
@Override
protected void stringDone(String string) {
result.getResponse().putAttachment(RESPONSE_BODY, string);
latch.countDown();
}
@Override
protected void error(IOException e) {
e.printStackTrace();
latch.countDown();
}
}.setup(result.getResponseChannel());
}
@Override
public void failed(IOException e) {
e.printStackTrace();
latch.countDown();
}
}
@Test
public void testPushPromises() throws Exception {
URI uri = new URI(DefaultServer.getDefaultServerSSLAddress());
final UndertowClient client = UndertowClient.getInstance();
final Map<String, ClientResponse> responses = new ConcurrentHashMap<>();
final CountDownLatch latch = new CountDownLatch(3);
final ClientConnection connection = client.connect(uri, worker, new UndertowXnioSsl(worker.getXnio(), OptionMap.EMPTY, DefaultServer.getClientSSLContext()), DefaultServer.getBufferPool(), OptionMap.create(UndertowOptions.ENABLE_HTTP2, true))
.get();
try {
connection.getIoThread().execute(new Runnable() {
@Override
public void run() {
final ClientRequest request = new ClientRequest().setMethod(Methods.GET).setPath("/push-example/index.html");
request.getRequestHeaders().put(Headers.HOST, DefaultServer.getHostAddress());
connection.sendRequest(request, createClientCallback(responses, latch));
}
});
latch.await(10, TimeUnit.SECONDS);
Assert.assertEquals(3, responses.size());
Assert.assertTrue(responses.containsKey("/push-example/index.html"));
Assert.assertEquals(StatusCodes.OK, responses.get("/push-example/index.html").getResponseCode());
Assert.assertNotNull(responses.get("/push-example/index.html").getAttachment(RESPONSE_BODY));
Assert.assertTrue(responses.containsKey("/push-example/resources/one.js"));
Assert.assertEquals(StatusCodes.OK, responses.get("/push-example/resources/one.js").getResponseCode());
Assert.assertNotNull(responses.get("/push-example/resources/one.js").getAttachment(RESPONSE_BODY));
Assert.assertTrue(responses.containsKey("/push-example/resources/one.css"));
Assert.assertEquals(StatusCodes.OK, responses.get("/push-example/resources/one.css").getResponseCode());
Assert.assertNotNull(responses.get("/push-example/resources/one.css").getAttachment(RESPONSE_BODY));
} finally {
IoUtils.safeClose(connection);
}
}
}