diff --git a/jetty-http3/http3-common/pom.xml b/jetty-http3/http3-common/pom.xml index 6c3a2e205c2..4c4ec5a30e4 100644 --- a/jetty-http3/http3-common/pom.xml +++ b/jetty-http3/http3-common/pom.xml @@ -35,6 +35,11 @@ jetty-util ${project.version} + + + org.eclipse.jetty + jetty-slf4j-impl + diff --git a/jetty-http3/http3-common/src/main/java/org/eclipse/jetty/http3/api/Stream.java b/jetty-http3/http3-common/src/main/java/org/eclipse/jetty/http3/api/Stream.java index b897ca232e0..ce8ab0ce151 100644 --- a/jetty-http3/http3-common/src/main/java/org/eclipse/jetty/http3/api/Stream.java +++ b/jetty-http3/http3-common/src/main/java/org/eclipse/jetty/http3/api/Stream.java @@ -15,7 +15,9 @@ package org.eclipse.jetty.http3.api; import java.util.concurrent.CompletableFuture; +import org.eclipse.jetty.http3.frames.DataFrame; import org.eclipse.jetty.http3.frames.HeadersFrame; +import org.eclipse.jetty.util.Callback; public interface Stream { @@ -27,6 +29,10 @@ public interface Stream { } + public default void onData(Stream stream, DataFrame frame, Callback callback) + { + } + public default void onTrailer(Stream stream, HeadersFrame frame) { } diff --git a/jetty-http3/http3-common/src/main/java/org/eclipse/jetty/http3/internal/HTTP3Session.java b/jetty-http3/http3-common/src/main/java/org/eclipse/jetty/http3/internal/HTTP3Session.java index a0337606fe1..6813b878f41 100644 --- a/jetty-http3/http3-common/src/main/java/org/eclipse/jetty/http3/internal/HTTP3Session.java +++ b/jetty-http3/http3-common/src/main/java/org/eclipse/jetty/http3/internal/HTTP3Session.java @@ -19,6 +19,7 @@ import java.util.concurrent.ConcurrentHashMap; import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http3.api.Session; import org.eclipse.jetty.http3.api.Stream; +import org.eclipse.jetty.http3.frames.DataFrame; import org.eclipse.jetty.http3.frames.Frame; import org.eclipse.jetty.http3.frames.HeadersFrame; import org.eclipse.jetty.http3.frames.SettingsFrame; @@ -60,6 +61,11 @@ public abstract class HTTP3Session implements Session, ParserListener return streams.computeIfAbsent(streamId, id -> new HTTP3Stream(this, streamId)); } + protected HTTP3Stream getStream(long streamId) + { + return streams.get(streamId); + } + protected abstract void writeFrame(long streamId, Frame frame, Callback callback); public Map onPreface() @@ -163,4 +169,27 @@ public abstract class HTTP3Session implements Session, ParserListener LOG.info("failure notifying listener {}", listener, x); } } + + @Override + public void onData(long streamId, DataFrame frame) + { + if (LOG.isDebugEnabled()) + LOG.debug("received {}#{} on {}", frame, streamId, this); + + // The stream must already exist. + HTTP3Stream stream = getStream(streamId); + // TODO: handle null stream. + + // TODO: implement demand mechanism like in HTTP2Stream + // demand(n) should be on Stream, or on a LongConsumer parameter? + + // TODO: the callback in HTTP2 was only to notify of data consumption for flow control. + // Here, we don't have to do flow control, but what about retain()/release() for the network buffer? + notifyData(stream, frame, Callback.NOOP); + } + + private void notifyData(HTTP3Stream stream, DataFrame frame, Callback callback) + { + stream.getListener().onData(stream, frame, callback); + } } diff --git a/jetty-http3/http3-common/src/main/java/org/eclipse/jetty/http3/internal/generator/DataGenerator.java b/jetty-http3/http3-common/src/main/java/org/eclipse/jetty/http3/internal/generator/DataGenerator.java index 9547ecf0e70..9994d28b57d 100644 --- a/jetty-http3/http3-common/src/main/java/org/eclipse/jetty/http3/internal/generator/DataGenerator.java +++ b/jetty-http3/http3-common/src/main/java/org/eclipse/jetty/http3/internal/generator/DataGenerator.java @@ -13,7 +13,12 @@ package org.eclipse.jetty.http3.internal.generator; +import java.nio.ByteBuffer; + +import org.eclipse.jetty.http3.frames.DataFrame; import org.eclipse.jetty.http3.frames.Frame; +import org.eclipse.jetty.http3.frames.FrameType; +import org.eclipse.jetty.http3.internal.VarLenInt; import org.eclipse.jetty.io.ByteBufferPool; public class DataGenerator extends FrameGenerator @@ -21,6 +26,21 @@ public class DataGenerator extends FrameGenerator @Override public int generate(ByteBufferPool.Lease lease, long streamId, Frame frame) { - return 0; + DataFrame dataFrame = (DataFrame)frame; + return generateDataFrame(lease, dataFrame); + } + + private int generateDataFrame(ByteBufferPool.Lease lease, DataFrame frame) + { + ByteBuffer data = frame.getData(); + int dataLength = data.remaining(); + int headerLength = VarLenInt.length(FrameType.DATA.type()) + VarLenInt.length(dataLength); + ByteBuffer header = ByteBuffer.allocate(headerLength); + VarLenInt.generate(header, FrameType.DATA.type()); + VarLenInt.generate(header, dataLength); + header.flip(); + lease.append(header, false); + lease.append(data, false); + return headerLength + dataLength; } } diff --git a/jetty-http3/http3-common/src/main/java/org/eclipse/jetty/http3/internal/generator/HeadersGenerator.java b/jetty-http3/http3-common/src/main/java/org/eclipse/jetty/http3/internal/generator/HeadersGenerator.java index 9ffd4d7ea6f..9e420b85049 100644 --- a/jetty-http3/http3-common/src/main/java/org/eclipse/jetty/http3/internal/generator/HeadersGenerator.java +++ b/jetty-http3/http3-common/src/main/java/org/eclipse/jetty/http3/internal/generator/HeadersGenerator.java @@ -50,15 +50,15 @@ public class HeadersGenerator extends FrameGenerator ByteBuffer buffer = lease.acquire(maxLength, useDirectByteBuffers); encoder.encode(buffer, streamId, frame.getMetaData()); buffer.flip(); - int length = buffer.remaining(); - int capacity = VarLenInt.length(FrameType.HEADERS.type()) + VarLenInt.length(length); - ByteBuffer header = ByteBuffer.allocate(capacity); + int dataLength = buffer.remaining(); + int headerLength = VarLenInt.length(FrameType.HEADERS.type()) + VarLenInt.length(dataLength); + ByteBuffer header = ByteBuffer.allocate(headerLength); VarLenInt.generate(header, FrameType.HEADERS.type()); - VarLenInt.generate(header, length); + VarLenInt.generate(header, dataLength); header.flip(); lease.append(header, false); lease.append(buffer, true); - return buffer.remaining(); + return headerLength + dataLength; } catch (QpackException e) { diff --git a/jetty-http3/http3-common/src/test/java/org/eclipse/jetty/http3/internal/DataGenerateParseTest.java b/jetty-http3/http3-common/src/test/java/org/eclipse/jetty/http3/internal/DataGenerateParseTest.java new file mode 100644 index 00000000000..a8c16d6bc97 --- /dev/null +++ b/jetty-http3/http3-common/src/test/java/org/eclipse/jetty/http3/internal/DataGenerateParseTest.java @@ -0,0 +1,80 @@ +// +// ======================================================================== +// Copyright (c) 1995-2021 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.http3.internal; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import org.eclipse.jetty.http3.frames.DataFrame; +import org.eclipse.jetty.http3.internal.generator.MessageGenerator; +import org.eclipse.jetty.http3.internal.parser.MessageParser; +import org.eclipse.jetty.http3.internal.parser.ParserListener; +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.NullByteBufferPool; +import org.eclipse.jetty.util.BufferUtil; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class DataGenerateParseTest +{ + @Test + public void testGenerateParseEmpty() + { + testGenerateParse(BufferUtil.EMPTY_BUFFER); + } + + @Test + public void testGenerateParse() + { + byte[] bytes = new byte[1024]; + new Random().nextBytes(bytes); + testGenerateParse(ByteBuffer.wrap(bytes)); + } + + private void testGenerateParse(ByteBuffer byteBuffer) + { + byte[] inputBytes = new byte[byteBuffer.remaining()]; + byteBuffer.get(inputBytes); + DataFrame input = new DataFrame(ByteBuffer.wrap(inputBytes)); + + ByteBufferPool.Lease lease = new ByteBufferPool.Lease(new NullByteBufferPool()); + new MessageGenerator(null, 8192, true).generate(lease, 0, input); + + List frames = new ArrayList<>(); + MessageParser parser = new MessageParser(0, null, new ParserListener() + { + @Override + public void onData(long streamId, DataFrame frame) + { + frames.add(frame); + } + }); + for (ByteBuffer buffer : lease.getByteBuffers()) + { + parser.parse(buffer); + assertFalse(buffer.hasRemaining()); + } + + assertEquals(1, frames.size()); + DataFrame output = frames.get(0); + byte[] outputBytes = new byte[output.getData().remaining()]; + output.getData().get(outputBytes); + assertArrayEquals(inputBytes, outputBytes); + } +} diff --git a/jetty-http3/http3-common/src/test/java/org/eclipse/jetty/http3/internal/HeadersGenerateParseTest.java b/jetty-http3/http3-common/src/test/java/org/eclipse/jetty/http3/internal/HeadersGenerateParseTest.java new file mode 100644 index 00000000000..e87d36614a3 --- /dev/null +++ b/jetty-http3/http3-common/src/test/java/org/eclipse/jetty/http3/internal/HeadersGenerateParseTest.java @@ -0,0 +1,78 @@ +// +// ======================================================================== +// Copyright (c) 1995-2021 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.http3.internal; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http3.frames.HeadersFrame; +import org.eclipse.jetty.http3.internal.generator.MessageGenerator; +import org.eclipse.jetty.http3.internal.parser.MessageParser; +import org.eclipse.jetty.http3.internal.parser.ParserListener; +import org.eclipse.jetty.http3.qpack.QpackDecoder; +import org.eclipse.jetty.http3.qpack.QpackEncoder; +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.NullByteBufferPool; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class HeadersGenerateParseTest +{ + @Test + public void testGenerateParse() + { + HttpURI uri = HttpURI.from("http://host:1234/path?a=b"); + HttpFields fields = HttpFields.build() + .put("User-Agent", "Jetty") + .put("Cookie", "c=d"); + HeadersFrame input = new HeadersFrame(new MetaData.Request(HttpMethod.GET.asString(), uri, HttpVersion.HTTP_3, fields)); + + QpackEncoder encoder = new QpackEncoder(instructions -> {}, 100); + ByteBufferPool.Lease lease = new ByteBufferPool.Lease(new NullByteBufferPool()); + new MessageGenerator(encoder, 8192, true).generate(lease, 0, input); + + QpackDecoder decoder = new QpackDecoder(instructions -> {}, 8192); + List frames = new ArrayList<>(); + MessageParser parser = new MessageParser(0, decoder, new ParserListener() + { + @Override + public void onHeaders(long streamId, HeadersFrame frame) + { + frames.add(frame); + } + }); + for (ByteBuffer buffer : lease.getByteBuffers()) + { + parser.parse(buffer); + assertFalse(buffer.hasRemaining()); + } + + assertEquals(1, frames.size()); + HeadersFrame output = frames.get(0); + + MetaData.Request inputMetaData = (MetaData.Request)input.getMetaData(); + MetaData.Request outputMetaData = (MetaData.Request)output.getMetaData(); + assertEquals(inputMetaData.getMethod(), outputMetaData.getMethod()); + assertEquals(inputMetaData.getURIString(), outputMetaData.getURIString()); + assertEquals(inputMetaData.getFields(), outputMetaData.getFields()); + } +} diff --git a/jetty-http3/http3-common/src/test/java/org/eclipse/jetty/http3/internal/SettingsGenerateParseTest.java b/jetty-http3/http3-common/src/test/java/org/eclipse/jetty/http3/internal/SettingsGenerateParseTest.java index b177b5e1e76..868d62765f6 100644 --- a/jetty-http3/http3-common/src/test/java/org/eclipse/jetty/http3/internal/SettingsGenerateParseTest.java +++ b/jetty-http3/http3-common/src/test/java/org/eclipse/jetty/http3/internal/SettingsGenerateParseTest.java @@ -19,8 +19,8 @@ import java.util.List; import java.util.Map; import org.eclipse.jetty.http3.frames.SettingsFrame; -import org.eclipse.jetty.http3.internal.generator.SettingsGenerator; -import org.eclipse.jetty.http3.internal.parser.MessageParser; +import org.eclipse.jetty.http3.internal.generator.ControlGenerator; +import org.eclipse.jetty.http3.internal.parser.ControlParser; import org.eclipse.jetty.http3.internal.parser.ParserListener; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.NullByteBufferPool; @@ -48,10 +48,10 @@ public class SettingsGenerateParseTest SettingsFrame input = new SettingsFrame(settings); ByteBufferPool.Lease lease = new ByteBufferPool.Lease(new NullByteBufferPool()); - new SettingsGenerator().generate(lease, 0, input); + new ControlGenerator().generate(lease, 0, input); List frames = new ArrayList<>(); - MessageParser parser = new MessageParser(0, null, new ParserListener() + ControlParser parser = new ControlParser(new ParserListener() { @Override public void onSettings(SettingsFrame frame) diff --git a/jetty-http3/http3-tests/src/test/java/org/eclipse/jetty/http3/tests/HTTP3ClientServerTest.java b/jetty-http3/http3-tests/src/test/java/org/eclipse/jetty/http3/tests/HTTP3ClientServerTest.java index 0f94208683c..5156fc74734 100644 --- a/jetty-http3/http3-tests/src/test/java/org/eclipse/jetty/http3/tests/HTTP3ClientServerTest.java +++ b/jetty-http3/http3-tests/src/test/java/org/eclipse/jetty/http3/tests/HTTP3ClientServerTest.java @@ -32,8 +32,10 @@ import org.eclipse.jetty.http3.frames.SettingsFrame; import org.eclipse.jetty.http3.server.RawHTTP3ServerConnectionFactory; import org.eclipse.jetty.quic.server.ServerQuicConnector; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -41,21 +43,42 @@ import static org.junit.jupiter.api.Assertions.assertTrue; public class HTTP3ClientServerTest { - @Test - public void testGETThenResponseWithoutContent() throws Exception + private Server server; + private ServerQuicConnector connector; + private HTTP3Client client; + + private void startServer(Session.Server.Listener listener) throws Exception { SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setKeyStorePath("src/test/resources/keystore.p12"); sslContextFactory.setKeyStorePassword("storepwd"); - QueuedThreadPool serverThreads = new QueuedThreadPool(); serverThreads.setName("server"); - Server server = new Server(serverThreads); + server = new Server(serverThreads); + connector = new ServerQuicConnector(server, sslContextFactory, new RawHTTP3ServerConnectionFactory(listener)); + server.addConnector(connector); + server.start(); + } + private void startClient() throws Exception + { + client = new HTTP3Client(); + client.start(); + } + + @AfterEach + public void dispose() + { + LifeCycle.stop(client); + LifeCycle.stop(server); + } + + @Test + public void testConnectTriggersSettingsFrame() throws Exception + { CountDownLatch serverPrefaceLatch = new CountDownLatch(1); CountDownLatch serverSettingsLatch = new CountDownLatch(1); - CountDownLatch serverRequestLatch = new CountDownLatch(1); - ServerQuicConnector connector = new ServerQuicConnector(server, sslContextFactory, new RawHTTP3ServerConnectionFactory(new Session.Server.Listener() + startServer(new Session.Server.Listener() { @Override public Map onPreface(Session session) @@ -69,22 +92,8 @@ public class HTTP3ClientServerTest { serverSettingsLatch.countDown(); } - - @Override - public Stream.Listener onRequest(Stream stream, HeadersFrame frame) - { - serverRequestLatch.countDown(); - // Send the response. - stream.respond(new HeadersFrame(new MetaData.Response(HttpVersion.HTTP_3, HttpStatus.OK_200, HttpFields.EMPTY))); - // Not interested in request data. - return null; - } - })); - server.addConnector(connector); - server.start(); - - HTTP3Client client = new HTTP3Client(); - client.start(); + }); + startClient(); CountDownLatch clientPrefaceLatch = new CountDownLatch(1); CountDownLatch clientSettingsLatch = new CountDownLatch(1); @@ -103,7 +112,30 @@ public class HTTP3ClientServerTest clientSettingsLatch.countDown(); } }) - .get(555, TimeUnit.SECONDS); + .get(5, TimeUnit.SECONDS); + assertNotNull(session); + } + + @Test + public void testGETThenResponseWithoutContent() throws Exception + { + CountDownLatch serverRequestLatch = new CountDownLatch(1); + startServer(new Session.Server.Listener() + { + @Override + public Stream.Listener onRequest(Stream stream, HeadersFrame frame) + { + serverRequestLatch.countDown(); + // Send the response. + stream.respond(new HeadersFrame(new MetaData.Response(HttpVersion.HTTP_3, HttpStatus.OK_200, HttpFields.EMPTY))); + // Not interested in request data. + return null; + } + }); + startClient(); + + Session.Client session = client.connect(new InetSocketAddress("localhost", connector.getLocalPort()), new Session.Client.Listener() {}) + .get(5, TimeUnit.SECONDS); CountDownLatch clientResponseLatch = new CountDownLatch(1); HttpURI uri = HttpURI.from("https://localhost:" + connector.getLocalPort() + "/"); @@ -120,11 +152,7 @@ public class HTTP3ClientServerTest .get(555, TimeUnit.SECONDS); assertNotNull(stream); - assertTrue(clientPrefaceLatch.await(555, TimeUnit.SECONDS)); - assertTrue(serverPrefaceLatch.await(555, TimeUnit.SECONDS)); - assertTrue(serverSettingsLatch.await(555, TimeUnit.SECONDS)); - assertTrue(clientSettingsLatch.await(555, TimeUnit.SECONDS)); - assertTrue(serverRequestLatch.await(555, TimeUnit.SECONDS)); - assertTrue(clientResponseLatch.await(555, TimeUnit.SECONDS)); + assertTrue(serverRequestLatch.await(5, TimeUnit.SECONDS)); + assertTrue(clientResponseLatch.await(5, TimeUnit.SECONDS)); } }