Issue #6728 - QUIC and HTTP/3

- Incremented test timeouts.

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
Simone Bordet 2021-11-08 11:28:37 +01:00
parent 4d778b1aff
commit eb8444e2c2
60 changed files with 3617 additions and 292 deletions

View File

@ -284,7 +284,7 @@
</dependency>
<dependency>
<groupId>org.eclipse.jetty.quic</groupId>
<artifactId>quic-quiche</artifactId>
<artifactId>quic-quiche-common</artifactId>
<version>10.0.8-SNAPSHOT</version>
</dependency>
<dependency>
@ -439,4 +439,37 @@
</dependency>
</dependencies>
</dependencyManagement>
<profiles>
<profile>
<id>earlier-than-jdk17</id>
<activation>
<jdk>(,17)</jdk>
</activation>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty.quic</groupId>
<artifactId>quic-quiche-jna</artifactId>
<version>10.0.8-SNAPSHOT</version>
</dependency>
</dependencies>
</dependencyManagement>
</profile>
<profile>
<id>jdk17</id>
<activation>
<jdk>[17,)</jdk>
</activation>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty.quic</groupId>
<artifactId>quic-quiche-panama-jdk</artifactId>
<version>10.0.8-SNAPSHOT</version>
</dependency>
</dependencies>
</dependencyManagement>
</profile>
</profiles>
</project>

View File

@ -22,25 +22,24 @@
<dependency>
<groupId>org.eclipse.jetty.quic</groupId>
<artifactId>quic-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.quic</groupId>
<artifactId>quic-quiche-jna</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.quic</groupId>
<artifactId>quic-server</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>http2-server</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>http2-http-client-transport</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
@ -49,4 +48,19 @@
<scope>test</scope>
</dependency>
</dependencies>
<profiles>
<profile>
<id>jdk17</id>
<activation>
<jdk>[17,)</jdk>
</activation>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty.quic</groupId>
<artifactId>quic-quiche-panama-jdk</artifactId>
</dependency>
</dependencies>
</profile>
</profiles>
</project>

View File

@ -21,14 +21,12 @@
</dependency>
<dependency>
<groupId>org.eclipse.jetty.quic</groupId>
<artifactId>quic-quiche</artifactId>
<version>${project.version}</version>
<artifactId>quic-quiche-common</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-io</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>

View File

@ -8,44 +8,23 @@
<modelVersion>4.0.0</modelVersion>
<artifactId>quic-quiche</artifactId>
<packaging>pom</packaging>
<name>Jetty :: QUIC :: Quiche</name>
<properties>
<bundle-symbolic-name>${project.groupId}.quic-quiche</bundle-symbolic-name>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>${maven.checkstyle.plugin.version}</version>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<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>
<modules>
<module>quic-quiche-common</module>
<module>quic-quiche-jna</module>
</modules>
<profiles>
<profile>
<id>jdk17</id>
<activation>
<jdk>[17,)</jdk>
</activation>
<modules>
<module>quic-quiche-panama-jdk</module>
</modules>
</profile>
</profiles>
</project>

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.eclipse.jetty.quic</groupId>
<artifactId>quic-quiche</artifactId>
<version>10.0.8-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>quic-quiche-common</artifactId>
<name>Jetty :: QUIC :: Quiche :: Common</name>
<properties>
<bundle-symbolic-name>${project.groupId}.quic-quiche-common</bundle-symbolic-name>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>${maven.checkstyle.plugin.version}</version>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-slf4j-impl</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -11,14 +11,12 @@
// ========================================================================
//
// The module must be open to allow JNA to find the native lib.
open module org.eclipse.jetty.quic.quiche
module org.eclipse.jetty.quic.quiche
{
requires org.eclipse.jetty.util;
requires org.slf4j;
requires transitive com.sun.jna;
exports org.eclipse.jetty.quic.quiche.ffi;
exports org.eclipse.jetty.quic.quiche;
uses org.eclipse.jetty.quic.quiche.QuicheBinding;
}

View File

@ -0,0 +1,28 @@
//
// ========================================================================
// 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;
interface Quiche
{
// The current QUIC wire version.
int QUICHE_PROTOCOL_VERSION = 0x00000001;
// The maximum length of a connection ID.
int QUICHE_MAX_CONN_ID_LEN = 20;
interface quiche_cc_algorithm
{
int QUICHE_CC_RENO = 0,
QUICHE_CC_CUBIC = 1;
}
}

View File

@ -0,0 +1,30 @@
//
// ========================================================================
// 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.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
public interface QuicheBinding
{
boolean isUsable();
int priority();
byte[] fromPacket(ByteBuffer packet);
QuicheConnection connect(QuicheConfig quicheConfig, InetSocketAddress peer, int connectionIdLength) throws IOException;
boolean negotiate(QuicheConnection.TokenMinter tokenMinter, ByteBuffer packetRead, ByteBuffer packetToSend) throws IOException;
QuicheConnection tryAccept(QuicheConfig quicheConfig, QuicheConnection.TokenValidator tokenValidator, ByteBuffer packetRead, SocketAddress peer) throws IOException;
}

View File

@ -13,19 +13,12 @@
package org.eclipse.jetty.quic.quiche;
import org.eclipse.jetty.quic.quiche.ffi.LibQuiche;
public class QuicheConfig
{
static
{
LibQuiche.Logging.enable();
}
public enum CongestionControl
{
RENO(LibQuiche.quiche_cc_algorithm.QUICHE_CC_RENO),
CUBIC(LibQuiche.quiche_cc_algorithm.QUICHE_CC_CUBIC);
RENO(Quiche.quiche_cc_algorithm.QUICHE_CC_RENO),
CUBIC(Quiche.quiche_cc_algorithm.QUICHE_CC_CUBIC);
private final int value;
CongestionControl(int value)
@ -39,7 +32,7 @@ public class QuicheConfig
}
}
private int version = LibQuiche.QUICHE_PROTOCOL_VERSION;
private int version = Quiche.QUICHE_PROTOCOL_VERSION;
private Boolean verifyPeer;
private String certChainPemPath;
private String privKeyPemPath;

View File

@ -0,0 +1,194 @@
//
// ========================================================================
// 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.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.util.Comparator;
import java.util.List;
import java.util.ServiceLoader;
import java.util.stream.Collectors;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.TypeUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class QuicheConnection
{
private static final Logger LOG = LoggerFactory.getLogger(QuicheConnection.class);
static final QuicheBinding QUICHE_BINDING;
static
{
LOG.info("found Quiche binding implementations: {}", TypeUtil.serviceStream(ServiceLoader.load(QuicheBinding.class)).sorted(Comparator.comparingInt(QuicheBinding::priority)).collect(Collectors.toList()));
if (LOG.isDebugEnabled())
{
List<QuicheBinding> bindings = TypeUtil.serviceStream(ServiceLoader.load(QuicheBinding.class))
.sorted(Comparator.comparingInt(QuicheBinding::priority))
.collect(Collectors.toList());
LOG.debug("found Quiche binding implementations: {}", bindings);
}
QUICHE_BINDING = TypeUtil.serviceStream(ServiceLoader.load(QuicheBinding.class))
.filter(QuicheBinding::isUsable)
.min(Comparator.comparingInt(QuicheBinding::priority))
.orElseThrow(() -> new IllegalStateException("no Quiche binding implementation found"));
if (LOG.isDebugEnabled())
LOG.debug("using Quiche binding implementation: {}", QUICHE_BINDING.getClass().getName());
LOG.info("using Quiche binding implementation: {}", QUICHE_BINDING.getClass().getName());
}
public static QuicheConnection connect(QuicheConfig quicheConfig, InetSocketAddress peer) throws IOException
{
return connect(quicheConfig, peer, Quiche.QUICHE_MAX_CONN_ID_LEN);
}
public static QuicheConnection connect(QuicheConfig quicheConfig, InetSocketAddress peer, int connectionIdLength) throws IOException
{
return QUICHE_BINDING.connect(quicheConfig, peer, connectionIdLength);
}
/**
* Fully consumes the {@code packetRead} buffer.
* @return true if a negotiation packet was written to the {@code packetToSend} buffer, false if negotiation failed
* and the {@code packetRead} buffer can be dropped.
*/
public static boolean negotiate(TokenMinter tokenMinter, ByteBuffer packetRead, ByteBuffer packetToSend) throws IOException
{
return QUICHE_BINDING.negotiate(tokenMinter, packetRead, packetToSend);
}
/**
* Fully consumes the {@code packetRead} buffer if the connection was accepted.
* @return an established connection if accept succeeded, null if accept failed and negotiation should be tried.
*/
public static QuicheConnection tryAccept(QuicheConfig quicheConfig, TokenValidator tokenValidator, ByteBuffer packetRead, SocketAddress peer) throws IOException
{
return QUICHE_BINDING.tryAccept(quicheConfig, tokenValidator, packetRead, peer);
}
public final List<Long> readableStreamIds()
{
return iterableStreamIds(false);
}
public final List<Long> writableStreamIds()
{
return iterableStreamIds(true);
}
protected abstract List<Long> iterableStreamIds(boolean write);
/**
* Read the buffer of cipher text coming from the network.
* @param buffer the buffer to read.
* @param peer the address of the peer from which the buffer was received.
* @return how many bytes were consumed.
* @throws IOException
*/
public abstract int feedCipherBytes(ByteBuffer buffer, SocketAddress peer) throws IOException;
/**
* Fill the given buffer with cipher text to be sent.
* @param buffer the buffer to fill.
* @return how many bytes were added to the buffer.
* @throws IOException
*/
public abstract int drainCipherBytes(ByteBuffer buffer) throws IOException;
public abstract boolean isConnectionClosed();
public abstract boolean isConnectionEstablished();
public abstract long nextTimeout();
public abstract void onTimeout();
public abstract String getNegotiatedProtocol();
public abstract boolean close(long error, String reason);
public abstract void dispose();
public abstract boolean isDraining();
public abstract int maxLocalStreams();
public abstract long windowCapacity();
public abstract long windowCapacity(long streamId) throws IOException;
public abstract void shutdownStream(long streamId, boolean writeSide, long error) throws IOException;
public final void feedFinForStream(long streamId) throws IOException
{
feedClearBytesForStream(streamId, BufferUtil.EMPTY_BUFFER, true);
}
public final int feedClearBytesForStream(long streamId, ByteBuffer buffer) throws IOException
{
return feedClearBytesForStream(streamId, buffer, false);
}
public abstract int feedClearBytesForStream(long streamId, ByteBuffer buffer, boolean last) throws IOException;
public abstract int drainClearBytesForStream(long streamId, ByteBuffer buffer) throws IOException;
public abstract boolean isStreamFinished(long streamId);
public abstract CloseInfo getRemoteCloseInfo();
public static class CloseInfo
{
private final long error;
private final String reason;
public CloseInfo(long error, String reason)
{
this.error = error;
this.reason = reason;
}
public long error()
{
return error;
}
public String reason()
{
return reason;
}
}
public interface TokenMinter
{
int MAX_TOKEN_LENGTH = 48;
byte[] mint(byte[] dcid, int len);
}
public interface TokenValidator
{
byte[] validate(byte[] token, int len);
}
public static class TokenValidationException extends IOException
{
public TokenValidationException(String msg)
{
super(msg);
}
}
}

View File

