From e347074975695a753d2dd9cb3f16f609e494bf1c Mon Sep 17 00:00:00 2001 From: Ludovic Orban Date: Mon, 15 Mar 2021 17:43:06 +0100 Subject: [PATCH] now capable of accepting a quic connection Signed-off-by: Ludovic Orban --- jetty-http3/http3-server/pom.xml | 5 + .../server/DatagramAdaptingEndPoint.java | 3 - .../jetty/http3/server/QuicConnection.java | 168 ++++++++++++------ .../jetty/http3/server/SSLKeyPair.java | 105 +++++++++++ .../http3/server/ServerDatagramConnector.java | 9 +- .../http3/server/ServerDatagramEndPoint.java | 4 + .../src/test/resources/keystore.p12 | Bin 0 -> 2445 bytes 7 files changed, 225 insertions(+), 69 deletions(-) create mode 100644 jetty-http3/http3-server/src/main/java/org/eclipse/jetty/http3/server/SSLKeyPair.java create mode 100644 jetty-http3/http3-server/src/test/resources/keystore.p12 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 0000000000000000000000000000000000000000..0b56dd34ee9df09c61bd635095485252c86e6df7 GIT binary patch literal 2445 zcmY+EX*3iH8^@UiGh=V4u}mXd)|s&jakJfSb_u!M&@dR;8nV|E8e5Eg--W~=k|mVT zGH8@##JD8V*vYQj`=0l`_uLQ9dCvL$|IhRBhao}80CpgT1i8Qgk&pc~c87ddVD<3_2nYCs@bW} z?geurRi%7;edgcBV6iEpy-NCO{*0)r;KRT)b5lF#V*RkIBIi0uJR3HNJ|B`sCVD8r z0dv@<8+bqdMFLhj9Y4S?GBaUh%h7KLvG?f4$3M6Dxa4AGD_e)?v@x5+ecGR1|8P5H z-ZR&usqoe7YE>h0iUrS_)Sc;k^}s>z%W(->QtR0TCNI~S#wF1Q7gs4?#uF*t0*IX5 zi-Xg3EE$f;NuWTOcP$kr%v6V4$TXQkex@(?B@Ih=4!igIXA|y2?xS$y@hK0Fa zO^@G=TtY_W9nNqEl(L}%;fUUfMXp#$WXF$`4CWl2jo8?9rO97f#5?Ym>}${QYU{IJ zJ^X4omN22Kd8Cxlv@-jP$(+B4KzC_BoP*Dfu`BW+&y)PbA;!Zgmum+3jsCRo+B{VD z_91^#re9JY^rVdc$0YYFbZ;Gn+_S|n(e_dCsax!)#4Q(;HNA&?hM|X=Dw_>Dpfr}J z)jWNAh{3N=HWW>qawSxmZ7QqYaJNY%gjTlq@4{M?pkp0aTKh`AId7KHS8MCzZ;gmd zK5td)wJzKHbQw(NE1yYh+U6rFu3jd~WwyS>+d;7*TlZCpTP(4ByFGnD{LapKs8cd5 z(dmpXb!y1WDO}Ptgk3fjdA71{huqj31pp`a==U%(BMRpw=)}}khjwtd&A87-6O8J( z<(sqbS3s7-$Xx7pk#R!0-?Z<<=9O{cs@t|FxU91vieA1J$HeUi1iio)VE5;YD?tm+ z(UZg{*H8pvflLFTVzkIP#^|w_4SX)9t z?(95|fX`aE=G$876N?7czB4$J6>atJ9Ak!8rG2%31oyj3Q&!xX7&j3p%xiq$NomT~ zJ}O~kxIRnTE78EhKBep2w8s5Sv5r>1F&5tS_%!G3jXz)RjLdG?Y66yG zN&R1(Fz!Uz9LK{h*e{!KXf+8+k<7*%I45JBh5ULr@@E3&8ib zx>KeIvH-?|yIb)0{-Xn18v56?M`_PmB&{Y!7KF~xxHS#M_)YS&#@y|8H_CE0HF9~^cIQK`MuyI8Nk#!*{N6#$Ox_-4A0t4mNXh!ZJymW^$ zTx5q`*V`ufQhKrAT#6M=DSMpjb?)vvD|1_}WDbUC-mO}`413u1n3pCMyl@z2>|U$s zMR?`D`to2RuR;ZsWE5qxa%yLC$sn|w*FXh6>&i@gsgk2UmM`><<0tZ} z)mvTfSyYdMv=>$&QtLU|W7t5@IG@cSk2=)msZ#<+$*(Qcg(DAwii-{?Qa_k8wJ)Um z^Uo$X;U3ST_$xPmsEr={3XD&!;EFQZqa`m)Ru?Uy_1<7O05DutFrnv2bH-Z$8z z^Bcna8m9({Gt9n&9j>a+Xt}2m*f8+iB>T5?SDm9+FX>9md5i=G!U0y`1p-9CY#{jk yGbbrc;oIWY{%(G2sw^p2ZNCN(d>!yEq)t(jm@ztr3OEo{