update to latest quic native, add quic tests

main
Jörg Prante 7 months ago
parent e293f2e603
commit 938371e9eb

@ -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

@ -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')

@ -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',

@ -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 */

@ -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

@ -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')
}

@ -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
* <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/Key_Log_Format">
* NSS Key Log Format</a>. This is intended for debugging use with tools like Wireshark.

@ -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();
}
}
}

@ -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));
}
}

@ -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();
}
}
}

@ -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<QuicDatagramExtensionEvent> 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<ByteBuf> receivedBuffer = ImmediateEventExecutor.INSTANCE.newPromise();
AtomicReference<QuicDatagramExtensionEvent> 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<Void> serverPromise = ImmediateEventExecutor.INSTANCE.newPromise();
Promise<ByteBuf> 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);
}
}
}

@ -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<Object[]> data() {
List<Object[]> 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<List<ChannelFuture>> 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<QuicStreamChannel> future) -> {
QuicStreamChannel stream = future.getNow();
setAllocator(stream, allocator);
List<ChannelFuture> 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<ChannelFuture> 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<ChannelFuture> 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<ChannelFuture> 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<ChannelFuture> 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<ByteBuf> {
private final boolean server;
private final boolean autoRead;
private final ByteBufAllocator allocator;
volatile Channel channel;
final AtomicReference<Throwable> 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();
}
}
}
}

@ -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;
}
}
}

@ -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> {
TestQuicCodecBuilder() {
super(true);
}
TestQuicCodecBuilder(TestQuicCodecBuilder builder) {
super(builder);
}
@Override
public TestQuicCodecBuilder clone() {
// no-op
return null;
}
@Override
protected ChannelHandler build(
QuicheConfig config,
Function<QuicChannel, ? extends QuicSslEngine> sslContextProvider,
Executor sslTaskExecutor,
int localConnIdLength,
FlushStrategy flushStrategy) {
// no-op
return null;
}
}
}

@ -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);
}
}

@ -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));
}
}

@ -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<QuicConnectionStats> serverActiveStats = ImmediateEventExecutor.INSTANCE.newPromise();
Promise<QuicConnectionStats> 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<QuicConnectionStats> 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));
}
}

@ -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));
}
}

@ -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<Throwable> serverErrorRef = new AtomicReference<>();
final AtomicReference<Throwable> 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<Channel> 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<Throwable> errorRef) throws Throwable {
Throwable cause = errorRef.get();
if (cause != null) {
throw cause;
}
}
}

@ -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());
}
}

@ -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<Channel> 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<Channel> 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<Channel> 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<? extends Throwable> 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<Channel> streamPromise;
StreamCreationAndTearDownHandler(QuicStreamType type, boolean halfClose, Promise<Channel> 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<Channel> streamPromise;
StreamCreationHandler(QuicStreamType type, boolean halfClose, Promise<Channel> 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);
}
}
}

@ -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<String> 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());
}
}

@ -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<Integer> 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());
}
}
}

@ -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<Integer> 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());
}
}
}

@ -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));
}
}

@ -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<Void> streamPromise = ImmediateEventExecutor.INSTANCE.newPromise();
Promise<Throwable> 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<QuicStreamChannel> future) -> {
if (future.isSuccess()) {
QuicStreamChannel stream = future.getNow();
streamPromise.setSuccess(null);
channel.createStream(type, new ChannelInboundHandlerAdapter())
.addListener((Future<QuicStreamChannel> 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);
}
}
}

@ -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);
}
}
}

@ -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<Throwable> 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<Void> serverWritePromise = ImmediateEventExecutor.INSTANCE.newPromise();
Promise<Throwable> 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);
}
}
}

@ -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<String> key = AttributeKey.valueOf(UUID.randomUUID().toString());
String value = "testValue";
Map<AttributeKey<?>, Object> attributes = new HashMap<>();
attributes.put(key, value);
Map.Entry<AttributeKey<?>, Object>[] array = Quic.toAttributesArray(attributes);
assertEquals(1, array.length);
attributes.put(key, "newTestValue");
Map.Entry<AttributeKey<?>, Object> entry = array[0];
assertEquals(key, entry.getKey());
assertEquals(value, entry.getValue());
}
@Test
public void testToOptionsArrayDoesCopy() {
Map<ChannelOption<?>, Object> options = new HashMap<>();
options.put(ChannelOption.AUTO_READ, true);
Map.Entry<ChannelOption<?>, Object>[] array = Quic.toOptionsArray(options);
assertEquals(1, array.length);
options.put(ChannelOption.AUTO_READ, false);
Map.Entry<ChannelOption<?>, Object> entry = array[0];
assertEquals(ChannelOption.AUTO_READ, entry.getKey());
assertEquals(true, entry.getValue());
}
}

@ -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();
}
}
}

@ -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<QuicTransportParameters> 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));
}
}

@ -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<Void> writePromise = ImmediateEventExecutor.INSTANCE.newPromise();
final AtomicReference<Throwable> serverErrorRef = new AtomicReference<>();
final AtomicReference<Throwable> 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<Void> writePromise = ImmediateEventExecutor.INSTANCE.newPromise();
final AtomicReference<Throwable> serverErrorRef = new AtomicReference<>();
final AtomicReference<Throwable> 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<Throwable> errorRef) throws Throwable {
Throwable cause = errorRef.get();
if (cause != null) {
throw cause;
}
}
}

@ -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<QuicClientCodecBuilder> {
@Override
protected QuicClientCodecBuilder newCodecBuilder() {
return QuicTestUtils.newQuicClientBuilder(ImmediateExecutor.INSTANCE);
}
}

@ -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<B extends QuicCodecBuilder<B>> 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());
}
}

@ -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<QuicServerCodecBuilder> {
@Override
protected QuicServerCodecBuilder newCodecBuilder() {
return QuicTestUtils.newQuicServerBuilder(ImmediateExecutor.INSTANCE)
.streamHandler(new ChannelInboundHandlerAdapter())
.tokenHandler(InsecureQuicTokenHandler.INSTANCE);
}
}

@ -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

@ -0,0 +1,6 @@
{
"rules": [
{"excludeClasses": "**"},
{"includeClasses": "io.netty.handler.codec.quic.**"}
]
}

@ -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.*"}
]
}

@ -86,3 +86,4 @@ include 'netty-tcnative-boringssl-static-native'
include 'netty-testsuite'
include 'netty-util'
include 'netty-zlib'
include 'test-results'

@ -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)
}
Loading…
Cancel
Save