now capable of accepting a quic connection

Signed-off-by: Ludovic Orban <lorban@bitronix.be>
This commit is contained in:
Ludovic Orban 2021-03-15 17:43:06 +01:00 committed by Simone Bordet
parent 1f588d719d
commit e347074975
7 changed files with 225 additions and 69 deletions

View File

@ -24,6 +24,11 @@
<artifactId>jetty-server</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.http3</groupId>
<artifactId>cloudflare-quiche-jna</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-slf4j-impl</artifactId>

View File

@ -75,10 +75,7 @@ public class DatagramAdaptingEndPoint implements EndPoint
if (filled == 0)
return 0;
int headerPosition = buffer.position();
remoteAddress = ServerDatagramEndPoint.decodeInetSocketAddress(buffer);
buffer.position(headerPosition + ServerDatagramEndPoint.ENCODED_ADDRESS_LENGTH);
return filled;
}

View File

@ -1,81 +1,133 @@
package org.eclipse.jetty.http3.server;
import java.util.EventListener;
import java.io.File;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executor;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.http3.quiche.QuicheConfig;
import org.eclipse.jetty.http3.quiche.QuicheConnection;
import org.eclipse.jetty.http3.quiche.QuicheConnectionId;
import org.eclipse.jetty.http3.quiche.ffi.LibQuiche;
import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.util.BufferUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class QuicConnection implements Connection
public class QuicConnection extends AbstractConnection
{
@Override
public void addEventListener(EventListener listener)
{
private static final Logger LOG = LoggerFactory.getLogger(QuicConnection.class);
private final ByteBufferPool byteBufferPool;
private final ConcurrentMap<QuicheConnectionId, QuicheConnection> connections = new ConcurrentHashMap<>();
private final QuicheConfig quicheConfig;
public QuicConnection(ByteBufferPool byteBufferPool, Executor executor, ServerDatagramEndPoint endp)
{
super(endp, executor);
this.byteBufferPool = byteBufferPool;
File[] files;
try
{
SSLKeyPair keyPair;
keyPair = new SSLKeyPair(new File("src/test/resources/keystore.p12"), "PKCS12", "storepwd".toCharArray(), "mykey", "storepwd".toCharArray());
files = keyPair.export(new File(System.getProperty("java.io.tmpdir")));
}
catch (Exception e)
{
throw new RuntimeException(e);
}
@Override
public void removeEventListener(EventListener listener)
{
quicheConfig = new QuicheConfig();
quicheConfig.setPrivKeyPemPath(files[0].getPath());
quicheConfig.setCertChainPemPath(files[1].getPath());
quicheConfig.setVerifyPeer(false);
quicheConfig.setMaxIdleTimeout(5000L);
quicheConfig.setInitialMaxData(10000000L);
quicheConfig.setInitialMaxStreamDataBidiLocal(10000000L);
quicheConfig.setInitialMaxStreamDataBidiRemote(10000000L);
quicheConfig.setInitialMaxStreamDataUni(10000000L);
quicheConfig.setInitialMaxStreamsBidi(100L);
quicheConfig.setCongestionControl(QuicheConfig.CongestionControl.RENO);
quicheConfig.setApplicationProtos(getProtocols().toArray(new String[0]));
}
private Collection<String> getProtocols()
{
return Arrays.asList("http/0.9");
}
@Override
public void onOpen()
{
super.onOpen();
fillInterested();
}
@Override
public void onClose(Throwable cause)
public void onFillable()
{
try
{
ByteBuffer buffer = byteBufferPool.acquire(LibQuiche.QUICHE_MIN_CLIENT_INITIAL_LEN + ServerDatagramEndPoint.ENCODED_ADDRESS_LENGTH, true);
// Read data
int fill = getEndPoint().fill(buffer);
if (fill < 0)
{
byteBufferPool.release(buffer);
getEndPoint().shutdownOutput();
return;
}
if (fill == 0)
{
byteBufferPool.release(buffer);
fillInterested();
return;
}
@Override
public EndPoint getEndPoint()
InetSocketAddress remoteAddress = ServerDatagramEndPoint.decodeInetSocketAddress(buffer);
QuicheConnectionId quicheConnectionId = QuicheConnectionId.fromPacket(buffer);
QuicheConnection quicheConnection = connections.get(quicheConnectionId);
if (quicheConnection == null)
{
return null;
quicheConnection = QuicheConnection.tryAccept(quicheConfig, remoteAddress, buffer);
if (quicheConnection == null)
{
ByteBuffer address = byteBufferPool.acquire(ServerDatagramEndPoint.ENCODED_ADDRESS_LENGTH, true);
BufferUtil.flipToFill(address);
ServerDatagramEndPoint.encodeInetSocketAddress(address, remoteAddress);
address.flip();
ByteBuffer buffer2 = byteBufferPool.acquire(LibQuiche.QUICHE_MIN_CLIENT_INITIAL_LEN, true);
BufferUtil.flipToFill(buffer2);
QuicheConnection.negotiate(remoteAddress, buffer, buffer2);
getEndPoint().flush(address, buffer2);
byteBufferPool.release(address);
byteBufferPool.release(buffer2);
}
@Override
public void close()
else
{
LOG.info("Quic connection accepted");
connections.put(quicheConnectionId, quicheConnection);
}
@Override
public boolean onIdleExpired()
{
return false;
}
@Override
public long getMessagesIn()
else
{
return 0;
quicheConnection.feedCipherText(buffer);
}
@Override
public long getMessagesOut()
{
return 0;
byteBufferPool.release(buffer);
fillInterested();
}
@Override
public long getBytesIn()
catch (Throwable x)
{
return 0;
close();
}
@Override
public long getBytesOut()
{
return 0;
}
@Override
public long getCreatedTimeStamp()
{
return 0;
}
}

View File

@ -0,0 +1,105 @@
//
// ========================================================================
// 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.http3.server;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.util.Base64;
public class SSLKeyPair
{
private static final String BEGIN_KEY = "-----BEGIN PRIVATE KEY-----";
private static final String END_KEY = "-----END PRIVATE KEY-----";
private static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----";
private static final String END_CERT = "-----END CERTIFICATE-----";
private static final String LINE_SEPARATOR = System.getProperty("line.separator");
private static final int LINE_LENGTH = 64;
private final Key key;
private final Certificate cert;
private final String alias;
public SSLKeyPair(Key key, Certificate cert, String alias)
{
this.key = key;
this.cert = cert;
this.alias = alias;
}
public SSLKeyPair(File storeFile, String storeType, char[] storePassword, String alias, char[] keyPassword) throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException, IOException, CertificateException
{
KeyStore keyStore = KeyStore.getInstance(storeType);
try (FileInputStream fis = new FileInputStream(storeFile))
{
keyStore.load(fis, storePassword);
this.alias = alias;
this.key = keyStore.getKey(alias, keyPassword);
this.cert = keyStore.getCertificate(alias);
}
}
/**
* @return [0] is the key file, [1] is the cert file.
*/
public File[] export(File targetFolder) throws Exception
{
File[] files = new File[2];
files[0] = new File(targetFolder, alias + ".key");
files[1] = new File(targetFolder, alias + ".crt");
try (FileOutputStream fos = new FileOutputStream(files[0]))
{
writeAsPem(fos, key);
}
try (FileOutputStream fos = new FileOutputStream(files[1]))
{
writeAsPem(fos, cert);
}
return files;
}
private void writeAsPem(OutputStream outputStream, Key key) throws IOException
{
Base64.Encoder encoder = Base64.getMimeEncoder(LINE_LENGTH, LINE_SEPARATOR.getBytes());
byte[] encoded = encoder.encode(key.getEncoded());
outputStream.write(BEGIN_KEY.getBytes(StandardCharsets.UTF_8));
outputStream.write(LINE_SEPARATOR.getBytes(StandardCharsets.UTF_8));
outputStream.write(encoded);
outputStream.write(LINE_SEPARATOR.getBytes(StandardCharsets.UTF_8));
outputStream.write(END_KEY.getBytes(StandardCharsets.UTF_8));
}
private static void writeAsPem(OutputStream outputStream, Certificate certificate) throws CertificateEncodingException, IOException
{
Base64.Encoder encoder = Base64.getMimeEncoder(LINE_LENGTH, LINE_SEPARATOR.getBytes());
byte[] encoded = encoder.encode(certificate.getEncoded());
outputStream.write(BEGIN_CERT.getBytes(StandardCharsets.UTF_8));
outputStream.write(LINE_SEPARATOR.getBytes(StandardCharsets.UTF_8));
outputStream.write(encoded);
outputStream.write(LINE_SEPARATOR.getBytes(StandardCharsets.UTF_8));
outputStream.write(END_CERT.getBytes(StandardCharsets.UTF_8));
}
}

View File

@ -10,7 +10,6 @@ import java.nio.channels.Selector;
import java.util.EventListener;
import java.util.concurrent.Executor;
import org.eclipse.jetty.http.HttpCompliance;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
@ -18,8 +17,6 @@ import org.eclipse.jetty.io.ManagedSelector;
import org.eclipse.jetty.io.SelectorManager;
import org.eclipse.jetty.server.AbstractNetworkConnector;
import org.eclipse.jetty.server.ConnectionFactory;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnection;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.annotation.Name;
@ -170,11 +167,7 @@ public class ServerDatagramConnector extends AbstractNetworkConnector
@Override
public Connection newConnection(SelectableChannel channel, EndPoint endpoint, Object attachment) throws IOException
{
//TODO: return quic connection
//return new QuicConnection();
HttpConfiguration config = new HttpConfiguration();
config.setHttpCompliance(HttpCompliance.LEGACY); // enable HTTP/0.9
return new HttpConnection(config, ServerDatagramConnector.this, new DatagramAdaptingEndPoint((ServerDatagramEndPoint)endpoint), false);
return new QuicConnection(getByteBufferPool(), getExecutor(), (ServerDatagramEndPoint)endpoint);
}
@Override

View File

@ -253,6 +253,7 @@ public class ServerDatagramEndPoint extends IdleTimeout implements EndPoint, Man
static InetSocketAddress decodeInetSocketAddress(ByteBuffer buffer) throws IOException
{
int headerPosition = buffer.position();
byte ipVersion = buffer.get();
byte[] address;
if (ipVersion == 4)
@ -262,11 +263,13 @@ public class ServerDatagramEndPoint extends IdleTimeout implements EndPoint, Man
else throw new IOException("Unsupported IP version: " + ipVersion);
buffer.get(address);
int port = buffer.getChar();
buffer.position(headerPosition + ENCODED_ADDRESS_LENGTH);
return new InetSocketAddress(InetAddress.getByAddress(address), port);
}
static void encodeInetSocketAddress(ByteBuffer buffer, InetSocketAddress peer) throws IOException
{
int headerPosition = buffer.position();
byte[] addressBytes = peer.getAddress().getAddress();
int port = peer.getPort();
byte ipVersion;
@ -279,5 +282,6 @@ public class ServerDatagramEndPoint extends IdleTimeout implements EndPoint, Man
buffer.put(ipVersion);
buffer.put(addressBytes);
buffer.putChar((char)port);
buffer.position(headerPosition + ENCODED_ADDRESS_LENGTH);
}
}