diff --git a/NOTICE.txt b/NOTICE.txt
index 2b8df6f..168cb50 100644
--- a/NOTICE.txt
+++ b/NOTICE.txt
@@ -42,12 +42,12 @@ Subproject organization
Original netty subproject names are not related to package names. I reorganized the names to allow better assignment
between subproject name, package name, artifact names, and java module. The following reorgnizations were performed:
-netty/all -> [todo]
-netty/bom -> [todo]
+netty/all ->
+netty/bom ->
netty/buffer -> netty-buffer
netty/codec -> netty-handler-codec, netty-handler-codec-compression, netty-handler-codec-protobuf
-netty/codec-dns -> [todo]
-netty/codec-haproxy -> [todo]
+netty/codec-dns -> netty-handler-codec-dns
+netty/codec-haproxy ->
netty/codec-http -> netty-handler-codec-http, netty-handler-codec-rtsp, netty-handler-codec-spdy
netty/codec-http2 ->
netty/codec-memcache ->
@@ -62,7 +62,7 @@ netty/handler -> netty-handler
netty/handler-proxy
netty/handler-ssl-ocsp
netty/resolver -> netty-resolver
-netty/resolver-dns ->
+netty/resolver-dns -> netty-resolver-dns
netty/resolver-dns-classes-macos -> [dropped]
netty/resolver-dns-native-macos -> [dropped]
netty/transport -> netty-channel
diff --git a/build.gradle b/build.gradle
index 04605a4..5f6a326 100644
--- a/build.gradle
+++ b/build.gradle
@@ -31,7 +31,7 @@ ext {
apply plugin: 'com.google.osdetector'
subprojects {
- if (!it.name.endsWith('-native')) {
+ if (!it.name.endsWith('-native') && it.name != 'test-results') {
apply from: rootProject.file('gradle/repositories/maven.gradle')
apply from: rootProject.file('gradle/compile/java.gradle')
apply from: rootProject.file('gradle/test/junit5.gradle')
diff --git a/gradle/test/junit5.gradle b/gradle/test/junit5.gradle
index d984d4a..6a29c60 100644
--- a/gradle/test/junit5.gradle
+++ b/gradle/test/junit5.gradle
@@ -12,8 +12,8 @@ test {
useJUnitPlatform()
failFast = false
ignoreFailures = true
- minHeapSize = "1g" // initial heap size
- maxHeapSize = "2g" // maximum heap size
+ minHeapSize = "2g" // initial heap size
+ maxHeapSize = "4g" // maximum heap size
jvmArgs '--add-exports=java.base/jdk.internal=ALL-UNNAMED',
'--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED',
'--add-exports=java.base/sun.nio.ch=ALL-UNNAMED',
diff --git a/netty-channel-unix-native/build.gradle b/netty-channel-unix-native/build.gradle
index 9f08e7d..c7fc085 100644
--- a/netty-channel-unix-native/build.gradle
+++ b/netty-channel-unix-native/build.gradle
@@ -1,3 +1,4 @@
+
/* currently we do not build our C code natively, but we provide copies of the binaries in META-INF/native */
/* the static library is included in other native builds, so nothing is provided here */
diff --git a/netty-handler-codec-quic-native/NOTICE.txt b/netty-handler-codec-quic-native/NOTICE.txt
new file mode 100644
index 0000000..289f946
--- /dev/null
+++ b/netty-handler-codec-quic-native/NOTICE.txt
@@ -0,0 +1,2 @@
+
+Because we moved from "io.netty.incubator" to "io.netty", we use only a specially prepared linux x86-64 shared library
\ No newline at end of file
diff --git a/netty-handler-codec-quic-native/src/main/resources/META-INF/native/libnetty_quiche_linux_aarch_64.so b/netty-handler-codec-quic-native/src/main/resources/META-INF/native/libnetty_quiche_linux_aarch_64.so
deleted file mode 100644
index 66b6693..0000000
Binary files a/netty-handler-codec-quic-native/src/main/resources/META-INF/native/libnetty_quiche_linux_aarch_64.so and /dev/null differ
diff --git a/netty-handler-codec-quic-native/src/main/resources/META-INF/native/libnetty_quiche_linux_x86_64.so b/netty-handler-codec-quic-native/src/main/resources/META-INF/native/libnetty_quiche_linux_x86_64.so
old mode 100644
new mode 100755
index dcf8086..ad2e610
Binary files a/netty-handler-codec-quic-native/src/main/resources/META-INF/native/libnetty_quiche_linux_x86_64.so and b/netty-handler-codec-quic-native/src/main/resources/META-INF/native/libnetty_quiche_linux_x86_64.so differ
diff --git a/netty-handler-codec-quic-native/src/main/resources/META-INF/native/libnetty_quiche_osx_aarch_64.jnilib b/netty-handler-codec-quic-native/src/main/resources/META-INF/native/libnetty_quiche_osx_aarch_64.jnilib
deleted file mode 100644
index 1d8d1fb..0000000
Binary files a/netty-handler-codec-quic-native/src/main/resources/META-INF/native/libnetty_quiche_osx_aarch_64.jnilib and /dev/null differ
diff --git a/netty-handler-codec-quic-native/src/main/resources/META-INF/native/libnetty_quiche_osx_x86_64.jnilib b/netty-handler-codec-quic-native/src/main/resources/META-INF/native/libnetty_quiche_osx_x86_64.jnilib
deleted file mode 100644
index 06786fe..0000000
Binary files a/netty-handler-codec-quic-native/src/main/resources/META-INF/native/libnetty_quiche_osx_x86_64.jnilib and /dev/null differ
diff --git a/netty-handler-codec-quic-native/src/main/resources/META-INF/native/netty_quiche_windows_x86_64.dll b/netty-handler-codec-quic-native/src/main/resources/META-INF/native/netty_quiche_windows_x86_64.dll
deleted file mode 100644
index 7098d06..0000000
Binary files a/netty-handler-codec-quic-native/src/main/resources/META-INF/native/netty_quiche_windows_x86_64.dll and /dev/null differ
diff --git a/netty-handler-codec-quic/build.gradle b/netty-handler-codec-quic/build.gradle
index 6f206b4..0d4cc50 100644
--- a/netty-handler-codec-quic/build.gradle
+++ b/netty-handler-codec-quic/build.gradle
@@ -3,4 +3,6 @@ dependencies {
implementation project(':netty-channel-epoll')
implementation project(':netty-channel-unix')
runtimeOnly project(path: ':netty-handler-codec-quic-native', configuration: osdetector.classifier)
+ testImplementation testLibs.assertj
+ testImplementation project(':netty-handler-ssl-bouncycastle')
}
diff --git a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicSslContextBuilder.java b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicSslContextBuilder.java
index 7ea7c97..cb415c2 100644
--- a/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicSslContextBuilder.java
+++ b/netty-handler-codec-quic/src/main/java/io/netty/handler/codec/quic/QuicSslContextBuilder.java
@@ -182,7 +182,7 @@ public final class QuicSslContextBuilder {
/**
* Enable / disable keylog. When enabled, TLS keys are logged to an internal logger named
- * "io.netty.incubator.codec.quic.BoringSSLLogginKeylog" with DEBUG level, see
+ * "io.netty.codec.quic.BoringSSLLogginKeylog" with DEBUG level, see
* {@link BoringSSLKeylog} for detail, logging keys are following
*
* NSS Key Log Format. This is intended for debugging use with tools like Wireshark.
diff --git a/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/AbstractQuicTest.java b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/AbstractQuicTest.java
new file mode 100644
index 0000000..62c6140
--- /dev/null
+++ b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/AbstractQuicTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2020 The Netty Project
+ *
+ * The Netty Project licenses this file to you 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:
+ *
+ * https://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.netty.handler.codec.quic;
+
+import io.netty.util.concurrent.ImmediateExecutor;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Timeout;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+@Timeout(10)
+public abstract class AbstractQuicTest {
+
+ @BeforeAll
+ public static void ensureAvailability() {
+ Quic.ensureAvailability();
+ }
+
+ static Executor[] newSslTaskExecutors() {
+ return new Executor[] {
+ ImmediateExecutor.INSTANCE,
+ Executors.newSingleThreadExecutor()
+ };
+ }
+
+ static void shutdown(Executor executor) {
+ if (executor instanceof ExecutorService) {
+ ((ExecutorService) executor).shutdown();
+ }
+ }
+}
diff --git a/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/FlushStrategyTest.java b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/FlushStrategyTest.java
new file mode 100644
index 0000000..39b5f2e
--- /dev/null
+++ b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/FlushStrategyTest.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2021 The Netty Project
+ *
+ * The Netty Project licenses this file to you 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:
+ *
+ * https://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.netty.handler.codec.quic;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class FlushStrategyTest {
+
+ @Test
+ public void testAfterNumBytes() {
+ FlushStrategy strategy = FlushStrategy.afterNumBytes(10);
+ assertFalse(strategy.shouldFlushNow(1, 10));
+ assertTrue(strategy.shouldFlushNow(1, 11));
+ }
+
+ @Test
+ public void testAfterNumPackets() {
+ FlushStrategy strategy = FlushStrategy.afterNumPackets(10);
+ assertFalse(strategy.shouldFlushNow(10, 10));
+ assertTrue(strategy.shouldFlushNow(11, 11));
+ }
+}
diff --git a/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/InsecureQuicTokenHandlerTest.java b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/InsecureQuicTokenHandlerTest.java
new file mode 100644
index 0000000..4719f3e
--- /dev/null
+++ b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/InsecureQuicTokenHandlerTest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2020 The Netty Project
+ *
+ * The Netty Project licenses this file to you 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:
+ *
+ * https://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.netty.handler.codec.quic;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+
+import org.junit.jupiter.api.Test;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.util.concurrent.ThreadLocalRandom;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.lessThanOrEqualTo;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+public class InsecureQuicTokenHandlerTest extends AbstractQuicTest {
+
+ @Test
+ public void testMaxTokenLength() {
+ assertEquals(InsecureQuicTokenHandler.MAX_TOKEN_LEN, InsecureQuicTokenHandler.INSTANCE.maxTokenLength());
+ }
+
+ @Test
+ public void testTokenProcessingIpv4() throws UnknownHostException {
+ testTokenProcessing(true);
+ }
+
+ @Test
+ public void testTokenProcessingIpv6() throws UnknownHostException {
+ testTokenProcessing(false);
+ }
+
+ private static void testTokenProcessing(boolean ipv4) throws UnknownHostException {
+ byte[] bytes = new byte[Quiche.QUICHE_MAX_CONN_ID_LEN];
+ ThreadLocalRandom.current().nextBytes(bytes);
+ ByteBuf dcid = Unpooled.wrappedBuffer(bytes);
+ ByteBuf out = Unpooled.buffer();
+ try {
+ final InetSocketAddress validAddress;
+ final InetSocketAddress invalidAddress;
+ if (ipv4) {
+ validAddress = new InetSocketAddress(
+ InetAddress.getByAddress(new byte[] { 10, 10, 10, 1}), 9999);
+ invalidAddress = new InetSocketAddress(
+ InetAddress.getByAddress(new byte[] { 10, 10, 10, 10}), 9999);
+ } else {
+ validAddress = new InetSocketAddress(InetAddress.getByAddress(
+ new byte[] { 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 1}), 9999);
+ invalidAddress = new InetSocketAddress(InetAddress.getByAddress(
+ new byte[] { 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10}), 9999);
+ }
+
+ InsecureQuicTokenHandler.INSTANCE.writeToken(out, dcid, validAddress);
+ assertThat(out.readableBytes(), lessThanOrEqualTo(InsecureQuicTokenHandler.INSTANCE.maxTokenLength()));
+ assertNotEquals(-1, InsecureQuicTokenHandler.INSTANCE.validateToken(out, validAddress));
+
+ // Use another address and check that the validate fails.
+ assertEquals(-1, InsecureQuicTokenHandler.INSTANCE.validateToken(out, invalidAddress));
+ } finally {
+ dcid.release();
+ out.release();
+ }
+ }
+}
diff --git a/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicChannelConnectTest.java b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicChannelConnectTest.java
new file mode 100644
index 0000000..4a92928
--- /dev/null
+++ b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicChannelConnectTest.java
@@ -0,0 +1,1531 @@
+/*
+ * Copyright 2020 The Netty Project
+ *
+ * The Netty Project licenses this file to you 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:
+ *
+ * https://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.netty.handler.codec.quic;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.ConnectTimeoutException;
+import io.netty.channel.socket.ChannelInputShutdownEvent;
+import io.netty.handler.ssl.ClientAuth;
+import io.netty.handler.ssl.SniCompletionEvent;
+import io.netty.handler.ssl.SslHandshakeCompletionEvent;
+import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
+import io.netty.handler.ssl.util.TrustManagerFactoryWrapper;
+import io.netty.util.DomainWildcardMappingBuilder;
+import io.netty.util.ReferenceCountUtil;
+import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.ImmediateEventExecutor;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Timeout;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.opentest4j.AssertionFailedError;
+
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLEngineResult;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.X509ExtendedKeyManager;
+import javax.net.ssl.X509ExtendedTrustManager;
+import javax.net.ssl.X509TrustManager;
+import java.io.File;
+import java.net.DatagramSocket;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.channels.AlreadyConnectedException;
+import java.nio.channels.ClosedChannelException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.security.spec.MGF1ParameterSpec;
+import java.security.spec.PSSParameterSpec;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class QuicChannelConnectTest extends AbstractQuicTest {
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ @Timeout(value = 5000, unit = TimeUnit.MILLISECONDS)
+ public void testConnectAndQLog(Executor executor) throws Throwable {
+ Path path = Files.createTempFile("qlog", ".quic");
+ assertTrue(path.toFile().delete());
+ testQLog(executor, path, p -> {
+ try {
+ // Some log should have been written at some point.
+ while (Files.readAllLines(p).isEmpty()) {
+ Thread.sleep(100);
+ }
+ } catch (Exception e) {
+ throw new AssertionError(e);
+ }
+ });
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ @Timeout(value = 5000, unit = TimeUnit.MILLISECONDS)
+ public void testConnectAndQLogDir(Executor executor) throws Throwable {
+ Path path = Files.createTempDirectory("qlogdir-");
+ testQLog(executor, path, p -> {
+ try {
+ for (;;) {
+ File[] files = path.toFile().listFiles();
+ if (files != null && files.length == 1) {
+ if (!Files.readAllLines(files[0].toPath()).isEmpty()) {
+ return;
+ }
+ }
+ Thread.sleep(100);
+ }
+ } catch (Exception e) {
+ throw new AssertionError(e);
+ }
+ });
+ }
+
+ private void testQLog(Executor executor, Path path, Consumer consumer) throws Throwable {
+ QuicChannelValidationHandler serverValidationHandler = new QuicChannelValidationHandler();
+ QuicChannelValidationHandler clientValidationHandler = new QuicChannelValidationHandler();
+ Channel server = QuicTestUtils.newServer(executor, serverValidationHandler,
+ new ChannelInboundHandlerAdapter());
+ InetSocketAddress address = (InetSocketAddress) server.localAddress();
+ Channel channel = QuicTestUtils.newClient(executor);
+ try {
+ QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel)
+ .handler(clientValidationHandler)
+ .option(QuicChannelOption.QLOG,
+ new QLogConfiguration(path.toString(), "testTitle", "test"))
+ .streamHandler(new ChannelInboundHandlerAdapter())
+ .remoteAddress(address)
+ .connect()
+ .get();
+ QuicStreamChannel stream = quicChannel.createStream(QuicStreamType.BIDIRECTIONAL,
+ new ChannelInboundHandlerAdapter()).get();
+
+ stream.writeAndFlush(Unpooled.directBuffer().writeZero(10)).sync();
+ stream.close().sync();
+ quicChannel.close().sync();
+ quicChannel.closeFuture().sync();
+ consumer.accept(path);
+
+ serverValidationHandler.assertState();
+ clientValidationHandler.assertState();
+ } finally {
+ server.close().sync();
+ // Close the parent Datagram channel as well.
+ channel.close().sync();
+
+ shutdown(executor);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testCustomKeylog(Executor executor) throws Throwable {
+ AtomicBoolean called = new AtomicBoolean();
+ testKeylog(executor, (BoringSSLKeylog) (engine, log) -> {
+ called.set(true);
+ });
+ assertTrue(called.get());
+ }
+
+ private static void testKeylog(Executor sslTaskExecutor, Object keylog) throws Throwable {
+ QuicChannelValidationHandler serverValidationHandler = new QuicChannelValidationHandler();
+ QuicChannelValidationHandler clientValidationHandler = new QuicChannelValidationHandler();
+ Channel server = QuicTestUtils.newServer(sslTaskExecutor, serverValidationHandler,
+ new ChannelInboundHandlerAdapter());
+ InetSocketAddress address = (InetSocketAddress) server.localAddress();
+ QuicSslContextBuilder ctxClientBuilder = QuicSslContextBuilder.forClient()
+ .trustManager(InsecureTrustManagerFactory.INSTANCE)
+ .applicationProtocols(QuicTestUtils.PROTOS);
+ if (keylog instanceof Boolean) {
+ ctxClientBuilder.keylog((Boolean) keylog);
+ } else {
+ ctxClientBuilder.keylog((BoringSSLKeylog) keylog);
+ }
+
+ QuicSslContext context = ctxClientBuilder.build();
+ Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(sslTaskExecutor, context));
+
+ try {
+ QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel)
+ .handler(clientValidationHandler)
+ .streamHandler(new ChannelInboundHandlerAdapter())
+ .remoteAddress(address)
+ .connect()
+ .get();
+
+ quicChannel.close().sync();
+ quicChannel.closeFuture().sync();
+ serverValidationHandler.assertState();
+ clientValidationHandler.assertState();
+ } finally {
+ server.close().sync();
+ // Close the parent Datagram channel as well.
+ channel.close().sync();
+
+ shutdown(sslTaskExecutor);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testAddressValidation(Executor executor) throws Throwable {
+ // Bind to something so we can use the port to connect too and so can ensure we really timeout.
+ DatagramSocket socket = new DatagramSocket();
+ Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor)
+ .localConnectionIdLength(10));
+ try {
+ ChannelStateVerifyHandler verifyHandler = new ChannelStateVerifyHandler();
+ Future future = QuicTestUtils.newQuicChannelBootstrap(channel)
+ .handler(verifyHandler)
+ .streamHandler(new ChannelInboundHandlerAdapter())
+ .remoteAddress(socket.getLocalSocketAddress())
+ .connectionAddress(QuicConnectionAddress.random(20))
+ .connect();
+ Throwable cause = future.await().cause();
+ assertThat(cause, CoreMatchers.instanceOf(IllegalArgumentException.class));
+ verifyHandler.assertState();
+ } finally {
+ socket.close();
+ // Close the parent Datagram channel as well.
+ channel.close().sync();
+
+ shutdown(executor);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testConnectWithCustomIdLength(Executor executor) throws Throwable {
+ testConnectWithCustomIdLength(executor, 10);
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testConnectWithCustomIdLengthOfZero(Executor executor) throws Throwable {
+ testConnectWithCustomIdLength(executor, 0);
+ }
+
+ private static void testConnectWithCustomIdLength(Executor executor, int idLength) throws Throwable {
+ ChannelActiveVerifyHandler serverQuicChannelHandler = new ChannelActiveVerifyHandler();
+ ChannelStateVerifyHandler serverQuicStreamHandler = new ChannelStateVerifyHandler();
+ Channel server = QuicTestUtils.newServer(QuicTestUtils.newQuicServerBuilder(executor)
+ .localConnectionIdLength(idLength),
+ InsecureQuicTokenHandler.INSTANCE, serverQuicChannelHandler, serverQuicStreamHandler);
+ InetSocketAddress address = (InetSocketAddress) server.localAddress();
+ Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor)
+ .localConnectionIdLength(idLength));
+ try {
+ ChannelActiveVerifyHandler clientQuicChannelHandler = new ChannelActiveVerifyHandler();
+ QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel)
+ .handler(clientQuicChannelHandler)
+ .streamHandler(new ChannelInboundHandlerAdapter())
+ .remoteAddress(address)
+ .connect()
+ .get();
+ assertTrue(quicChannel.close().await().isSuccess());
+ ChannelFuture closeFuture = quicChannel.closeFuture().await();
+ assertTrue(closeFuture.isSuccess());
+ clientQuicChannelHandler.assertState();
+ } finally {
+ serverQuicChannelHandler.assertState();
+ serverQuicStreamHandler.assertState();
+
+ server.close().sync();
+ // Close the parent Datagram channel as well.
+ channel.close().sync();
+ shutdown(executor);
+ }
+ }
+
+ private void testConnectWithDroppedPackets(Executor executor, int numDroppedPackets,
+ QuicConnectionIdGenerator connectionIdGenerator) throws Throwable {
+ Channel server = QuicTestUtils.newServer(QuicTestUtils.newQuicServerBuilder(executor)
+ .connectionIdAddressGenerator(connectionIdGenerator),
+ NoQuicTokenHandler.INSTANCE,
+ new ChannelInboundHandlerAdapter() {
+ @Override
+ public boolean isSharable() {
+ return true;
+ }
+ },
+ new ChannelInboundHandlerAdapter() {
+ @Override
+ public boolean isSharable() {
+ return true;
+ }
+
+ @Override
+ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
+ // Server closes the stream whenever the client sends a FIN.
+ if (evt instanceof ChannelInputShutdownEvent) {
+ ctx.close();
+ }
+ ctx.fireUserEventTriggered(evt);
+ }
+ });
+
+ // Have the server drop the few first numDroppedPackets incoming packets.
+ server.pipeline().addFirst(
+ new ChannelInboundHandlerAdapter() {
+ private int counter = 0;
+
+ public void channelRead(ChannelHandlerContext ctx, Object msg) {
+ if (counter++ < numDroppedPackets) {
+ System.out.println("Server dropping incoming packet #" + counter);
+ ReferenceCountUtil.release(msg);
+ } else {
+ ctx.fireChannelRead(msg);
+ }
+ }
+ });
+
+ InetSocketAddress address = (InetSocketAddress) server.localAddress();
+ Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor));
+ ChannelActiveVerifyHandler clientQuicChannelHandler = new ChannelActiveVerifyHandler();
+ try {
+ QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel)
+ .handler(clientQuicChannelHandler)
+ .remoteAddress(address)
+ .connect()
+ .get();
+
+ QuicStreamChannel quicStream = quicChannel.createStream(QuicStreamType.BIDIRECTIONAL,
+ new ChannelInboundHandlerAdapter()).get();
+
+ ByteBuf payload = Unpooled.wrappedBuffer("HELLO!".getBytes(StandardCharsets.US_ASCII));
+ quicStream.writeAndFlush(payload).sync();
+ quicStream.shutdownOutput().sync();
+ assertTrue(quicStream.closeFuture().await().isSuccess());
+
+ ChannelFuture closeFuture = channel.close().await();
+ assertTrue(closeFuture.isSuccess());
+ } finally {
+ clientQuicChannelHandler.assertState();
+ channel.close().sync();
+ server.close().sync();
+ shutdown(executor);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ @Timeout(3)
+ public void testConnectWithNoDroppedPacketsAndRandomConnectionIdGenerator(Executor executor) throws Throwable {
+ testConnectWithDroppedPackets(executor, 0, QuicConnectionIdGenerator.randomGenerator());
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ @Timeout(5)
+ public void testConnectWithDroppedPacketsAndRandomConnectionIdGenerator(Executor executor) throws Throwable {
+ testConnectWithDroppedPackets(executor, 2, QuicConnectionIdGenerator.randomGenerator());
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ @Timeout(3)
+ public void testConnectWithNoDroppedPacketsAndSignConnectionIdGenerator(Executor executor) throws Throwable {
+ testConnectWithDroppedPackets(executor, 0, QuicConnectionIdGenerator.signGenerator());
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ @Timeout(5)
+ public void testConnectWithDroppedPacketsAndSignConnectionIdGenerator(Executor executor) throws Throwable {
+ testConnectWithDroppedPackets(executor, 2, QuicConnectionIdGenerator.signGenerator());
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testConnectTimeout(Executor executor) throws Throwable {
+ // Bind to something so we can use the port to connect too and so can ensure we really timeout.
+ DatagramSocket socket = new DatagramSocket();
+ Channel channel = QuicTestUtils.newClient(executor);
+ try {
+ ChannelStateVerifyHandler verifyHandler = new ChannelStateVerifyHandler();
+ Future future = QuicTestUtils.newQuicChannelBootstrap(channel)
+ .handler(verifyHandler)
+ .streamHandler(new ChannelInboundHandlerAdapter())
+ .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10)
+ .remoteAddress(socket.getLocalSocketAddress())
+ .connect();
+ Throwable cause = future.await().cause();
+ assertThat(cause, CoreMatchers.instanceOf(ConnectTimeoutException.class));
+ verifyHandler.assertState();
+ } finally {
+ socket.close();
+ // Close the parent Datagram channel as well.
+ channel.close().sync();
+
+ shutdown(executor);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testConnectAlreadyConnected(Executor executor) throws Throwable {
+ ChannelActiveVerifyHandler serverQuicChannelHandler = new ChannelActiveVerifyHandler();
+ ChannelStateVerifyHandler serverQuicStreamHandler = new ChannelStateVerifyHandler();
+
+ Channel server = QuicTestUtils.newServer(executor, serverQuicChannelHandler, serverQuicStreamHandler);
+ InetSocketAddress address = (InetSocketAddress) server.localAddress();
+ Channel channel = QuicTestUtils.newClient(executor);
+ try {
+ ChannelActiveVerifyHandler clientQuicChannelHandler = new ChannelActiveVerifyHandler();
+ QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel)
+ .handler(clientQuicChannelHandler)
+ .streamHandler(new ChannelInboundHandlerAdapter())
+ .remoteAddress(address)
+ .connect()
+ .get();
+
+ // Try to connect again
+ ChannelFuture connectFuture = quicChannel.connect(QuicConnectionAddress.random());
+ Throwable cause = connectFuture.await().cause();
+ assertThat(cause, CoreMatchers.instanceOf(AlreadyConnectedException.class));
+ assertTrue(quicChannel.close().await().isSuccess());
+ ChannelFuture closeFuture = quicChannel.closeFuture().await();
+ assertTrue(closeFuture.isSuccess());
+ clientQuicChannelHandler.assertState();
+ serverQuicChannelHandler.assertState();
+ serverQuicStreamHandler.assertState();
+ } finally {
+ server.close().sync();
+ // Close the parent Datagram channel as well.
+ channel.close().sync();
+
+ shutdown(executor);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testConnectWithoutTokenValidation(Executor executor) throws Throwable {
+ int numBytes = 8;
+ ChannelActiveVerifyHandler serverQuicChannelHandler = new ChannelActiveVerifyHandler();
+ CountDownLatch serverLatch = new CountDownLatch(1);
+ CountDownLatch clientLatch = new CountDownLatch(1);
+
+ // Disable token validation
+ Channel server = QuicTestUtils.newServer(executor, NoQuicTokenHandler.INSTANCE,
+ serverQuicChannelHandler, new BytesCountingHandler(serverLatch, numBytes));
+ InetSocketAddress address = (InetSocketAddress) server.localAddress();
+ Channel channel = QuicTestUtils.newClient(executor);
+ try {
+ ChannelActiveVerifyHandler clientQuicChannelHandler = new ChannelActiveVerifyHandler();
+ QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel)
+ .handler(clientQuicChannelHandler)
+ .streamHandler(new ChannelInboundHandlerAdapter())
+ .remoteAddress(address)
+ .connect()
+ .get();
+ QuicConnectionAddress localAddress = (QuicConnectionAddress) quicChannel.localAddress();
+ QuicConnectionAddress remoteAddress = (QuicConnectionAddress) quicChannel.remoteAddress();
+ assertNotNull(localAddress);
+ assertNotNull(remoteAddress);
+
+ QuicStreamChannel stream = quicChannel.createStream(QuicStreamType.BIDIRECTIONAL,
+ new BytesCountingHandler(clientLatch, numBytes)).get();
+ stream.writeAndFlush(Unpooled.directBuffer().writeZero(numBytes)).sync();
+ clientLatch.await();
+
+ assertEquals(QuicTestUtils.PROTOS[0],
+ // Just do the cast as getApplicationProtocol() only exists in SSLEngine itself since Java9+ and
+ // we may run on an earlier version
+ ((QuicheQuicSslEngine) quicChannel.sslEngine()).getApplicationProtocol());
+ stream.close().sync();
+ quicChannel.close().sync();
+ ChannelFuture closeFuture = quicChannel.closeFuture().await();
+ assertTrue(closeFuture.isSuccess());
+
+ clientQuicChannelHandler.assertState();
+ serverQuicChannelHandler.assertState();
+
+ assertEquals(serverQuicChannelHandler.localAddress(), remoteAddress);
+ assertEquals(serverQuicChannelHandler.remoteAddress(), localAddress);
+
+ // Check if we also can access these after the channel was closed.
+ assertNotNull(quicChannel.localAddress());
+ assertNotNull(quicChannel.remoteAddress());
+ } finally {
+ serverLatch.await();
+
+ server.close().sync();
+ // Close the parent Datagram channel as well.
+ channel.close().sync();
+
+ shutdown(executor);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testConnectWith0RTT(Executor executor) throws Throwable {
+ final CountDownLatch readLatch = new CountDownLatch(1);
+ Channel server = QuicTestUtils.newServer(QuicTestUtils.newQuicServerBuilder(executor,
+ QuicSslContextBuilder.forServer(
+ QuicTestUtils.SELF_SIGNED_CERTIFICATE.privateKey(), null,
+ QuicTestUtils.SELF_SIGNED_CERTIFICATE.certificate())
+ .applicationProtocols(QuicTestUtils.PROTOS)
+ .earlyData(true)
+ .build()),
+ InsecureQuicTokenHandler.INSTANCE, new ChannelInboundHandlerAdapter() {
+ @Override
+ public boolean isSharable() {
+ return true;
+ }
+ }, new ChannelInboundHandlerAdapter() {
+ @Override
+ public boolean isSharable() {
+ return true;
+ }
+
+ @Override
+ public void channelRead(ChannelHandlerContext ctx, Object msg) {
+ ByteBuf buffer = (ByteBuf) msg;
+ try {
+ assertEquals(4, buffer.readableBytes());
+ assertEquals(1, buffer.readInt());
+ readLatch.countDown();
+ ctx.close();
+ ctx.channel().parent().close();
+ } finally {
+ buffer.release();
+ }
+ }
+ });
+ InetSocketAddress address = (InetSocketAddress) server.localAddress();
+
+ QuicSslContext sslContext = QuicSslContextBuilder.forClient()
+ .trustManager(InsecureTrustManagerFactory.INSTANCE)
+ .applicationProtocols(QuicTestUtils.PROTOS)
+ .earlyData(true)
+ .build();
+ Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor, sslContext)
+ .sslEngineProvider(q -> sslContext.newEngine(q.alloc(), "localhost", 9999)));
+ final CountDownLatch activeLatch = new CountDownLatch(1);
+ final CountDownLatch eventLatch = new CountDownLatch(1);
+ final CountDownLatch streamLatch = new CountDownLatch(1);
+ final AtomicReference errorRef = new AtomicReference<>();
+
+ try {
+ QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel)
+ .handler(new ChannelInboundHandlerAdapter() {
+ @Override
+ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
+ if (evt instanceof SslEarlyDataReadyEvent) {
+ errorRef.set(new AssertionFailedError("Shouldn't be called on the first connection"));
+ }
+ ctx.fireUserEventTriggered(evt);
+ }
+ })
+ .streamHandler(new ChannelInboundHandlerAdapter())
+ .remoteAddress(address)
+ .connect()
+ .get();
+
+ QuicClientSessionCache cache = ((QuicheQuicSslContext) sslContext).getSessionCache();
+
+ // Let's spin until the session shows up in the cache. This is needed as this might happen a bit after
+ // the connection is already established.
+ // See https://commondatastorage.googleapis.com/chromium-boringssl-docs/ssl.h.html#SSL_CTX_sess_set_new_cb
+ while (!cache.hasSession("localhost", 9999)) {
+ // Check again in 100ms.
+ Thread.sleep(100);
+ }
+
+ quicChannel.close().sync();
+
+ if (errorRef.get() != null) {
+ throw errorRef.get();
+ }
+
+ quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel)
+ .handler(new ChannelInboundHandlerAdapter() {
+ @Override
+ public void channelActive(ChannelHandlerContext ctx) {
+ activeLatch.countDown();
+ ctx.fireChannelActive();
+ }
+
+ @Override
+ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
+ if (evt instanceof SslEarlyDataReadyEvent) {
+ eventLatch.countDown();
+ ((QuicChannel) ctx.channel()).createStream(QuicStreamType.BIDIRECTIONAL,
+ new ChannelInboundHandlerAdapter()).addListener(f -> {
+ try {
+ // This should succeed as we have the transport params cached as part of
+ // the session.
+ assertTrue(f.isSuccess());
+ Channel stream = (Channel) f.getNow();
+
+ // Let's write some data as part of the client hello.
+ stream.writeAndFlush(stream.alloc().buffer().writeInt(1));
+ } catch (Throwable error) {
+ errorRef.set(error);
+ } finally {
+ streamLatch.countDown();
+ }
+ });
+ }
+ ctx.fireUserEventTriggered(evt);
+ }
+ })
+ .streamHandler(new ChannelInboundHandlerAdapter())
+ .remoteAddress(address)
+ .connect()
+ .get();
+
+ awaitAndCheckError(activeLatch, errorRef);
+ awaitAndCheckError(eventLatch, errorRef);
+ awaitAndCheckError(streamLatch, errorRef);
+
+ quicChannel.closeFuture().sync();
+ readLatch.await();
+ } finally {
+ server.close().sync();
+ // Close the parent Datagram channel as well.
+ channel.close().sync();
+
+ shutdown(executor);
+ }
+ }
+
+ private static void awaitAndCheckError(CountDownLatch latch, AtomicReference errorRef) throws Throwable {
+ while (!latch.await(500, TimeUnit.MILLISECONDS)) {
+ if (errorRef.get() != null) {
+ throw errorRef.get();
+ }
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testConnectAndStreamPriority(Executor executor) throws Throwable {
+ int numBytes = 8;
+ ChannelActiveVerifyHandler serverQuicChannelHandler = new ChannelActiveVerifyHandler();
+ CountDownLatch serverLatch = new CountDownLatch(1);
+ CountDownLatch clientLatch = new CountDownLatch(1);
+
+ Channel server = QuicTestUtils.newServer(executor, serverQuicChannelHandler,
+ new BytesCountingHandler(serverLatch, numBytes));
+ InetSocketAddress address = (InetSocketAddress) server.localAddress();
+ Channel channel = QuicTestUtils.newClient(executor);
+ try {
+ ChannelActiveVerifyHandler clientQuicChannelHandler = new ChannelActiveVerifyHandler();
+ QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel)
+ .handler(clientQuicChannelHandler)
+ .streamHandler(new ChannelInboundHandlerAdapter())
+ .remoteAddress(address)
+ .connect()
+ .get();
+ QuicStreamChannel stream = quicChannel.createStream(QuicStreamType.BIDIRECTIONAL,
+ new BytesCountingHandler(clientLatch, numBytes)).get();
+ assertNull(stream.priority());
+ QuicStreamPriority priority = new QuicStreamPriority(0, false);
+ stream.updatePriority(priority).sync();
+ assertEquals(priority, stream.priority());
+
+ stream.writeAndFlush(Unpooled.directBuffer().writeZero(numBytes)).sync();
+ clientLatch.await();
+
+ stream.close().sync();
+ quicChannel.close().sync();
+ ChannelFuture closeFuture = quicChannel.closeFuture().await();
+ assertTrue(closeFuture.isSuccess());
+ clientQuicChannelHandler.assertState();
+ } finally {
+ serverLatch.await();
+ serverQuicChannelHandler.assertState();
+
+ server.close().sync();
+ // Close the parent Datagram channel as well.
+ channel.close().sync();
+
+ shutdown(executor);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testExtendedTrustManagerFailureOnTheClient(Executor executor) throws Throwable {
+ testTrustManagerFailureOnTheClient(executor, true);
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testTrustManagerFailureOnTheClient(Executor executor) throws Throwable {
+ testTrustManagerFailureOnTheClient(executor, false);
+ }
+
+ private void testTrustManagerFailureOnTheClient(Executor executor, boolean extended) throws Throwable {
+ final X509TrustManager trustManager;
+ if (extended) {
+ trustManager = new TestX509ExtendedTrustManager() {
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine)
+ throws CertificateException {
+ throw new CertificateException();
+ }
+ };
+ } else {
+ trustManager = new TestX509TrustManager() {
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
+ throw new CertificateException();
+ }
+ };
+ }
+ Channel server = QuicTestUtils.newServer(executor, new ChannelInboundHandlerAdapter(),
+ new ChannelInboundHandlerAdapter());
+ InetSocketAddress address = (InetSocketAddress) server.localAddress();
+ Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor,
+ QuicSslContextBuilder.forClient()
+ .trustManager(new TrustManagerFactoryWrapper(trustManager))
+ .applicationProtocols(QuicTestUtils.PROTOS).build()));
+ try {
+ Throwable cause = QuicTestUtils.newQuicChannelBootstrap(channel)
+ .handler(new ChannelInboundHandlerAdapter())
+ .streamHandler(new ChannelInboundHandlerAdapter())
+ .remoteAddress(address)
+ .connect()
+ .await().cause();
+ assertThat(cause, Matchers.instanceOf(SSLException.class));
+ } finally {
+ server.close().sync();
+ // Close the parent Datagram channel as well.
+ channel.close().sync();
+
+ shutdown(executor);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testALPNProtocolMissmatch(Executor executor) throws Throwable {
+ CountDownLatch latch = new CountDownLatch(1);
+ CountDownLatch eventLatch = new CountDownLatch(1);
+
+ Channel server = QuicTestUtils.newServer(QuicTestUtils.newQuicServerBuilder(executor,
+ QuicSslContextBuilder.forServer(
+ QuicTestUtils.SELF_SIGNED_CERTIFICATE.privateKey(), null,
+ QuicTestUtils.SELF_SIGNED_CERTIFICATE.certificate())
+ .applicationProtocols("my-protocol").build()),
+ InsecureQuicTokenHandler.INSTANCE, new ChannelInboundHandlerAdapter() {
+
+ @Override
+ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
+ if (evt instanceof SslHandshakeCompletionEvent) {
+ if (((SslHandshakeCompletionEvent) evt).cause() instanceof SSLHandshakeException) {
+ eventLatch.countDown();
+ return;
+ }
+ }
+ ctx.fireUserEventTriggered(evt);
+ }
+
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+ if (cause instanceof SSLHandshakeException) {
+ latch.countDown();
+ } else {
+ ctx.fireExceptionCaught(cause);
+ }
+ }
+ },
+ new ChannelInboundHandlerAdapter());
+ InetSocketAddress address = (InetSocketAddress) server.localAddress();
+ Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor,
+ QuicSslContextBuilder.forClient()
+ .trustManager(InsecureTrustManagerFactory.INSTANCE).applicationProtocols("protocol").build()));
+ AtomicReference closeEventRef = new AtomicReference<>();
+ try {
+ Throwable cause = QuicTestUtils.newQuicChannelBootstrap(channel)
+ .handler(new ChannelInboundHandlerAdapter() {
+ @Override
+ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
+ if (evt instanceof QuicConnectionCloseEvent) {
+ closeEventRef.set((QuicConnectionCloseEvent) evt);
+ }
+ super.userEventTriggered(ctx, evt);
+ }
+ })
+ .streamHandler(new ChannelInboundHandlerAdapter())
+ .remoteAddress(address)
+ .connect()
+ .await().cause();
+ assertThat(cause, Matchers.instanceOf(ClosedChannelException.class));
+ latch.await();
+ eventLatch.await();
+ QuicConnectionCloseEvent closeEvent = closeEventRef.get();
+ assertNotNull(closeEvent);
+ assertTrue(closeEvent.isTlsError());
+ // 120 is the ALPN error.
+ // See https://datatracker.ietf.org/doc/html/rfc8446#section-6
+ assertEquals(120, QuicConnectionCloseEvent.extractTlsError(closeEvent.error()));
+ assertEquals(closeEvent, ((QuicClosedChannelException) cause).event());
+ } finally {
+ server.close().sync();
+ // Close the parent Datagram channel as well.
+ channel.close().sync();
+
+ shutdown(executor);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testConnectSuccessWhenTrustManagerBuildFromSameCert(Executor executor) throws Throwable {
+ Channel server = QuicTestUtils.newServer(QuicTestUtils.newQuicServerBuilder(executor,
+ QuicSslContextBuilder.forServer(
+ QuicTestUtils.SELF_SIGNED_CERTIFICATE.privateKey(), null,
+ QuicTestUtils.SELF_SIGNED_CERTIFICATE.certificate())
+ .applicationProtocols(QuicTestUtils.PROTOS).clientAuth(ClientAuth.NONE).build()),
+ InsecureQuicTokenHandler.INSTANCE, new ChannelInboundHandlerAdapter(),
+ new ChannelInboundHandlerAdapter());
+ InetSocketAddress address = (InetSocketAddress) server.localAddress();
+
+ Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor,
+ QuicSslContextBuilder.forClient()
+ .trustManager(QuicTestUtils.SELF_SIGNED_CERTIFICATE.certificate())
+ .applicationProtocols(QuicTestUtils.PROTOS).build()));
+ try {
+ ChannelActiveVerifyHandler clientQuicChannelHandler = new ChannelActiveVerifyHandler();
+ QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel)
+ .handler(clientQuicChannelHandler)
+ .streamHandler(new ChannelInboundHandlerAdapter())
+ .remoteAddress(address)
+ .connect()
+ .get();
+ assertTrue(quicChannel.close().await().isSuccess());
+ ChannelFuture closeFuture = quicChannel.closeFuture().await();
+ assertTrue(closeFuture.isSuccess());
+ clientQuicChannelHandler.assertState();
+ } finally {
+ server.close().sync();
+ // Close the parent Datagram channel as well.
+ channel.close().sync();
+
+ shutdown(executor);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testConnectMutualAuthRequiredSuccess(Executor executor) throws Throwable {
+ testConnectMutualAuthSuccess(executor, MutalAuthTestMode.REQUIRED);
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testConnectMutualAuthOptionalWithCertSuccess(Executor executor) throws Throwable {
+ testConnectMutualAuthSuccess(executor, MutalAuthTestMode.OPTIONAL_CERT);
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testConnectMutualAuthOptionalWithoutKeyManagerSuccess(Executor executor) throws Throwable {
+ testConnectMutualAuthSuccess(executor, MutalAuthTestMode.OPTIONAL_NO_KEYMANAGER);
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testConnectMutualAuthOptionalWithoutKeyInKeyManagerSuccess(Executor executor) throws Throwable {
+ testConnectMutualAuthSuccess(executor, MutalAuthTestMode.OPTIONAL_NO_KEY_IN_KEYMANAGER);
+ }
+
+ private void testConnectMutualAuthSuccess(Executor executor, MutalAuthTestMode mode) throws Throwable {
+ Channel server = QuicTestUtils.newServer(QuicTestUtils.newQuicServerBuilder(executor,
+ QuicSslContextBuilder.forServer(
+ QuicTestUtils.SELF_SIGNED_CERTIFICATE.privateKey(), null,
+ QuicTestUtils.SELF_SIGNED_CERTIFICATE.certificate()).trustManager(
+ InsecureTrustManagerFactory.INSTANCE)
+ .applicationProtocols(QuicTestUtils.PROTOS)
+ .clientAuth(mode == MutalAuthTestMode.REQUIRED ?
+ ClientAuth.REQUIRE : ClientAuth.OPTIONAL).build()),
+ InsecureQuicTokenHandler.INSTANCE, new ChannelInboundHandlerAdapter(),
+ new ChannelInboundHandlerAdapter());
+ InetSocketAddress address = (InetSocketAddress) server.localAddress();
+
+ QuicSslContextBuilder clientSslCtxBuilder = QuicSslContextBuilder.forClient()
+ .trustManager(InsecureTrustManagerFactory.INSTANCE)
+ .applicationProtocols(QuicTestUtils.PROTOS);
+ switch (mode) {
+ case OPTIONAL_CERT:
+ case REQUIRED:
+ clientSslCtxBuilder.keyManager(
+ QuicTestUtils.SELF_SIGNED_CERTIFICATE.privateKey(), null,
+ QuicTestUtils.SELF_SIGNED_CERTIFICATE.certificate());
+ break;
+ case OPTIONAL_NO_KEY_IN_KEYMANAGER:
+ clientSslCtxBuilder.keyManager(new X509ExtendedKeyManager() {
+ @Override
+ public String[] getClientAliases(String keyType, Principal[] issuers) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
+ return null;
+ }
+
+ @Override
+ public String[] getServerAliases(String keyType, Principal[] issuers) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public X509Certificate[] getCertificateChain(String alias) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public PrivateKey getPrivateKey(String alias) {
+ throw new UnsupportedOperationException();
+ }
+ }, null);
+ break;
+ case OPTIONAL_NO_KEYMANAGER:
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+
+ Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor,
+ clientSslCtxBuilder.build()));
+ try {
+ ChannelActiveVerifyHandler clientQuicChannelHandler = new ChannelActiveVerifyHandler();
+ QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel)
+ .handler(clientQuicChannelHandler)
+ .streamHandler(new ChannelInboundHandlerAdapter())
+ .remoteAddress(address)
+ .connect()
+ .get();
+ assertTrue(quicChannel.close().await().isSuccess());
+ ChannelFuture closeFuture = quicChannel.closeFuture().await();
+ assertTrue(closeFuture.isSuccess());
+ clientQuicChannelHandler.assertState();
+ } finally {
+ server.close().sync();
+ // Close the parent Datagram channel as well.
+ channel.close().sync();
+
+ shutdown(executor);
+ }
+ }
+
+ private enum MutalAuthTestMode {
+ REQUIRED,
+ OPTIONAL_CERT,
+ OPTIONAL_NO_KEYMANAGER,
+ OPTIONAL_NO_KEY_IN_KEYMANAGER
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testConnectMutualAuthFailsIfClientNotSendCertificate(Executor executor) throws Throwable {
+ CountDownLatch latch = new CountDownLatch(1);
+ AtomicReference causeRef = new AtomicReference<>();
+
+ Channel server = QuicTestUtils.newServer(QuicTestUtils.newQuicServerBuilder(executor,
+ QuicSslContextBuilder.forServer(
+ QuicTestUtils.SELF_SIGNED_CERTIFICATE.privateKey(), null,
+ QuicTestUtils.SELF_SIGNED_CERTIFICATE.certificate())
+ .trustManager(InsecureTrustManagerFactory.INSTANCE)
+ .applicationProtocols(QuicTestUtils.PROTOS).clientAuth(ClientAuth.REQUIRE).build()),
+ InsecureQuicTokenHandler.INSTANCE, new ChannelInboundHandlerAdapter() {
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+ causeRef.compareAndSet(null, cause);
+ latch.countDown();
+ ctx.close();
+ }
+ },
+ new ChannelInboundHandlerAdapter());
+ InetSocketAddress address = (InetSocketAddress) server.localAddress();
+ Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor,
+ QuicSslContextBuilder.forClient()
+ .trustManager(InsecureTrustManagerFactory.INSTANCE)
+ .applicationProtocols(QuicTestUtils.PROTOS).build()));
+ QuicChannel client = null;
+ try {
+ client = QuicTestUtils.newQuicChannelBootstrap(channel)
+ .handler(new ChannelInboundHandlerAdapter() {
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+ cause.printStackTrace();
+ }
+ })
+ .streamHandler(new ChannelInboundHandlerAdapter())
+ .remoteAddress(address)
+ .connect()
+ .get();
+ latch.await();
+
+ assertThat(causeRef.get(), Matchers.instanceOf(SSLHandshakeException.class));
+ } finally {
+ server.close().sync();
+
+ if (client != null) {
+ client.close().sync();
+ }
+ // Close the parent Datagram channel as well.
+ channel.close().sync();
+
+ shutdown(executor);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testSniMatch(Executor executor) throws Throwable {
+ QuicSslContext defaultServerSslContext = QuicSslContextBuilder.forServer(
+ QuicTestUtils.SELF_SIGNED_CERTIFICATE.privateKey(), null,
+ QuicTestUtils.SELF_SIGNED_CERTIFICATE.certificate())
+ .applicationProtocols("default-protocol").build();
+
+ QuicSslContext sniServerSslContext = QuicSslContextBuilder.forServer(
+ QuicTestUtils.SELF_SIGNED_CERTIFICATE.privateKey(), null,
+ QuicTestUtils.SELF_SIGNED_CERTIFICATE.certificate())
+ .applicationProtocols("sni-protocol").build();
+
+ CountDownLatch sniEventLatch = new CountDownLatch(1);
+ CountDownLatch sslEventLatch = new CountDownLatch(1);
+ String hostname = "quic.netty.io";
+ QuicSslContext serverSslContext = QuicSslContextBuilder.buildForServerWithSni(
+ new DomainWildcardMappingBuilder<>(defaultServerSslContext)
+ .add(hostname, sniServerSslContext).build());
+
+ Channel server = QuicTestUtils.newServer(QuicTestUtils.newQuicServerBuilder(executor, serverSslContext),
+ InsecureQuicTokenHandler.INSTANCE, new ChannelInboundHandlerAdapter() {
+ @Override
+ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
+ if (evt instanceof SniCompletionEvent) {
+ if (hostname.equals(((SniCompletionEvent) evt).hostname())) {
+ sniEventLatch.countDown();
+ }
+ } else if (evt instanceof SslHandshakeCompletionEvent) {
+ if (((SslHandshakeCompletionEvent) evt).isSuccess()) {
+ sslEventLatch.countDown();
+ }
+ }
+ super.userEventTriggered(ctx, evt);
+ }
+ },
+ new ChannelInboundHandlerAdapter());
+
+ InetSocketAddress address = (InetSocketAddress) server.localAddress();
+
+ QuicSslContext clientSslContext = QuicSslContextBuilder.forClient()
+ .trustManager(InsecureTrustManagerFactory.INSTANCE).applicationProtocols("sni-protocol").build();
+
+ Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor)
+ .sslEngineProvider(c -> clientSslContext.newEngine(c.alloc(), hostname, 8080)));
+ try {
+ ChannelActiveVerifyHandler clientQuicChannelHandler = new ChannelActiveVerifyHandler();
+ QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel)
+ .handler(clientQuicChannelHandler)
+ .streamHandler(new ChannelInboundHandlerAdapter())
+ .remoteAddress(address)
+ .connect()
+ .get();
+
+ quicChannel.close().sync();
+ ChannelFuture closeFuture = quicChannel.closeFuture().await();
+ assertTrue(closeFuture.isSuccess());
+ clientQuicChannelHandler.assertState();
+ sniEventLatch.await();
+ sslEventLatch.await();
+ } finally {
+ server.close().sync();
+ // Close the parent Datagram channel as well.
+ channel.close().sync();
+
+ shutdown(executor);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testSniFallbackToDefault(Executor executor) throws Throwable {
+ testSniFallbackToDefault(executor, true);
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testNoSniFallbackToDefault(Executor executor) throws Throwable {
+ testSniFallbackToDefault(executor, false);
+ }
+
+ private void testSniFallbackToDefault(Executor executor, boolean sendSni) throws Throwable {
+ QuicSslContext defaultServerSslContext = QuicSslContextBuilder.forServer(
+ QuicTestUtils.SELF_SIGNED_CERTIFICATE.privateKey(), null,
+ QuicTestUtils.SELF_SIGNED_CERTIFICATE.certificate())
+ .applicationProtocols("default-protocol").build();
+
+ QuicSslContext sniServerSslContext = QuicSslContextBuilder.forServer(
+ QuicTestUtils.SELF_SIGNED_CERTIFICATE.privateKey(), null,
+ QuicTestUtils.SELF_SIGNED_CERTIFICATE.certificate())
+ .applicationProtocols("sni-protocol").build();
+
+ QuicSslContext serverSslContext = QuicSslContextBuilder.buildForServerWithSni(
+ new DomainWildcardMappingBuilder<>(defaultServerSslContext)
+ .add("quic.netty.io", sniServerSslContext).build());
+
+ Channel server = QuicTestUtils.newServer(QuicTestUtils.newQuicServerBuilder(executor, serverSslContext),
+ InsecureQuicTokenHandler.INSTANCE, new ChannelInboundHandlerAdapter(),
+ new ChannelInboundHandlerAdapter());
+
+ InetSocketAddress address = (InetSocketAddress) server.localAddress();
+
+ QuicSslContext clientSslContext = QuicSslContextBuilder.forClient()
+ .trustManager(InsecureTrustManagerFactory.INSTANCE).applicationProtocols("default-protocol").build();
+
+ Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor)
+ .sslEngineProvider(c -> {
+ if (sendSni) {
+ return clientSslContext.newEngine(c.alloc(), "netty.io", 8080);
+ } else {
+ return clientSslContext.newEngine(c.alloc());
+ }
+ }));
+ try {
+ ChannelActiveVerifyHandler clientQuicChannelHandler = new ChannelActiveVerifyHandler();
+ QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel)
+ .handler(clientQuicChannelHandler)
+ .streamHandler(new ChannelInboundHandlerAdapter())
+ .remoteAddress(address)
+ .connect()
+ .get();
+
+ quicChannel.close().sync();
+ ChannelFuture closeFuture = quicChannel.closeFuture().await();
+ assertTrue(closeFuture.isSuccess());
+ clientQuicChannelHandler.assertState();
+ } finally {
+ server.close().sync();
+ // Close the parent Datagram channel as well.
+ channel.close().sync();
+
+ shutdown(executor);
+ }
+ }
+
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testConnectKeyless(Executor executor) throws Throwable {
+ testConnectKeyless0(executor, false);
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testConnectKeylessSignFailure(Executor executor) throws Throwable {
+ testConnectKeyless0(executor, true);
+ }
+
+ public void testConnectKeyless0(Executor executor, boolean fail) throws Throwable {
+ AtomicReference causeRef = new AtomicReference<>();
+ AtomicBoolean signCalled = new AtomicBoolean();
+ BoringSSLAsyncPrivateKeyMethod keyMethod = new BoringSSLAsyncPrivateKeyMethod() {
+ @Override
+ public Future sign(SSLEngine engine, int signatureAlgorithm, byte[] input) {
+ signCalled.set(true);
+
+ assertEquals(QuicTestUtils.SELF_SIGNED_CERTIFICATE.cert().getPublicKey(),
+ engine.getSession().getLocalCertificates()[0].getPublicKey());
+
+ try {
+ if (fail) {
+ return ImmediateEventExecutor.INSTANCE.newFailedFuture(new SignatureException());
+ }
+ // Delegate signing to Java implementation.
+ final Signature signature;
+ // Depending on the Java version it will pick one or the other.
+ if (signatureAlgorithm == SSL_SIGN_RSA_PKCS1_SHA256) {
+ signature = Signature.getInstance("SHA256withRSA");
+ } else if (signatureAlgorithm == SSL_SIGN_RSA_PSS_RSAE_SHA256) {
+ signature = Signature.getInstance("RSASSA-PSS");
+ signature.setParameter(new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256,
+ 32, 1));
+ } else {
+ throw new AssertionError("Unexpected signature algorithm " + signatureAlgorithm);
+ }
+ signature.initSign(QuicTestUtils.SELF_SIGNED_CERTIFICATE.key());
+ signature.update(input);
+ return ImmediateEventExecutor.INSTANCE.newSucceededFuture(signature.sign());
+ } catch (Throwable cause) {
+ return ImmediateEventExecutor.INSTANCE.newFailedFuture(cause);
+ }
+ }
+
+ @Override
+ public Future decrypt(SSLEngine engine, byte[] input) {
+ throw new UnsupportedOperationException();
+ }
+ };
+
+ BoringSSLKeylessManagerFactory factory = BoringSSLKeylessManagerFactory.newKeyless(
+ keyMethod, QuicTestUtils.SELF_SIGNED_CERTIFICATE.certificate());
+ Channel server = QuicTestUtils.newServer(QuicTestUtils.newQuicServerBuilder(executor,
+ QuicSslContextBuilder.forServer(factory, null)
+ .applicationProtocols(QuicTestUtils.PROTOS).clientAuth(ClientAuth.NONE).build()),
+ InsecureQuicTokenHandler.INSTANCE, new ChannelInboundHandlerAdapter() {
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+ causeRef.set(cause);
+ }
+ } ,
+ new ChannelInboundHandlerAdapter());
+ InetSocketAddress address = (InetSocketAddress) server.localAddress();
+
+ Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor,
+ QuicSslContextBuilder.forClient()
+ .trustManager(InsecureTrustManagerFactory.INSTANCE)
+ .applicationProtocols(QuicTestUtils.PROTOS).build()));
+ try {
+ ChannelActiveVerifyHandler clientQuicChannelHandler = new ChannelActiveVerifyHandler();
+ Future connectFuture = QuicTestUtils.newQuicChannelBootstrap(channel)
+ .handler(clientQuicChannelHandler)
+ .streamHandler(new ChannelInboundHandlerAdapter())
+ .remoteAddress(address)
+ .connect().await();
+ if (fail) {
+ assertThat(connectFuture.cause(), Matchers.instanceOf(ClosedChannelException.class));
+ assertThat(causeRef.get(), Matchers.instanceOf(SSLHandshakeException.class));
+ } else {
+ QuicChannel quicChannel = connectFuture.get();
+ assertTrue(quicChannel.close().await().isSuccess());
+ ChannelFuture closeFuture = quicChannel.closeFuture().await();
+ assertTrue(closeFuture.isSuccess());
+ clientQuicChannelHandler.assertState();
+ assertNull(causeRef.get());
+ }
+ assertTrue(signCalled.get());
+ } finally {
+ server.close().sync();
+ // Close the parent Datagram channel as well.
+ channel.close().sync();
+
+ shutdown(executor);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testSessionTickets(Executor executor) throws Throwable {
+ testSessionReuse(executor, true);
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ @Timeout(5)
+ public void testSessionReusedOnClientSide(Executor executor) throws Exception {
+ testSessionReuse(executor, false);
+ }
+
+ private static void testSessionReuse(Executor executor, boolean ticketKey) throws Exception {
+ QuicSslContext sslServerCtx = QuicSslContextBuilder.forServer(
+ QuicTestUtils.SELF_SIGNED_CERTIFICATE.key(), null,
+ QuicTestUtils.SELF_SIGNED_CERTIFICATE.cert())
+ .applicationProtocols(QuicTestUtils.PROTOS)
+ .build();
+ QuicSslContext sslClientCtx = QuicSslContextBuilder.forClient()
+ .trustManager(InsecureTrustManagerFactory.INSTANCE).applicationProtocols(QuicTestUtils.PROTOS).build();
+
+ if (ticketKey) {
+
+ SslSessionTicketKey key = new SslSessionTicketKey(new byte[SslSessionTicketKey.NAME_SIZE],
+ new byte[SslSessionTicketKey.HMAC_KEY_SIZE], new byte[SslSessionTicketKey.AES_KEY_SIZE]);
+ sslClientCtx.sessionContext().setTicketKeys(key);
+ sslServerCtx.sessionContext().setTicketKeys(key);
+ }
+ CountDownLatch serverSslCompletionEventLatch = new CountDownLatch(2);
+ Channel server = QuicTestUtils.newServer(QuicTestUtils.newQuicServerBuilder(executor, sslServerCtx),
+ InsecureQuicTokenHandler.INSTANCE,
+ new ChannelInboundHandlerAdapter() {
+ @Override
+ public boolean isSharable() {
+ return true;
+ }
+
+ @Override
+ public void channelActive(ChannelHandlerContext ctx) {
+ ((QuicChannel) ctx.channel()).createStream(QuicStreamType.BIDIRECTIONAL,
+ new ChannelInboundHandlerAdapter() {
+ @Override
+ public void channelActive(ChannelHandlerContext ctx) {
+ ctx.writeAndFlush(ctx.alloc().directBuffer(10).writeZero(10))
+ .addListener(f -> ctx.close());
+ }
+ });
+ ctx.fireChannelActive();
+ }
+
+ @Override
+ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
+ if (evt instanceof SslHandshakeCompletionEvent) {
+ serverSslCompletionEventLatch.countDown();
+ }
+ }
+ },
+ new ChannelInboundHandlerAdapter());
+ InetSocketAddress address = (InetSocketAddress) server.localAddress();
+
+ Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor).sslEngineProvider(c ->
+ sslClientCtx.newEngine(c.alloc(), "localhost", 9999)));
+ try {
+ CountDownLatch clientSslCompletionEventLatch = new CountDownLatch(2);
+
+ QuicChannelBootstrap bootstrap = QuicTestUtils.newQuicChannelBootstrap(channel)
+ .handler(new ChannelInboundHandlerAdapter() {
+ @Override
+ public boolean isSharable() {
+ return true;
+ }
+
+ @Override
+ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
+ if (evt instanceof SslHandshakeCompletionEvent) {
+ clientSslCompletionEventLatch.countDown();
+ }
+ }
+ })
+ .streamHandler(new ChannelInboundHandlerAdapter())
+ .remoteAddress(address);
+
+ CountDownLatch latch1 = new CountDownLatch(1);
+ QuicChannel quicChannel1 = bootstrap
+ .streamHandler(new BytesCountingHandler(latch1, 10))
+ .connect()
+ .get();
+ latch1.await();
+ assertSessionReused(quicChannel1, false);
+
+ CountDownLatch latch2 = new CountDownLatch(1);
+ QuicChannel quicChannel2 = bootstrap
+ .streamHandler(new BytesCountingHandler(latch2, 10))
+ .connect()
+ .get();
+
+ latch2.await();
+
+ // Ensure the session is reused.
+ assertSessionReused(quicChannel2, true);
+
+ quicChannel1.close().sync();
+ quicChannel2.close().sync();
+
+ serverSslCompletionEventLatch.await();
+ clientSslCompletionEventLatch.await();
+ } finally {
+ server.close().sync();
+ // Close the parent Datagram channel as well.
+ channel.close().sync();
+
+ shutdown(executor);
+ }
+ }
+
+ private static void assertSessionReused(QuicChannel channel, boolean reused) throws Exception {
+ QuicheQuicSslEngine engine = (QuicheQuicSslEngine) channel.sslEngine();
+ while (engine.getHandshakeStatus() != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) {
+ // Let's wait a bit and re-check if the handshake is done.
+ Thread.sleep(50);
+ }
+ assertEquals(reused, engine.isSessionReused());
+ }
+
+ private static final class BytesCountingHandler extends ChannelInboundHandlerAdapter {
+ private final CountDownLatch latch;
+ private final int numBytes;
+ private int bytes;
+
+ BytesCountingHandler(CountDownLatch latch, int numBytes) {
+ this.latch = latch;
+ this.numBytes = numBytes;
+ }
+
+ @Override
+ public void channelRead(ChannelHandlerContext ctx, Object msg) {
+ ByteBuf buffer = (ByteBuf) msg;
+ bytes += buffer.readableBytes();
+ ctx.writeAndFlush(buffer);
+ if (bytes == numBytes) {
+ latch.countDown();
+ }
+ }
+ }
+
+ private static final class ChannelStateVerifyHandler extends QuicChannelValidationHandler {
+ @Override
+ public void channelActive(ChannelHandlerContext ctx) {
+ ctx.fireChannelActive();
+ fail();
+ }
+
+ @Override
+ public void channelInactive(ChannelHandlerContext ctx) {
+ ctx.fireChannelInactive();
+ fail();
+ }
+ }
+
+ private static final class ChannelActiveVerifyHandler extends QuicChannelValidationHandler {
+ private final BlockingQueue states = new LinkedBlockingQueue<>();
+ private volatile QuicConnectionAddress localAddress;
+ private volatile QuicConnectionAddress remoteAddress;
+
+ @Override
+ public void channelRegistered(ChannelHandlerContext ctx) {
+ ctx.fireChannelRegistered();
+ states.add(0);
+ }
+
+ @Override
+ public void channelUnregistered(ChannelHandlerContext ctx) {
+ ctx.fireChannelUnregistered();
+ states.add(3);
+ }
+
+ @Override
+ public void channelActive(ChannelHandlerContext ctx) {
+ localAddress = (QuicConnectionAddress) ctx.channel().localAddress();
+ remoteAddress = (QuicConnectionAddress) ctx.channel().remoteAddress();
+ ctx.fireChannelActive();
+ states.add(1);
+ }
+
+ @Override
+ public void channelInactive(ChannelHandlerContext ctx) {
+ ctx.fireChannelInactive();
+ states.add(2);
+ }
+
+ void assertState() throws Throwable {
+ // Check that we receive the different events in the correct order.
+ for (long i = 0; i < 4; i++) {
+ assertEquals(i, (int) states.take());
+ }
+ assertNull(states.poll());
+ super.assertState();
+ }
+
+ QuicConnectionAddress localAddress() {
+ return localAddress;
+ }
+
+ QuicConnectionAddress remoteAddress() {
+ return remoteAddress;
+ }
+ }
+
+ private abstract static class TestX509ExtendedTrustManager extends X509ExtendedTrustManager {
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket)
+ throws CertificateException {
+ // NOOP
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket)
+ throws CertificateException {
+ // NOOP
+ }
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine)
+ throws CertificateException {
+ // NOOP
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine)
+ throws CertificateException {
+ // NOOP
+ }
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+ // NOOP
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+ // NOOP
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return new X509Certificate[0];
+ }
+ }
+
+ private abstract static class TestX509TrustManager implements X509TrustManager {
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+ // NOOP
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+ // NOOP
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return new X509Certificate[0];
+ }
+ }
+}
diff --git a/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicChannelDatagramTest.java b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicChannelDatagramTest.java
new file mode 100644
index 0000000..0b6042d
--- /dev/null
+++ b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicChannelDatagramTest.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright 2020 The Netty Project
+ *
+ * The Netty Project licenses this file to you 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:
+ *
+ * https://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.netty.handler.codec.quic;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.ChannelOption;
+import io.netty.util.ReferenceCountUtil;
+import io.netty.util.concurrent.ImmediateEventExecutor;
+import io.netty.util.concurrent.Promise;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.net.InetSocketAddress;
+import java.util.Random;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class QuicChannelDatagramTest extends AbstractQuicTest {
+
+ private static final Random random = new Random();
+ static final byte[] data = new byte[512];
+
+ static {
+ random.nextBytes(data);
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testDatagramFlushInChannelRead(Executor executor) throws Throwable {
+ testDatagram(executor, false);
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testDatagramFlushInChannelReadComplete(Executor executor) throws Throwable {
+ testDatagram(executor, true);
+ }
+
+ private void testDatagram(Executor executor, boolean flushInReadComplete) throws Throwable {
+ AtomicReference serverEventRef = new AtomicReference<>();
+
+ QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler() {
+
+ @Override
+ public void channelRead(ChannelHandlerContext ctx, Object msg) {
+ if (msg instanceof ByteBuf) {
+ final ChannelFuture future;
+ if (!flushInReadComplete) {
+ future = ctx.writeAndFlush(msg);
+ } else {
+ future = ctx.write(msg);
+ }
+ future.addListener(ChannelFutureListener.CLOSE);
+ } else {
+ ctx.fireChannelRead(msg);
+ }
+ }
+
+ @Override
+ public void channelReadComplete(ChannelHandlerContext ctx) {
+ if (flushInReadComplete) {
+ ctx.flush();
+ }
+ }
+
+ @Override
+ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
+ if (evt instanceof QuicDatagramExtensionEvent) {
+ serverEventRef.set((QuicDatagramExtensionEvent) evt);
+ }
+ super.userEventTriggered(ctx, evt);
+ }
+ };
+ Channel server = QuicTestUtils.newServer(QuicTestUtils.newQuicServerBuilder(executor)
+ .datagram(10, 10),
+ InsecureQuicTokenHandler.INSTANCE, serverHandler , new ChannelInboundHandlerAdapter());
+ InetSocketAddress address = (InetSocketAddress) server.localAddress();
+
+ Promise receivedBuffer = ImmediateEventExecutor.INSTANCE.newPromise();
+ AtomicReference clientEventRef = new AtomicReference<>();
+ Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor)
+ .datagram(10, 10));
+
+ QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler() {
+ @Override
+ public void channelRead(ChannelHandlerContext ctx, Object msg) {
+ if (!receivedBuffer.trySuccess((ByteBuf) msg)) {
+ ReferenceCountUtil.release(msg);
+ }
+ }
+
+ @Override
+ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
+ if (evt instanceof QuicDatagramExtensionEvent) {
+ clientEventRef.set((QuicDatagramExtensionEvent) evt);
+ }
+ super.userEventTriggered(ctx, evt);
+ }
+
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+ receivedBuffer.tryFailure(cause);
+ super.exceptionCaught(ctx, cause);
+ }
+ };
+
+ try {
+ QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel)
+ .handler(clientHandler)
+ .remoteAddress(address)
+ .connect()
+ .get();
+ quicChannel.writeAndFlush(Unpooled.copiedBuffer(data)).sync();
+
+ ByteBuf buffer = receivedBuffer.get();
+ ByteBuf expected = Unpooled.wrappedBuffer(data);
+ assertEquals(expected, buffer);
+ buffer.release();
+ expected.release();
+
+ assertNotEquals(0, serverEventRef.get().maxLength());
+ assertNotEquals(0, clientEventRef.get().maxLength());
+
+ quicChannel.close().sync();
+
+ serverHandler.assertState();
+ clientHandler.assertState();
+ } finally {
+ server.close().sync();
+ // Close the parent Datagram channel as well.
+ channel.close().sync();
+
+ shutdown(executor);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testDatagramNoAutoReadMaxMessagesPerRead1(Executor executor) throws Throwable {
+ testDatagramNoAutoRead(executor, 1, false);
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testDatagramNoAutoReadMaxMessagesPerRead3(Executor executor) throws Throwable {
+ testDatagramNoAutoRead(executor, 3, false);
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testDatagramNoAutoReadMaxMessagesPerRead1OutSideEventLoop(Executor executor) throws Throwable {
+ testDatagramNoAutoRead(executor, 1, true);
+ }
+
+ @ParameterizedTest
+ @MethodSource("newSslTaskExecutors")
+ public void testDatagramNoAutoReadMaxMessagesPerRead3OutSideEventLoop(Executor executor) throws Throwable {
+ testDatagramNoAutoRead(executor, 3, true);
+ }
+
+ private void testDatagramNoAutoRead(Executor executor, int maxMessagesPerRead, boolean readLater) throws Throwable {
+ Promise serverPromise = ImmediateEventExecutor.INSTANCE.newPromise();
+ Promise clientPromise = ImmediateEventExecutor.INSTANCE.newPromise();
+
+ int numDatagrams = 5;
+ AtomicInteger serverReadCount = new AtomicInteger();
+ CountDownLatch latch = new CountDownLatch(numDatagrams);
+ QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler() {
+ private int readPerLoop;
+
+ @Override
+ public void channelActive(ChannelHandlerContext ctx) {
+ ctx.read();
+ }
+
+ @Override
+ public void channelRead(ChannelHandlerContext ctx, Object msg) {
+ if (msg instanceof ByteBuf) {
+ readPerLoop++;
+
+ ctx.writeAndFlush(msg).addListener(future -> {
+ if (future.isSuccess()) {
+ latch.countDown();
+ }
+ });
+ if (serverReadCount.incrementAndGet() == numDatagrams) {
+ serverPromise.trySuccess(null);
+ }
+ } else {
+ ctx.fireChannelRead(msg);
+ }
+ }
+
+ @Override
+ public void channelReadComplete(ChannelHandlerContext ctx) {
+ if (readPerLoop > maxMessagesPerRead) {
+ ctx.close();
+ serverPromise.tryFailure(new AssertionError(
+ "Read more then " + maxMessagesPerRead + " time per read loop"));
+ return;
+ }
+ readPerLoop = 0;
+ if (serverReadCount.get() < numDatagrams) {
+ if (readLater) {
+ ctx.executor().execute(ctx::read);
+ } else {
+ ctx.read();
+ }
+ }
+ }
+ };
+ Channel server = QuicTestUtils.newServer(QuicTestUtils.newQuicServerBuilder(executor)
+ .option(ChannelOption.AUTO_READ, false)
+ .option(ChannelOption.MAX_MESSAGES_PER_READ, maxMessagesPerRead)
+ .datagram(10, 10),
+ InsecureQuicTokenHandler.INSTANCE, serverHandler, new ChannelInboundHandlerAdapter());
+ InetSocketAddress address = (InetSocketAddress) server.localAddress();
+
+ Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor)
+ .datagram(10, 10));
+ AtomicInteger clientReadCount = new AtomicInteger();
+ QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler() {
+
+ @Override
+ public void channelRead(ChannelHandlerContext ctx, Object msg) {
+ if (msg instanceof ByteBuf) {
+
+ if (clientReadCount.incrementAndGet() == numDatagrams) {
+ if (!clientPromise.trySuccess((ByteBuf) msg)) {
+ ReferenceCountUtil.release(msg);
+ }
+ } else {
+ ReferenceCountUtil.release(msg);
+ }
+ } else {
+ ctx.fireChannelRead(msg);
+ }
+ }
+
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+ clientPromise.tryFailure(cause);
+ }
+ };
+ try {
+ QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel)
+ .handler(clientHandler)
+ .remoteAddress(address)
+ .connect()
+ .get();
+ for (int i = 0; i < numDatagrams; i++) {
+ quicChannel.writeAndFlush(Unpooled.copiedBuffer(data)).sync();
+ // Let's add some sleep in between as this is UDP so we may loose some data otherwise.
+ Thread.sleep(50);
+ }
+ assertTrue(serverPromise.await(3000), "Server received: " + serverReadCount.get() +
+ ", Client received: " + clientReadCount.get());
+ serverPromise.sync();
+
+ assertTrue(clientPromise.await(3000), "Server received: " + serverReadCount.get() +
+ ", Client received: " + clientReadCount.get());
+ ByteBuf buffer = clientPromise.get();
+ ByteBuf expected = Unpooled.wrappedBuffer(data);
+ assertEquals(expected, buffer);
+ buffer.release();
+ expected.release();
+
+ quicChannel.close().sync();
+
+ serverHandler.assertState();
+ clientHandler.assertState();
+ } finally {
+ server.close().sync();
+ // Close the parent Datagram channel as well.
+ channel.close().sync();
+
+ shutdown(executor);
+ }
+ }
+}
diff --git a/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicChannelEchoTest.java b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicChannelEchoTest.java
new file mode 100644
index 0000000..f7b36d1
--- /dev/null
+++ b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicChannelEchoTest.java
@@ -0,0 +1,439 @@
+/*
+ * Copyright 2020 The Netty Project
+ *
+ * The Netty Project licenses this file to you 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:
+ *
+ * https://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.netty.handler.codec.quic;
+
+import io.netty.buffer.AbstractByteBufAllocator;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
+import io.netty.buffer.CompositeByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.buffer.UnpooledByteBufAllocator;
+import io.netty.buffer.UnpooledDirectByteBuf;
+import io.netty.buffer.UnpooledHeapByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.ChannelOption;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.ImmediateExecutor;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class QuicChannelEchoTest extends AbstractQuicTest {
+
+ private static final Random random = new Random();
+ static final byte[] data = new byte[1048576];
+
+ static {
+ random.nextBytes(data);
+ }
+
+ public static Collection