@ -18,42 +18,22 @@ import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Objects;
import org.eclipse.jetty.quic.quiche.ffi.LibQuiche;
import org.eclipse.jetty.quic.quiche.ffi.size_t;
import org.eclipse.jetty.quic.quiche.ffi.size_t_pointer;
import org.eclipse.jetty.quic.quiche.ffi.uint32_t_pointer;
import org.eclipse.jetty.quic.quiche.ffi.uint8_t_pointer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class QuicheConnectionId
{
private static final Logger LOG = LoggerFactory.getLogger(QuicheConnectionId.class);
private static final byte[] HEX_ARRAY = "0123456789abcdef".getBytes(StandardCharsets.US_ASCII);
static
{
LibQuiche.Logging.enable();
}
private final byte[] dcid;
private final int hashCode;
private final String string;
private String string;
private QuicheConnectionId(byte[] dcid)
{
this.dcid = Objects.requireNonNull(dcid);
this.hashCode = Arrays.hashCode(dcid);
this.string = bytesToHex(dcid);
}
private static String bytesToHex(byte[] bytes)
{
byte[] hexChars = new byte[bytes.length * 2];
for (int i = 0; i < bytes.length; i++)
{
int c = bytes[i] & 0xFF;
hexChars[i * 2] = HEX_ARRAY[c >>> 4];
hexChars[i * 2 + 1] = HEX_ARRAY[c & 0x0F];
}
return new String(hexChars, StandardCharsets.US_ASCII);
}
/**
@ -61,44 +41,13 @@ public class QuicheConnectionId
*/
public static QuicheConnectionId fromPacket(ByteBuffer packet)
{
uint8_t_pointer type = new uint8_t_pointer();
uint32_t_pointer version = new uint32_t_pointer();
// Source Connection ID
byte[] scid = new byte[LibQuiche.QUICHE_MAX_CONN_ID_LEN];
size_t_pointer scidLen = new size_t_pointer(scid.length);
// Destination Connection ID
byte[] dcid = new byte[LibQuiche.QUICHE_MAX_CONN_ID_LEN];
size_t_pointer dcidLen = new size_t_pointer(dcid.length);
byte[] token = new byte[48]; // TODO the token buffer size depends on the token minter.
size_t_pointer tokenLen = new size_t_pointer(token.length);
int rc = LibQuiche.INSTANCE.quiche_header_info(packet, new size_t(packet.remaining()), new size_t(LibQuiche.QUICHE_MAX_CONN_ID_LEN),
version, type,
scid, scidLen,
dcid, dcidLen,
token, tokenLen);
if (rc < 0)
return null;
byte[] sizedDcid = resizeIfNeeded(dcid, (int)dcidLen.getValue());
return new QuicheConnectionId(sizedDcid);
}
private static byte[] resizeIfNeeded(byte[] buffer, int length)
{
byte[] sizedBuffer;
if (length == buffer.length)
{
sizedBuffer = buffer;
}
else
{
sizedBuffer = new byte[length];
System.arraycopy(buffer, 0, sizedBuffer, 0, sizedBuffer.length);
}
return sizedBuffer;
byte[] bytes = QuicheConnection.QUICHE_BINDING.fromPacket(packet);
if (bytes != null && bytes.length == 0)
throw new IllegalStateException();
QuicheConnectionId connectionId = bytes == null ? null : new QuicheConnectionId(bytes);
if (LOG.isDebugEnabled())
LOG.debug("snooped connection ID from packet: [{}]", connectionId);
return connectionId;
}
@Override
@ -121,6 +70,21 @@ public class QuicheConnectionId
@Override
public String toString()
{
if (string == null)
string = bytesToHex(dcid);
return string;
}
private static String bytesToHex(byte[] bytes)
{
byte[] hexChars = new byte[bytes.length * 2];
for (int i = 0; i < bytes.length; i++)
{
int c = bytes[i] & 0xFF;
hexChars[i * 2] = HEX_ARRAY[c >>> 4];
hexChars[i * 2 + 1] = HEX_ARRAY[c & 0x0F];
}
return new String(hexChars, StandardCharsets.US_ASCII);
}
}

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.eclipse.jetty.quic</groupId>
<artifactId>quic-quiche</artifactId>
<version>10.0.8-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>quic-quiche-jna</artifactId>
<name>Jetty :: QUIC :: Quiche :: JNA Binding</name>
<properties>
<bundle-symbolic-name>${project.groupId}.quic-quiche-jna</bundle-symbolic-name>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>${maven.checkstyle.plugin.version}</version>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.quic</groupId>
<artifactId>quic-quiche-common</artifactId>
</dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-slf4j-impl</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,29 @@
//
// ========================================================================
// 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
// ========================================================================
//
module org.eclipse.jetty.quic.quiche.jna
{
requires com.sun.jna;
requires org.eclipse.jetty.quic.quiche;
requires org.eclipse.jetty.util;
requires org.slf4j;
// Allow JNA to use reflection on the implementation classes.
opens org.eclipse.jetty.quic.quiche.jna to com.sun.jna;
opens org.eclipse.jetty.quic.quiche.jna.linux to com.sun.jna;
opens org.eclipse.jetty.quic.quiche.jna.macos to com.sun.jna;
opens org.eclipse.jetty.quic.quiche.jna.windows to com.sun.jna;
provides org.eclipse.jetty.quic.quiche.QuicheBinding with
org.eclipse.jetty.quic.quiche.jna.JnaQuicheBinding;
}

View File

