add low-level quiche tests

Signed-off-by: Ludovic Orban <lorban@bitronix.be>
This commit is contained in:
Ludovic Orban 2021-09-20 15:52:09 +02:00 committed by Simone Bordet
parent fcdabeb933
commit 8bf9227db2
6 changed files with 280 additions and 7 deletions

View File

@ -54,6 +54,11 @@
<artifactId>jetty-util</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-slf4j-impl</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.quic.server.internal;
package org.eclipse.jetty.quic.quiche;
import java.io.File;
import java.io.FileInputStream;
@ -31,11 +31,11 @@ import java.util.Base64;
public class SSLKeyPair
{
private static final byte[] BEGIN_KEY = "-----BEGIN PRIVATE KEY-----".getBytes(StandardCharsets.UTF_8);
private static final byte[] END_KEY = "-----END PRIVATE KEY-----".getBytes(StandardCharsets.UTF_8);
private static final byte[] BEGIN_CERT = "-----BEGIN CERTIFICATE-----".getBytes(StandardCharsets.UTF_8);
private static final byte[] END_CERT = "-----END CERTIFICATE-----".getBytes(StandardCharsets.UTF_8);
private static final byte[] LINE_SEPARATOR = System.getProperty("line.separator").getBytes(StandardCharsets.UTF_8);
private static final byte[] BEGIN_KEY = "-----BEGIN PRIVATE KEY-----".getBytes(StandardCharsets.US_ASCII);
private static final byte[] END_KEY = "-----END PRIVATE KEY-----".getBytes(StandardCharsets.US_ASCII);
private static final byte[] BEGIN_CERT = "-----BEGIN CERTIFICATE-----".getBytes(StandardCharsets.US_ASCII);
private static final byte[] END_CERT = "-----END CERTIFICATE-----".getBytes(StandardCharsets.US_ASCII);
private static final byte[] LINE_SEPARATOR = System.getProperty("line.separator").getBytes(StandardCharsets.US_ASCII);
private static final int LINE_LENGTH = 64;
private final Base64.Encoder encoder = Base64.getMimeEncoder(LINE_LENGTH, LINE_SEPARATOR);

View File

@ -0,0 +1,265 @@
//
// ========================================================================
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.quic.quiche;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.file.Paths;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.eclipse.jetty.quic.quiche.ffi.LibQuiche;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.core.Is.is;
public class LowLevelQuicheTest
{
private final Collection<QuicheConnection> connectionsToDisposeOf = new ArrayList<>();
private InetSocketAddress clientSocketAddress;
private InetSocketAddress serverSocketAddress;
private QuicheConfig quicheClientConfig;
private QuicheConfig serverQuicheConfig;
private QuicheConnection.TokenMinter tokenMinter;
private QuicheConnection.TokenValidator tokenValidator;
@BeforeEach
protected void setUp() throws Exception
{
clientSocketAddress = new InetSocketAddress("localhost", 9999);
serverSocketAddress = new InetSocketAddress("localhost", 8888);
quicheClientConfig = new QuicheConfig();
quicheClientConfig.setApplicationProtos("http/0.9");
quicheClientConfig.setDisableActiveMigration(true);
quicheClientConfig.setVerifyPeer(false);
quicheClientConfig.setMaxIdleTimeout(1_000L);
quicheClientConfig.setInitialMaxData(10_000_000L);
quicheClientConfig.setInitialMaxStreamDataBidiLocal(10_000_000L);
quicheClientConfig.setInitialMaxStreamDataBidiRemote(10_000_000L);
quicheClientConfig.setInitialMaxStreamDataUni(10_000_000L);
quicheClientConfig.setInitialMaxStreamsUni(100L);
quicheClientConfig.setInitialMaxStreamsBidi(100L);
quicheClientConfig.setCongestionControl(QuicheConfig.CongestionControl.RENO);
SSLKeyPair serverKeyPair = new SSLKeyPair(Paths.get(Objects.requireNonNull(getClass().getResource("/keystore.p12")).toURI()).toFile(), "PKCS12", "storepwd".toCharArray(), "mykey", "storepwd".toCharArray());
File[] pemFiles = serverKeyPair.export(new File(System.getProperty("java.io.tmpdir")));
serverQuicheConfig = new QuicheConfig();
serverQuicheConfig.setPrivKeyPemPath(pemFiles[0].getPath());
serverQuicheConfig.setCertChainPemPath(pemFiles[1].getPath());
serverQuicheConfig.setApplicationProtos("http/0.9");
serverQuicheConfig.setVerifyPeer(false);
serverQuicheConfig.setMaxIdleTimeout(1_000L);
serverQuicheConfig.setInitialMaxData(10_000_000L);
serverQuicheConfig.setInitialMaxStreamDataBidiLocal(10_000_000L);
serverQuicheConfig.setInitialMaxStreamDataBidiRemote(10_000_000L);
serverQuicheConfig.setInitialMaxStreamDataUni(10_000_000L);
serverQuicheConfig.setInitialMaxStreamsUni(100L);
serverQuicheConfig.setInitialMaxStreamsBidi(100L);
serverQuicheConfig.setCongestionControl(QuicheConfig.CongestionControl.RENO);
tokenMinter = new TestTokenMinter();
tokenValidator = new TestTokenValidator();
}
@AfterEach
protected void tearDown()
{
connectionsToDisposeOf.forEach(QuicheConnection::dispose);
connectionsToDisposeOf.clear();
}
@Test
public void testFinishedAsSoonAsFinIsFed() throws Exception
{
// establish connection
Map.Entry<QuicheConnection, QuicheConnection> entry = connectClientToServer();
QuicheConnection clientQuicheConnection = entry.getKey();
QuicheConnection serverQuicheConnection = entry.getValue();
// client sends 16 bytes of payload over stream 0
assertThat(clientQuicheConnection.feedClearTextForStream(0, ByteBuffer.allocate(16)
.putInt(0xdeadbeef)
.putInt(0xcafebabe)
.putInt(0xdeadc0de)
.putInt(0xbaddecaf)
.flip()), is(16));
drainClientToFeedServer(entry, 59);
// server checks that stream 0 is readable
List<Long> readableStreamIds = serverQuicheConnection.readableStreamIds();
assertThat(readableStreamIds.size(), is(1));
assertThat(readableStreamIds.get(0), is(0L));
// server reads 16 bytes from stream 0
assertThat(serverQuicheConnection.drainClearTextForStream(0, ByteBuffer.allocate(1000)), is(16));
// assert that stream 0 is not finished on server
assertThat(serverQuicheConnection.isStreamFinished(0), is(false));
// client finishes stream 0
clientQuicheConnection.feedFinForStream(0);
drainClientToFeedServer(entry, 43);
readableStreamIds = serverQuicheConnection.readableStreamIds();
assertThat(readableStreamIds.size(), is(1));
assertThat(readableStreamIds.get(0), is(0L));
// assert that stream 0 is finished on server
assertThat(serverQuicheConnection.isStreamFinished(0), is(true));
}
@Test
public void testNotFinishedAsLongAsStreamHasReadableBytes() throws Exception
{
// establish connection
Map.Entry<QuicheConnection, QuicheConnection> entry = connectClientToServer();
QuicheConnection clientQuicheConnection = entry.getKey();
QuicheConnection serverQuicheConnection = entry.getValue();
// client sends 16 bytes of payload over stream 0 and finish it
assertThat(clientQuicheConnection.feedClearTextForStream(0, ByteBuffer.allocate(16)
.putInt(0xdeadbeef)
.putInt(0xcafebabe)
.putInt(0xdeadc0de)
.putInt(0xbaddecaf)
.flip()), is(16));
clientQuicheConnection.feedFinForStream(0);
drainClientToFeedServer(entry, 59);
// server checks that stream 0 is readable
List<Long> readableStreamIds = serverQuicheConnection.readableStreamIds();
assertThat(readableStreamIds.size(), is(1));
assertThat(readableStreamIds.get(0), is(0L));
// assert that stream 0 is not finished on server
assertThat(serverQuicheConnection.isStreamFinished(0), is(false));
// server reads 16 bytes from stream 0
assertThat(serverQuicheConnection.drainClearTextForStream(0, ByteBuffer.allocate(1000)), is(16));
// assert that stream 0 is finished on server
assertThat(serverQuicheConnection.isStreamFinished(0), is(true));
}
private void drainServerToFeedClient(Map.Entry<QuicheConnection, QuicheConnection> entry, int expectedSize) throws IOException
{
QuicheConnection clientQuicheConnection = entry.getKey();
QuicheConnection serverQuicheConnection = entry.getValue();
ByteBuffer buffer = ByteBuffer.allocate(LibQuiche.QUICHE_MIN_CLIENT_INITIAL_LEN);
int drained = serverQuicheConnection.drainCipherText(buffer);
assertThat(drained, is(expectedSize));
buffer.flip();
int fed = clientQuicheConnection.feedCipherText(buffer, serverSocketAddress);
assertThat(fed, is(expectedSize));
}
private void drainClientToFeedServer(Map.Entry<QuicheConnection, QuicheConnection> entry, int expectedSize) throws IOException
{
QuicheConnection clientQuicheConnection = entry.getKey();
QuicheConnection serverQuicheConnection = entry.getValue();
ByteBuffer buffer = ByteBuffer.allocate(LibQuiche.QUICHE_MIN_CLIENT_INITIAL_LEN);
int drained = clientQuicheConnection.drainCipherText(buffer);
assertThat(drained, is(expectedSize));
buffer.flip();
int fed = serverQuicheConnection.feedCipherText(buffer, clientSocketAddress);
assertThat(fed, is(expectedSize));
}
private Map.Entry<QuicheConnection, QuicheConnection> connectClientToServer() throws IOException
{
ByteBuffer buffer = ByteBuffer.allocate(LibQuiche.QUICHE_MIN_CLIENT_INITIAL_LEN);
ByteBuffer buffer2 = ByteBuffer.allocate(LibQuiche.QUICHE_MIN_CLIENT_INITIAL_LEN);
QuicheConnection clientQuicheConnection = QuicheConnection.connect(quicheClientConfig, serverSocketAddress);
connectionsToDisposeOf.add(clientQuicheConnection);
int drained = clientQuicheConnection.drainCipherText(buffer);
assertThat(drained, is(1200));
buffer.flip();
QuicheConnection serverQuicheConnection = QuicheConnection.tryAccept(serverQuicheConfig, tokenValidator, buffer, clientSocketAddress);
assertThat(serverQuicheConnection, is(nullValue()));
boolean negotiated = QuicheConnection.negotiate(tokenMinter, buffer, buffer2);
assertThat(negotiated, is(true));
buffer2.flip();
int fed = clientQuicheConnection.feedCipherText(buffer2, serverSocketAddress);
assertThat(fed, is(79));
buffer.clear();
drained = clientQuicheConnection.drainCipherText(buffer);
assertThat(drained, is(1200));
buffer.flip();
serverQuicheConnection = QuicheConnection.tryAccept(serverQuicheConfig, tokenValidator, buffer, clientSocketAddress);
assertThat(serverQuicheConnection, is(not(nullValue())));
connectionsToDisposeOf.add(serverQuicheConnection);
buffer.clear();
drained = serverQuicheConnection.drainCipherText(buffer);
assertThat(drained, is(1200));
buffer.flip();
fed = clientQuicheConnection.feedCipherText(buffer, serverSocketAddress);
assertThat(fed, is(1200));
assertThat(serverQuicheConnection.isConnectionEstablished(), is(false));
assertThat(clientQuicheConnection.isConnectionEstablished(), is(false));
AbstractMap.SimpleImmutableEntry<QuicheConnection, QuicheConnection> entry = new AbstractMap.SimpleImmutableEntry<>(clientQuicheConnection, serverQuicheConnection);
drainServerToFeedClient(entry, 309);
assertThat(serverQuicheConnection.isConnectionEstablished(), is(false));
assertThat(clientQuicheConnection.isConnectionEstablished(), is(true));
drainClientToFeedServer(entry, 1200);
assertThat(serverQuicheConnection.isConnectionEstablished(), is(true));
assertThat(clientQuicheConnection.isConnectionEstablished(), is(true));
return entry;
}
private static class TestTokenMinter implements QuicheConnection.TokenMinter
{
@Override
public byte[] mint(byte[] dcid, int len)
{
return ByteBuffer.allocate(len).put(dcid, 0, len).array();
}
}
private static class TestTokenValidator implements QuicheConnection.TokenValidator
{
@Override
public byte[] validate(byte[] token, int len)
{
return ByteBuffer.allocate(len).put(token, 0, len).array();
}
}
}

View File

@ -0,0 +1,3 @@
#org.eclipse.jetty.LEVEL=DEBUG
#org.eclipse.jetty.quic.LEVEL=DEBUG
org.eclipse.jetty.quic.quiche.LEVEL=INFO

Binary file not shown.

View File

@ -30,7 +30,7 @@ import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.ManagedSelector;
import org.eclipse.jetty.io.SelectorManager;
import org.eclipse.jetty.quic.quiche.QuicheConfig;
import org.eclipse.jetty.quic.server.internal.SSLKeyPair;
import org.eclipse.jetty.quic.quiche.SSLKeyPair;
import org.eclipse.jetty.server.AbstractNetworkConnector;
import org.eclipse.jetty.server.ConnectionFactory;
import org.eclipse.jetty.server.Server;