package org.jooby.issues;
import com.google.common.collect.ImmutableMap;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http.MetaData.Request;
import org.eclipse.jetty.http2.api.Session;
import org.eclipse.jetty.http2.api.Stream;
import org.eclipse.jetty.http2.api.server.ServerSessionListener;
import org.eclipse.jetty.http2.client.HTTP2Client;
import org.eclipse.jetty.http2.frames.DataFrame;
import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.frames.PushPromiseFrame;
import org.eclipse.jetty.http2.generator.Generator;
import org.eclipse.jetty.http2.parser.Parser;
import org.eclipse.jetty.io.MappedByteBufferPool;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.FuturePromise;
import org.eclipse.jetty.util.Jetty;
import org.eclipse.jetty.util.Promise;
import org.eclipse.jetty.util.Utf8StringBuilder;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.jooby.MediaType;
import org.jooby.Results;
import org.jooby.test.ServerFeature;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Phaser;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class Issue418 extends ServerFeature {
{
http2();
securePort(9443);
get("/", req -> req.protocol() + (req.secure() ? ":secure" : ""));
get("/app.js", () -> Results.ok("(function(){})()").type(MediaType.js));
get("/push", req -> {
req.push("/app.js");
return "<html><script src=\"app.js\"></script><body>H2</body></html>";
});
get("/push-with-header", req -> {
req.push("/app.js", ImmutableMap.of("etag", "123"));
return "<html><script src=\"app.js\"></script><body>H2</body></html>";
});
}
@SuppressWarnings("unchecked")
@Test
public void h2c() throws Throwable {
Map<Integer, Object> rsp = call("/", false);
assertNotNull(rsp);
Map<String, Object> stream = (Map<String, Object>) rsp.get(1);
assertEquals(1, stream.get("streamId"));
assertEquals("HTTP/2.0", stream.get("body"));
assertEquals("8", stream.get("content-length"));
assertEquals("text/html;charset=utf-8", stream.get("content-type"));
}
@SuppressWarnings("unchecked")
@Test
public void h2() throws Throwable {
Map<Integer, Object> rsp = call("/", true);
assertNotNull(rsp);
Map<String, Object> stream = (Map<String, Object>) rsp.get(1);
assertEquals(1, stream.get("streamId"));
assertEquals("HTTP/2.0:secure", stream.get("body"));
assertEquals("15", stream.get("content-length"));
assertEquals("text/html;charset=utf-8", stream.get("content-type"));
}
@SuppressWarnings("unchecked")
@Test
public void h2push() throws Throwable {
Map<Integer, Object> rsp = call("/push", true);
assertNotNull(rsp);
// stream 1
Map<String, Object> stream1 = (Map<String, Object>) rsp.get(1);
assertEquals(1, stream1.get("streamId"));
assertEquals("<html><script src=\"app.js\"></script><body>H2</body></html>",
stream1.get("body"));
assertEquals("58", stream1.get("content-length"));
assertEquals("text/html;charset=utf-8", stream1.get("content-type"));
// stream 2
Map<String, Object> stream2 = (Map<String, Object>) rsp.get(2);
assertEquals(2, stream2.get("streamId"));
assertEquals("(function(){})()", stream2.get("body"));
assertEquals("16", stream2.get("content-length"));
assertEquals("application/javascript;charset=utf-8", stream2.get("content-type"));
Map<String, Object> pushPromise = (Map<String, Object>) stream2.get("push-promise");
assertEquals(1, pushPromise.get("streamId"));
assertEquals(2, pushPromise.get("promisedStreamId"));
assertEquals("https://localhost:9443/app.js", pushPromise.get("uri"));
assertEquals("GET", pushPromise.get("method"));
}
@SuppressWarnings("unchecked")
@Test
public void h2cpush() throws Throwable {
Map<Integer, Object> rsp = call("/push", false);
assertNotNull(rsp);
// stream 1
Map<String, Object> stream1 = (Map<String, Object>) rsp.get(1);
assertEquals(1, stream1.get("streamId"));
assertEquals("<html><script src=\"app.js\"></script><body>H2</body></html>",
stream1.get("body"));
assertEquals("58", stream1.get("content-length"));
assertEquals("text/html;charset=utf-8", stream1.get("content-type"));
// stream 2
Map<String, Object> stream2 = (Map<String, Object>) rsp.get(2);
assertEquals(2, stream2.get("streamId"));
assertEquals("(function(){})()", stream2.get("body"));
assertEquals("16", stream2.get("content-length"));
assertEquals("application/javascript;charset=utf-8", stream2.get("content-type"));
Map<String, Object> pushPromise = (Map<String, Object>) stream2.get("push-promise");
assertEquals(1, pushPromise.get("streamId"));
assertEquals(2, pushPromise.get("promisedStreamId"));
assertEquals("http://localhost:" + port + "/app.js", pushPromise.get("uri"));
assertEquals("GET", pushPromise.get("method"));
}
@SuppressWarnings("unchecked")
@Test
public void h2pushWithHeader() throws Throwable {
Map<Integer, Object> rsp = call("/push-with-header", true);
assertNotNull(rsp);
// stream 1
Map<String, Object> stream1 = (Map<String, Object>) rsp.get(1);
assertEquals(1, stream1.get("streamId"));
assertEquals("<html><script src=\"app.js\"></script><body>H2</body></html>",
stream1.get("body"));
assertEquals("58", stream1.get("content-length"));
assertEquals("text/html;charset=utf-8", stream1.get("content-type"));
// stream 2
Map<String, Object> stream2 = (Map<String, Object>) rsp.get(2);
assertEquals(2, stream2.get("streamId"));
assertEquals("(function(){})()", stream2.get("body"));
assertEquals("16", stream2.get("content-length"));
assertEquals("application/javascript;charset=utf-8", stream2.get("content-type"));
Map<String, Object> pushPromise = (Map<String, Object>) stream2.get("push-promise");
assertEquals(1, pushPromise.get("streamId"));
assertEquals(2, pushPromise.get("promisedStreamId"));
assertEquals("123", pushPromise.get("etag"));
assertEquals("https://localhost:9443/app.js", pushPromise.get("uri"));
assertEquals("GET", pushPromise.get("method"));
}
@SuppressWarnings("unchecked")
@Test
public void h2cpushWithHeader() throws Throwable {
Map<Integer, Object> rsp = call("/push-with-header", false);
assertNotNull(rsp);
// stream 1
Map<String, Object> stream1 = (Map<String, Object>) rsp.get(1);
assertEquals(1, stream1.get("streamId"));
assertEquals("<html><script src=\"app.js\"></script><body>H2</body></html>",
stream1.get("body"));
assertEquals("58", stream1.get("content-length"));
assertEquals("text/html;charset=utf-8", stream1.get("content-type"));
// stream 2
Map<String, Object> stream2 = (Map<String, Object>) rsp.get(2);
assertEquals(2, stream2.get("streamId"));
assertEquals("(function(){})()", stream2.get("body"));
assertEquals("16", stream2.get("content-length"));
assertEquals("application/javascript;charset=utf-8", stream2.get("content-type"));
Map<String, Object> pushPromise = (Map<String, Object>) stream2.get("push-promise");
assertEquals(1, pushPromise.get("streamId"));
assertEquals(2, pushPromise.get("promisedStreamId"));
assertEquals("123", pushPromise.get("etag"));
assertEquals("http://localhost:" + port + "/app.js", pushPromise.get("uri"));
assertEquals("GET", pushPromise.get("method"));
}
@Test
public void http1_1_Upgrade() throws Throwable {
// copy from:
// https://github.com/eclipse/jetty.project/blob/master/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/HTTP2CServerTest.java#L116
try (Socket client = new Socket("localhost", port)) {
OutputStream output = client.getOutputStream();
output.write(("" +
"GET / HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Connection: upgrade, HTTP2-Settings\r\n" +
"Upgrade: h2c\r\n" +
"HTTP2-Settings: \r\n" +
"\r\n").getBytes(StandardCharsets.UTF_8));
output.flush();
InputStream input = client.getInputStream();
Utf8StringBuilder upgrade = new Utf8StringBuilder();
int crlfs = 0;
while (true) {
int read = input.read();
if (read == '\r' || read == '\n') {
++crlfs;
} else {
crlfs = 0;
}
upgrade.append((byte) read);
if (crlfs == 4) {
break;
}
}
assertTrue(upgrade.toString().startsWith("HTTP/1.1 101 "));
MappedByteBufferPool byteBufferPool = new MappedByteBufferPool();
new Generator(byteBufferPool);
final AtomicReference<HeadersFrame> headersRef = new AtomicReference<>();
final AtomicReference<DataFrame> dataRef = new AtomicReference<>();
final AtomicReference<CountDownLatch> latchRef = new AtomicReference<>(new CountDownLatch(2));
Parser parser = new Parser(byteBufferPool, new Parser.Listener.Adapter() {
@Override
public void onHeaders(final HeadersFrame frame) {
headersRef.set(frame);
latchRef.get().countDown();
}
@Override
public void onData(final DataFrame frame) {
dataRef.set(frame);
latchRef.get().countDown();
}
}, 4096, 8192);
parseResponse(client, parser);
assertTrue(latchRef.get().await(5, TimeUnit.SECONDS));
HeadersFrame response = headersRef.get();
assertNotNull(response);
MetaData.Response responseMetaData = (MetaData.Response) response.getMetaData();
assertEquals(200, responseMetaData.getStatus());
DataFrame responseData = dataRef.get();
assertNotNull(responseData);
String content = BufferUtil.toString(responseData.getData());
// The upgrade request is seen as HTTP/1.1.
assertTrue(content, content.contains("HTTP/1.1"));
}
}
@Test
public void http1_1() throws Throwable {
request()
.get("/")
.expect("HTTP/1.1");
}
@Test
public void https1_1() throws Throwable {
https()
.get("/")
.expect("HTTP/1.1:secure");
}
@SuppressWarnings("unchecked")
private Map<Integer, Object> call(final String path, final boolean secure) throws Throwable {
HTTP2Client client = new HTTP2Client();
try {
SslContextFactory sslContextFactory = new SslContextFactory(true);
sslContextFactory.setIncludeCipherSuites("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256");
client.addBean(sslContextFactory);
client.start();
String host = "localhost";
int port = secure ? this.securePort : this.port;
String scheme = secure ? "https" : "http";
FuturePromise<Session> sessionPromise = new FuturePromise<>();
client.connect(secure ? sslContextFactory : null, new InetSocketAddress(host, port),
new ServerSessionListener.Adapter(), sessionPromise);
Session session = sessionPromise.get();// 5, TimeUnit.SECONDS);
HttpFields requestFields = new HttpFields();
requestFields.put("User-Agent", client.getClass().getName() + "/" + Jetty.VERSION);
MetaData.Request metaData = new MetaData.Request("GET",
new HttpURI(scheme + "://" + host + ":" + port + path), HttpVersion.HTTP_2,
requestFields);
HeadersFrame frame = new HeadersFrame(metaData, null, true);
final Phaser phaser = new Phaser(2);
Map<Integer, Object> result = new HashMap<>();
session.newStream(frame, new Promise.Adapter<>(), new Stream.Listener.Adapter() {
@Override
public void onHeaders(final Stream stream, final HeadersFrame frame) {
Map<String, Object> body = stream(stream);
result.put(stream.getId(), body);
body.put("streamId", stream.getId());
frame.getMetaData()
.forEach(f -> body.put(f.getName().toLowerCase(), f.getValue().toLowerCase()));
if (frame.isEndStream()) {
phaser.arrive();
}
}
private Map<String, Object> stream(final Stream stream) {
Map<String, Object> data = (Map<String, Object>) result.get(stream.getId());
if (data == null) {
data = new HashMap<>();
result.put(stream.getId(), data);
}
return data;
}
@Override
public void onData(final Stream stream, final DataFrame frame, final Callback callback) {
byte[] bytes = new byte[frame.getData().remaining()];
frame.getData().get(bytes);
Map<String, Object> body = stream(stream);
body.put("body", new String(bytes));
callback.succeeded();
if (frame.isEndStream()) {
phaser.arrive();
}
}
@Override
public Stream.Listener onPush(final Stream stream, final PushPromiseFrame frame) {
Map<String, Object> body = stream(stream);
MetaData md = frame.getMetaData();
Map<String, Object> push = new HashMap<>();
body.put("push-promise", push);
md.forEach(f -> push.put(f.getName().toString(), f.getValue().toLowerCase()));
MetaData.Request req = (Request) md;
push.put("method", req.getMethod());
push.put("uri", req.getURIString());
push.put("streamId", frame.getStreamId());
push.put("promisedStreamId", frame.getPromisedStreamId());
phaser.register();
return this;
}
});
phaser.awaitAdvanceInterruptibly(phaser.arrive());// , 5, TimeUnit.SECONDS);
return result;
} finally {
client.stop();
}
}
protected boolean parseResponse(final Socket client, final Parser parser) throws IOException {
return parseResponse(client, parser, 1000);
}
protected boolean parseResponse(final Socket client, final Parser parser, final long timeout)
throws IOException {
byte[] buffer = new byte[2048];
InputStream input = client.getInputStream();
client.setSoTimeout((int) timeout);
while (true) {
try {
int read = input.read(buffer);
if (read < 0) {
return true;
}
parser.parse(ByteBuffer.wrap(buffer, 0, read));
if (client.isClosed()) {
return true;
}
} catch (SocketTimeoutException x) {
return false;
}
}
}
}