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 data() { + List config = new ArrayList<>(); + for (int a = 0; a < 2; a++) { + for (int b = 0; b < 2; b++) { + for (int c = 0; c < 2; c++) { + config.add(new Object[] { a == 0, b == 0, c == 0 }); + } + } + } + return config; + } + + private void setAllocator(Channel channel, ByteBufAllocator allocator) { + channel.config().setAllocator(allocator); + } + + private ByteBufAllocator getAllocator(boolean directBuffer) { + if (directBuffer) { + return new UnpooledByteBufAllocator(true); + } else { + // Force usage of heap buffers and also ensure memoryAddress() is not not supported. + return new AbstractByteBufAllocator(false) { + + @Override + public ByteBuf ioBuffer() { + return heapBuffer(); + } + + @Override + public ByteBuf ioBuffer(int initialCapacity) { + return heapBuffer(initialCapacity); + } + + @Override + public ByteBuf ioBuffer(int initialCapacity, int maxCapacity) { + return heapBuffer(initialCapacity, maxCapacity); + } + + @Override + protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) { + return new UnpooledHeapByteBuf(this, initialCapacity, maxCapacity); + } + + @Override + protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) { + return new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity); + } + + @Override + public boolean isDirectBufferPooled() { + return false; + } + }; + } + } + + @ParameterizedTest(name = + "{index}: autoRead = {0}, directBuffer = {1}, composite = {2}") + @MethodSource("data") + public void testEchoStartedFromServer(boolean autoRead, boolean directBuffer, boolean composite) throws Throwable { + ByteBufAllocator allocator = getAllocator(directBuffer); + final EchoHandler sh = new EchoHandler(true, autoRead, allocator); + final EchoHandler ch = new EchoHandler(false, autoRead, allocator); + AtomicReference> writeFutures = new AtomicReference<>(); + Channel server = QuicTestUtils.newServer(ImmediateExecutor.INSTANCE, new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + setAllocator(ctx.channel(), allocator); + ((QuicChannel) ctx.channel()).createStream(QuicStreamType.BIDIRECTIONAL, sh) + .addListener((Future future) -> { + QuicStreamChannel stream = future.getNow(); + setAllocator(stream, allocator); + List futures = writeAllData(stream, composite, allocator); + writeFutures.set(futures); + }); + + ctx.channel().config().setAutoRead(autoRead); + if (!autoRead) { + ctx.read(); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + if (!autoRead) { + ctx.read(); + } + } + }, sh); + setAllocator(server, allocator); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + Channel channel = QuicTestUtils.newClient(ImmediateExecutor.INSTANCE); + QuicChannel quicChannel = null; + try { + quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + if (!autoRead) { + ctx.read(); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + if (!autoRead) { + ctx.read(); + } + } + }) + .streamHandler(ch) + // Use the same allocator for the streams. + .streamOption(ChannelOption.ALLOCATOR, allocator) + .remoteAddress(address) + .option(ChannelOption.AUTO_READ, autoRead) + .option(ChannelOption.ALLOCATOR, allocator) + .connect() + .get(); + + waitForData(ch, sh); + + for (;;) { + List futures = writeFutures.get(); + if (futures != null) { + for (ChannelFuture f: futures) { + f.sync(); + } + break; + } + + try { + Thread.sleep(50); + } catch (InterruptedException e) { + // Ignore. + } + } + waitForData(sh, ch); + + // Close underlying streams. + sh.channel.close().sync(); + ch.channel.close().sync(); + + // Close underlying quic channels + sh.channel.parent().close().sync(); + ch.channel.parent().close().sync(); + + checkForException(ch, sh); + } finally { + server.close().sync(); + QuicTestUtils.closeIfNotNull(quicChannel); + // Close the parent Datagram channel as well. + channel.close().sync(); + } + } + + @ParameterizedTest(name = + "{index}: autoRead = {0}, directBuffer = {1}, composite = {2}") + @MethodSource("data") + public void testEchoStartedFromClient(boolean autoRead, boolean directBuffer, boolean composite) throws Throwable { + ByteBufAllocator allocator = getAllocator(directBuffer); + + final EchoHandler sh = new EchoHandler(true, autoRead, allocator); + final EchoHandler ch = new EchoHandler(false, autoRead, allocator); + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + setAllocator(ctx.channel(), allocator); + ctx.channel().config().setAutoRead(autoRead); + if (!autoRead) { + ctx.read(); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + if (!autoRead) { + ctx.read(); + } + } + }; + + Channel server = QuicTestUtils.newServer(ImmediateExecutor.INSTANCE, serverHandler, sh); + setAllocator(server, allocator); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + Channel channel = QuicTestUtils.newClient(ImmediateExecutor.INSTANCE); + QuicChannel quicChannel = null; + try { + QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + if (!autoRead) { + ctx.read(); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + if (!autoRead) { + ctx.read(); + } + } + }; + quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(ch) + // Use the same allocator for the streams. + .streamOption(ChannelOption.ALLOCATOR, allocator) + .remoteAddress(address) + .option(ChannelOption.AUTO_READ, autoRead) + .option(ChannelOption.ALLOCATOR, allocator) + .connect() + .get(); + + QuicStreamChannel stream = quicChannel.createStream(QuicStreamType.BIDIRECTIONAL, ch).sync().getNow(); + setAllocator(stream, allocator); + + assertEquals(QuicStreamType.BIDIRECTIONAL, stream.type()); + assertEquals(0, stream.streamId()); + assertTrue(stream.isLocalCreated()); + + for (int i = 0; i < 5; i++) { + ch.counter = 0; + sh.counter = 0; + List futures = writeAllData(stream, composite, allocator); + + for (ChannelFuture f : futures) { + f.sync(); + } + waitForData(ch, sh); + waitForData(sh, ch); + Thread.sleep(100); + } + + // Close underlying streams. + sh.channel.close().sync(); + ch.channel.close().sync(); + + // Close underlying quic channels + sh.channel.parent().close().sync(); + ch.channel.parent().close().sync(); + checkForException(ch, sh); + + serverHandler.assertState(); + clientHandler.assertState(); + } finally { + server.close().syncUninterruptibly(); + QuicTestUtils.closeIfNotNull(quicChannel); + // Close the parent Datagram channel as well. + channel.close().sync(); + } + } + + private List writeAllData(Channel channel, boolean composite, ByteBufAllocator allocator) { + if (composite) { + CompositeByteBuf compositeByteBuf = allocator.compositeBuffer(); + for (int i = 0; i < data.length;) { + int length = Math.min(random.nextInt(1024 * 64), data.length - i); + ByteBuf buf = allocator.buffer().writeBytes(data, i, length); + compositeByteBuf.addComponent(true, buf); + i += length; + } + return Collections.singletonList(channel.writeAndFlush(compositeByteBuf)); + } else { + List futures = new ArrayList<>(); + for (int i = 0; i < data.length;) { + int length = Math.min(random.nextInt(1024 * 64), data.length - i); + ByteBuf buf = allocator.buffer().writeBytes(data, i, length); + futures.add(channel.writeAndFlush(buf)); + i += length; + } + return futures; + } + } + + private static void waitForData(EchoHandler h1, EchoHandler h2) { + while (h1.counter < data.length) { + if (h2.exception.get() != null) { + break; + } + if (h1.exception.get() != null) { + break; + } + + try { + Thread.sleep(50); + } catch (InterruptedException e) { + // Ignore. + } + } + } + + private static void checkForException(EchoHandler h1, EchoHandler h2) throws Throwable { + if (h1.exception.get() != null && !(h1.exception.get() instanceof IOException)) { + throw h1.exception.get(); + } + if (h2.exception.get() != null && !(h2.exception.get() instanceof IOException)) { + throw h2.exception.get(); + } + if (h1.exception.get() != null) { + throw h1.exception.get(); + } + if (h2.exception.get() != null) { + throw h2.exception.get(); + } + } + + private class EchoHandler extends SimpleChannelInboundHandler { + private final boolean server; + private final boolean autoRead; + private final ByteBufAllocator allocator; + volatile Channel channel; + final AtomicReference exception = new AtomicReference<>(); + volatile int counter; + + EchoHandler(boolean server, boolean autoRead, ByteBufAllocator allocator) { + this.server = server; + this.autoRead = autoRead; + this.allocator = allocator; + } + + @Override + public void channelRegistered(ChannelHandlerContext ctx) { + ctx.channel().config().setAutoRead(autoRead); + setAllocator(ctx.channel(), allocator); + ctx.fireChannelRegistered(); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + channel = ctx.channel(); + QuicStreamChannel channel = (QuicStreamChannel) ctx.channel(); + assertEquals(QuicStreamType.BIDIRECTIONAL, channel.type()); + if (channel.isLocalCreated()) { + // Server starts with 1, client with 0 + assertEquals(server ? 1 : 0, channel.streamId()); + } else { + // Server starts with 1, client with 0 + assertEquals(server ? 0 : 1, channel.streamId()); + } + if (!autoRead) { + ctx.read(); + } + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, ByteBuf in) { + byte[] actual = new byte[in.readableBytes()]; + in.readBytes(actual); + + int lastIdx = counter; + for (int i = 0; i < actual.length; i ++) { + assertEquals(data[i + lastIdx], actual[i]); + } + + if (!((QuicStreamChannel) ctx.channel()).isLocalCreated()) { + channel.write(Unpooled.wrappedBuffer(actual)); + } + + counter += actual.length; + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + try { + ctx.flush(); + } finally { + if (!autoRead) { + ctx.read(); + } + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, + Throwable cause) { + if (exception.compareAndSet(null, cause)) { + cause.printStackTrace(); + ctx.close(); + } + } + } +} diff --git a/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicChannelValidationHandler.java b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicChannelValidationHandler.java new file mode 100644 index 0000000..a60859a --- /dev/null +++ b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicChannelValidationHandler.java @@ -0,0 +1,36 @@ +/* + * 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 io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; + +class QuicChannelValidationHandler extends ChannelInboundHandlerAdapter { + + private volatile Throwable cause; + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + this.cause = cause; + } + + void assertState() throws Throwable { + if (cause != null) { + throw cause; + } + } + +} diff --git a/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicCodecBuilderTest.java b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicCodecBuilderTest.java new file mode 100644 index 0000000..35863f7 --- /dev/null +++ b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicCodecBuilderTest.java @@ -0,0 +1,100 @@ +/* + * 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 io.netty.channel.ChannelHandler; +import io.netty.util.concurrent.ImmediateExecutor; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.util.concurrent.Executor; +import java.util.function.Function; + +import static org.assertj.core.api.Assertions.assertThat; + +class QuicCodecBuilderTest { + + @Test + void testCopyConstructor() throws IllegalAccessException { + TestQuicCodecBuilder original = new TestQuicCodecBuilder(); + init(original); + TestQuicCodecBuilder copy = new TestQuicCodecBuilder(original); + assertThat(copy).usingRecursiveComparison().isEqualTo(original); + } + + private static void init(TestQuicCodecBuilder builder) throws IllegalAccessException { + Field[] fields = builder.getClass().getSuperclass().getDeclaredFields(); + for (Field field : fields) { + modifyField(builder, field); + } + } + + private static void modifyField(TestQuicCodecBuilder builder, Field field) throws IllegalAccessException { + field.setAccessible(true); + Class clazz = field.getType(); + if (Boolean.class == clazz) { + field.set(builder, Boolean.TRUE); + } else if (Integer.class == clazz) { + field.set(builder, Integer.MIN_VALUE); + } else if (Long.class == clazz) { + field.set(builder, Long.MIN_VALUE); + } else if (QuicCongestionControlAlgorithm.class == clazz) { + field.set(builder, QuicCongestionControlAlgorithm.CUBIC); + } else if (FlushStrategy.class == clazz) { + field.set(builder, FlushStrategy.afterNumBytes(10)); + } else if (Function.class == clazz) { + field.set(builder, Function.identity()); + } else if (boolean.class == clazz) { + field.setBoolean(builder, true); + } else if (int.class == clazz) { + field.setInt(builder, -1); + } else if (byte[].class == clazz) { + field.set(builder, new byte[16]); + } else if (Executor.class == clazz) { + field.set(builder, ImmediateExecutor.INSTANCE); + } else { + throw new IllegalArgumentException("Unknown field type " + clazz); + } + } + + private static final class TestQuicCodecBuilder extends QuicCodecBuilder { + + TestQuicCodecBuilder() { + super(true); + } + + TestQuicCodecBuilder(TestQuicCodecBuilder builder) { + super(builder); + } + + @Override + public TestQuicCodecBuilder clone() { + // no-op + return null; + } + + @Override + protected ChannelHandler build( + QuicheConfig config, + Function sslContextProvider, + Executor sslTaskExecutor, + int localConnIdLength, + FlushStrategy flushStrategy) { + // no-op + return null; + } + } +} diff --git a/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicConnectionAddressTest.java b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicConnectionAddressTest.java new file mode 100644 index 0000000..b10ad60 --- /dev/null +++ b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicConnectionAddressTest.java @@ -0,0 +1,59 @@ +/* + * 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 org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.concurrent.ThreadLocalRandom; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class QuicConnectionAddressTest extends AbstractQuicTest { + + @Test + public void testNullByteArray() { + assertThrows(NullPointerException.class, () -> new QuicConnectionAddress((byte[]) null)); + } + + @Test + public void testNullByteBuffer() { + assertThrows(NullPointerException.class, () -> new QuicConnectionAddress((ByteBuffer) null)); + } + + @Test + public void testByteArrayIsCloned() { + byte[] bytes = new byte[8]; + ThreadLocalRandom.current().nextBytes(bytes); + QuicConnectionAddress address = new QuicConnectionAddress(bytes); + assertEquals(ByteBuffer.wrap(bytes), address.connId); + ThreadLocalRandom.current().nextBytes(bytes); + assertNotEquals(ByteBuffer.wrap(bytes), address.connId); + } + + @Test + public void tesByteBufferIsDuplicated() { + byte[] bytes = new byte[8]; + ThreadLocalRandom.current().nextBytes(bytes); + ByteBuffer buffer = ByteBuffer.wrap(bytes); + QuicConnectionAddress address = new QuicConnectionAddress(bytes); + assertEquals(buffer, address.connId); + buffer.position(1); + assertNotEquals(buffer, address.connId); + } +} diff --git a/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicConnectionIdGeneratorTest.java b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicConnectionIdGeneratorTest.java new file mode 100644 index 0000000..7669f29 --- /dev/null +++ b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicConnectionIdGeneratorTest.java @@ -0,0 +1,91 @@ +/* + * 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 org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.concurrent.ThreadLocalRandom; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class QuicConnectionIdGeneratorTest extends AbstractQuicTest { + + @Test + public void testRandomness() { + QuicConnectionIdGenerator idGenerator = QuicConnectionIdGenerator.randomGenerator(); + ByteBuffer id = idGenerator.newId(Quiche.QUICHE_MAX_CONN_ID_LEN); + ByteBuffer id2 = idGenerator.newId(Quiche.QUICHE_MAX_CONN_ID_LEN); + assertThat(id.remaining(), greaterThan(0)); + assertThat(id2.remaining(), greaterThan(0)); + assertNotEquals(id, id2); + + id = idGenerator.newId(10); + id2 = idGenerator.newId(10); + assertEquals(10, id.remaining()); + assertEquals(10, id2.remaining()); + assertNotEquals(id, id2); + + byte[] input = new byte[1024]; + ThreadLocalRandom.current().nextBytes(input); + id = idGenerator.newId(ByteBuffer.wrap(input), 10); + id2 = idGenerator.newId(ByteBuffer.wrap(input), 10); + assertEquals(10, id.remaining()); + assertEquals(10, id2.remaining()); + assertNotEquals(id, id2); + } + + @Test + public void testThrowsIfInputTooBig() { + QuicConnectionIdGenerator idGenerator = QuicConnectionIdGenerator.randomGenerator(); + assertThrows(IllegalArgumentException.class, () -> idGenerator.newId(Integer.MAX_VALUE)); + } + + @Test + public void testThrowsIfInputTooBig2() { + QuicConnectionIdGenerator idGenerator = QuicConnectionIdGenerator.randomGenerator(); + assertThrows(IllegalArgumentException.class, () -> + idGenerator.newId(ByteBuffer.wrap(new byte[8]), Integer.MAX_VALUE)); + } + + @Test + public void testSignIdGenerator() { + QuicConnectionIdGenerator idGenerator = QuicConnectionIdGenerator.signGenerator(); + + byte[] input = new byte[1024]; + byte[] input2 = new byte[1024]; + ThreadLocalRandom.current().nextBytes(input); + ThreadLocalRandom.current().nextBytes(input2); + ByteBuffer id = idGenerator.newId(ByteBuffer.wrap(input), 10); + ByteBuffer id2 = idGenerator.newId(ByteBuffer.wrap(input), 10); + ByteBuffer id3 = idGenerator.newId(ByteBuffer.wrap(input2), 10); + assertEquals(10, id.remaining()); + assertEquals(10, id2.remaining()); + assertEquals(10, id3.remaining()); + assertEquals(id, id2); + assertNotEquals(id, id3); + + assertThrows(UnsupportedOperationException.class, () -> idGenerator.newId(10)); + assertThrows(NullPointerException.class, () -> idGenerator.newId(null, 10)); + assertThrows(IllegalArgumentException.class, () -> idGenerator.newId(ByteBuffer.wrap(new byte[0]), 10)); + assertThrows(IllegalArgumentException.class, () -> + idGenerator.newId(ByteBuffer.wrap(input), Integer.MAX_VALUE)); + } +} diff --git a/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicConnectionStatsTest.java b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicConnectionStatsTest.java new file mode 100644 index 0000000..337eb64 --- /dev/null +++ b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicConnectionStatsTest.java @@ -0,0 +1,143 @@ +/* + * 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.util.concurrent.ImmediateEventExecutor; +import io.netty.util.concurrent.Promise; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; + +public class QuicConnectionStatsTest extends AbstractQuicTest { + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testStatsAreCollected(Executor executor) throws Throwable { + Channel server = null; + Channel channel = null; + AtomicInteger counter = new AtomicInteger(); + + Promise serverActiveStats = ImmediateEventExecutor.INSTANCE.newPromise(); + Promise serverInactiveStats = ImmediateEventExecutor.INSTANCE.newPromise(); + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + collectStats(ctx, serverActiveStats); + ctx.fireChannelActive(); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + collectStats(ctx, serverInactiveStats); + ctx.fireChannelInactive(); + } + + private void collectStats(ChannelHandlerContext ctx, Promise promise) { + QuicheQuicChannel channel = (QuicheQuicChannel) ctx.channel(); + channel.collectStats(promise); + } + }; + QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler(); + try { + server = QuicTestUtils.newServer(executor, serverHandler, new ChannelInboundHandlerAdapter() { + + @Override + public void channelActive(ChannelHandlerContext ctx) { + counter.incrementAndGet(); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + // Let's just echo back the message. + ctx.writeAndFlush(msg); + } + + @Override + public boolean isSharable() { + return true; + } + }); + channel = QuicTestUtils.newClient(executor); + + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(server.localAddress()) + .connect().get(); + assertNotNull(quicChannel.collectStats().sync().getNow()); + quicChannel.createStream(QuicStreamType.BIDIRECTIONAL, new ChannelInboundHandlerAdapter() { + private final int bufferSize = 8; + private int received; + + @Override + public void channelActive(ChannelHandlerContext ctx) { + ctx.writeAndFlush(Unpooled.buffer().writeZero(bufferSize)); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ByteBuf buffer = (ByteBuf) msg; + received += buffer.readableBytes(); + buffer.release(); + if (received == bufferSize) { + ctx.close().addListener((ChannelFuture future) -> { + // Close the underlying QuicChannel as well. + future.channel().parent().close(); + }); + } + } + }).sync(); + + // Wait until closure + quicChannel.closeFuture().sync(); + assertStats(quicChannel.collectStats().sync().getNow()); + assertNotNull(serverActiveStats.sync().getNow()); + assertStats(serverInactiveStats.sync().getNow()); + assertEquals(1, counter.get()); + + serverHandler.assertState(); + clientHandler.assertState(); + } finally { + QuicTestUtils.closeIfNotNull(channel); + QuicTestUtils.closeIfNotNull(server); + + shutdown(executor); + } + } + + private static void assertStats(QuicConnectionStats stats) { + assertNotNull(stats); + assertThat(stats.lost(), greaterThanOrEqualTo(0L)); + assertThat(stats.recv(), greaterThan(0L)); + assertThat(stats.sent(), greaterThan(0L)); + assertThat(stats.sentBytes(), greaterThan(0L)); + assertThat(stats.recvBytes(), greaterThan(0L)); + } +} diff --git a/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicPacketTypeTest.java b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicPacketTypeTest.java new file mode 100644 index 0000000..9229e0b --- /dev/null +++ b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicPacketTypeTest.java @@ -0,0 +1,36 @@ +/* + * 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 org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class QuicPacketTypeTest extends AbstractQuicTest { + + @Test + public void testOfValidType() { + for (QuicPacketType type: QuicPacketType.values()) { + assertEquals(type, QuicPacketType.of(type.type)); + } + } + + @Test + public void testOfInvalidType() { + assertThrows(IllegalArgumentException.class, () -> QuicPacketType.of((byte) -1)); + } +} diff --git a/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicReadableTest.java b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicReadableTest.java new file mode 100644 index 0000000..d2ca570 --- /dev/null +++ b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicReadableTest.java @@ -0,0 +1,138 @@ +/* + * 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.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +public class QuicReadableTest extends AbstractQuicTest { + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testCorrectlyHandleReadableStreams(Executor executor) throws Throwable { + int numOfStreams = 256; + int readStreams = numOfStreams / 2; + // We do write longs. + int expectedDataRead = readStreams * Long.BYTES; + final CountDownLatch latch = new CountDownLatch(numOfStreams); + final AtomicInteger bytesRead = new AtomicInteger(); + final AtomicReference serverErrorRef = new AtomicReference<>(); + final AtomicReference clientErrorRef = new AtomicReference<>(); + + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler(); + Channel server = QuicTestUtils.newServer( + QuicTestUtils.newQuicServerBuilder(executor).initialMaxStreamsBidirectional(5000), + InsecureQuicTokenHandler.INSTANCE, + serverHandler, new ChannelInboundHandlerAdapter() { + private int counter; + @Override + public void channelRegistered(ChannelHandlerContext ctx) { + // Ensure we dont read from the streams so all of these will be reported as readable + ctx.channel().config().setAutoRead(false); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + counter++; + latch.countDown(); + if (counter > readStreams) { + // Now set it to readable again for some channels + ctx.channel().config().setAutoRead(true); + } + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ByteBuf buffer = (ByteBuf) msg; + bytesRead.addAndGet(buffer.readableBytes()); + buffer.release(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + serverErrorRef.set(cause); + } + + @Override + public boolean isSharable() { + return true; + } + }); + Channel channel = QuicTestUtils.newClient(executor); + QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler(); + ByteBuf data = Unpooled.directBuffer().writeLong(8); + try { + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(server.localAddress()) + .connect() + .get(); + + List streams = new ArrayList<>(); + for (int i = 0; i < numOfStreams; i++) { + QuicStreamChannel stream = quicChannel.createStream( + QuicStreamType.BIDIRECTIONAL, new ChannelInboundHandlerAdapter() { + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + clientErrorRef.set(cause); + } + }).get(); + streams.add(stream.writeAndFlush(data.retainedSlice()).sync().channel()); + } + latch.await(); + while (bytesRead.get() < expectedDataRead) { + Thread.sleep(50); + } + for (Channel stream: streams) { + stream.close().sync(); + } + quicChannel.close().sync(); + + throwIfNotNull(serverErrorRef); + throwIfNotNull(clientErrorRef); + + serverHandler.assertState(); + clientHandler.assertState(); + } finally { + data.release(); + server.close().sync(); + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(executor); + } + } + + private static void throwIfNotNull(AtomicReference errorRef) throws Throwable { + Throwable cause = errorRef.get(); + if (cause != null) { + throw cause; + } + } +} diff --git a/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicSslContextTest.java b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicSslContextTest.java new file mode 100644 index 0000000..113a628 --- /dev/null +++ b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicSslContextTest.java @@ -0,0 +1,90 @@ +/* + * 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 io.netty.handler.ssl.SslContext; +import io.netty.util.internal.EmptyArrays; +import org.junit.jupiter.api.Test; + +import javax.net.ssl.SSLSessionContext; +import javax.net.ssl.X509ExtendedKeyManager; + +import java.net.Socket; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class QuicSslContextTest { + + @Test + public void testSessionContextSettingsForClient() { + testSessionContextSettings(QuicSslContextBuilder.forClient(), 20, 50); + } + + @Test + public void testSessionContextSettingsForServer() { + testSessionContextSettings(QuicSslContextBuilder.forServer(new X509ExtendedKeyManager() { + @Override + public String[] getClientAliases(String keyType, Principal[] issuers) { + return EmptyArrays.EMPTY_STRINGS; + } + + @Override + public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { + return null; + } + + @Override + public String[] getServerAliases(String keyType, Principal[] issuers) { + return EmptyArrays.EMPTY_STRINGS; + } + + @Override + public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { + return null; + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + return new X509Certificate[0]; + } + + @Override + public PrivateKey getPrivateKey(String alias) { + return null; + } + }, null), 20, 50); + } + + private void testSessionContextSettings(QuicSslContextBuilder builder, int size, int timeout) { + SslContext context = builder.sessionCacheSize(size).sessionTimeout(timeout).build(); + assertEquals(size, context.sessionCacheSize()); + assertEquals(timeout, context.sessionTimeout()); + SSLSessionContext sessionContext = context.sessionContext(); + assertEquals(size, sessionContext.getSessionCacheSize()); + assertEquals(timeout, sessionContext.getSessionTimeout()); + + int newSize = size / 2; + sessionContext.setSessionCacheSize(newSize); + assertEquals(newSize, context.sessionCacheSize()); + + int newTimeout = timeout / 2; + sessionContext.setSessionTimeout(newTimeout); + assertEquals(newTimeout, sessionContext.getSessionTimeout()); + } +} diff --git a/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamChannelCloseTest.java b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamChannelCloseTest.java new file mode 100644 index 0000000..85b0553 --- /dev/null +++ b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamChannelCloseTest.java @@ -0,0 +1,304 @@ +/* + * 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.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.socket.ChannelInputShutdownReadComplete; +import io.netty.channel.socket.ChannelOutputShutdownException; +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.nio.channels.ClosedChannelException; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +public class QuicStreamChannelCloseTest extends AbstractQuicTest { + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testCloseFromServerWhileInActiveUnidirectional(Executor executor) throws Throwable { + testCloseFromServerWhileInActive(executor, QuicStreamType.UNIDIRECTIONAL, false); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testCloseFromServerWhileInActiveBidirectional(Executor executor) throws Throwable { + testCloseFromServerWhileInActive(executor, QuicStreamType.BIDIRECTIONAL, false); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testHalfCloseFromServerWhileInActiveUnidirectional(Executor executor) throws Throwable { + testCloseFromServerWhileInActive(executor, QuicStreamType.UNIDIRECTIONAL, true); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testHalfCloseFromServerWhileInActiveBidirectional(Executor executor) throws Throwable { + testCloseFromServerWhileInActive(executor, QuicStreamType.BIDIRECTIONAL, true); + } + + private static void testCloseFromServerWhileInActive(Executor executor, QuicStreamType type, + boolean halfClose) throws Throwable { + Channel server = null; + Channel channel = null; + try { + final Promise streamPromise = ImmediateEventExecutor.INSTANCE.newPromise(); + QuicChannelValidationHandler serverHandler = new StreamCreationHandler(type, halfClose, streamPromise); + server = QuicTestUtils.newServer(executor, serverHandler, + new ChannelInboundHandlerAdapter()); + channel = QuicTestUtils.newClient(executor); + + QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler(); + + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(new StreamHandler()) + .remoteAddress(server.localAddress()) + .connect() + .get(); + + Channel streamChannel = streamPromise.get(); + + // Wait for the steam to close. It needs to happen before the 5-second connection idle timeout. + streamChannel.closeFuture().get(3000, TimeUnit.MILLISECONDS); + + streamChannel.parent().close(); + + // Wait till the client was closed + quicChannel.closeFuture().sync(); + + serverHandler.assertState(); + clientHandler.assertState(); + } finally { + QuicTestUtils.closeIfNotNull(channel); + QuicTestUtils.closeIfNotNull(server); + + shutdown(executor); + } + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testCloseFromClientWhileInActiveUnidirectional(Executor executor) throws Throwable { + testCloseFromClientWhileInActive(executor, QuicStreamType.UNIDIRECTIONAL, false); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testCloseFromClientWhileInActiveBidirectional(Executor executor) throws Throwable { + testCloseFromClientWhileInActive(executor, QuicStreamType.BIDIRECTIONAL, false); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testHalfCloseFromClientWhileInActiveUnidirectional(Executor executor) throws Throwable { + testCloseFromClientWhileInActive(executor, QuicStreamType.UNIDIRECTIONAL, true); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testHalfCloseFromClientWhileInActiveBidirectional(Executor executor) throws Throwable { + testCloseFromClientWhileInActive(executor, QuicStreamType.BIDIRECTIONAL, true); + } + + private static void testCloseFromClientWhileInActive(Executor executor, QuicStreamType type, + boolean halfClose) throws Throwable { + Channel server = null; + Channel channel = null; + try { + final Promise streamPromise = ImmediateEventExecutor.INSTANCE.newPromise(); + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler(); + server = QuicTestUtils.newServer(executor, serverHandler, new StreamHandler()); + channel = QuicTestUtils.newClient(executor); + + StreamCreationHandler creationHandler = new StreamCreationHandler(type, halfClose, streamPromise); + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(creationHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(server.localAddress()) + .connect() + .get(); + + Channel streamChannel = streamPromise.get(); + + // Wait for the steam to close. It needs to happen before the 5-second connection idle timeout. + streamChannel.closeFuture().get(3000, TimeUnit.MILLISECONDS); + + streamChannel.parent().close(); + + // Wait till the client was closed + quicChannel.closeFuture().sync(); + + serverHandler.assertState(); + creationHandler.assertState(); + } finally { + QuicTestUtils.closeIfNotNull(channel); + QuicTestUtils.closeIfNotNull(server); + + shutdown(executor); + } + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testWriteToUnidirectionalAfterShutdownOutput(Executor executor) throws Throwable { + testWriteAfterClosedOrShutdown(executor, QuicStreamType.UNIDIRECTIONAL, true); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testWriteToBidirectionalAfterShutdownOutput(Executor executor) throws Throwable { + testWriteAfterClosedOrShutdown(executor, QuicStreamType.BIDIRECTIONAL, true); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testWriteToUnidirectionalAfterClose(Executor executor) throws Throwable { + testWriteAfterClosedOrShutdown(executor, QuicStreamType.UNIDIRECTIONAL, false); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testWriteToBidirectionalAfterClose(Executor executor) throws Throwable { + testWriteAfterClosedOrShutdown(executor, QuicStreamType.BIDIRECTIONAL, false); + } + + private static void testWriteAfterClosedOrShutdown(Executor executor, QuicStreamType type, + boolean halfClose) throws Throwable { + Channel server = null; + Channel channel = null; + try { + final Promise streamPromise = ImmediateEventExecutor.INSTANCE.newPromise(); + server = QuicTestUtils.newServer(executor, new ChannelInboundHandlerAdapter(), new StreamHandler()); + channel = QuicTestUtils.newClient(executor); + + StreamCreationAndTearDownHandler creationHandler = + new StreamCreationAndTearDownHandler(type, halfClose, streamPromise); + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(creationHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(server.localAddress()) + .connect() + .get(); + + // ChannelOutputShutdownException should only be used when its a BIDIRECTIONAL channel and half-closure + // is used. + Class causeClass = + halfClose && type != QuicStreamType.UNIDIRECTIONAL ? + ChannelOutputShutdownException.class : ClosedChannelException.class; + assertInstanceOf(causeClass, streamPromise.await().cause()); + quicChannel.close().sync(); + + // Wait till the client was closed + quicChannel.closeFuture().sync(); + creationHandler.assertState(); + } finally { + QuicTestUtils.closeIfNotNull(channel); + QuicTestUtils.closeIfNotNull(server); + + shutdown(executor); + } + } + + private static final class StreamCreationAndTearDownHandler extends QuicChannelValidationHandler { + private final QuicStreamType type; + private final boolean halfClose; + private final Promise streamPromise; + + StreamCreationAndTearDownHandler(QuicStreamType type, boolean halfClose, Promise streamPromise) { + this.type = type; + this.halfClose = halfClose; + this.streamPromise = streamPromise; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + QuicChannel channel = (QuicChannel) ctx.channel(); + channel.createStream(type, new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + final ChannelFuture future; + if (halfClose) { + future = ((QuicStreamChannel) ctx.channel()).shutdownOutput(); + } else { + future = ctx.channel().close(); + } + future.addListener(f -> { + ctx.channel().writeAndFlush("Unsupported message").addListener(wf -> { + streamPromise.setFailure(wf.cause()); + }); + }); + } + }); + } + } + + private static final class StreamCreationHandler extends QuicChannelValidationHandler { + private final QuicStreamType type; + private final boolean halfClose; + private final Promise streamPromise; + + StreamCreationHandler(QuicStreamType type, boolean halfClose, Promise streamPromise) { + this.type = type; + this.halfClose = halfClose; + this.streamPromise = streamPromise; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + QuicChannel channel = (QuicChannel) ctx.channel(); + channel.createStream(type, new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + streamPromise.trySuccess(ctx.channel()); + // Do the write and close the channel + ctx.writeAndFlush(Unpooled.buffer().writeZero(8)) + .addListener(halfClose + ? QuicStreamChannel.SHUTDOWN_OUTPUT + : ChannelFutureListener.CLOSE); + } + }); + } + } + + private static final class StreamHandler extends ChannelInboundHandlerAdapter { + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt == ChannelInputShutdownReadComplete.INSTANCE) { + // Received a FIN + ctx.close(); + } + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ReferenceCountUtil.release(msg); + } + } +} diff --git a/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamChannelCreationTest.java b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamChannelCreationTest.java new file mode 100644 index 0000000..6f119d2 --- /dev/null +++ b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamChannelCreationTest.java @@ -0,0 +1,132 @@ +/* + * 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.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelOption; +import io.netty.util.AttributeKey; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.net.InetSocketAddress; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class QuicStreamChannelCreationTest extends AbstractQuicTest { + + private static final AttributeKey ATTRIBUTE_KEY = AttributeKey.newInstance("testKey"); + private static final String ATTRIBUTE_VALUE = "Test"; + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testCreateStream(Executor executor) throws Throwable { + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler(); + Channel server = QuicTestUtils.newServer(executor, serverHandler, + new ChannelInboundHandlerAdapter()); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + Channel channel = QuicTestUtils.newClient(executor); + QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler(); + + try { + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(address) + .connect() + .get(); + CountDownLatch latch = new CountDownLatch(1); + QuicStreamChannel stream = quicChannel.createStream(QuicStreamType.UNIDIRECTIONAL, + new ChannelInboundHandlerAdapter() { + @Override + public void channelRegistered(ChannelHandlerContext ctx) { + assertQuicStreamChannel((QuicStreamChannel) ctx.channel(), + QuicStreamType.UNIDIRECTIONAL, Boolean.TRUE, null); + latch.countDown(); + } + }).sync().get(); + assertQuicStreamChannel(stream, QuicStreamType.UNIDIRECTIONAL, Boolean.TRUE, null); + latch.await(); + stream.close().sync(); + 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 testCreateStreamViaBootstrap(Executor executor) throws Throwable { + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler(); + Channel server = QuicTestUtils.newServer(executor, serverHandler, + new ChannelInboundHandlerAdapter()); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + Channel channel = QuicTestUtils.newClient(executor); + QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler(); + + try { + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(address) + .connect() + .get(); + CountDownLatch latch = new CountDownLatch(1); + QuicStreamChannel stream = quicChannel.newStreamBootstrap() + .type(QuicStreamType.UNIDIRECTIONAL) + .attr(ATTRIBUTE_KEY, ATTRIBUTE_VALUE) + .option(ChannelOption.AUTO_READ, Boolean.FALSE) + .handler(new ChannelInboundHandlerAdapter() { + @Override + public void channelRegistered(ChannelHandlerContext ctx) { + assertQuicStreamChannel((QuicStreamChannel) ctx.channel(), + QuicStreamType.UNIDIRECTIONAL, Boolean.FALSE, ATTRIBUTE_VALUE); + latch.countDown(); + } + }).create().sync().get(); + assertQuicStreamChannel(stream, QuicStreamType.UNIDIRECTIONAL, Boolean.FALSE, ATTRIBUTE_VALUE); + latch.await(); + stream.close().sync(); + quicChannel.close().sync(); + + serverHandler.assertState(); + clientHandler.assertState(); + } finally { + server.close().syncUninterruptibly(); + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(executor); + } + } + + private static void assertQuicStreamChannel(QuicStreamChannel channel, QuicStreamType expectedType, + Boolean expectedAutoRead, String expectedAttribute) { + assertEquals(expectedType, channel.type()); + assertEquals(expectedAutoRead, channel.config().getOption(ChannelOption.AUTO_READ)); + assertEquals(expectedAttribute, channel.attr(ATTRIBUTE_KEY).get()); + } +} diff --git a/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamFrameTest.java b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamFrameTest.java new file mode 100644 index 0000000..e22afc2 --- /dev/null +++ b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamFrameTest.java @@ -0,0 +1,143 @@ +/* + * 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.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.socket.ChannelInputShutdownReadComplete; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +public class QuicStreamFrameTest extends AbstractQuicTest { + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testCloseHalfClosureUnidirectional(Executor executor) throws Throwable { + testCloseHalfClosure(executor, QuicStreamType.UNIDIRECTIONAL); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testCloseHalfClosureBidirectional(Executor executor) throws Throwable { + testCloseHalfClosure(executor, QuicStreamType.BIDIRECTIONAL); + } + + private static void testCloseHalfClosure(Executor executor, QuicStreamType type) throws Throwable { + Channel server = null; + Channel channel = null; + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler(); + QuicChannelValidationHandler clientHandler = new StreamCreationHandler(type); + try { + StreamHandler handler = new StreamHandler(); + server = QuicTestUtils.newServer(executor, serverHandler, handler); + channel = QuicTestUtils.newClient(executor); + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(server.localAddress()) + .connect() + .get(); + + handler.assertSequence(); + quicChannel.closeFuture().sync(); + + serverHandler.assertState(); + clientHandler.assertState(); + } finally { + QuicTestUtils.closeIfNotNull(channel); + QuicTestUtils.closeIfNotNull(server); + + shutdown(executor); + } + } + + private static final class StreamCreationHandler extends QuicChannelValidationHandler { + private final QuicStreamType type; + + StreamCreationHandler(QuicStreamType type) { + this.type = type; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + QuicChannel channel = (QuicChannel) ctx.channel(); + channel.createStream(type, new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + // Do the write and close the channel + ctx.writeAndFlush(Unpooled.buffer().writeZero(8)) + .addListener(QuicStreamChannel.SHUTDOWN_OUTPUT); + } + }); + } + } + + private static final class StreamHandler extends ChannelInboundHandlerAdapter { + private final BlockingQueue queue = new LinkedBlockingQueue<>(); + + @Override + public void channelRegistered(ChannelHandlerContext ctx) { + ctx.channel().config().setOption(QuicChannelOption.READ_FRAMES, true); + queue.add(0); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + queue.add(3); + // Close the QUIC channel as well. + ctx.channel().parent().close(); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt == ChannelInputShutdownReadComplete.INSTANCE) { + queue.add(2); + if (((QuicStreamChannel) ctx.channel()).type() == QuicStreamType.BIDIRECTIONAL) { + // Let's write back a fin which will also close the channel and so call channelInactive(...) + ctx.writeAndFlush(new DefaultQuicStreamFrame(Unpooled.EMPTY_BUFFER, true)); + } + ctx.channel().parent().close(); + } + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + QuicStreamFrame frame = (QuicStreamFrame) msg; + if (frame.hasFin()) { + queue.add(1); + } + frame.release(); + } + + void assertSequence() throws Exception { + assertEquals(0, (int) queue.take()); + assertEquals(1, (int) queue.take()); + assertEquals(2, (int) queue.take()); + assertEquals(3, (int) queue.take()); + assertTrue(queue.isEmpty()); + } + } +} diff --git a/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamHalfClosureTest.java b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamHalfClosureTest.java new file mode 100644 index 0000000..62ad30a --- /dev/null +++ b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamHalfClosureTest.java @@ -0,0 +1,155 @@ +/* + * 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.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.socket.ChannelInputShutdownEvent; +import io.netty.channel.socket.ChannelInputShutdownReadComplete; +import io.netty.util.ReferenceCountUtil; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class QuicStreamHalfClosureTest extends AbstractQuicTest { + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testCloseHalfClosureUnidirectional(Executor executor) throws Throwable { + testCloseHalfClosure(executor, QuicStreamType.UNIDIRECTIONAL); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testCloseHalfClosureBidirectional(Executor executor) throws Throwable { + testCloseHalfClosure(executor, QuicStreamType.BIDIRECTIONAL); + } + + private static void testCloseHalfClosure(Executor executor, QuicStreamType type) throws Throwable { + Channel server = null; + Channel channel = null; + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler(); + QuicChannelValidationHandler clientHandler = new StreamCreationHandler(type); + try { + StreamHandler handler = new StreamHandler(); + server = QuicTestUtils.newServer(executor, serverHandler, handler); + channel = QuicTestUtils.newClient(executor); + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(server.localAddress()) + .connect() + .get(); + + handler.assertSequence(); + quicChannel.closeFuture().sync(); + + serverHandler.assertState(); + clientHandler.assertState(); + } finally { + QuicTestUtils.closeIfNotNull(channel); + QuicTestUtils.closeIfNotNull(server); + + shutdown(executor); + } + } + + private static final class StreamCreationHandler extends QuicChannelValidationHandler { + private final QuicStreamType type; + + StreamCreationHandler(QuicStreamType type) { + this.type = type; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + QuicChannel channel = (QuicChannel) ctx.channel(); + channel.createStream(type, new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + // Do the write and close the channel + ctx.writeAndFlush(Unpooled.buffer().writeZero(8)) + .addListener(ChannelFutureListener.CLOSE); + } + }); + } + } + + private static final class StreamHandler extends ChannelInboundHandlerAdapter { + private final BlockingQueue queue = new LinkedBlockingQueue<>(); + + @Override + public void channelRegistered(ChannelHandlerContext ctx) { + queue.add(0); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + queue.add(5); + // Close the QUIC channel as well. + ctx.channel().parent().close(); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ReferenceCountUtil.release(msg); + if (((QuicStreamChannel) ctx.channel()).isInputShutdown()) { + queue.add(1); + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt == ChannelInputShutdownEvent.INSTANCE) { + addIsShutdown(ctx); + queue.add(3); + } else if (evt == ChannelInputShutdownReadComplete.INSTANCE) { + queue.add(4); + ctx.close(); + } + } + + private void addIsShutdown(ChannelHandlerContext ctx) { + if (((QuicStreamChannel) ctx.channel()).isInputShutdown()) { + queue.add(2); + } + } + + void assertSequence() throws Exception { + assertEquals(0, (int) queue.take()); + int value = queue.take(); + if (value == 1) { + // If we did see the value of 1 it should be followed by 2 directly. + assertEquals(2, (int) queue.take()); + } else { + assertEquals(2, value); + } + assertEquals(3, (int) queue.take()); + assertEquals(4, (int) queue.take()); + assertEquals(5, (int) queue.take()); + assertTrue(queue.isEmpty()); + } + } +} diff --git a/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamIdGeneratorTest.java b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamIdGeneratorTest.java new file mode 100644 index 0000000..1c4e32e --- /dev/null +++ b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamIdGeneratorTest.java @@ -0,0 +1,43 @@ +/* + * 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 org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class QuicStreamIdGeneratorTest extends AbstractQuicTest { + + @Test + public void testServerStreamIds() { + QuicStreamIdGenerator generator = new QuicStreamIdGenerator(true); + assertEquals(1, generator.nextStreamId(true)); + assertEquals(5, generator.nextStreamId(true)); + assertEquals(3, generator.nextStreamId(false)); + assertEquals(9, generator.nextStreamId(true)); + assertEquals(7, generator.nextStreamId(false)); + } + + @Test + public void testClientStreamIds() { + QuicStreamIdGenerator generator = new QuicStreamIdGenerator(false); + assertEquals(0, generator.nextStreamId(true)); + assertEquals(4, generator.nextStreamId(true)); + assertEquals(2, generator.nextStreamId(false)); + assertEquals(8, generator.nextStreamId(true)); + assertEquals(6, generator.nextStreamId(false)); + } +} diff --git a/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamLimitTest.java b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamLimitTest.java new file mode 100644 index 0000000..43eb8c7 --- /dev/null +++ b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamLimitTest.java @@ -0,0 +1,193 @@ +/* + * 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.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.socket.ChannelInputShutdownReadComplete; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.ImmediateEventExecutor; +import io.netty.util.concurrent.Promise; +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class QuicStreamLimitTest extends AbstractQuicTest { + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testStreamLimitEnforcedWhenCreatingViaClientBidirectional(Executor executor) throws Throwable { + testStreamLimitEnforcedWhenCreatingViaClient(executor, QuicStreamType.BIDIRECTIONAL); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testStreamLimitEnforcedWhenCreatingViaClientUnidirectional(Executor executor) throws Throwable { + testStreamLimitEnforcedWhenCreatingViaClient(executor, QuicStreamType.UNIDIRECTIONAL); + } + + private static void testStreamLimitEnforcedWhenCreatingViaClient(Executor executor, QuicStreamType type) throws Throwable { + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler(); + Channel server = QuicTestUtils.newServer( + QuicTestUtils.newQuicServerBuilder(executor).initialMaxStreamsBidirectional(1) + .initialMaxStreamsUnidirectional(1), + InsecureQuicTokenHandler.INSTANCE, + serverHandler, new ChannelInboundHandlerAdapter() { + @Override + public boolean isSharable() { + return true; + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt == ChannelInputShutdownReadComplete.INSTANCE) { + ctx.close(); + } + } + }); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + Channel channel = QuicTestUtils.newClient(executor); + + CountDownLatch latch = new CountDownLatch(1); + CountDownLatch latch2 = new CountDownLatch(1); + QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler() { + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof QuicStreamLimitChangedEvent) { + if (latch.getCount() == 0) { + latch2.countDown(); + } else { + latch.countDown(); + } + } + super.userEventTriggered(ctx, evt); + } + }; + try { + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(address) + .connect().get(); + latch.await(); + assertEquals(1, quicChannel.peerAllowedStreams(QuicStreamType.UNIDIRECTIONAL)); + assertEquals(1, quicChannel.peerAllowedStreams(QuicStreamType.BIDIRECTIONAL)); + QuicStreamChannel stream = quicChannel.createStream( + type, new ChannelInboundHandlerAdapter()).get(); + + assertEquals(0, quicChannel.peerAllowedStreams(type)); + + // Second stream creation should fail. + Throwable cause = quicChannel.createStream( + type, new ChannelInboundHandlerAdapter()).await().cause(); + assertThat(cause, CoreMatchers.instanceOf(IOException.class)); + stream.close().sync(); + latch2.await(); + + assertEquals(1, quicChannel.peerAllowedStreams(type)); + 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 testStreamLimitEnforcedWhenCreatingViaServerBidirectional(Executor executor) throws Throwable { + testStreamLimitEnforcedWhenCreatingViaServer(executor, QuicStreamType.BIDIRECTIONAL); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testStreamLimitEnforcedWhenCreatingViaServerUnidirectional(Executor executor) throws Throwable { + testStreamLimitEnforcedWhenCreatingViaServer(executor, QuicStreamType.UNIDIRECTIONAL); + } + + private static void testStreamLimitEnforcedWhenCreatingViaServer(Executor executor, QuicStreamType type) throws Throwable { + Promise streamPromise = ImmediateEventExecutor.INSTANCE.newPromise(); + Promise stream2Promise = ImmediateEventExecutor.INSTANCE.newPromise(); + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + QuicChannel channel = (QuicChannel) ctx.channel(); + channel.createStream(type, new ChannelInboundHandlerAdapter()) + .addListener((Future future) -> { + if (future.isSuccess()) { + QuicStreamChannel stream = future.getNow(); + streamPromise.setSuccess(null); + channel.createStream(type, new ChannelInboundHandlerAdapter()) + .addListener((Future f) -> { + stream.close(); + stream2Promise.setSuccess(f.cause()); + }); + } else { + streamPromise.setFailure(future.cause()); + } + }); + } + }; + Channel server = QuicTestUtils.newServer( + QuicTestUtils.newQuicServerBuilder(executor), + InsecureQuicTokenHandler.INSTANCE, + serverHandler, new ChannelInboundHandlerAdapter() { + @Override + public boolean isSharable() { + return true; + } + }); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor) + .initialMaxStreamsBidirectional(1).initialMaxStreamsUnidirectional(1)); + + QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler(); + try { + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(address) + .connect().get(); + streamPromise.sync(); + // Second stream creation should fail. + assertThat(stream2Promise.get(), CoreMatchers.instanceOf(IOException.class)); + 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/QuicStreamShutdownTest.java b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamShutdownTest.java new file mode 100644 index 0000000..0817b2c --- /dev/null +++ b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamShutdownTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2023 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 org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class QuicStreamShutdownTest extends AbstractQuicTest { + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testShutdownInputClosureCausesStreamStopped(Executor executor) throws Throwable { + Channel server = null; + Channel channel = null; + CountDownLatch latch = new CountDownLatch(2); + try { + server = QuicTestUtils.newServer(executor, new ChannelInboundHandlerAdapter(), new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + ChannelFutureListener futureListener = new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture channelFuture) { + Throwable cause = channelFuture.cause(); + if (cause instanceof QuicException && + ((QuicException) cause).error() == QuicError.STREAM_STOPPED) { + latch.countDown(); + } + } + }; + ByteBuf buffer = (ByteBuf) msg; + ctx.write(buffer.retainedDuplicate()).addListener(futureListener); + ctx.writeAndFlush(buffer).addListener(futureListener); + } + }); + channel = QuicTestUtils.newClient(executor); + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(new ChannelInboundHandlerAdapter()) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(server.localAddress()) + .connect() + .get(); + + QuicStreamChannel streamChannel = quicChannel.createStream(QuicStreamType.BIDIRECTIONAL, + new ChannelInboundHandlerAdapter()).sync().getNow(); + streamChannel.shutdownInput().sync(); + assertTrue(streamChannel.isInputShutdown()); + streamChannel.writeAndFlush(Unpooled.buffer().writeLong(8)).sync(); + + latch.await(); + } finally { + QuicTestUtils.closeIfNotNull(channel); + QuicTestUtils.closeIfNotNull(server); + + shutdown(executor); + } + } +} diff --git a/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamTypeTest.java b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamTypeTest.java new file mode 100644 index 0000000..be0255f --- /dev/null +++ b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamTypeTest.java @@ -0,0 +1,157 @@ +/* + * 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.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.concurrent.ImmediateEventExecutor; +import io.netty.util.concurrent.Promise; +import io.netty.util.concurrent.PromiseNotifier; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.concurrent.Executor; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +public class QuicStreamTypeTest extends AbstractQuicTest { + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testUnidirectionalCreatedByClient(Executor executor) throws Throwable { + Channel server = null; + Channel channel = null; + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler(); + QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler(); + + try { + Promise serverWritePromise = ImmediateEventExecutor.INSTANCE.newPromise(); + server = QuicTestUtils.newServer(executor, serverHandler, new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + QuicStreamChannel channel = (QuicStreamChannel) ctx.channel(); + assertEquals(QuicStreamType.UNIDIRECTIONAL, channel.type()); + assertFalse(channel.isLocalCreated()); + ctx.writeAndFlush(Unpooled.buffer().writeZero(8)) + .addListener(future -> serverWritePromise.setSuccess(future.cause())); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ReferenceCountUtil.release(msg); + } + }); + + channel = QuicTestUtils.newClient(executor); + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(server.localAddress()) + .connect() + .sync() + .get(); + QuicStreamChannel streamChannel = quicChannel.createStream( + QuicStreamType.UNIDIRECTIONAL, new ChannelInboundHandlerAdapter()).get(); + // Do the write which should succeed + streamChannel.writeAndFlush(Unpooled.buffer().writeZero(8)).sync(); + + // Close stream and quic channel + streamChannel.close().sync(); + quicChannel.close().sync(); + assertThat(serverWritePromise.get(), instanceOf(UnsupportedOperationException.class)); + + serverHandler.assertState(); + clientHandler.assertState(); + } finally { + QuicTestUtils.closeIfNotNull(channel); + QuicTestUtils.closeIfNotNull(server); + } + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testUnidirectionalCreatedByServer(Executor executor) throws Throwable { + Channel server = null; + Channel channel = null; + Promise serverWritePromise = ImmediateEventExecutor.INSTANCE.newPromise(); + Promise clientWritePromise = ImmediateEventExecutor.INSTANCE.newPromise(); + + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + QuicChannel channel = (QuicChannel) ctx.channel(); + channel.createStream(QuicStreamType.UNIDIRECTIONAL, new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + // Do the write which should succeed + ctx.writeAndFlush(Unpooled.buffer().writeZero(8)) + .addListener(new PromiseNotifier<>(serverWritePromise)); + } + }); + } + }; + QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler(); + try { + server = QuicTestUtils.newServer(executor, serverHandler, new ChannelInboundHandlerAdapter()); + + channel = QuicTestUtils.newClient(executor); + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + // Do the write should fail + ctx.writeAndFlush(Unpooled.buffer().writeZero(8)) + .addListener(future -> clientWritePromise.setSuccess(future.cause())); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + // Close the QUIC channel as well. + ctx.channel().parent().close(); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ReferenceCountUtil.release(msg); + // Let's close the stream + ctx.close(); + } + }) + .remoteAddress(server.localAddress()) + .connect() + .get(); + + quicChannel.closeFuture().sync(); + assertTrue(serverWritePromise.await().isSuccess()); + assertThat(clientWritePromise.get(), instanceOf(UnsupportedOperationException.class)); + + serverHandler.assertState(); + clientHandler.assertState(); + } finally { + QuicTestUtils.closeIfNotNull(channel); + QuicTestUtils.closeIfNotNull(server); + } + } +} diff --git a/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicTest.java b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicTest.java new file mode 100644 index 0000000..680d72c --- /dev/null +++ b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicTest.java @@ -0,0 +1,73 @@ +/* + * 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.channel.ChannelOption; +import io.netty.util.AttributeKey; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class QuicTest extends AbstractQuicTest { + + @Test + public void test() { + Quic.ensureAvailability(); + assertNotNull(Quiche.quiche_version()); + } + + @Test + public void testVersionSupported() { + // Only v1 should be supported. + assertFalse(Quic.isVersionSupported(0xff00_001c)); + assertFalse(Quic.isVersionSupported(0xff00_001d)); + assertFalse(Quic.isVersionSupported(0xff00_001c)); + assertTrue(Quic.isVersionSupported(0x0000_0001)); + } + + @Test + public void testToAttributesArrayDoesCopy() { + AttributeKey key = AttributeKey.valueOf(UUID.randomUUID().toString()); + String value = "testValue"; + Map, Object> attributes = new HashMap<>(); + attributes.put(key, value); + Map.Entry, Object>[] array = Quic.toAttributesArray(attributes); + assertEquals(1, array.length); + attributes.put(key, "newTestValue"); + Map.Entry, Object> entry = array[0]; + assertEquals(key, entry.getKey()); + assertEquals(value, entry.getValue()); + } + + @Test + public void testToOptionsArrayDoesCopy() { + Map, Object> options = new HashMap<>(); + options.put(ChannelOption.AUTO_READ, true); + Map.Entry, Object>[] array = Quic.toOptionsArray(options); + assertEquals(1, array.length); + options.put(ChannelOption.AUTO_READ, false); + Map.Entry, Object> entry = array[0]; + assertEquals(ChannelOption.AUTO_READ, entry.getKey()); + assertEquals(true, entry.getValue()); + } +} diff --git a/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicTestUtils.java b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicTestUtils.java new file mode 100644 index 0000000..1aa3cee --- /dev/null +++ b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicTestUtils.java @@ -0,0 +1,189 @@ +/* + * 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.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.FixedRecvByteBufAllocator; +import io.netty.channel.epoll.Epoll; +import io.netty.channel.epoll.EpollChannelOption; +import io.netty.channel.epoll.EpollDatagramChannel; +import io.netty.channel.epoll.EpollEventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.handler.ssl.bouncycastle.SelfSignedCertificate; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.util.NetUtil; + +import java.net.InetSocketAddress; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +final class QuicTestUtils { + static final String[] PROTOS = new String[]{"hq-29"}; + static final SelfSignedCertificate SELF_SIGNED_CERTIFICATE; + + private static final int DATAGRAM_SIZE = 2048; + + static { + SelfSignedCertificate cert; + try { + cert = new SelfSignedCertificate(); + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + } + SELF_SIGNED_CERTIFICATE = cert; + } + + private QuicTestUtils() { + } + + private static final EventLoopGroup GROUP = Epoll.isAvailable() ? new EpollEventLoopGroup() : + new NioEventLoopGroup(); + + static { + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + try { + GROUP.shutdownGracefully().sync(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }); + } + + static Channel newClient(Executor sslTaskExecutor) throws Exception { + return newClient(newQuicClientBuilder(sslTaskExecutor)); + } + + private static Bootstrap newBootstrap() { + Bootstrap bs = new Bootstrap(); + if (GROUP instanceof EpollEventLoopGroup) { + bs.channel(EpollDatagramChannel.class) + // Use recvmmsg when possible. + .option(EpollChannelOption.MAX_DATAGRAM_PAYLOAD_SIZE, DATAGRAM_SIZE) + .option(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(DATAGRAM_SIZE * 8)); + } else { + bs.channel(NioDatagramChannel.class) + .option(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(DATAGRAM_SIZE)); + } + return bs.group(GROUP); + } + + static Channel newClient(QuicClientCodecBuilder builder) throws Exception { + return newBootstrap() + // We don't want any special handling of the channel so just use a dummy handler. + .handler(builder.build()) + .bind(new InetSocketAddress(NetUtil.LOCALHOST4, 0)).sync().channel(); + } + + static QuicChannelBootstrap newQuicChannelBootstrap(Channel channel) { + QuicChannelBootstrap bs = QuicChannel.newBootstrap(channel); + if (GROUP instanceof EpollEventLoopGroup) { + bs.option(QuicChannelOption.SEGMENTED_DATAGRAM_PACKET_ALLOCATOR, + EpollQuicUtils.newSegmentedAllocator(10)); + } + return bs; + } + + static QuicClientCodecBuilder newQuicClientBuilder(Executor sslTaskExecutor) { + return newQuicClientBuilder(sslTaskExecutor, QuicSslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE).applicationProtocols(PROTOS).build()); + } + + static QuicClientCodecBuilder newQuicClientBuilder(Executor sslTaskExecutor, QuicSslContext sslContext) { + return new QuicClientCodecBuilder() + .sslEngineProvider(q -> sslContext.newEngine(q.alloc())) + .maxIdleTimeout(5000, TimeUnit.MILLISECONDS) + .initialMaxData(10000000) + .initialMaxStreamDataBidirectionalLocal(1000000) + .initialMaxStreamDataBidirectionalRemote(1000000) + .initialMaxStreamsBidirectional(100) + .initialMaxStreamsUnidirectional(100) + .initialMaxStreamDataUnidirectional(1000000) + .activeMigration(false).sslTaskExecutor(sslTaskExecutor); + } + + static QuicServerCodecBuilder newQuicServerBuilder(Executor sslTaskExecutor) { + return newQuicServerBuilder(sslTaskExecutor, QuicSslContextBuilder.forServer( + SELF_SIGNED_CERTIFICATE.privateKey(), null, SELF_SIGNED_CERTIFICATE.certificate()) + .applicationProtocols(PROTOS).build()); + } + + static QuicServerCodecBuilder newQuicServerBuilder(Executor sslTaskExecutor, QuicSslContext context) { + QuicServerCodecBuilder builder = new QuicServerCodecBuilder() + .sslEngineProvider(q -> context.newEngine(q.alloc())) + .maxIdleTimeout(5000, TimeUnit.MILLISECONDS) + .initialMaxData(10000000) + .initialMaxStreamDataBidirectionalLocal(1000000) + .initialMaxStreamDataBidirectionalRemote(1000000) + .initialMaxStreamDataUnidirectional(1000000) + .initialMaxStreamsBidirectional(100) + .initialMaxStreamsUnidirectional(100) + .activeMigration(false) + .sslTaskExecutor(sslTaskExecutor); + if (GROUP instanceof EpollEventLoopGroup) { + builder.option(QuicChannelOption.SEGMENTED_DATAGRAM_PACKET_ALLOCATOR, + EpollQuicUtils.newSegmentedAllocator(10)); + } + return builder; + } + + private static Bootstrap newServerBootstrap(QuicServerCodecBuilder serverBuilder, + QuicTokenHandler tokenHandler, ChannelHandler handler, + ChannelHandler streamHandler) { + serverBuilder.tokenHandler(tokenHandler) + .streamHandler(streamHandler); + if (handler != null) { + serverBuilder.handler(handler); + } + ChannelHandler codec = serverBuilder.build(); + return newBootstrap() + // We don't want any special handling of the channel so just use a dummy handler. + .handler(codec) + .localAddress(new InetSocketAddress(NetUtil.LOCALHOST4, 0)); + } + + static Channel newServer(QuicServerCodecBuilder serverBuilder, QuicTokenHandler tokenHandler, + ChannelHandler handler, ChannelHandler streamHandler) + throws Exception { + return newServerBootstrap(serverBuilder, tokenHandler, handler, streamHandler) + .bind().sync().channel(); + } + + static Channel newServer(Executor sslTaskExecutor, QuicTokenHandler tokenHandler, + ChannelHandler handler, ChannelHandler streamHandler) + throws Exception { + return newServer(newQuicServerBuilder(sslTaskExecutor), tokenHandler, handler, streamHandler); + } + + static Channel newServer(Executor sslTaskExecutor, ChannelHandler handler, + ChannelHandler streamHandler) throws Exception { + return newServer(sslTaskExecutor, InsecureQuicTokenHandler.INSTANCE, handler, streamHandler); + } + + static void closeIfNotNull(Channel channel) throws Exception { + if (channel != null) { + channel.close().sync(); + } + } + +} diff --git a/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicTransportParametersTest.java b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicTransportParametersTest.java new file mode 100644 index 0000000..b429236 --- /dev/null +++ b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicTransportParametersTest.java @@ -0,0 +1,94 @@ +/* + * Copyright 2023 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.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +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.util.concurrent.Executor; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class QuicTransportParametersTest extends AbstractQuicTest { + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testParameters(Executor executor) throws Throwable { + Channel server = null; + Channel channel = null; + Promise serverParams = ImmediateEventExecutor.INSTANCE.newPromise(); + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + QuicheQuicChannel channel = (QuicheQuicChannel) ctx.channel(); + serverParams.setSuccess(channel.peerTransportParameters()); + ctx.fireChannelActive(); + } + }; + QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler(); + try { + server = QuicTestUtils.newServer(executor, serverHandler, new ChannelInboundHandlerAdapter() { + @Override + public boolean isSharable() { + return true; + } + }); + channel = QuicTestUtils.newClient(executor); + + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(server.localAddress()) + .connect().get(); + assertTransportParameters(quicChannel.peerTransportParameters()); + assertTransportParameters(serverParams.sync().getNow()); + + quicChannel.close().sync(); + serverHandler.assertState(); + clientHandler.assertState(); + } finally { + QuicTestUtils.closeIfNotNull(channel); + QuicTestUtils.closeIfNotNull(server); + + shutdown(executor); + } + } + + private static void assertTransportParameters(QuicTransportParameters parameters) { + assertNotNull(parameters); + assertThat(parameters.maxIdleTimeout(), greaterThanOrEqualTo(1L)); + assertThat(parameters.maxUdpPayloadSize(), greaterThanOrEqualTo(1L)); + assertThat(parameters.initialMaxData(), greaterThanOrEqualTo(1L)); + assertThat(parameters.initialMaxStreamDataBidiLocal(), greaterThanOrEqualTo(1L)); + assertThat(parameters.initialMaxStreamDataBidiRemote(), greaterThanOrEqualTo(1L)); + assertThat(parameters.initialMaxStreamDataUni(), greaterThanOrEqualTo(1L)); + assertThat(parameters.initialMaxStreamsBidi(), greaterThanOrEqualTo(1L)); + assertThat(parameters.initialMaxStreamsUni(), greaterThanOrEqualTo(1L)); + assertThat(parameters.ackDelayExponent(), greaterThanOrEqualTo(1L)); + assertThat(parameters.maxAckDelay(), greaterThanOrEqualTo(1L)); + assertFalse(parameters.disableActiveMigration()); + assertThat(parameters.activeConnIdLimit(), greaterThanOrEqualTo(1L)); + assertThat(parameters.maxDatagramFrameSize(), greaterThanOrEqualTo(0L)); + } +} diff --git a/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicWritableTest.java b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicWritableTest.java new file mode 100644 index 0000000..3c6e62c --- /dev/null +++ b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicWritableTest.java @@ -0,0 +1,301 @@ +/* + * 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.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.concurrent.ImmediateEventExecutor; +import io.netty.util.concurrent.Promise; +import io.netty.util.concurrent.PromiseNotifier; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.net.InetSocketAddress; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class QuicWritableTest extends AbstractQuicTest { + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testCorrectlyHandleWritabilityReadRequestedInReadComplete(Executor executor) throws Throwable { + testCorrectlyHandleWritability(executor, true); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testCorrectlyHandleWritabilityReadRequestedInRead(Executor executor) throws Throwable { + testCorrectlyHandleWritability(executor, false); + } + + private static void testCorrectlyHandleWritability(Executor executor, boolean readInComplete) throws Throwable { + int bufferSize = 64 * 1024; + Promise writePromise = ImmediateEventExecutor.INSTANCE.newPromise(); + final AtomicReference serverErrorRef = new AtomicReference<>(); + final AtomicReference clientErrorRef = new AtomicReference<>(); + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler(); + Channel server = QuicTestUtils.newServer( + QuicTestUtils.newQuicServerBuilder(executor).initialMaxStreamsBidirectional(5000), + InsecureQuicTokenHandler.INSTANCE, + serverHandler, new ChannelInboundHandlerAdapter() { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ByteBuf buffer = (ByteBuf) msg; + buffer.release(); + ctx.writeAndFlush(ctx.alloc().buffer(bufferSize).writeZero(bufferSize)) + .addListener(new PromiseNotifier<>(writePromise)); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + serverErrorRef.set(cause); + } + + @Override + public boolean isSharable() { + return true; + } + }); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor) + .initialMaxStreamDataBidirectionalLocal(bufferSize / 4)); + + QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler(); + try { + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(address) + .connect() + .get(); + QuicStreamChannel stream = quicChannel.createStream( + QuicStreamType.BIDIRECTIONAL, new ChannelInboundHandlerAdapter() { + int bytes; + + @Override + public void channelRegistered(ChannelHandlerContext ctx) { + ctx.channel().config().setAutoRead(false); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + ctx.writeAndFlush(ctx.alloc().buffer(8).writeLong(8)); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (bytes == 0) { + // First read + assertFalse(writePromise.isDone()); + } + ByteBuf buffer = (ByteBuf) msg; + bytes += buffer.readableBytes(); + buffer.release(); + if (bytes == bufferSize) { + ctx.close(); + assertTrue(writePromise.isDone()); + } + + if (!readInComplete) { + ctx.read(); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + if (readInComplete) { + ctx.read(); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + clientErrorRef.set(cause); + } + }).get(); + assertFalse(writePromise.isDone()); + + // Let's trigger the reads. This will ensure we will consume the data and the remote peer + // should be notified that it can write more data. + stream.read(); + + writePromise.sync(); + stream.closeFuture().sync(); + quicChannel.close().sync(); + + throwIfNotNull(serverErrorRef); + throwIfNotNull(clientErrorRef); + + serverHandler.assertState(); + clientHandler.assertState(); + } finally { + server.close().sync(); + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(executor); + } + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + @Timeout(value = 5000, unit = TimeUnit.MILLISECONDS) + public void testBytesUntilUnwritable(Executor executor) throws Throwable { + Promise writePromise = ImmediateEventExecutor.INSTANCE.newPromise(); + final AtomicReference serverErrorRef = new AtomicReference<>(); + final AtomicReference clientErrorRef = new AtomicReference<>(); + final CountDownLatch writableAgainLatch = new CountDownLatch(1); + int firstWriteNumBytes = 8; + int maxData = 32 * 1024; + final AtomicLong beforeWritableRef = new AtomicLong(); + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler(); + Channel server = QuicTestUtils.newServer( + QuicTestUtils.newQuicServerBuilder(executor).initialMaxStreamsBidirectional(5000), + InsecureQuicTokenHandler.INSTANCE, + serverHandler, new ChannelInboundHandlerAdapter() { + + private int numBytesRead; + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ByteBuf buffer = (ByteBuf) msg; + numBytesRead += buffer.readableBytes(); + buffer.release(); + if (numBytesRead == firstWriteNumBytes) { + long before = ctx.channel().bytesBeforeUnwritable(); + beforeWritableRef.set(before); + assertTrue(before > 0); + + while (before != 0) { + int size = (int) Math.min(before, 1024); + ctx.write(ctx.alloc().buffer(size).writeZero(size)); + long newBefore = ctx.channel().bytesBeforeUnwritable(); + + assertEquals(before, newBefore + size); + before = newBefore; + } + ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(new PromiseNotifier<>(writePromise)); + } + } + + @Override + public void channelWritabilityChanged(ChannelHandlerContext ctx) { + if (ctx.channel().isWritable()) { + if (ctx.channel().bytesBeforeUnwritable() > 0) { + writableAgainLatch.countDown(); + } + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + serverErrorRef.set(cause); + } + + @Override + public boolean isSharable() { + return true; + } + }); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor) + .initialMaxStreamDataBidirectionalLocal(maxData)); + + QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler(); + try { + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(address) + .connect() + .get(); + QuicStreamChannel stream = quicChannel.createStream( + QuicStreamType.BIDIRECTIONAL, new ChannelInboundHandlerAdapter() { + int bytes; + + @Override + public void channelRegistered(ChannelHandlerContext ctx) { + ctx.channel().config().setAutoRead(false); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + ctx.writeAndFlush(ctx.alloc().buffer(firstWriteNumBytes).writeZero(firstWriteNumBytes)); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ByteBuf buffer = (ByteBuf) msg; + bytes += buffer.readableBytes(); + buffer.release(); + if (bytes == beforeWritableRef.get()) { + assertTrue(writePromise.isDone()); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.read(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + clientErrorRef.set(cause); + } + }).get(); + + // Let's trigger the reads. This will ensure we will consume the data and the remote peer + // should be notified that it can write more data. + stream.read(); + + writePromise.sync(); + writableAgainLatch.await(); + stream.close().sync(); + stream.closeFuture().sync(); + quicChannel.close().sync(); + + throwIfNotNull(serverErrorRef); + throwIfNotNull(clientErrorRef); + + serverHandler.assertState(); + clientHandler.assertState(); + } finally { + server.close().sync(); + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(executor); + } + } + + private static void throwIfNotNull(AtomicReference errorRef) throws Throwable { + Throwable cause = errorRef.get(); + if (cause != null) { + throw cause; + } + } +} diff --git a/netty-channel/src/test/java/io/netty/channel/NativeImageHandlerMetadataTest.java b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicheQuicClientCodecTest.java similarity index 62% rename from netty-channel/src/test/java/io/netty/channel/NativeImageHandlerMetadataTest.java rename to netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicheQuicClientCodecTest.java index 77e9ed2..a78e1b5 100644 --- a/netty-channel/src/test/java/io/netty/channel/NativeImageHandlerMetadataTest.java +++ b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicheQuicClientCodecTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Netty Project + * 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 @@ -13,16 +13,13 @@ * License for the specific language governing permissions and limitations * under the License. */ -package io.netty.channel; +package io.netty.handler.codec.quic; -import io.netty.nativeimage.ChannelHandlerMetadataUtil; -import org.junit.jupiter.api.Test; +import io.netty.util.concurrent.ImmediateExecutor; -public class NativeImageHandlerMetadataTest { - - @Test - public void collectAndCompareMetadata() { - ChannelHandlerMetadataUtil.generateMetadata("io.netty.bootstrap", "io.netty.channel"); +public class QuicheQuicClientCodecTest extends QuicheQuicCodecTest { + @Override + protected QuicClientCodecBuilder newCodecBuilder() { + return QuicTestUtils.newQuicClientBuilder(ImmediateExecutor.INSTANCE); } - } diff --git a/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicheQuicCodecTest.java b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicheQuicCodecTest.java new file mode 100644 index 0000000..631d77b --- /dev/null +++ b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicheQuicCodecTest.java @@ -0,0 +1,111 @@ +/* + * 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 io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.channel.socket.DatagramPacket; +import org.junit.jupiter.api.Test; + +import java.net.InetSocketAddress; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public abstract class QuicheQuicCodecTest> extends AbstractQuicTest { + + protected abstract B newCodecBuilder(); + + @Test + public void testDefaultVersionIsV1() { + B builder = newCodecBuilder(); + assertEquals(0x0000_0001, builder.version); + } + + @Test + public void testFlushStrategyUsedWithBytes() { + testFlushStrategy(true); + } + + @Test + public void testFlushStrategyUsedWithPackets() { + testFlushStrategy(false); + } + + private void testFlushStrategy(boolean useBytes) { + final int bytes = 8; + final AtomicInteger numBytesTracker = new AtomicInteger(); + final AtomicInteger numPacketsTracker = new AtomicInteger(); + final AtomicInteger flushCount = new AtomicInteger(); + B builder = newCodecBuilder(); + builder.flushStrategy((numPackets, numBytes) -> { + numPacketsTracker.set(numPackets); + numBytesTracker.set(numBytes); + if (useBytes) { + return numBytes > 8; + } + if (numPackets == 2) { + return true; + } + return false; + }); + + EmbeddedChannel channel = new EmbeddedChannel(new ChannelOutboundHandlerAdapter() { + @Override + public void flush(ChannelHandlerContext ctx) throws Exception { + flushCount.incrementAndGet(); + super.flush(ctx); + } + }, builder.build()); + assertEquals(0, numPacketsTracker.get()); + assertEquals(0, numBytesTracker.get()); + assertEquals(0, flushCount.get()); + + channel.write(new DatagramPacket(Unpooled.buffer().writeZero(bytes), new InetSocketAddress(0))); + assertEquals(1, numPacketsTracker.get()); + assertEquals(8, numBytesTracker.get()); + assertEquals(0, flushCount.get()); + + channel.write(new DatagramPacket(Unpooled.buffer().writeZero(bytes), new InetSocketAddress(0))); + assertEquals(2, numPacketsTracker.get()); + assertEquals(16, numBytesTracker.get()); + assertEquals(1, flushCount.get()); + + // As a flush did happen we should see two packets in the outbound queue. + for (int i = 0; i < 2; i++) { + DatagramPacket packet = channel.readOutbound(); + assertNotNull(packet); + packet.release(); + } + + ChannelFuture future = channel.write(new DatagramPacket(Unpooled.buffer().writeZero(bytes), + new InetSocketAddress(0))); + assertEquals(1, numPacketsTracker.get()); + assertEquals(8, numBytesTracker.get()); + assertEquals(1, flushCount.get()); + + // We never flushed the last datagram packet so it should be failed. + assertFalse(channel.finish()); + assertTrue(future.isDone()); + assertFalse(future.isSuccess()); + } +} diff --git a/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicheQuicServerCodecTest.java b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicheQuicServerCodecTest.java new file mode 100644 index 0000000..59e38c3 --- /dev/null +++ b/netty-handler-codec-quic/src/test/java/io/netty/handler/codec/quic/QuicheQuicServerCodecTest.java @@ -0,0 +1,28 @@ +/* + * 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 io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.concurrent.ImmediateExecutor; + +public class QuicheQuicServerCodecTest extends QuicheQuicCodecTest { + @Override + protected QuicServerCodecBuilder newCodecBuilder() { + return QuicTestUtils.newQuicServerBuilder(ImmediateExecutor.INSTANCE) + .streamHandler(new ChannelInboundHandlerAdapter()) + .tokenHandler(InsecureQuicTokenHandler.INSTANCE); + } +} diff --git a/netty-handler-codec-quic/src/test/resources/logging.properties b/netty-handler-codec-quic/src/test/resources/logging.properties new file mode 100644 index 0000000..3cd7309 --- /dev/null +++ b/netty-handler-codec-quic/src/test/resources/logging.properties @@ -0,0 +1,7 @@ +handlers=java.util.logging.ConsoleHandler +.level=ALL +java.util.logging.SimpleFormatter.format=%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %5$s %6$s%n +java.util.logging.ConsoleHandler.level=ALL +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +jdk.event.security.level=INFO +org.junit.jupiter.engine.execution.ConditionEvaluator.level=OFF diff --git a/netty-handler-codec-quic/src/test/resources/netty-filter.json b/netty-handler-codec-quic/src/test/resources/netty-filter.json new file mode 100644 index 0000000..3440819 --- /dev/null +++ b/netty-handler-codec-quic/src/test/resources/netty-filter.json @@ -0,0 +1,6 @@ +{ + "rules": [ + {"excludeClasses": "**"}, + {"includeClasses": "io.netty.handler.codec.quic.**"} + ] +} diff --git a/netty-handler-codec-quic/src/test/resources/test-class-filter.json b/netty-handler-codec-quic/src/test/resources/test-class-filter.json new file mode 100644 index 0000000..215d67c --- /dev/null +++ b/netty-handler-codec-quic/src/test/resources/test-class-filter.json @@ -0,0 +1,12 @@ +{ + "rules": [ + {"includeClasses": "**"}, + {"excludeClasses": "ch.qos.logback.**"}, + {"excludeClasses": "io.netty.handler.codec.quic.NoValidationQuicTokenHandler"}, + {"excludeClasses": "io.netty.handler.codec.quic.QuicChannelValidationHandler"}, + {"excludeClasses": "org.apache.maven.surefire.**"} + ], + "regexRules": [ + {"excludeClasses": ".*Test.*"} + ] +} diff --git a/settings.gradle b/settings.gradle index fad8554..15e0205 100644 --- a/settings.gradle +++ b/settings.gradle @@ -86,3 +86,4 @@ include 'netty-tcnative-boringssl-static-native' include 'netty-testsuite' include 'netty-util' include 'netty-zlib' +include 'test-results' diff --git a/test-results/build.gradle b/test-results/build.gradle new file mode 100644 index 0000000..a02bd3a --- /dev/null +++ b/test-results/build.gradle @@ -0,0 +1,45 @@ +plugins { + id 'base' + id 'test-report-aggregation' +} + +dependencies { + testReportAggregation project(':netty-buffer') + testReportAggregation project(':netty-bzip2') + testReportAggregation project(':netty-channel') + testReportAggregation project(':netty-channel-epoll') + testReportAggregation project(':netty-channel-sctp') + testReportAggregation project(':netty-channel-unix') + testReportAggregation project(':netty-handler') + testReportAggregation project(':netty-handler-codec') + testReportAggregation project(':netty-handler-codec-compression') + testReportAggregation project(':netty-handler-codec-dns') + testReportAggregation project(':netty-handler-codec-http') + testReportAggregation project(':netty-handler-codec-http2') + testReportAggregation project(':netty-handler-codec-http3') + testReportAggregation project(':netty-handler-codec-quic') + testReportAggregation project(':netty-handler-codec-rtsp') + testReportAggregation project(':netty-handler-codec-sctp') + testReportAggregation project(':netty-handler-codec-spdy') + testReportAggregation project(':netty-handler-ssl') + testReportAggregation project(':netty-handler-ssl-bouncycastle') + testReportAggregation project(':netty-internal-tcnative') + testReportAggregation project(':netty-jctools') + testReportAggregation project(':netty-resolver') + testReportAggregation project(':netty-resolver-dns') + testReportAggregation project(':netty-testsuite') + testReportAggregation project(':netty-util') + testReportAggregation project(':netty-zlib') +} + +reporting { + reports { + testAggregateTestReport(AggregateTestReport) { + testType = TestSuiteType.UNIT_TEST + } + } +} + +tasks.named('check') { + dependsOn tasks.named('testAggregateTestReport', TestReport) +}