add low-level quiche tests
Signed-off-by: Ludovic Orban <lorban@bitronix.be>
This commit is contained in:
parent
fcdabeb933
commit
8bf9227db2
|
@ -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>
|
||||
|
|
|
@ -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);
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue