diff --git a/jetty-http3/http3-server/pom.xml b/jetty-http3/http3-server/pom.xml index f826fc15a2b..4f4d6186734 100644 --- a/jetty-http3/http3-server/pom.xml +++ b/jetty-http3/http3-server/pom.xml @@ -24,6 +24,11 @@ jetty-server ${project.version} + + org.eclipse.jetty.http3 + cloudflare-quiche-jna + ${project.version} + org.eclipse.jetty jetty-slf4j-impl diff --git a/jetty-http3/http3-server/src/main/java/org/eclipse/jetty/http3/server/DatagramAdaptingEndPoint.java b/jetty-http3/http3-server/src/main/java/org/eclipse/jetty/http3/server/DatagramAdaptingEndPoint.java index 118c19db24f..1cab6a68fab 100644 --- a/jetty-http3/http3-server/src/main/java/org/eclipse/jetty/http3/server/DatagramAdaptingEndPoint.java +++ b/jetty-http3/http3-server/src/main/java/org/eclipse/jetty/http3/server/DatagramAdaptingEndPoint.java @@ -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; } diff --git a/jetty-http3/http3-server/src/main/java/org/eclipse/jetty/http3/server/QuicConnection.java b/jetty-http3/http3-server/src/main/java/org/eclipse/jetty/http3/server/QuicConnection.java index ca05038f7d5..a75c89dc2d4 100644 --- a/jetty-http3/http3-server/src/main/java/org/eclipse/jetty/http3/server/QuicConnection.java +++ b/jetty-http3/http3-server/src/main/java/org/eclipse/jetty/http3/server/QuicConnection.java @@ -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 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); + } + + 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])); } - @Override - public void removeEventListener(EventListener listener) + private Collection 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; + } - } + InetSocketAddress remoteAddress = ServerDatagramEndPoint.decodeInetSocketAddress(buffer); + QuicheConnectionId quicheConnectionId = QuicheConnectionId.fromPacket(buffer); + QuicheConnection quicheConnection = connections.get(quicheConnectionId); + if (quicheConnection == 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(); - @Override - public EndPoint getEndPoint() - { - return null; - } + ByteBuffer buffer2 = byteBufferPool.acquire(LibQuiche.QUICHE_MIN_CLIENT_INITIAL_LEN, true); + BufferUtil.flipToFill(buffer2); + QuicheConnection.negotiate(remoteAddress, buffer, buffer2); - @Override - public void close() - { - - } - - @Override - public boolean onIdleExpired() - { - return false; - } - - @Override - public long getMessagesIn() - { - return 0; - } - - @Override - public long getMessagesOut() - { - return 0; - } - - @Override - public long getBytesIn() - { - return 0; - } - - @Override - public long getBytesOut() - { - return 0; - } - - @Override - public long getCreatedTimeStamp() - { - return 0; + getEndPoint().flush(address, buffer2); + byteBufferPool.release(address); + byteBufferPool.release(buffer2); + } + else + { + LOG.info("Quic connection accepted"); + connections.put(quicheConnectionId, quicheConnection); + } + } + else + { + quicheConnection.feedCipherText(buffer); + } + byteBufferPool.release(buffer); + fillInterested(); + } + catch (Throwable x) + { + close(); + } } } diff --git a/jetty-http3/http3-server/src/main/java/org/eclipse/jetty/http3/server/SSLKeyPair.java b/jetty-http3/http3-server/src/main/java/org/eclipse/jetty/http3/server/SSLKeyPair.java new file mode 100644 index 00000000000..3b5948d0028 --- /dev/null +++ b/jetty-http3/http3-server/src/main/java/org/eclipse/jetty/http3/server/SSLKeyPair.java @@ -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)); + } +} diff --git a/jetty-http3/http3-server/src/main/java/org/eclipse/jetty/http3/server/ServerDatagramConnector.java b/jetty-http3/http3-server/src/main/java/org/eclipse/jetty/http3/server/ServerDatagramConnector.java index 4c226c69bd8..edb5f89f18b 100644 --- a/jetty-http3/http3-server/src/main/java/org/eclipse/jetty/http3/server/ServerDatagramConnector.java +++ b/jetty-http3/http3-server/src/main/java/org/eclipse/jetty/http3/server/ServerDatagramConnector.java @@ -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 diff --git a/jetty-http3/http3-server/src/main/java/org/eclipse/jetty/http3/server/ServerDatagramEndPoint.java b/jetty-http3/http3-server/src/main/java/org/eclipse/jetty/http3/server/ServerDatagramEndPoint.java index b360cc30b5a..700a55f9a70 100644 --- a/jetty-http3/http3-server/src/main/java/org/eclipse/jetty/http3/server/ServerDatagramEndPoint.java +++ b/jetty-http3/http3-server/src/main/java/org/eclipse/jetty/http3/server/ServerDatagramEndPoint.java @@ -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); } } diff --git a/jetty-http3/http3-server/src/test/resources/keystore.p12 b/jetty-http3/http3-server/src/test/resources/keystore.p12 new file mode 100644 index 00000000000..0b56dd34ee9 Binary files /dev/null and b/jetty-http3/http3-server/src/test/resources/keystore.p12 differ