@ -0,0 +1,82 @@
//
// ========================================================================
// 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.jna;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import org.eclipse.jetty.quic.quiche.QuicheBinding;
import org.eclipse.jetty.quic.quiche.QuicheConfig;
import org.eclipse.jetty.quic.quiche.QuicheConnection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class JnaQuicheBinding implements QuicheBinding
{
private static final Logger LOG = LoggerFactory.getLogger(JnaQuicheBinding.class);
@Override
public boolean isUsable()
{
try
{
// Make a Quiche call to confirm.
LibQuiche.INSTANCE.quiche_version();
return true;
}
catch (Throwable x)
{
LOG.debug("JNA quiche binding is not usable", x);
return false;
}
}
@Override
public int priority()
{
return 200;
}
@Override
public byte[] fromPacket(ByteBuffer packet)
{
return JnaQuicheConnection.fromPacket(packet);
}
@Override
public QuicheConnection connect(QuicheConfig quicheConfig, InetSocketAddress peer, int connectionIdLength) throws IOException
{
return JnaQuicheConnection.connect(quicheConfig, peer, connectionIdLength);
}
@Override
public boolean negotiate(QuicheConnection.TokenMinter tokenMinter, ByteBuffer packetRead, ByteBuffer packetToSend) throws IOException
{
return JnaQuicheConnection.negotiate(tokenMinter, packetRead, packetToSend);
}
@Override
public QuicheConnection tryAccept(QuicheConfig quicheConfig, QuicheConnection.TokenValidator tokenValidator, ByteBuffer packetRead, SocketAddress peer) throws IOException
{
return JnaQuicheConnection.tryAccept(quicheConfig, tokenValidator, packetRead, peer);
}
@Override
public String toString()
{
return getClass().getSimpleName() + "{p=" + priority() + " u=" + isUsable() + "}";
}
}

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.quic.quiche;
package org.eclipse.jetty.quic.quiche.jna;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@ -22,28 +22,16 @@ import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jetty.quic.quiche.ffi.LibQuiche;
import org.eclipse.jetty.quic.quiche.ffi.SizedStructure;
import org.eclipse.jetty.quic.quiche.ffi.bool_pointer;
import org.eclipse.jetty.quic.quiche.ffi.char_pointer;
import org.eclipse.jetty.quic.quiche.ffi.size_t;
import org.eclipse.jetty.quic.quiche.ffi.size_t_pointer;
import org.eclipse.jetty.quic.quiche.ffi.sockaddr;
import org.eclipse.jetty.quic.quiche.ffi.sockaddr_storage;
import org.eclipse.jetty.quic.quiche.ffi.ssize_t;
import org.eclipse.jetty.quic.quiche.ffi.uint32_t;
import org.eclipse.jetty.quic.quiche.ffi.uint32_t_pointer;
import org.eclipse.jetty.quic.quiche.ffi.uint64_t;
import org.eclipse.jetty.quic.quiche.ffi.uint64_t_pointer;
import org.eclipse.jetty.quic.quiche.ffi.uint8_t_pointer;
import org.eclipse.jetty.quic.quiche.QuicheConfig;
import org.eclipse.jetty.quic.quiche.QuicheConnection;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.thread.AutoLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class QuicheConnection
public class JnaQuicheConnection extends QuicheConnection
{
private static final Logger LOG = LoggerFactory.getLogger(QuicheConnection.class);
private static final Logger LOG = LoggerFactory.getLogger(JnaQuicheConnection.class);
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
static
@ -56,18 +44,64 @@ public class QuicheConnection
private LibQuiche.quiche_conn quicheConn;
private LibQuiche.quiche_config quicheConfig;
private QuicheConnection(LibQuiche.quiche_conn quicheConn, LibQuiche.quiche_config quicheConfig)
private JnaQuicheConnection(LibQuiche.quiche_conn quicheConn, LibQuiche.quiche_config quicheConfig)
{
this.quicheConn = quicheConn;
this.quicheConfig = quicheConfig;
}
public static QuicheConnection connect(QuicheConfig quicheConfig, InetSocketAddress peer) throws IOException
public static byte[] fromPacket(ByteBuffer packet)
{
uint8_t_pointer type = new uint8_t_pointer();
uint32_t_pointer version = new uint32_t_pointer();
// Source Connection ID
byte[] scid = new byte[LibQuiche.QUICHE_MAX_CONN_ID_LEN];
size_t_pointer scidLen = new size_t_pointer(scid.length);
// Destination Connection ID
byte[] dcid = new byte[LibQuiche.QUICHE_MAX_CONN_ID_LEN];
size_t_pointer dcidLen = new size_t_pointer(dcid.length);
byte[] token = new byte[QuicheConnection.TokenMinter.MAX_TOKEN_LENGTH];
size_t_pointer tokenLen = new size_t_pointer(token.length);
LOG.debug("getting header info (fromPacket)...");
int rc = LibQuiche.INSTANCE.quiche_header_info(packet, new size_t(packet.remaining()), new size_t(LibQuiche.QUICHE_MAX_CONN_ID_LEN),
version, type,
scid, scidLen,
dcid, dcidLen,
token, tokenLen);
if (rc < 0)
{
if (LOG.isDebugEnabled())
LOG.debug("quiche cannot read header info from packet {}", BufferUtil.toDetailString(packet));
return null;
}
return resizeIfNeeded(dcid, (int)dcidLen.getValue());
}
private static byte[] resizeIfNeeded(byte[] buffer, int length)
{
byte[] sizedBuffer;
if (length == buffer.length)
{
sizedBuffer = buffer;
}
else
{
sizedBuffer = new byte[length];
System.arraycopy(buffer, 0, sizedBuffer, 0, sizedBuffer.length);
}
return sizedBuffer;
}
public static JnaQuicheConnection connect(QuicheConfig quicheConfig, InetSocketAddress peer) throws IOException
{
return connect(quicheConfig, peer, LibQuiche.QUICHE_MAX_CONN_ID_LEN);
}
public static QuicheConnection connect(QuicheConfig quicheConfig, InetSocketAddress peer, int connectionIdLength) throws IOException
public static JnaQuicheConnection connect(QuicheConfig quicheConfig, InetSocketAddress peer, int connectionIdLength) throws IOException
{
if (connectionIdLength > LibQuiche.QUICHE_MAX_CONN_ID_LEN)
throw new IOException("Connection ID length is too large: " + connectionIdLength + " > " + LibQuiche.QUICHE_MAX_CONN_ID_LEN);
@ -77,7 +111,7 @@ public class QuicheConnection
SizedStructure<sockaddr> s = sockaddr.convert(peer);
LibQuiche.quiche_conn quicheConn = LibQuiche.INSTANCE.quiche_connect(peer.getHostName(), scid, new size_t(scid.length), s.getStructure(), s.getSize(), libQuicheConfig);
return new QuicheConnection(quicheConn, libQuicheConfig);
return new JnaQuicheConnection(quicheConn, libQuicheConfig);
}
private static LibQuiche.quiche_config buildConfig(QuicheConfig config) throws IOException
@ -168,9 +202,10 @@ public class QuicheConnection
byte[] dcid = new byte[LibQuiche.QUICHE_MAX_CONN_ID_LEN];
size_t_pointer dcid_len = new size_t_pointer(dcid.length);
byte[] token = new byte[512]; // TODO the token buffer size depends on the token minter.
byte[] token = new byte[TokenMinter.MAX_TOKEN_LENGTH];
size_t_pointer token_len = new size_t_pointer(token.length);
LOG.debug("getting header info (packetType)...");
int rc = LibQuiche.INSTANCE.quiche_header_info(packet, new size_t(packet.remaining()), new size_t(LibQuiche.QUICHE_MAX_CONN_ID_LEN),
version, type,
scid, scid_len,
@ -200,10 +235,10 @@ public class QuicheConnection
byte[] dcid = new byte[LibQuiche.QUICHE_MAX_CONN_ID_LEN];
size_t_pointer dcid_len = new size_t_pointer(dcid.length);
byte[] token = new byte[512]; // TODO the token buffer size depends on the token minter.
byte[] token = new byte[TokenMinter.MAX_TOKEN_LENGTH];
size_t_pointer token_len = new size_t_pointer(token.length);
LOG.debug("getting header info...");
LOG.debug("getting header info (negotiate)...");
int rc = LibQuiche.INSTANCE.quiche_header_info(packetRead, new size_t(packetRead.remaining()), new size_t(LibQuiche.QUICHE_MAX_CONN_ID_LEN),
version, type,
scid, scid_len,
@ -259,7 +294,7 @@ public class QuicheConnection
* Fully consumes the {@code packetRead} buffer if the connection was accepted.
* @return an established connection if accept succeeded, null if accept failed and negotiation should be tried.
*/
public static QuicheConnection tryAccept(QuicheConfig quicheConfig, TokenValidator tokenValidator, ByteBuffer packetRead, SocketAddress peer) throws IOException
public static JnaQuicheConnection tryAccept(QuicheConfig quicheConfig, TokenValidator tokenValidator, ByteBuffer packetRead, SocketAddress peer) throws IOException
{
uint8_t_pointer type = new uint8_t_pointer();
uint32_t_pointer version = new uint32_t_pointer();
@ -272,10 +307,10 @@ public class QuicheConnection
byte[] dcid = new byte[LibQuiche.QUICHE_MAX_CONN_ID_LEN];
size_t_pointer dcid_len = new size_t_pointer(dcid.length);
byte[] token = new byte[512]; // TODO the token buffer size depends on the token minter.
byte[] token = new byte[TokenMinter.MAX_TOKEN_LENGTH];
size_t_pointer token_len = new size_t_pointer(token.length);
LOG.debug("getting header info...");
LOG.debug("getting header info (tryAccept)...");
int rc = LibQuiche.INSTANCE.quiche_header_info(packetRead, new size_t(packetRead.remaining()), new size_t(LibQuiche.QUICHE_MAX_CONN_ID_LEN),
version, type,
scid, scid_len,
@ -322,7 +357,7 @@ public class QuicheConnection
}
LOG.debug("connection created");
QuicheConnection quicheConnection = new QuicheConnection(quicheConn, libQuicheConfig);
JnaQuicheConnection quicheConnection = new JnaQuicheConnection(quicheConn, libQuicheConfig);
LOG.debug("accepted, immediately receiving the same packet - remaining in buffer: {}", packetRead.remaining());
while (packetRead.hasRemaining())
{
@ -337,17 +372,8 @@ public class QuicheConnection
throw new IOException("unable to set qlog path to " + filename);
}
public List<Long> readableStreamIds()
{
return iterableStreamIds(false);
}
public List<Long> writableStreamIds()
{
return iterableStreamIds(true);
}
private List<Long> iterableStreamIds(boolean write)
@Override
protected List<Long> iterableStreamIds(boolean write)
{
try (AutoLock ignore = lock.lock())
{
@ -378,6 +404,7 @@ public class QuicheConnection
* @return how many bytes were consumed.
* @throws IOException
*/
@Override
public int feedCipherBytes(ByteBuffer buffer, SocketAddress peer) throws IOException
{
try (AutoLock ignore = lock.lock())
@ -403,6 +430,7 @@ public class QuicheConnection
* @return how many bytes were added to the buffer.
* @throws IOException
*/
@Override
public int drainCipherBytes(ByteBuffer buffer) throws IOException
{
try (AutoLock ignore = lock.lock())
@ -424,6 +452,7 @@ public class QuicheConnection
}
}
@Override
public boolean isConnectionClosed()
{
try (AutoLock ignore = lock.lock())
@ -434,6 +463,7 @@ public class QuicheConnection
}
}
@Override
public boolean isConnectionEstablished()
{
try (AutoLock ignore = lock.lock())
@ -454,6 +484,7 @@ public class QuicheConnection
}
}
@Override
public long nextTimeout()
{
try (AutoLock ignore = lock.lock())
@ -464,6 +495,7 @@ public class QuicheConnection
}
}
@Override
public void onTimeout()
{
try (AutoLock ignore = lock.lock())
@ -474,6 +506,7 @@ public class QuicheConnection
}
}
@Override
public String getNegotiatedProtocol()
{
try (AutoLock ignore = lock.lock())
@ -487,6 +520,7 @@ public class QuicheConnection
}
}
@Override
public boolean close(long error, String reason)
{
try (AutoLock ignore = lock.lock())
@ -509,6 +543,7 @@ public class QuicheConnection
}
}
@Override
public void dispose()
{
try (AutoLock ignore = lock.lock())
@ -522,6 +557,7 @@ public class QuicheConnection
}
}
@Override
public boolean isDraining()
{
try (AutoLock ignore = lock.lock())
@ -532,6 +568,7 @@ public class QuicheConnection
}
}
@Override
public int maxLocalStreams()
{
try (AutoLock ignore = lock.lock())
@ -544,6 +581,7 @@ public class QuicheConnection
}
}
@Override
public long windowCapacity()
{
try (AutoLock ignore = lock.lock())
@ -556,6 +594,7 @@ public class QuicheConnection
}
}
@Override
public long windowCapacity(long streamId) throws IOException
{
try (AutoLock ignore = lock.lock())
@ -572,6 +611,7 @@ public class QuicheConnection
}
}
@Override
public void shutdownStream(long streamId, boolean writeSide, long error) throws IOException
{
try (AutoLock ignore = lock.lock())
@ -586,25 +626,7 @@ public class QuicheConnection
}
}
public void feedFinForStream(long streamId) throws IOException
{
try (AutoLock ignore = lock.lock())
{
if (quicheConn == null)
throw new IOException("connection was released");
int written = LibQuiche.INSTANCE.quiche_conn_stream_send(quicheConn, new uint64_t(streamId), BufferUtil.EMPTY_BUFFER, new size_t(0), true).intValue();
if (written == LibQuiche.quiche_error.QUICHE_ERR_DONE)
return;
if (written < 0L)
throw new IOException("failed to write FIN to stream " + streamId + "; err=" + LibQuiche.quiche_error.errToString(written));
}
}
public int feedClearBytesForStream(long streamId, ByteBuffer buffer) throws IOException
{
return feedClearBytesForStream(streamId, buffer, false);
}
@Override
public int feedClearBytesForStream(long streamId, ByteBuffer buffer, boolean last) throws IOException
{
try (AutoLock ignore = lock.lock())
@ -621,6 +643,7 @@ public class QuicheConnection
}
}
@Override
public int drainClearBytesForStream(long streamId, ByteBuffer buffer) throws IOException
{
try (AutoLock ignore = lock.lock())
@ -638,6 +661,7 @@ public class QuicheConnection
}
}
@Override
public boolean isStreamFinished(long streamId)
{
try (AutoLock ignore = lock.lock())
@ -648,6 +672,7 @@ public class QuicheConnection
}
}
@Override
public CloseInfo getRemoteCloseInfo()
{
try (AutoLock ignore = lock.lock())
@ -663,44 +688,4 @@ public class QuicheConnection
return null;
}
}
public static class CloseInfo
{
private final long error;
private final String reason;
private CloseInfo(long error, String reason)
{
this.error = error;
this.reason = reason;
}
public long error()
{
return error;
}
public String reason()
{
return reason;
}
}
public interface TokenMinter
{
byte[] mint(byte[] dcid, int len);
}
public interface TokenValidator
{
byte[] validate(byte[] token, int len);
}
public static class TokenValidationException extends IOException
{
public TokenValidationException(String msg)
{
super(msg);
}
}
}

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.quic.quiche.ffi;
package org.eclipse.jetty.quic.quiche.jna;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.quic.quiche.ffi;
package org.eclipse.jetty.quic.quiche.jna;
import com.sun.jna.Structure;

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.quic.quiche.ffi;
package org.eclipse.jetty.quic.quiche.jna;
import com.sun.jna.ptr.ByReference;

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.quic.quiche.ffi;
package org.eclipse.jetty.quic.quiche.jna;
import java.nio.charset.Charset;

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.quic.quiche.ffi.linux;
package org.eclipse.jetty.quic.quiche.jna.linux;
import java.net.Inet4Address;
import java.net.Inet6Address;
@ -21,11 +21,11 @@ import java.net.SocketAddress;
import java.nio.ByteBuffer;
import com.sun.jna.Structure;
import org.eclipse.jetty.quic.quiche.ffi.SizedStructure;
import org.eclipse.jetty.quic.quiche.ffi.size_t;
import org.eclipse.jetty.quic.quiche.ffi.sockaddr;
import org.eclipse.jetty.quic.quiche.ffi.uint16_t;
import org.eclipse.jetty.quic.quiche.ffi.uint32_t;
import org.eclipse.jetty.quic.quiche.jna.SizedStructure;
import org.eclipse.jetty.quic.quiche.jna.size_t;
import org.eclipse.jetty.quic.quiche.jna.sockaddr;
import org.eclipse.jetty.quic.quiche.jna.uint16_t;
import org.eclipse.jetty.quic.quiche.jna.uint32_t;
public interface netinet_linux
{

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.quic.quiche.ffi.macos;
package org.eclipse.jetty.quic.quiche.jna.macos;
import java.net.Inet4Address;
import java.net.Inet6Address;
@ -21,12 +21,12 @@ import java.net.SocketAddress;
import java.nio.ByteBuffer;
import com.sun.jna.Structure;
import org.eclipse.jetty.quic.quiche.ffi.SizedStructure;
import org.eclipse.jetty.quic.quiche.ffi.size_t;
import org.eclipse.jetty.quic.quiche.ffi.sockaddr;
import org.eclipse.jetty.quic.quiche.ffi.uint16_t;
import org.eclipse.jetty.quic.quiche.ffi.uint32_t;
import org.eclipse.jetty.quic.quiche.ffi.uint8_t;
import org.eclipse.jetty.quic.quiche.jna.SizedStructure;
import org.eclipse.jetty.quic.quiche.jna.size_t;
import org.eclipse.jetty.quic.quiche.jna.sockaddr;
import org.eclipse.jetty.quic.quiche.jna.uint16_t;
import org.eclipse.jetty.quic.quiche.jna.uint32_t;
import org.eclipse.jetty.quic.quiche.jna.uint8_t;
public interface netinet_macos
{

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.quic.quiche.ffi;
package org.eclipse.jetty.quic.quiche.jna;
import com.sun.jna.IntegerType;
import com.sun.jna.Native;

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.quic.quiche.ffi;
package org.eclipse.jetty.quic.quiche.jna;
import com.sun.jna.Native;
import com.sun.jna.ptr.ByReference;

View File

@ -11,15 +11,15 @@
// ========================================================================
//
package org.eclipse.jetty.quic.quiche.ffi;
package org.eclipse.jetty.quic.quiche.jna;
import java.net.SocketAddress;
import com.sun.jna.Pointer;
import com.sun.jna.Structure;
import org.eclipse.jetty.quic.quiche.ffi.linux.netinet_linux;
import org.eclipse.jetty.quic.quiche.ffi.macos.netinet_macos;
import org.eclipse.jetty.quic.quiche.ffi.windows.netinet_windows;
import org.eclipse.jetty.quic.quiche.jna.linux.netinet_linux;
import org.eclipse.jetty.quic.quiche.jna.macos.netinet_macos;
import org.eclipse.jetty.quic.quiche.jna.windows.netinet_windows;
import static com.sun.jna.Platform.isLinux;
import static com.sun.jna.Platform.isMac;

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.quic.quiche.ffi;
package org.eclipse.jetty.quic.quiche.jna;
import com.sun.jna.Structure;

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.quic.quiche.ffi;
package org.eclipse.jetty.quic.quiche.jna;
import com.sun.jna.IntegerType;
import com.sun.jna.Native;

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.quic.quiche.ffi;
package org.eclipse.jetty.quic.quiche.jna;
import com.sun.jna.Structure;

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.quic.quiche.ffi;
package org.eclipse.jetty.quic.quiche.jna;
import com.sun.jna.IntegerType;

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.quic.quiche.ffi;
package org.eclipse.jetty.quic.quiche.jna;
import com.sun.jna.IntegerType;

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.quic.quiche.ffi;
package org.eclipse.jetty.quic.quiche.jna;
import com.sun.jna.ptr.ByReference;

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.quic.quiche.ffi;
package org.eclipse.jetty.quic.quiche.jna;
import com.sun.jna.IntegerType;

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.quic.quiche.ffi;
package org.eclipse.jetty.quic.quiche.jna;
import com.sun.jna.ptr.ByReference;

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.quic.quiche.ffi;
package org.eclipse.jetty.quic.quiche.jna;
import com.sun.jna.IntegerType;

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.quic.quiche.ffi;
package org.eclipse.jetty.quic.quiche.jna;
import com.sun.jna.ptr.ByReference;

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.quic.quiche.ffi.windows;
package org.eclipse.jetty.quic.quiche.jna.windows;
import java.net.Inet4Address;
import java.net.Inet6Address;
@ -21,11 +21,11 @@ import java.net.SocketAddress;
import java.nio.ByteBuffer;
import com.sun.jna.Structure;
import org.eclipse.jetty.quic.quiche.ffi.SizedStructure;
import org.eclipse.jetty.quic.quiche.ffi.size_t;
import org.eclipse.jetty.quic.quiche.ffi.sockaddr;
import org.eclipse.jetty.quic.quiche.ffi.uint16_t;
import org.eclipse.jetty.quic.quiche.ffi.uint32_t;
import org.eclipse.jetty.quic.quiche.jna.SizedStructure;
import org.eclipse.jetty.quic.quiche.jna.size_t;
import org.eclipse.jetty.quic.quiche.jna.sockaddr;
import org.eclipse.jetty.quic.quiche.jna.uint16_t;
import org.eclipse.jetty.quic.quiche.jna.uint32_t;
public interface netinet_windows
{

View File

@ -0,0 +1 @@
org.eclipse.jetty.quic.quiche.jna.JnaQuicheBinding

View File

@ -11,7 +11,7 @@
// ========================================================================
//
package org.eclipse.jetty.quic.quiche;
package org.eclipse.jetty.quic.quiche.jna;
import java.io.File;
import java.io.IOException;
@ -25,7 +25,9 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.eclipse.jetty.quic.quiche.ffi.LibQuiche;
import org.eclipse.jetty.quic.quiche.QuicheConfig;
import org.eclipse.jetty.quic.quiche.QuicheConnection;
import org.eclipse.jetty.quic.quiche.SSLKeyPair;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -37,14 +39,14 @@ import static org.hamcrest.core.Is.is;
public class LowLevelQuicheTest
{
private final Collection<QuicheConnection> connectionsToDisposeOf = new ArrayList<>();
private final Collection<JnaQuicheConnection> connectionsToDisposeOf = new ArrayList<>();
private InetSocketAddress clientSocketAddress;
private InetSocketAddress serverSocketAddress;
private QuicheConfig clientQuicheConfig;
private QuicheConfig serverQuicheConfig;
private QuicheConnection.TokenMinter tokenMinter;
private QuicheConnection.TokenValidator tokenValidator;
private JnaQuicheConnection.TokenMinter tokenMinter;
private JnaQuicheConnection.TokenValidator tokenValidator;
@BeforeEach
protected void setUp() throws Exception
@ -88,7 +90,7 @@ public class LowLevelQuicheTest
@AfterEach
protected void tearDown()
{
connectionsToDisposeOf.forEach(QuicheConnection::dispose);
connectionsToDisposeOf.forEach(JnaQuicheConnection::dispose);
connectionsToDisposeOf.clear();
}
@ -96,9 +98,9 @@ public class LowLevelQuicheTest
public void testFinishedAsSoonAsFinIsFed() throws Exception
{
// establish connection
Map.Entry<QuicheConnection, QuicheConnection> entry = connectClientToServer();
QuicheConnection clientQuicheConnection = entry.getKey();
QuicheConnection serverQuicheConnection = entry.getValue();
Map.Entry<JnaQuicheConnection, JnaQuicheConnection> entry = connectClientToServer();
JnaQuicheConnection clientQuicheConnection = entry.getKey();
JnaQuicheConnection serverQuicheConnection = entry.getValue();
// client sends 16 bytes of payload over stream 0
assertThat(clientQuicheConnection.feedClearBytesForStream(0, ByteBuffer.allocate(16)
@ -136,9 +138,9 @@ public class LowLevelQuicheTest
public void testNotFinishedAsLongAsStreamHasReadableBytes() throws Exception
{
// establish connection
Map.Entry<QuicheConnection, QuicheConnection> entry = connectClientToServer();
QuicheConnection clientQuicheConnection = entry.getKey();
QuicheConnection serverQuicheConnection = entry.getValue();
Map.Entry<JnaQuicheConnection, JnaQuicheConnection> entry = connectClientToServer();
JnaQuicheConnection clientQuicheConnection = entry.getKey();
JnaQuicheConnection serverQuicheConnection = entry.getValue();
// client sends 16 bytes of payload over stream 0 and finish it
assertThat(clientQuicheConnection.feedClearBytesForStream(0, ByteBuffer.allocate(16)
@ -172,18 +174,18 @@ public class LowLevelQuicheTest
clientQuicheConfig.setApplicationProtos("");
// establish connection
Map.Entry<QuicheConnection, QuicheConnection> entry = connectClientToServer();
QuicheConnection clientQuicheConnection = entry.getKey();
QuicheConnection serverQuicheConnection = entry.getValue();
Map.Entry<JnaQuicheConnection, JnaQuicheConnection> entry = connectClientToServer();
JnaQuicheConnection clientQuicheConnection = entry.getKey();
JnaQuicheConnection serverQuicheConnection = entry.getValue();
assertThat(clientQuicheConnection.getNegotiatedProtocol(), is(""));
assertThat(serverQuicheConnection.getNegotiatedProtocol(), is(""));
}
private void drainServerToFeedClient(Map.Entry<QuicheConnection, QuicheConnection> entry, int expectedSize) throws IOException
private void drainServerToFeedClient(Map.Entry<JnaQuicheConnection, JnaQuicheConnection> entry, int expectedSize) throws IOException
{
QuicheConnection clientQuicheConnection = entry.getKey();
QuicheConnection serverQuicheConnection = entry.getValue();
JnaQuicheConnection clientQuicheConnection = entry.getKey();
JnaQuicheConnection serverQuicheConnection = entry.getValue();
ByteBuffer buffer = ByteBuffer.allocate(LibQuiche.QUICHE_MIN_CLIENT_INITIAL_LEN);
int drained = serverQuicheConnection.drainCipherBytes(buffer);
@ -193,10 +195,10 @@ public class LowLevelQuicheTest
assertThat(fed, is(expectedSize));
}
private void drainClientToFeedServer(Map.Entry<QuicheConnection, QuicheConnection> entry, int expectedSize) throws IOException
private void drainClientToFeedServer(Map.Entry<JnaQuicheConnection, JnaQuicheConnection> entry, int expectedSize) throws IOException
{
QuicheConnection clientQuicheConnection = entry.getKey();
QuicheConnection serverQuicheConnection = entry.getValue();
JnaQuicheConnection clientQuicheConnection = entry.getKey();
JnaQuicheConnection serverQuicheConnection = entry.getValue();
ByteBuffer buffer = ByteBuffer.allocate(LibQuiche.QUICHE_MIN_CLIENT_INITIAL_LEN);
int drained = clientQuicheConnection.drainCipherBytes(buffer);
@ -206,21 +208,21 @@ public class LowLevelQuicheTest
assertThat(fed, is(expectedSize));
}
private Map.Entry<QuicheConnection, QuicheConnection> connectClientToServer() throws IOException
private Map.Entry<JnaQuicheConnection, JnaQuicheConnection> 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(clientQuicheConfig, serverSocketAddress);
JnaQuicheConnection clientQuicheConnection = JnaQuicheConnection.connect(clientQuicheConfig, serverSocketAddress);
connectionsToDisposeOf.add(clientQuicheConnection);
int drained = clientQuicheConnection.drainCipherBytes(buffer);
assertThat(drained, is(1200));
buffer.flip();
QuicheConnection serverQuicheConnection = QuicheConnection.tryAccept(serverQuicheConfig, tokenValidator, buffer, clientSocketAddress);
JnaQuicheConnection serverQuicheConnection = JnaQuicheConnection.tryAccept(serverQuicheConfig, tokenValidator, buffer, clientSocketAddress);
assertThat(serverQuicheConnection, is(nullValue()));
boolean negotiated = QuicheConnection.negotiate(tokenMinter, buffer, buffer2);
boolean negotiated = JnaQuicheConnection.negotiate(tokenMinter, buffer, buffer2);
assertThat(negotiated, is(true));
buffer2.flip();
@ -232,7 +234,7 @@ public class LowLevelQuicheTest
assertThat(drained, is(1200));
buffer.flip();
serverQuicheConnection = QuicheConnection.tryAccept(serverQuicheConfig, tokenValidator, buffer, clientSocketAddress);
serverQuicheConnection = JnaQuicheConnection.tryAccept(serverQuicheConfig, tokenValidator, buffer, clientSocketAddress);
assertThat(serverQuicheConnection, is(not(nullValue())));
connectionsToDisposeOf.add(serverQuicheConnection);
@ -247,7 +249,7 @@ public class LowLevelQuicheTest
assertThat(serverQuicheConnection.isConnectionEstablished(), is(false));
assertThat(clientQuicheConnection.isConnectionEstablished(), is(false));
AbstractMap.SimpleImmutableEntry<QuicheConnection, QuicheConnection> entry = new AbstractMap.SimpleImmutableEntry<>(clientQuicheConnection, serverQuicheConnection);
AbstractMap.SimpleImmutableEntry<JnaQuicheConnection, JnaQuicheConnection> entry = new AbstractMap.SimpleImmutableEntry<>(clientQuicheConnection, serverQuicheConnection);
int protosLen = 0;
for (String proto : clientQuicheConfig.getApplicationProtos())

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.eclipse.jetty.quic</groupId>
<artifactId>quic-parent</artifactId>
<version>10.0.8-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>quic-quiche-panama-jdk</artifactId>
<name>Jetty :: QUIC :: Quiche :: Panama Binding (jdk package)</name>
<properties>
<bundle-symbolic-name>${project.groupId}.quic-quiche-panama-jdk</bundle-symbolic-name>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>${maven.checkstyle.plugin.version}</version>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
<compilerArgs>
<arg>--add-modules</arg>
<arg>jdk.incubator.foreign</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>
@{argLine} ${jetty.surefire.argLine} --enable-native-access org.eclipse.jetty.quic.quiche.panama.jdk
</argLine>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.quic</groupId>
<artifactId>quic-quiche-common</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-slf4j-impl</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,23 @@
//
// ========================================================================
// 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
// ========================================================================
//
module org.eclipse.jetty.quic.quiche.panama.jdk
{
requires jdk.incubator.foreign;
requires org.eclipse.jetty.quic.quiche;
requires org.eclipse.jetty.util;
requires org.slf4j;
provides org.eclipse.jetty.quic.quiche.QuicheBinding with
org.eclipse.jetty.quic.quiche.panama.jdk.PanamaJdkQuicheBinding;
}

View File

@ -0,0 +1,117 @@
//
// ========================================================================
// 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.panama.jdk;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.Locale;
import jdk.incubator.foreign.CLinker;
import jdk.incubator.foreign.FunctionDescriptor;
import jdk.incubator.foreign.MemoryAddress;
import jdk.incubator.foreign.ResourceScope;
import jdk.incubator.foreign.SymbolLookup;
import org.eclipse.jetty.util.IO;
class NativeHelper
{
private static final CLinker LINKER = CLinker.getInstance();
private static final ClassLoader CLASSLOADER = NativeHelper.class.getClassLoader();
private static final SymbolLookup LIBRARIES = lookup();
private static final MethodHandles.Lookup MH_LOOKUP = MethodHandles.lookup();
private static void loadNativeLibraryFromClasspath() {
try
{
String libName = getNativePrefix() + "/" + System.mapLibraryName("quiche");
File lib = extractFromResourcePath(libName, NativeHelper.class.getClassLoader());
System.load(lib.getAbsolutePath());
//TODO delete lib?
}
catch (IOException e)
{
throw (UnsatisfiedLinkError) new UnsatisfiedLinkError("Cannot find quiche native library").initCause(e);
}
}
private static String getNativePrefix()
{
// TODO: add macos and windows
String osName = System.getProperty("os.name").toLowerCase(Locale.ROOT);
String osArch = System.getProperty("os.arch");
switch (osArch)
{
case "amd64":
osArch = "x86-64";
break;
// TODO: add arm64
}
return osName + "-" + osArch;
}
private static File extractFromResourcePath(String libName, ClassLoader classLoader) throws IOException
{
File target = new File(System.getProperty("java.io.tmpdir"), libName);
target.getParentFile().mkdir();
try (InputStream is = classLoader.getResourceAsStream(libName); OutputStream os = new FileOutputStream(target))
{
IO.copy(is, os);
}
return target;
}
private static SymbolLookup lookup()
{
loadNativeLibraryFromClasspath();
SymbolLookup loaderLookup = SymbolLookup.loaderLookup();
SymbolLookup systemLookup = CLinker.systemLookup();
return name -> loaderLookup.lookup(name).or(() -> systemLookup.lookup(name));
}
static MethodHandle downcallHandle(String name, String desc, FunctionDescriptor fdesc)
{
return LIBRARIES.lookup(name)
.map(addr ->
{
MethodType mt = MethodType.fromMethodDescriptorString(desc, CLASSLOADER);
return LINKER.downcallHandle(addr, mt, fdesc);
})
.orElseThrow(() ->
{
throw new UnsatisfiedLinkError("unresolved symbol: " + name);
});
}
static <T> MemoryAddress upcallHandle(Class<T> clazz, T t, String name, String desc, FunctionDescriptor fdesc, ResourceScope scope)
{
try
{
MethodHandle handle = MH_LOOKUP.findVirtual(clazz, name, MethodType.fromMethodDescriptorString(desc, CLASSLOADER));
handle = handle.bindTo(t);
return LINKER.upcallStub(handle, fdesc, scope);
}
catch (NoSuchMethodException | IllegalAccessException e)
{
throw (UnsatisfiedLinkError) new UnsatisfiedLinkError("unresolved symbol: " + name).initCause(e);
}
}
}

View File

@ -0,0 +1,82 @@
//
// ========================================================================
// 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.panama.jdk;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import org.eclipse.jetty.quic.quiche.QuicheBinding;
import org.eclipse.jetty.quic.quiche.QuicheConfig;
import org.eclipse.jetty.quic.quiche.QuicheConnection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PanamaJdkQuicheBinding implements QuicheBinding
{
private static final Logger LOG = LoggerFactory.getLogger(PanamaJdkQuicheBinding.class);
@Override
public boolean isUsable()
{
try
{
// Make a Quiche call to confirm.
quiche_h.quiche_version();
return true;
}
catch (Throwable x)
{
LOG.debug("Panama(jdk) quiche binding is not usable", x);
return false;
}
}
@Override
public int priority()
{
return 100;
}
@Override
public byte[] fromPacket(ByteBuffer packet)
{
return PanamaJdkQuicheConnection.fromPacket(packet);
}
@Override
public QuicheConnection connect(QuicheConfig quicheConfig, InetSocketAddress peer, int connectionIdLength) throws IOException
{
return PanamaJdkQuicheConnection.connect(quicheConfig, peer, connectionIdLength);
}
@Override
public boolean negotiate(QuicheConnection.TokenMinter tokenMinter, ByteBuffer packetRead, ByteBuffer packetToSend) throws IOException
{
return PanamaJdkQuicheConnection.negotiate(tokenMinter, packetRead, packetToSend);
}
@Override
public QuicheConnection tryAccept(QuicheConfig quicheConfig, QuicheConnection.TokenValidator tokenValidator, ByteBuffer packetRead, SocketAddress peer) throws IOException
{
return PanamaJdkQuicheConnection.tryAccept(quicheConfig, tokenValidator, packetRead, peer);
}
@Override
public String toString()
{
return getClass().getSimpleName() + "{p=" + priority() + " u=" + isUsable() + "}";
}
}

View File

@ -0,0 +1,890 @@
//
// ========================================================================
// 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.panama.jdk;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import jdk.incubator.foreign.CLinker;
import jdk.incubator.foreign.MemoryAddress;
import jdk.incubator.foreign.MemorySegment;
import jdk.incubator.foreign.ResourceScope;
import org.eclipse.jetty.quic.quiche.QuicheConfig;
import org.eclipse.jetty.quic.quiche.QuicheConnection;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.thread.AutoLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.eclipse.jetty.quic.quiche.panama.jdk.quiche_h.C_FALSE;
import static org.eclipse.jetty.quic.quiche.panama.jdk.quiche_h.C_TRUE;
public class PanamaJdkQuicheConnection extends QuicheConnection
{
private static final Logger LOG = LoggerFactory.getLogger(PanamaJdkQuicheConnection.class);
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
// Quiche does not allow concurrent calls with the same connection.
private final AutoLock lock = new AutoLock();
private MemoryAddress quicheConn;
private MemoryAddress quicheConfig;
private ResourceScope scope;
private MemorySegment sendInfo;
private MemorySegment recvInfo;
private MemorySegment stats;
private PanamaJdkQuicheConnection(MemoryAddress quicheConn, MemoryAddress quicheConfig, ResourceScope scope)
{
this.quicheConn = quicheConn;
this.quicheConfig = quicheConfig;
this.scope = scope;
this.sendInfo = quiche_send_info.allocate(scope);
this.recvInfo = quiche_recv_info.allocate(scope);
this.stats = quiche_stats.allocate(scope);
}
public static byte[] fromPacket(ByteBuffer packet)
{
try (ResourceScope scope = ResourceScope.newConfinedScope())
{
MemorySegment type = MemorySegment.allocateNative(CLinker.C_CHAR, scope);
MemorySegment version = MemorySegment.allocateNative(CLinker.C_INT, scope);
// Source Connection ID
MemorySegment scid = MemorySegment.allocateNative(quiche_h.QUICHE_MAX_CONN_ID_LEN, scope);
MemorySegment scid_len = MemorySegment.allocateNative(CLinker.C_LONG, scope);
scid_len.asByteBuffer().order(ByteOrder.nativeOrder()).putLong(scid.byteSize());
// Destination Connection ID
MemorySegment dcid = MemorySegment.allocateNative(quiche_h.QUICHE_MAX_CONN_ID_LEN, scope);
MemorySegment dcid_len = MemorySegment.allocateNative(CLinker.C_LONG, scope);
dcid_len.asByteBuffer().order(ByteOrder.nativeOrder()).putLong(dcid.byteSize());
MemorySegment token = MemorySegment.allocateNative(QuicheConnection.TokenMinter.MAX_TOKEN_LENGTH, scope);
MemorySegment token_len = MemorySegment.allocateNative(CLinker.C_LONG, scope);
token_len.asByteBuffer().order(ByteOrder.nativeOrder()).putLong(token.byteSize());
LOG.debug("getting header info (fromPacket)...");
int rc;
if (packet.isDirect())
{
// If the ByteBuffer is direct, it can be used without any copy.
MemorySegment packetReadSegment = MemorySegment.ofByteBuffer(packet);
rc = quiche_h.quiche_header_info(packetReadSegment.address(), packet.remaining(), quiche_h.QUICHE_MAX_CONN_ID_LEN,
version.address(), type.address(),
scid.address(), scid_len.address(),
dcid.address(), dcid_len.address(),
token.address(), token_len.address());
}
else
{
// If the ByteBuffer is heap-allocated, it must be copied to native memory.
MemorySegment packetReadSegment = MemorySegment.allocateNative(packet.remaining(), scope);
int prevPosition = packet.position();
packetReadSegment.asByteBuffer().order(ByteOrder.nativeOrder()).put(packet);
packet.position(prevPosition);
rc = quiche_h.quiche_header_info(packetReadSegment.address(), packet.remaining(), quiche_h.QUICHE_MAX_CONN_ID_LEN,
version.address(), type.address(),
scid.address(), scid_len.address(),
dcid.address(), dcid_len.address(),
token.address(), token_len.address());
}
if (rc < 0)
{
if (LOG.isDebugEnabled())
LOG.debug("quiche cannot read header info from packet {}", BufferUtil.toDetailString(packet));
return null;
}
byte[] bytes = new byte[(int)dcid_len.asByteBuffer().order(ByteOrder.nativeOrder()).getLong()];
dcid.asByteBuffer().order(ByteOrder.nativeOrder()).get(bytes);
return bytes;
}
}
public static PanamaJdkQuicheConnection connect(QuicheConfig quicheConfig, InetSocketAddress peer) throws IOException
{
return connect(quicheConfig, peer, quiche_h.QUICHE_MAX_CONN_ID_LEN);
}
public static PanamaJdkQuicheConnection connect(QuicheConfig quicheConfig, InetSocketAddress peer, int connectionIdLength) throws IOException
{
if (connectionIdLength > quiche_h.QUICHE_MAX_CONN_ID_LEN)
throw new IOException("Connection ID length is too large: " + connectionIdLength + " > " + quiche_h.QUICHE_MAX_CONN_ID_LEN);
ResourceScope scope = ResourceScope.newSharedScope();
byte[] scidBytes = new byte[connectionIdLength];
SECURE_RANDOM.nextBytes(scidBytes);
MemorySegment scid = MemorySegment.allocateNative(scidBytes.length, scope);
scid.asByteBuffer().order(ByteOrder.nativeOrder()).put(scidBytes);
MemoryAddress libQuicheConfig = buildConfig(quicheConfig, scope);
MemorySegment s = sockaddr.convert(peer, scope);
MemoryAddress quicheConn = quiche_h.quiche_connect(CLinker.toCString(peer.getHostName(), scope), scid, scid.byteSize(), s, s.byteSize(), libQuicheConfig);
return new PanamaJdkQuicheConnection(quicheConn, libQuicheConfig, scope);
}
private static MemoryAddress buildConfig(QuicheConfig config, ResourceScope scope) throws IOException
{
MemoryAddress quicheConfig = quiche_h.quiche_config_new(config.getVersion());
if (quicheConfig == null)
throw new IOException("Failed to create quiche config");
Boolean verifyPeer = config.getVerifyPeer();
if (verifyPeer != null)
quiche_h.quiche_config_verify_peer(quicheConfig, verifyPeer ? C_TRUE : C_FALSE);
String certChainPemPath = config.getCertChainPemPath();
if (certChainPemPath != null)
quiche_h.quiche_config_load_cert_chain_from_pem_file(quicheConfig, CLinker.toCString(certChainPemPath, scope).address());
String privKeyPemPath = config.getPrivKeyPemPath();
if (privKeyPemPath != null)
quiche_h.quiche_config_load_priv_key_from_pem_file(quicheConfig, CLinker.toCString(privKeyPemPath, scope).address());
String[] applicationProtos = config.getApplicationProtos();
if (applicationProtos != null)
{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
for (String proto : applicationProtos)
{
byte[] bytes = proto.getBytes(StandardCharsets.UTF_8);
baos.write(bytes.length);
baos.write(bytes);
}
byte[] bytes = baos.toByteArray();
MemorySegment segment = MemorySegment.allocateNative(bytes.length, scope);
segment.asByteBuffer().order(ByteOrder.nativeOrder()).put(bytes);
quiche_h.quiche_config_set_application_protos(quicheConfig, segment.address(), segment.byteSize());
}
QuicheConfig.CongestionControl cc = config.getCongestionControl();
if (cc != null)
quiche_h.quiche_config_set_cc_algorithm(quicheConfig, cc.getValue());
Long maxIdleTimeout = config.getMaxIdleTimeout();
if (maxIdleTimeout != null)
quiche_h.quiche_config_set_max_idle_timeout(quicheConfig, maxIdleTimeout);
Long initialMaxData = config.getInitialMaxData();
if (initialMaxData != null)
quiche_h.quiche_config_set_initial_max_data(quicheConfig, initialMaxData);
Long initialMaxStreamDataBidiLocal = config.getInitialMaxStreamDataBidiLocal();
if (initialMaxStreamDataBidiLocal != null)
quiche_h.quiche_config_set_initial_max_stream_data_bidi_local(quicheConfig, initialMaxStreamDataBidiLocal);
Long initialMaxStreamDataBidiRemote = config.getInitialMaxStreamDataBidiRemote();
if (initialMaxStreamDataBidiRemote != null)
quiche_h.quiche_config_set_initial_max_stream_data_bidi_remote(quicheConfig, initialMaxStreamDataBidiRemote);
Long initialMaxStreamDataUni = config.getInitialMaxStreamDataUni();
if (initialMaxStreamDataUni != null)
quiche_h.quiche_config_set_initial_max_stream_data_uni(quicheConfig, initialMaxStreamDataUni);
Long initialMaxStreamsBidi = config.getInitialMaxStreamsBidi();
if (initialMaxStreamsBidi != null)
quiche_h.quiche_config_set_initial_max_streams_bidi(quicheConfig, initialMaxStreamsBidi);
Long initialMaxStreamsUni = config.getInitialMaxStreamsUni();
if (initialMaxStreamsUni != null)
quiche_h.quiche_config_set_initial_max_streams_uni(quicheConfig, initialMaxStreamsUni);
Boolean disableActiveMigration = config.getDisableActiveMigration();
if (disableActiveMigration != null)
quiche_h.quiche_config_set_disable_active_migration(quicheConfig, disableActiveMigration ? C_TRUE : C_FALSE);
return quicheConfig;
}
public static boolean negotiate(TokenMinter tokenMinter, ByteBuffer packetRead, ByteBuffer packetToSend) throws IOException
{
try (ResourceScope scope = ResourceScope.newConfinedScope())
{
MemorySegment packetReadSegment;
if (packetRead.isDirect())
{
// If the ByteBuffer is direct, it can be used without any copy.
packetReadSegment = MemorySegment.ofByteBuffer(packetRead);
}
else
{
// If the ByteBuffer is heap-allocated, it must be copied to native memory.
packetReadSegment = MemorySegment.allocateNative(packetRead.remaining(), scope);
int prevPosition = packetRead.position();
packetReadSegment.asByteBuffer().order(ByteOrder.nativeOrder()).put(packetRead);
packetRead.position(prevPosition);
}
MemorySegment type = MemorySegment.allocateNative(CLinker.C_CHAR, scope);
MemorySegment version = MemorySegment.allocateNative(CLinker.C_INT, scope);
// Source Connection ID
MemorySegment scid = MemorySegment.allocateNative(quiche_h.QUICHE_MAX_CONN_ID_LEN, scope);
MemorySegment scid_len = MemorySegment.allocateNative(CLinker.C_LONG, scope);
scid_len.asByteBuffer().order(ByteOrder.nativeOrder()).putLong(scid.byteSize());
// Destination Connection ID
MemorySegment dcid = MemorySegment.allocateNative(quiche_h.QUICHE_MAX_CONN_ID_LEN, scope);
MemorySegment dcid_len = MemorySegment.allocateNative(CLinker.C_LONG, scope);
dcid_len.asByteBuffer().order(ByteOrder.nativeOrder()).putLong(dcid.byteSize());
MemorySegment token = MemorySegment.allocateNative(TokenMinter.MAX_TOKEN_LENGTH, scope);
MemorySegment token_len = MemorySegment.allocateNative(CLinker.C_LONG, scope);
token_len.asByteBuffer().order(ByteOrder.nativeOrder()).putLong(token.byteSize());
LOG.debug("getting header info (negotiate)...");
int rc = quiche_h.quiche_header_info(packetReadSegment.address(), packetRead.remaining(), quiche_h.QUICHE_MAX_CONN_ID_LEN,
version.address(), type.address(),
scid.address(), scid_len.address(),
dcid.address(), dcid_len.address(),
token.address(), token_len.address());
if (rc < 0)
throw new IOException("failed to parse header: " + quiche_h.quiche_error.errToString(rc));
packetRead.position(packetRead.limit());
LOG.debug("version: {}", version.asByteBuffer().order(ByteOrder.nativeOrder()).getInt());
LOG.debug("type: {}", type.asByteBuffer().order(ByteOrder.nativeOrder()).get());
LOG.debug("scid len: {}", scid_len.asByteBuffer().order(ByteOrder.nativeOrder()).getLong());
LOG.debug("dcid len: {}", dcid_len.asByteBuffer().order(ByteOrder.nativeOrder()).getLong());
LOG.debug("token len: {}", token_len.asByteBuffer().order(ByteOrder.nativeOrder()).getLong());
if (quiche_h.quiche_version_is_supported(version.asByteBuffer().order(ByteOrder.nativeOrder()).getInt()) == C_FALSE)
{
LOG.debug("version negotiation");
MemorySegment packetToSendSegment;
if (packetToSend.isDirect())
{
// If the ByteBuffer is direct, it can be used without any copy.
packetToSendSegment = MemorySegment.ofByteBuffer(packetToSend);
}
else
{
// If the ByteBuffer is heap-allocated, native memory must be copied to it.
packetToSendSegment = MemorySegment.allocateNative(packetToSend.remaining(), scope);
}
long generated = quiche_h.quiche_negotiate_version(scid.address(), scid_len.asByteBuffer().order(ByteOrder.nativeOrder()).getLong(), dcid.address(), dcid_len.asByteBuffer().order(ByteOrder.nativeOrder()).getLong(), packetToSendSegment.address(), packetToSend.remaining());
if (generated < 0)
throw new IOException("failed to create vneg packet : " + quiche_h.quiche_error.errToString(generated));
if (!packetToSend.isDirect())
packetToSend.put(packetToSendSegment.asByteBuffer().order(ByteOrder.nativeOrder()).limit((int)generated));
else
packetToSend.position((int)(packetToSend.position() + generated));
return true;
}
if (token_len.asByteBuffer().order(ByteOrder.nativeOrder()).getLong() == 0L)
{
LOG.debug("stateless retry");
byte[] dcidBytes = new byte[(int)dcid_len.asByteBuffer().order(ByteOrder.nativeOrder()).getLong()];
dcid.asByteBuffer().order(ByteOrder.nativeOrder()).get(dcidBytes);
byte[] tokenBytes = tokenMinter.mint(dcidBytes, dcidBytes.length);
token.asByteBuffer().order(ByteOrder.nativeOrder()).put(tokenBytes);
byte[] newCid = new byte[quiche_h.QUICHE_MAX_CONN_ID_LEN];
SECURE_RANDOM.nextBytes(newCid);
MemorySegment newCidSegment = MemorySegment.allocateNative(newCid.length, scope);
newCidSegment.asByteBuffer().order(ByteOrder.nativeOrder()).put(newCid);
MemorySegment packetToSendSegment;
if (packetToSend.isDirect())
{
// If the ByteBuffer is direct, it can be used without any copy.
packetToSendSegment = MemorySegment.ofByteBuffer(packetToSend);
}
else
{
// If the ByteBuffer is heap-allocated, native memory must be copied to it.
packetToSendSegment = MemorySegment.allocateNative(packetToSend.remaining(), scope);
}
long generated = quiche_h.quiche_retry(scid.address(), scid_len.asByteBuffer().order(ByteOrder.nativeOrder()).getLong(),
dcid.address(), dcid_len.asByteBuffer().order(ByteOrder.nativeOrder()).getLong(),
newCidSegment.address(), newCid.length,
token.address(), tokenBytes.length,
version.asByteBuffer().order(ByteOrder.nativeOrder()).getInt(),
packetToSendSegment.address(), packetToSendSegment.byteSize()
);
if (generated < 0)
throw new IOException("failed to create retry packet: " + quiche_h.quiche_error.errToString(generated));
if (!packetToSend.isDirect())
packetToSend.put(packetToSendSegment.asByteBuffer().order(ByteOrder.nativeOrder()).limit((int)generated));
else
packetToSend.position((int)(packetToSend.position() + generated));
return true;
}
return false;
}
}
public static PanamaJdkQuicheConnection tryAccept(QuicheConfig quicheConfig, TokenValidator tokenValidator, ByteBuffer packetRead, SocketAddress peer) throws IOException
{
ResourceScope scope = ResourceScope.newSharedScope();
MemorySegment type = MemorySegment.allocateNative(CLinker.C_CHAR, scope);
MemorySegment version = MemorySegment.allocateNative(CLinker.C_INT, scope);
// Source Connection ID
MemorySegment scid = MemorySegment.allocateNative(quiche_h.QUICHE_MAX_CONN_ID_LEN, scope);
MemorySegment scid_len = MemorySegment.allocateNative(CLinker.C_LONG, scope);
scid_len.asByteBuffer().order(ByteOrder.nativeOrder()).putLong(scid.byteSize());
// Destination Connection ID
MemorySegment dcid = MemorySegment.allocateNative(quiche_h.QUICHE_MAX_CONN_ID_LEN, scope);
MemorySegment dcid_len = MemorySegment.allocateNative(CLinker.C_LONG, scope);
dcid_len.asByteBuffer().order(ByteOrder.nativeOrder()).putLong(dcid.byteSize());
MemorySegment token = MemorySegment.allocateNative(TokenMinter.MAX_TOKEN_LENGTH, scope);
MemorySegment token_len = MemorySegment.allocateNative(CLinker.C_LONG, scope);
token_len.asByteBuffer().order(ByteOrder.nativeOrder()).putLong(token.byteSize());
LOG.debug("getting header info (tryAccept)...");
int rc;
if (packetRead.isDirect())
{
// If the ByteBuffer is direct, it can be used without any copy.
MemorySegment packetReadSegment = MemorySegment.ofByteBuffer(packetRead);
rc = quiche_h.quiche_header_info(packetReadSegment.address(), packetRead.remaining(), quiche_h.QUICHE_MAX_CONN_ID_LEN,
version.address(), type.address(),
scid.address(), scid_len.address(),
dcid.address(), dcid_len.address(),
token.address(), token_len.address());
}
else
{
// If the ByteBuffer is heap-allocated, it must be copied to native memory.
try (ResourceScope segmentScope = ResourceScope.newConfinedScope())
{
MemorySegment packetReadSegment = MemorySegment.allocateNative(packetRead.remaining(), segmentScope);
int prevPosition = packetRead.position();
packetReadSegment.asByteBuffer().order(ByteOrder.nativeOrder()).put(packetRead);
packetRead.position(prevPosition);
rc = quiche_h.quiche_header_info(packetReadSegment.address(), packetRead.remaining(), quiche_h.QUICHE_MAX_CONN_ID_LEN,
version.address(), type.address(),
scid.address(), scid_len.address(),
dcid.address(), dcid_len.address(),
token.address(), token_len.address());
}
}
if (rc < 0)
{
scope.close();
throw new IOException("failed to parse header: " + quiche_h.quiche_error.errToString(rc));
}
LOG.debug("version: {}", version.asByteBuffer().order(ByteOrder.nativeOrder()).getInt());
LOG.debug("type: {}", type.asByteBuffer().order(ByteOrder.nativeOrder()).get());
LOG.debug("scid len: {}", scid_len.asByteBuffer().order(ByteOrder.nativeOrder()).getLong());
LOG.debug("dcid len: {}", dcid_len.asByteBuffer().order(ByteOrder.nativeOrder()).getLong());
LOG.debug("token len: {}", token_len.asByteBuffer().order(ByteOrder.nativeOrder()).getLong());
if (quiche_h.quiche_version_is_supported(version.asByteBuffer().order(ByteOrder.nativeOrder()).getInt()) == C_FALSE)
{
LOG.debug("need version negotiation");
scope.close();
return null;
}
int tokenLen = (int)token_len.asByteBuffer().order(ByteOrder.nativeOrder()).getLong();
if (tokenLen == 0)
{
LOG.debug("need stateless retry");
scope.close();
return null;
}
LOG.debug("token validation...");
// Original Destination Connection ID
byte[] tokenBytes = new byte[(int)token.byteSize()];
token.asByteBuffer().order(ByteOrder.nativeOrder()).get(tokenBytes, 0, tokenLen);
byte[] odcidBytes = tokenValidator.validate(tokenBytes, tokenLen);
if (odcidBytes == null)
{
scope.close();
throw new TokenValidationException("invalid address validation token");
}
LOG.debug("validated token");
MemorySegment odcid = MemorySegment.allocateNative(odcidBytes.length, scope);
odcid.asByteBuffer().order(ByteOrder.nativeOrder()).put(odcidBytes);
LOG.debug("connection creation...");
MemoryAddress libQuicheConfig = buildConfig(quicheConfig, scope);
MemorySegment s = sockaddr.convert(peer, scope);
MemoryAddress quicheConn = quiche_h.quiche_accept(dcid.address(), dcid_len.asByteBuffer().order(ByteOrder.nativeOrder()).getLong(), odcid.address(), odcid.byteSize(), s.address(), s.byteSize(), libQuicheConfig);
if (quicheConn == null)
{
quiche_h.quiche_config_free(libQuicheConfig);
scope.close();
throw new IOException("failed to create connection");
}
LOG.debug("connection created");
PanamaJdkQuicheConnection quicheConnection = new PanamaJdkQuicheConnection(quicheConn, libQuicheConfig, scope);
LOG.debug("accepted, immediately receiving the same packet - remaining in buffer: {}", packetRead.remaining());
while (packetRead.hasRemaining())
{
quicheConnection.feedCipherBytes(packetRead, peer);
}
return quicheConnection;
}
@Override
protected List<Long> iterableStreamIds(boolean write)
{
try (AutoLock ignore = lock.lock())
{
if (quicheConn == null)
throw new IllegalStateException("connection was released");
MemoryAddress quiche_stream_iter;
if (write)
quiche_stream_iter = quiche_h.quiche_conn_writable(quicheConn);
else
quiche_stream_iter = quiche_h.quiche_conn_readable(quicheConn);
List<Long> result = new ArrayList<>();
try (ResourceScope scope = ResourceScope.newConfinedScope())
{
MemorySegment streamIdSegment = MemorySegment.allocateNative(CLinker.C_LONG, scope);
while (quiche_h.quiche_stream_iter_next(quiche_stream_iter, streamIdSegment.address()) != C_FALSE)
{
long streamId = streamIdSegment.asByteBuffer().order(ByteOrder.nativeOrder()).getLong();
result.add(streamId);
}
}
quiche_h.quiche_stream_iter_free(quiche_stream_iter);
return result;
}
}
@Override
public int feedCipherBytes(ByteBuffer buffer, SocketAddress peer) throws IOException
{
try (AutoLock ignore = lock.lock())
{
if (quicheConn == null)
throw new IOException("Cannot receive when not connected");
long received;
try (ResourceScope scope = ResourceScope.newConfinedScope())
{
quiche_recv_info.setSocketAddress(recvInfo, peer, scope);
if (buffer.isDirect())
{
// If the ByteBuffer is direct, it can be used without any copy.
MemorySegment bufferSegment = MemorySegment.ofByteBuffer(buffer);
received = quiche_h.quiche_conn_recv(quicheConn, bufferSegment.address(), buffer.remaining(), recvInfo.address());
}
else
{
// If the ByteBuffer is heap-allocated, it must be copied to native memory.
MemorySegment bufferSegment = MemorySegment.allocateNative(buffer.remaining(), scope);
int prevPosition = buffer.position();
bufferSegment.asByteBuffer().order(ByteOrder.nativeOrder()).put(buffer);
buffer.position(prevPosition);
received = quiche_h.quiche_conn_recv(quicheConn, bufferSegment.address(), buffer.remaining(), recvInfo.address());
}
}
if (received < 0)
throw new IOException("failed to receive packet; err=" + quiche_h.quiche_error.errToString(received));
buffer.position((int)(buffer.position() + received));
return (int)received;
}
}
@Override
public int drainCipherBytes(ByteBuffer buffer) throws IOException
{
try (AutoLock ignore = lock.lock())
{
if (quicheConn == null)
throw new IOException("Cannot send when not connected");
int prevPosition = buffer.position();
long written;
if (buffer.isDirect())
{
// If the ByteBuffer is direct, it can be used without any copy.
MemorySegment bufferSegment = MemorySegment.ofByteBuffer(buffer);
written = quiche_h.quiche_conn_send(quicheConn, bufferSegment.address(), buffer.remaining(), sendInfo.address());
}
else
{
// If the ByteBuffer is heap-allocated, native memory must be copied to it.
try (ResourceScope scope = ResourceScope.newConfinedScope())
{
MemorySegment bufferSegment = MemorySegment.allocateNative(buffer.remaining(), scope);
written = quiche_h.quiche_conn_send(quicheConn, bufferSegment.address(), buffer.remaining(), sendInfo.address());
buffer.put(bufferSegment.asByteBuffer().order(ByteOrder.nativeOrder()).slice().limit((int)written));
buffer.position(prevPosition);
}
}
if (written == quiche_h.quiche_error.QUICHE_ERR_DONE)
return 0;
if (written < 0L)
throw new IOException("failed to send packet; err=" + quiche_h.quiche_error.errToString(written));
buffer.position((int)(prevPosition + written));
return (int)written;
}
}
@Override
public boolean isConnectionClosed()
{
try (AutoLock ignore = lock.lock())
{
if (quicheConn == null)
throw new IllegalStateException("connection was released");
return quiche_h.quiche_conn_is_closed(quicheConn) != C_FALSE;
}
}
@Override
public boolean isConnectionEstablished()
{
try (AutoLock ignore = lock.lock())
{
if (quicheConn == null)
throw new IllegalStateException("connection was released");
return quiche_h.quiche_conn_is_established(quicheConn) != C_FALSE;
}
}
@Override
public long nextTimeout()
{
try (AutoLock ignore = lock.lock())
{
if (quicheConn == null)
throw new IllegalStateException("connection was released");
return quiche_h.quiche_conn_timeout_as_millis(quicheConn);
}
}
@Override
public void onTimeout()
{
try (AutoLock ignore = lock.lock())
{
if (quicheConn == null)
throw new IllegalStateException("connection was released");
quiche_h.quiche_conn_on_timeout(quicheConn);
}
}
@Override
public String getNegotiatedProtocol()
{
try (AutoLock ignore = lock.lock(); ResourceScope scope = ResourceScope.newConfinedScope())
{
if (quicheConn == null)
throw new IllegalStateException("connection was released");
MemorySegment outSegment = MemorySegment.allocateNative(CLinker.C_POINTER, scope);
MemorySegment outLenSegment = MemorySegment.allocateNative(CLinker.C_LONG, scope);
quiche_h.quiche_conn_application_proto(quicheConn, outSegment.address(), outLenSegment.address());
long outLen = outLenSegment.asByteBuffer().order(ByteOrder.nativeOrder()).getLong();
if (outLen == 0L)
return null;
byte[] out = new byte[(int)outLen];
// dereference outSegment pointer
MemoryAddress memoryAddress = MemoryAddress.ofLong(outSegment.asByteBuffer().order(ByteOrder.nativeOrder()).getLong());
memoryAddress.asSegment(outLen, ResourceScope.globalScope()).asByteBuffer().get(out);
return new String(out, StandardCharsets.UTF_8);
}
}
@Override
public boolean close(long error, String reason)
{
try (AutoLock ignore = lock.lock())
{
if (quicheConn == null)
{
if (LOG.isDebugEnabled())
LOG.debug("connection was released");
return false;
}
int rc;
if (reason == null)
{
rc = quiche_h.quiche_conn_close(quicheConn, C_TRUE, error, MemoryAddress.NULL, 0);
}
else
{
try (ResourceScope scope = ResourceScope.newConfinedScope())
{
byte[] reasonBytes = reason.getBytes(StandardCharsets.UTF_8);
MemorySegment reasonSegment = MemorySegment.allocateNative(reasonBytes.length, scope);
reasonSegment.asByteBuffer().order(ByteOrder.nativeOrder()).put(reasonBytes);
int length = reasonBytes.length;
MemoryAddress reasonAddress = reasonSegment.address();
rc = quiche_h.quiche_conn_close(quicheConn, C_TRUE, error, reasonAddress, length);
}
}
if (rc == 0)
return true;
if (rc == quiche_h.quiche_error.QUICHE_ERR_DONE)
return false;
if (LOG.isDebugEnabled())
LOG.debug("could not close connection: {}", quiche_h.quiche_error.errToString(rc));
return false;
}
}
@Override
public void dispose()
{
try (AutoLock ignore = lock.lock())
{
if (quicheConn != null)
quiche_h.quiche_conn_free(quicheConn);
quicheConn = null;
if (quicheConfig != null)
quiche_h.quiche_config_free(quicheConfig);
quicheConfig = null;
if (scope != null)
scope.close();
scope = null;
sendInfo = null;
recvInfo = null;
stats = null;
}
}
@Override
public boolean isDraining()
{
try (AutoLock ignore = lock.lock())
{
if (quicheConn == null)
throw new IllegalStateException("connection was released");
return quiche_h.quiche_conn_is_draining(quicheConn) != 0;
}
}
@Override
public int maxLocalStreams()
{
try (AutoLock ignore = lock.lock())
{
if (quicheConn == null)
throw new IllegalStateException("connection was released");
quiche_h.quiche_conn_stats(quicheConn, stats.address());
return (int)quiche_stats.get_peer_initial_max_streams_bidi(stats);
}
}
@Override
public long windowCapacity()
{
try (AutoLock ignore = lock.lock())
{
if (quicheConn == null)
throw new IllegalStateException("connection was released");
quiche_h.quiche_conn_stats(quicheConn, stats.address());
return quiche_stats.get_cwnd(stats);
}
}
@Override
public long windowCapacity(long streamId) throws IOException
{
try (AutoLock ignore = lock.lock())
{
if (quicheConn == null)
throw new IOException("connection was released");
long value = quiche_h.quiche_conn_stream_capacity(quicheConn, streamId);
if (value < 0)
{
if (LOG.isDebugEnabled())
LOG.debug("could not read window capacity for stream {} err={}", streamId, quiche_h.quiche_error.errToString(value));
}
return value;
}
}
@Override
public void shutdownStream(long streamId, boolean writeSide, long error) throws IOException
{
try (AutoLock ignore = lock.lock())
{
if (quicheConn == null)
throw new IOException("connection was released");
int direction = writeSide ? quiche_h.quiche_shutdown.QUICHE_SHUTDOWN_WRITE : quiche_h.quiche_shutdown.QUICHE_SHUTDOWN_READ;
int rc = quiche_h.quiche_conn_stream_shutdown(quicheConn, streamId, direction, error);
if (rc == 0 || rc == quiche_h.quiche_error.QUICHE_ERR_DONE)
return;
throw new IOException("failed to shutdown stream " + streamId + ": " + quiche_h.quiche_error.errToString(rc));
}
}
@Override
public int feedClearBytesForStream(long streamId, ByteBuffer buffer, boolean last) throws IOException
{
try (AutoLock ignore = lock.lock())
{
if (quicheConn == null)
throw new IOException("connection was released");
long written;
if (buffer.isDirect())
{
// If the ByteBuffer is direct, it can be used without any copy.
MemorySegment bufferSegment = MemorySegment.ofByteBuffer(buffer);
written = quiche_h.quiche_conn_stream_send(quicheConn, streamId, bufferSegment.address(), buffer.remaining(), last ? C_TRUE : C_FALSE);
}
else
{
// If the ByteBuffer is heap-allocated, it must be copied to native memory.
try (ResourceScope scope = ResourceScope.newConfinedScope())
{
if (buffer.remaining() == 0)
{
written = quiche_h.quiche_conn_stream_send(quicheConn, streamId, MemoryAddress.NULL, 0, last ? C_TRUE : C_FALSE);
}
else
{
MemorySegment bufferSegment = MemorySegment.allocateNative(buffer.remaining(), scope);
int prevPosition = buffer.position();
bufferSegment.asByteBuffer().order(ByteOrder.nativeOrder()).put(buffer);
buffer.position(prevPosition);
written = quiche_h.quiche_conn_stream_send(quicheConn, streamId, bufferSegment.address(), buffer.remaining(), last ? C_TRUE : C_FALSE);
}
}
}
if (written == quiche_h.quiche_error.QUICHE_ERR_DONE)
return 0;
if (written < 0L)
throw new IOException("failed to write to stream " + streamId + "; err=" + quiche_h.quiche_error.errToString(written));
buffer.position((int)(buffer.position() + written));
return (int)written;
}
}
@Override
public int drainClearBytesForStream(long streamId, ByteBuffer buffer) throws IOException
{
try (AutoLock ignore = lock.lock())
{
if (quicheConn == null)
throw new IOException("connection was released");
long read;
try (ResourceScope scope = ResourceScope.newConfinedScope())
{
if (buffer.isDirect())
{
// If the ByteBuffer is direct, it can be used without any copy.
MemorySegment bufferSegment = MemorySegment.ofByteBuffer(buffer);
MemorySegment fin = MemorySegment.allocateNative(CLinker.C_CHAR, scope);
read = quiche_h.quiche_conn_stream_recv(quicheConn, streamId, bufferSegment.address(), buffer.remaining(), fin.address());
}
else
{
// If the ByteBuffer is heap-allocated, native memory must be copied to it.
MemorySegment bufferSegment = MemorySegment.allocateNative(buffer.remaining(), scope);
MemorySegment fin = MemorySegment.allocateNative(CLinker.C_CHAR, scope);
read = quiche_h.quiche_conn_stream_recv(quicheConn, streamId, bufferSegment.address(), buffer.remaining(), fin.address());
int prevPosition = buffer.position();
buffer.put(bufferSegment.asByteBuffer().order(ByteOrder.nativeOrder()).limit((int)read));
buffer.position(prevPosition);
}
}
if (read == quiche_h.quiche_error.QUICHE_ERR_DONE)
return isStreamFinished(streamId) ? -1 : 0;
if (read < 0L)
throw new IOException("failed to read from stream " + streamId + "; err=" + quiche_h.quiche_error.errToString(read));
buffer.position((int)(buffer.position() + read));
return (int)read;
}
}
@Override
public boolean isStreamFinished(long streamId)
{
try (AutoLock ignore = lock.lock())
{
if (quicheConn == null)
throw new IllegalStateException("connection was released");
return quiche_h.quiche_conn_stream_finished(quicheConn, streamId) != C_FALSE;
}
}
@Override
public CloseInfo getRemoteCloseInfo()
{
try (AutoLock ignore = lock.lock())
{
if (quicheConn == null)
throw new IllegalStateException("connection was released");
try (ResourceScope scope = ResourceScope.newConfinedScope())
{
MemorySegment app = MemorySegment.allocateNative(CLinker.C_CHAR, scope);
MemorySegment error = MemorySegment.allocateNative(CLinker.C_LONG, scope);
MemorySegment reason = MemorySegment.allocateNative(CLinker.C_POINTER, scope);
MemorySegment reasonLength = MemorySegment.allocateNative(CLinker.C_LONG, scope);
if (quiche_h.quiche_conn_peer_error(quicheConn, app.address(), error.address(), reason.address(), reasonLength.address()) != C_FALSE)
{
long errorValue = error.asByteBuffer().order(ByteOrder.nativeOrder()).getLong();
long reasonLengthValue = reasonLength.asByteBuffer().order(ByteOrder.nativeOrder()).getLong();
String reasonValue;
if (reasonLengthValue == 0L)
{
reasonValue = null;
}
else
{
byte[] reasonBytes = new byte[(int)reasonLengthValue];
// dereference reason pointer
MemoryAddress memoryAddress = MemoryAddress.ofLong(reason.asByteBuffer().order(ByteOrder.nativeOrder()).getLong());
memoryAddress.asSegment(reasonLengthValue, ResourceScope.globalScope()).asByteBuffer().get(reasonBytes);
reasonValue = new String(reasonBytes, StandardCharsets.UTF_8);
}
return new CloseInfo(errorValue, reasonValue);
}
return null;
}
}
}
}

View File

@ -0,0 +1,49 @@
//
// ========================================================================
// 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.panama.jdk;
import java.lang.invoke.VarHandle;
import java.net.SocketAddress;
import jdk.incubator.foreign.MemoryHandles;
import jdk.incubator.foreign.MemoryLayout;
import jdk.incubator.foreign.MemorySegment;
import jdk.incubator.foreign.ResourceScope;
import static jdk.incubator.foreign.CLinker.C_INT;
import static jdk.incubator.foreign.CLinker.C_POINTER;
public class quiche_recv_info
{
private static final MemoryLayout LAYOUT = MemoryLayout.structLayout(
C_POINTER.withName("from"),
C_INT.withName("from_len"),
MemoryLayout.paddingLayout(32)
);
private static final VarHandle from = MemoryHandles.asAddressVarHandle(LAYOUT.varHandle(long.class, MemoryLayout.PathElement.groupElement("from")));
private static final VarHandle from_len = LAYOUT.varHandle(int.class, MemoryLayout.PathElement.groupElement("from_len"));
public static MemorySegment allocate(ResourceScope scope)
{
return MemorySegment.allocateNative(LAYOUT, scope);
}
public static void setSocketAddress(MemorySegment recvInfo, SocketAddress peer, ResourceScope scope)
{
MemorySegment sockAddrSegment = sockaddr.convert(peer, scope);
from.set(recvInfo, sockAddrSegment.address());
from_len.set(recvInfo, (int)sockAddrSegment.byteSize());
}
}

View File

@ -0,0 +1,45 @@
//
// ========================================================================
// 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.panama.jdk;
import jdk.incubator.foreign.MemoryLayout;
import jdk.incubator.foreign.MemorySegment;
import jdk.incubator.foreign.ResourceScope;
import static jdk.incubator.foreign.CLinker.C_CHAR;
import static jdk.incubator.foreign.CLinker.C_INT;
import static jdk.incubator.foreign.CLinker.C_LONG;
import static jdk.incubator.foreign.CLinker.C_SHORT;
public class quiche_send_info
{
private static final MemoryLayout LAYOUT = MemoryLayout.structLayout( // struct sockaddr_storage
MemoryLayout.structLayout(
C_SHORT.withName("ss_family"),
MemoryLayout.sequenceLayout(118, C_CHAR).withName("__ss_padding"),
C_LONG.withName("__ss_align")
).withName("to"),
C_INT.withName("to_len"),
MemoryLayout.paddingLayout(32),
MemoryLayout.structLayout( // struct timespec
C_LONG.withName("tv_sec"),
C_LONG.withName("tv_nsec")
).withName("at")
);
public static MemorySegment allocate(ResourceScope scope)
{
return MemorySegment.allocateNative(LAYOUT, scope);
}
}

View File

@ -0,0 +1,73 @@
//
// ========================================================================
// 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.panama.jdk;
import java.lang.invoke.VarHandle;
import jdk.incubator.foreign.MemoryLayout;
import jdk.incubator.foreign.MemorySegment;
import jdk.incubator.foreign.ResourceScope;
import static jdk.incubator.foreign.CLinker.C_CHAR;
import static jdk.incubator.foreign.CLinker.C_LONG;
public class quiche_stats
{
private static final MemoryLayout LAYOUT = MemoryLayout.structLayout(
C_LONG.withName("recv"),
C_LONG.withName("sent"),
C_LONG.withName("lost"),
C_LONG.withName("retrans"),
C_LONG.withName("rtt"),
C_LONG.withName("cwnd"),
C_LONG.withName("sent_bytes"),
C_LONG.withName("recv_bytes"),
C_LONG.withName("lost_bytes"),
C_LONG.withName("stream_retrans_bytes"),
C_LONG.withName("pmtu"),
C_LONG.withName("delivery_rate"),
C_LONG.withName("peer_max_idle_timeout"),
C_LONG.withName("peer_max_udp_payload_size"),
C_LONG.withName("peer_initial_max_data"),
C_LONG.withName("peer_initial_max_stream_data_bidi_local"),
C_LONG.withName("peer_initial_max_stream_data_bidi_remote"),
C_LONG.withName("peer_initial_max_stream_data_uni"),
C_LONG.withName("peer_initial_max_streams_bidi"),
C_LONG.withName("peer_initial_max_streams_uni"),
C_LONG.withName("peer_ack_delay_exponent"),
C_LONG.withName("peer_max_ack_delay"),
C_CHAR.withName("peer_disable_active_migration"),
MemoryLayout.paddingLayout(56),
C_LONG.withName("peer_active_conn_id_limit"),
C_LONG.withName("peer_max_datagram_frame_size")
);
public static MemorySegment allocate(ResourceScope scope)
{
return MemorySegment.allocateNative(LAYOUT, scope);
}
private static final VarHandle cwnd = LAYOUT.varHandle(long.class, MemoryLayout.PathElement.groupElement("cwnd"));
private static final VarHandle peer_initial_max_streams_bidi = LAYOUT.varHandle(long.class, MemoryLayout.PathElement.groupElement("peer_initial_max_streams_bidi"));
public static long get_cwnd(MemorySegment stats)
{
return (long)cwnd.get(stats);
}
public static long get_peer_initial_max_streams_bidi(MemorySegment stats)
{
return (long)peer_initial_max_streams_bidi.get(stats);
}
}

View File

@ -0,0 +1,63 @@
//
// ========================================================================
// 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.panama.jdk;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import jdk.incubator.foreign.MemorySegment;
import jdk.incubator.foreign.ResourceScope;
public class sockaddr
{
// TODO: linux-specific constants
private static final short AF_INET = 2;
private static final short AF_INET6 = 10;
public static MemorySegment convert(SocketAddress socketAddress, ResourceScope scope)
{
if (!(socketAddress instanceof InetSocketAddress))
throw new IllegalArgumentException("Expected InetSocketAddress instance, got: " + socketAddress);
InetSocketAddress inetSocketAddress = (InetSocketAddress)socketAddress;
InetAddress address = inetSocketAddress.getAddress();
if (address instanceof Inet4Address)
{
// TODO: linux-specific implementation
MemorySegment sin = sockaddr_in.allocate(scope);
sockaddr_in.set_sin_family(sin, AF_INET);
sockaddr_in.set_sin_port(sin, (short) inetSocketAddress.getPort());
sockaddr_in.set_sin_addr(sin, ByteBuffer.wrap(address.getAddress()).getInt());
return sin;
}
else if (address instanceof Inet6Address)
{
// TODO: linux-specific implementation
MemorySegment sin6 = sockaddr_in6.allocate(scope);
sockaddr_in6.set_sin6_family(sin6, AF_INET6);
sockaddr_in6.set_sin6_port(sin6, (short) inetSocketAddress.getPort());
sockaddr_in6.set_sin6_addr(sin6, address.getAddress());
sockaddr_in6.set_sin6_scope_id(sin6, 0);
sockaddr_in6.set_sin6_flowinfo(sin6, 0);
return sin6;
}
else
{
throw new UnsupportedOperationException("Unsupported InetAddress: " + address);
}
}
}

View File

@ -0,0 +1,61 @@
//
// ========================================================================
// 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.panama.jdk;
import java.lang.invoke.VarHandle;
import jdk.incubator.foreign.GroupLayout;
import jdk.incubator.foreign.MemoryLayout;
import jdk.incubator.foreign.MemorySegment;
import jdk.incubator.foreign.ResourceScope;
import static jdk.incubator.foreign.CLinker.C_CHAR;
import static jdk.incubator.foreign.CLinker.C_INT;
import static jdk.incubator.foreign.CLinker.C_SHORT;
class sockaddr_in
{
private static final MemoryLayout LAYOUT = MemoryLayout.structLayout(
C_SHORT.withName("sin_family"),
C_SHORT.withName("sin_port"),
C_INT.withName("sin_addr"),
MemoryLayout.sequenceLayout(8, C_CHAR).withName("sin_zero")
).withName("sockaddr_in");
private static final VarHandle sin_family = LAYOUT.varHandle(short.class, MemoryLayout.PathElement.groupElement("sin_family"));
private static final VarHandle sin_port = LAYOUT.varHandle(short.class, MemoryLayout.PathElement.groupElement("sin_port"));
private static final VarHandle sin_addr = LAYOUT.varHandle(int.class, MemoryLayout.PathElement.groupElement("sin_addr"));
public static MemorySegment allocate(ResourceScope scope)
{
return MemorySegment.allocateNative(LAYOUT, scope);
}
public static void set_sin_family(MemorySegment sin, short value)
{
sin_family.set(sin, value);
}
public static void set_sin_port(MemorySegment sin, short value)
{
sin_port.set(sin, value);
}
public static void set_sin_addr(MemorySegment sin, int value)
{
sin_addr.set(sin, value);
}
}

View File

@ -0,0 +1,71 @@
//
// ========================================================================
// 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.panama.jdk;
import java.lang.invoke.VarHandle;
import java.nio.ByteOrder;
import jdk.incubator.foreign.MemoryLayout;
import jdk.incubator.foreign.MemorySegment;
import jdk.incubator.foreign.ResourceScope;
import static jdk.incubator.foreign.CLinker.C_CHAR;
import static jdk.incubator.foreign.CLinker.C_INT;
import static jdk.incubator.foreign.CLinker.C_SHORT;
class sockaddr_in6
{
private static final MemoryLayout LAYOUT = MemoryLayout.structLayout(
C_SHORT.withName("sin6_family"),
C_SHORT.withName("sin6_port"),
C_INT.withName("sin6_flowinfo"),
MemoryLayout.sequenceLayout(16, C_CHAR).withName("sin6_addr"),
C_INT.withName("sin6_scope_id")
).withName("sockaddr_in6");
private static final VarHandle sin6_family = LAYOUT.varHandle(short.class, MemoryLayout.PathElement.groupElement("sin6_family"));
private static final VarHandle sin6_port = LAYOUT.varHandle(short.class, MemoryLayout.PathElement.groupElement("sin6_port"));
private static final VarHandle sin6_scope_id = LAYOUT.varHandle(int.class, MemoryLayout.PathElement.groupElement("sin6_scope_id"));
private static final VarHandle sin6_flowinfo = LAYOUT.varHandle(int.class, MemoryLayout.PathElement.groupElement("sin6_flowinfo"));
public static MemorySegment allocate(ResourceScope scope)
{
return MemorySegment.allocateNative(LAYOUT, scope);
}
public static void set_sin6_addr(MemorySegment sin6, byte[] value)
{
sin6.asSlice(8, 16).asByteBuffer().order(ByteOrder.nativeOrder()).put(value);
}
public static void set_sin6_family(MemorySegment sin6, short value)
{
sin6_family.set(sin6, value);
}
public static void set_sin6_port(MemorySegment sin6, short value)
{
sin6_port.set(sin6, value);
}
public static void set_sin6_scope_id(MemorySegment sin6, int value)
{
sin6_scope_id.set(sin6, value);
}
public static void set_sin6_flowinfo(MemorySegment sin6, int value)
{
sin6_flowinfo.set(sin6, value);
}
}

View File

@ -0,0 +1 @@
org.eclipse.jetty.quic.quiche.panama.jdk.PanamaJdkQuicheBinding

View File

@ -0,0 +1,287 @@
//
// ========================================================================
// 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.panama.jdk;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
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.QuicheConfig;
import org.eclipse.jetty.quic.quiche.QuicheConnection;
import org.eclipse.jetty.quic.quiche.SSLKeyPair;
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<PanamaJdkQuicheConnection> connectionsToDisposeOf = new ArrayList<>();
private InetSocketAddress clientSocketAddress;
private InetSocketAddress serverSocketAddress;
private QuicheConfig clientQuicheConfig;
private QuicheConfig serverQuicheConfig;
private PanamaJdkQuicheConnection.TokenMinter tokenMinter;
private PanamaJdkQuicheConnection.TokenValidator tokenValidator;
@BeforeEach
protected void setUp() throws Exception
{
clientSocketAddress = new InetSocketAddress("localhost", 9999);
serverSocketAddress = new InetSocketAddress("localhost", 8888);
clientQuicheConfig = new QuicheConfig();
clientQuicheConfig.setApplicationProtos("http/0.9");
clientQuicheConfig.setDisableActiveMigration(true);
clientQuicheConfig.setVerifyPeer(false);
clientQuicheConfig.setMaxIdleTimeout(1_000L);
clientQuicheConfig.setInitialMaxData(10_000_000L);
clientQuicheConfig.setInitialMaxStreamDataBidiLocal(10_000_000L);
clientQuicheConfig.setInitialMaxStreamDataBidiRemote(10_000_000L);
clientQuicheConfig.setInitialMaxStreamDataUni(10_000_000L);
clientQuicheConfig.setInitialMaxStreamsUni(100L);
clientQuicheConfig.setInitialMaxStreamsBidi(100L);
clientQuicheConfig.setCongestionControl(QuicheConfig.CongestionControl.CUBIC);
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.CUBIC);
tokenMinter = new TestTokenMinter();
tokenValidator = new TestTokenValidator();
}
@AfterEach
protected void tearDown()
{
connectionsToDisposeOf.forEach(PanamaJdkQuicheConnection::dispose);
connectionsToDisposeOf.clear();
}
@Test
public void testFinishedAsSoonAsFinIsFed() throws Exception
{
// establish connection
Map.Entry<PanamaJdkQuicheConnection, PanamaJdkQuicheConnection> entry = connectClientToServer();
PanamaJdkQuicheConnection clientQuicheConnection = entry.getKey();
PanamaJdkQuicheConnection serverQuicheConnection = entry.getValue();
// client sends 16 bytes of payload over stream 0
assertThat(clientQuicheConnection.feedClearBytesForStream(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.drainClearBytesForStream(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<PanamaJdkQuicheConnection, PanamaJdkQuicheConnection> entry = connectClientToServer();
PanamaJdkQuicheConnection clientQuicheConnection = entry.getKey();
PanamaJdkQuicheConnection serverQuicheConnection = entry.getValue();
// client sends 16 bytes of payload over stream 0 and finish it
assertThat(clientQuicheConnection.feedClearBytesForStream(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.drainClearBytesForStream(0, ByteBuffer.allocate(1000)), is(16));
// assert that stream 0 is finished on server
assertThat(serverQuicheConnection.isStreamFinished(0), is(true));
}
@Test
public void testApplicationProtocol() throws Exception
{
serverQuicheConfig.setApplicationProtos("");
clientQuicheConfig.setApplicationProtos("");
// establish connection
Map.Entry<PanamaJdkQuicheConnection, PanamaJdkQuicheConnection> entry = connectClientToServer();
PanamaJdkQuicheConnection clientQuicheConnection = entry.getKey();
PanamaJdkQuicheConnection serverQuicheConnection = entry.getValue();
assertThat(clientQuicheConnection.getNegotiatedProtocol(), is(""));
assertThat(serverQuicheConnection.getNegotiatedProtocol(), is(""));
}
private void drainServerToFeedClient(Map.Entry<PanamaJdkQuicheConnection, PanamaJdkQuicheConnection> entry, int expectedSize) throws IOException
{
PanamaJdkQuicheConnection clientQuicheConnection = entry.getKey();
PanamaJdkQuicheConnection serverQuicheConnection = entry.getValue();
ByteBuffer buffer = ByteBuffer.allocate(quiche_h.QUICHE_MIN_CLIENT_INITIAL_LEN);
int drained = serverQuicheConnection.drainCipherBytes(buffer);
assertThat(drained, is(expectedSize));
buffer.flip();
int fed = clientQuicheConnection.feedCipherBytes(buffer, serverSocketAddress);
assertThat(fed, is(expectedSize));
}
private void drainClientToFeedServer(Map.Entry<PanamaJdkQuicheConnection, PanamaJdkQuicheConnection> entry, int expectedSize) throws IOException
{
PanamaJdkQuicheConnection clientQuicheConnection = entry.getKey();
PanamaJdkQuicheConnection serverQuicheConnection = entry.getValue();
ByteBuffer buffer = ByteBuffer.allocate(quiche_h.QUICHE_MIN_CLIENT_INITIAL_LEN);
int drained = clientQuicheConnection.drainCipherBytes(buffer);
assertThat(drained, is(expectedSize));
buffer.flip();
int fed = serverQuicheConnection.feedCipherBytes(buffer, clientSocketAddress);
assertThat(fed, is(expectedSize));
}
private Map.Entry<PanamaJdkQuicheConnection, PanamaJdkQuicheConnection> connectClientToServer() throws IOException
{
ByteBuffer buffer = ByteBuffer.allocate(quiche_h.QUICHE_MIN_CLIENT_INITIAL_LEN);
ByteBuffer buffer2 = ByteBuffer.allocate(quiche_h.QUICHE_MIN_CLIENT_INITIAL_LEN);
PanamaJdkQuicheConnection clientQuicheConnection = PanamaJdkQuicheConnection.connect(clientQuicheConfig, serverSocketAddress);
connectionsToDisposeOf.add(clientQuicheConnection);
int drained = clientQuicheConnection.drainCipherBytes(buffer);
assertThat(drained, is(1200));
buffer.flip();
PanamaJdkQuicheConnection serverQuicheConnection = PanamaJdkQuicheConnection.tryAccept(serverQuicheConfig, tokenValidator, buffer, clientSocketAddress);
assertThat(serverQuicheConnection, is(nullValue()));
boolean negotiated = PanamaJdkQuicheConnection.negotiate(tokenMinter, buffer, buffer2);
assertThat(negotiated, is(true));
buffer2.flip();
int fed = clientQuicheConnection.feedCipherBytes(buffer2, serverSocketAddress);
assertThat(fed, is(79));
buffer.clear();
drained = clientQuicheConnection.drainCipherBytes(buffer);
assertThat(drained, is(1200));
buffer.flip();
serverQuicheConnection = PanamaJdkQuicheConnection.tryAccept(serverQuicheConfig, tokenValidator, buffer, clientSocketAddress);
assertThat(serverQuicheConnection, is(not(nullValue())));
connectionsToDisposeOf.add(serverQuicheConnection);
buffer.clear();
drained = serverQuicheConnection.drainCipherBytes(buffer);
assertThat(drained, is(1200));
buffer.flip();
fed = clientQuicheConnection.feedCipherBytes(buffer, serverSocketAddress);
assertThat(fed, is(1200));
assertThat(serverQuicheConnection.isConnectionEstablished(), is(false));
assertThat(clientQuicheConnection.isConnectionEstablished(), is(false));
AbstractMap.SimpleImmutableEntry<PanamaJdkQuicheConnection, PanamaJdkQuicheConnection> entry = new AbstractMap.SimpleImmutableEntry<>(clientQuicheConnection, serverQuicheConnection);
int protosLen = 0;
for (String proto : clientQuicheConfig.getApplicationProtos())
protosLen += 1 + proto.getBytes(StandardCharsets.UTF_8).length;
drainServerToFeedClient(entry, 300 + protosLen);
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

View File

@ -22,12 +22,14 @@
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.quic</groupId>
<artifactId>quic-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.quic</groupId>
<artifactId>quic-quiche-jna</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
@ -35,4 +37,19 @@
<scope>test</scope>
</dependency>
</dependencies>
<profiles>
<profile>
<id>jdk17</id>
<activation>
<jdk>[17,)</jdk>
</activation>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty.quic</groupId>
<artifactId>quic-quiche-panama-jdk</artifactId>
</dependency>
</dependencies>
</profile>
</profiles>
</project>

12
pom.xml
View File

@ -1681,7 +1681,17 @@
</dependency>
<dependency>
<groupId>org.eclipse.jetty.quic</groupId>
<artifactId>quic-quiche</artifactId>
<artifactId>quic-quiche-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.quic</groupId>
<artifactId>quic-quiche-jna</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.quic</groupId>
<artifactId>quic-quiche-panama-jdk</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>

View File

@ -87,6 +87,9 @@ public class HttpClientLoadTest extends AbstractTest<HttpClientLoadTest.LoadTran
client.setMaxConnectionsPerDestination(32768);
client.setMaxRequestsQueuedPerDestination(1024 * 1024);
});
scenario.setConnectionIdleTimeout(120000);
scenario.setRequestIdleTimeout(120000);
scenario.client.setIdleTimeout(120000);
// At least 25k requests to warmup properly (use -XX:+PrintCompilation to verify JIT activity)
int runs = 1;
@ -165,7 +168,7 @@ public class HttpClientLoadTest extends AbstractTest<HttpClientLoadTest.LoadTran
// Dumps the state of the client if the test takes too long
Thread testThread = Thread.currentThread();
long maxTime = Math.max(15000, (long)iterations * factor);
long maxTime = Math.max(60000, (long)iterations * factor);
Scheduler.Task task = scenario.client.getScheduler().schedule(() ->
{
logger.warn("Interrupting test, it is taking too long (maxTime={} ms){}{}{}{}", maxTime,
@ -292,7 +295,7 @@ public class HttpClientLoadTest extends AbstractTest<HttpClientLoadTest.LoadTran
latch.countDown();
}
});
int maxTime = 15000;
int maxTime = 30000;
if (!await(requestLatch, maxTime, TimeUnit.MILLISECONDS))
{
logger.warn("Request {} took too long (maxTime={} ms){}{}{}{}", requestId, maxTime,