commit b85679f001d40bf8977b82e3b25c5cff3db52c44 Author: Jörg Prante Date: Sat Jan 6 22:38:53 2024 +0100 fork from https://github.com/netty/netty-incubator-codec-quic diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..58733e5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,57 @@ +# Eclipse project files +.project +.classpath +.settings + +# IntelliJ IDEA project files and directories +*.iml +*.ipr +*.iws +.idea/ +.shelf/ + +# Geany project file +.geany + +# KDevelop project file and directory +.kdev4/ +*.kdev4 + +# Build targets +/target +*/target + +# Report directories +/reports +*/reports + +# Mac-specific directory that no other operating system needs. +.DS_Store + +# JVM crash logs +hs_err_pid*.log +replay_pid*.log + +# JVM dumps +*.hprof.xz +*.threads + +dependency-reduced-pom.xml + +*/.unison.* + +# exclude mainframer files +mainframer +.mainframer + +# exclude docker-sync stuff +.docker-sync +*/.docker-sync + +# exclude vscode files +.vscode/ +*.factorypath + +# exclude file created by the flatten plugin +.flattened-pom.xml +.java-version diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..cb28b0e Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..37d7d4e --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +distributionUrl=https://maven-central.storage-download.googleapis.com/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip +distributionSha256Sum=7822eb593d29558d8edf87845a2c47e36e2a89d17a84cd2390824633214ed423 diff --git a/Brewfile b/Brewfile new file mode 100644 index 0000000..39c885c --- /dev/null +++ b/Brewfile @@ -0,0 +1,10 @@ +brew 'autoconf' +brew 'automake' +brew 'libtool' +brew 'openssl' +brew 'perl' +brew 'ninja' +brew 'golang' +brew 'cmake' +brew 'apr' +brew 'rust' diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..62589ed --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 0000000..22ec9d6 --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1,49 @@ + + The Netty Project + ================= + +Please visit the Netty web site for more information: + + * https://netty.io/ + +Copyright 2020 The Netty Project + +The Netty Project licenses this file to you under the Apache License, +version 2.0 (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at: + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations +under the License. + +Also, please refer to each LICENSE..txt file, which is located in +the 'license' directory of the distribution file, for the license terms of the +components that this product depends on. + +------------------------------------------------------------------------------- + +This product contains the Maven wrapper scripts from 'Maven Wrapper', that provides an easy way to ensure a user has everything necessary to run the Maven build. + + * LICENSE: + * license/LICENSE.mvn-wrapper.txt (Apache License 2.0) + * HOMEPAGE: + * https://github.com/takari/maven-wrapper + +This product is statically linked against Quiche. + + * LICENSE: + * license/LICENSE.quiche.txt (BSD2) + * HOMEPAGE: + * https://github.com/cloudflare/quiche + + +This product is statically linked against boringssl. + + * LICENSE (Combination ISC and OpenSSL license) + * license/LICENSE.boringssl.txt (Combination ISC and OpenSSL license) + * HOMEPAGE: + * https://boringssl.googlesource.com/boringssl/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..beb8011 --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +![Build project](https://github.com/netty/netty-incubator-codec-quic/workflows/Build%20project/badge.svg) + +# Netty QUIC codec + +This is a new experimental QUIC codec for netty which makes use of [quiche](https://github.com/cloudflare/quiche). + +## How to include the dependency + +To include the dependency you need to ensure you also specify the right classifier. At the moment we only support Linux + x86_64 / aarch_64, macOS / OSX x86_64 / aarch_64 and Windows x86_64 but this may change. + +As an example this is how you would include the dependency in maven: +For Linux x86_64: +``` + + io.netty.incubator + netty-incubator-codec-native-quic + 0.0.21.Final + linux-x86_64 + +``` + +For macOS / OSX: + +``` + + io.netty.incubator + netty-incubator-codec-native-quic + 0.0.21.Final + osx-x86_64 + +``` + +For Windows: + +``` + + io.netty.incubator + netty-incubator-codec-native-quic + 0.0.21.Final + windows-x86_64 + +``` + +## How to use this codec ? + +For some examples please check our +[example package](https://github.com/netty/netty-incubator-codec-quic/tree/main/codec-native-quic/src/test/java/io/netty/incubator/codec/quic). +This contains a server and a client that can speak some limited HTTP/0.9 with each other. + +For more "advanced" use cases, consider checking our +[netty-incubator-codec-http3](https://github.com/netty/netty-incubator-codec-http3) project. diff --git a/codec-classes-quic/pom.xml b/codec-classes-quic/pom.xml new file mode 100644 index 0000000..d619650 --- /dev/null +++ b/codec-classes-quic/pom.xml @@ -0,0 +1,89 @@ + + + + 4.0.0 + + org.xbib.netty + netty-handler-codec-parent-quic + 0.0.56.Final-SNAPSHOT + + + netty-handler-codec-classes-quic + 0.0.56.Final-SNAPSHOT + Netty/Handler/Codec/Classes/Quic + jar + + + io.netty.handler.codec.classes.quic + + + + + + maven-jar-plugin + 3.2.0 + + + default-jar + + + + true + true + + + ${javaModuleName} + + true + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + + + + + + + + + + io.netty + netty-common + + + io.netty + netty-buffer + + + io.netty + netty-codec + + + io.netty + netty-handler + + + io.netty + netty-transport + + + io.netty + netty-transport-classes-epoll + + true + + + diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSL.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSL.java new file mode 100644 index 0000000..46f0bc9 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSL.java @@ -0,0 +1,129 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.handler.ssl.util.LazyX509Certificate; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; + +final class BoringSSL { + private BoringSSL() { } + + static final int SSL_VERIFY_NONE = BoringSSLNativeStaticallyReferencedJniMethods.ssl_verify_none(); + static final int SSL_VERIFY_FAIL_IF_NO_PEER_CERT = BoringSSLNativeStaticallyReferencedJniMethods + .ssl_verify_fail_if_no_peer_cert(); + static final int SSL_VERIFY_PEER = BoringSSLNativeStaticallyReferencedJniMethods.ssl_verify_peer(); + static final int X509_V_OK = BoringSSLNativeStaticallyReferencedJniMethods.x509_v_ok(); + static final int X509_V_ERR_CERT_HAS_EXPIRED = + BoringSSLNativeStaticallyReferencedJniMethods.x509_v_err_cert_has_expired(); + static final int X509_V_ERR_CERT_NOT_YET_VALID = + BoringSSLNativeStaticallyReferencedJniMethods.x509_v_err_cert_not_yet_valid(); + static final int X509_V_ERR_CERT_REVOKED = BoringSSLNativeStaticallyReferencedJniMethods.x509_v_err_cert_revoked(); + static final int X509_V_ERR_UNSPECIFIED = BoringSSLNativeStaticallyReferencedJniMethods.x509_v_err_unspecified(); + + static long SSLContext_new(boolean server, String[] applicationProtocols, + BoringSSLHandshakeCompleteCallback handshakeCompleteCallback, + BoringSSLCertificateCallback certificateCallback, + BoringSSLCertificateVerifyCallback verifyCallback, + BoringSSLTlsextServernameCallback servernameCallback, + BoringSSLKeylogCallback keylogCallback, + BoringSSLSessionCallback sessionCallback, + BoringSSLPrivateKeyMethod privateKeyMethod, + BoringSSLSessionTicketCallback sessionTicketCallback, + int verifyMode, + byte[][] subjectNames) { + return SSLContext_new0(server, toWireFormat(applicationProtocols), + handshakeCompleteCallback, certificateCallback, verifyCallback, servernameCallback, + keylogCallback, sessionCallback, privateKeyMethod, sessionTicketCallback, verifyMode, subjectNames); + } + + private static byte[] toWireFormat(String[] applicationProtocols) { + if (applicationProtocols == null) { + return null; + } + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + for (String p : applicationProtocols) { + byte[] bytes = p.getBytes(StandardCharsets.US_ASCII); + out.write(bytes.length); + out.write(bytes); + } + return out.toByteArray(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + private static native long SSLContext_new0(boolean server, + byte[] applicationProtocols, Object handshakeCompleteCallback, + Object certificateCallback, Object verifyCallback, + Object servernameCallback, Object keylogCallback, + Object sessionCallback, + Object privateKeyMethod, + Object sessionTicketCallback, + int verifyDepth, byte[][] subjectNames); + static native void SSLContext_set_early_data_enabled(long context, boolean enabled); + static native long SSLContext_setSessionCacheSize(long context, long size); + static native long SSLContext_setSessionCacheTimeout(long context, long size); + + static native void SSLContext_setSessionTicketKeys(long context, boolean enableCallback); + + static native void SSLContext_free(long context); + static long SSL_new(long context, boolean server, String hostname) { + return SSL_new0(context, server, tlsExtHostName(hostname)); + } + static native long SSL_new0(long context, boolean server, String hostname); + static native void SSL_free(long ssl); + + static native Runnable SSL_getTask(long ssl); + + static native void SSL_cleanup(long ssl); + + static native long EVP_PKEY_parse(byte[] bytes, String pass); + static native void EVP_PKEY_free(long key); + + static native long CRYPTO_BUFFER_stack_new(long ssl, byte[][] bytes); + static native void CRYPTO_BUFFER_stack_free(long chain); + + static native String ERR_last_error(); + + private static String tlsExtHostName(String hostname) { + if (hostname != null && hostname.endsWith(".")) { + // Strip trailing dot if included. + // See https://github.com/netty/netty-tcnative/issues/400 + hostname = hostname.substring(0, hostname.length() - 1); + } + return hostname; + } + + static X509Certificate[] certificates(byte[][] chain) { + X509Certificate[] peerCerts = new X509Certificate[chain.length]; + for (int i = 0; i < peerCerts.length; i++) { + peerCerts[i] = new LazyX509Certificate(chain[i]); + } + return peerCerts; + } + + static byte[][] subjectNames(X509Certificate[] certificates) { + byte[][] subjectNames = new byte[certificates.length][]; + for (int i = 0; i < certificates.length; i++) { + subjectNames[i] = certificates[i].getSubjectX500Principal().getEncoded(); + } + return subjectNames; + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLAsyncPrivateKeyMethod.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLAsyncPrivateKeyMethod.java new file mode 100644 index 0000000..c4a87b6 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLAsyncPrivateKeyMethod.java @@ -0,0 +1,57 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.util.concurrent.Future; + +import javax.net.ssl.SSLEngine; + +public interface BoringSSLAsyncPrivateKeyMethod { + int SSL_SIGN_RSA_PKCS1_SHA1 = BoringSSLPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA1; + int SSL_SIGN_RSA_PKCS1_SHA256 = BoringSSLPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA256; + int SSL_SIGN_RSA_PKCS1_SHA384 = BoringSSLPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA384; + int SSL_SIGN_RSA_PKCS1_SHA512 = BoringSSLPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA512; + int SSL_SIGN_ECDSA_SHA1 = BoringSSLPrivateKeyMethod.SSL_SIGN_ECDSA_SHA1; + int SSL_SIGN_ECDSA_SECP256R1_SHA256 = BoringSSLPrivateKeyMethod.SSL_SIGN_ECDSA_SECP256R1_SHA256; + int SSL_SIGN_ECDSA_SECP384R1_SHA384 = BoringSSLPrivateKeyMethod.SSL_SIGN_ECDSA_SECP384R1_SHA384; + int SSL_SIGN_ECDSA_SECP521R1_SHA512 = BoringSSLPrivateKeyMethod.SSL_SIGN_ECDSA_SECP521R1_SHA512; + int SSL_SIGN_RSA_PSS_RSAE_SHA256 = BoringSSLPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA256; + int SSL_SIGN_RSA_PSS_RSAE_SHA384 = BoringSSLPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA384; + int SSL_SIGN_RSA_PSS_RSAE_SHA512 = BoringSSLPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA512; + int SSL_SIGN_ED25519 = BoringSSLPrivateKeyMethod.SSL_SIGN_ED25519; + int SSL_SIGN_RSA_PKCS1_MD5_SHA1 = BoringSSLPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_MD5_SHA1; + + /** + * Signs the input with the given key and notifies the returned {@link Future} with the signed bytes. + * + * @param engine the {@link SSLEngine} + * @param signatureAlgorithm the algorithm to use for signing + * @param input the digest itself + * @return the {@link Future} that will be notified with the signed data + * (must not be {@code null}) when the operation completes. + */ + Future sign(SSLEngine engine, int signatureAlgorithm, byte[] input); + + /** + * Decrypts the input with the given key and notifies the returned {@link Future} with the decrypted bytes. + * + * @param engine the {@link SSLEngine} + * @param input the input which should be decrypted + * @return the {@link Future} that will be notified with the decrypted data + * (must not be {@code null}) when the operation completes. + */ + Future decrypt(SSLEngine engine, byte[] input); +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLCertificateCallback.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLCertificateCallback.java new file mode 100644 index 0000000..d251676 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLCertificateCallback.java @@ -0,0 +1,276 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + + +import io.netty.util.CharsetUtil; + +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.security.auth.x500.X500Principal; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.PrivateKey; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +final class BoringSSLCertificateCallback { + private static final byte[] BEGIN_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n".getBytes(CharsetUtil.US_ASCII); + private static final byte[] END_PRIVATE_KEY = "\n-----END PRIVATE KEY-----\n".getBytes(CharsetUtil.US_ASCII); + + /** + * The types contained in the {@code keyTypeBytes} array. + */ + // Extracted from https://github.com/openssl/openssl/blob/master/include/openssl/tls1.h + private static final byte TLS_CT_RSA_SIGN = 1; + private static final byte TLS_CT_DSS_SIGN = 2; + private static final byte TLS_CT_RSA_FIXED_DH = 3; + private static final byte TLS_CT_DSS_FIXED_DH = 4; + private static final byte TLS_CT_ECDSA_SIGN = 64; + private static final byte TLS_CT_RSA_FIXED_ECDH = 65; + private static final byte TLS_CT_ECDSA_FIXED_ECDH = 66; + + // Code in this class is inspired by code of conscrypts: + // - https://android.googlesource.com/platform/external/ + // conscrypt/+/master/src/main/java/org/conscrypt/OpenSSLEngineImpl.java + // - https://android.googlesource.com/platform/external/ + // conscrypt/+/master/src/main/java/org/conscrypt/SSLParametersImpl.java + // + static final String KEY_TYPE_RSA = "RSA"; + static final String KEY_TYPE_DH_RSA = "DH_RSA"; + static final String KEY_TYPE_EC = "EC"; + static final String KEY_TYPE_EC_EC = "EC_EC"; + static final String KEY_TYPE_EC_RSA = "EC_RSA"; + + // key type mappings for types. + private static final Map KEY_TYPES = new HashMap(); + static { + KEY_TYPES.put("RSA", KEY_TYPE_RSA); + KEY_TYPES.put("DHE_RSA", KEY_TYPE_RSA); + KEY_TYPES.put("ECDHE_RSA", KEY_TYPE_RSA); + KEY_TYPES.put("ECDHE_ECDSA", KEY_TYPE_EC); + KEY_TYPES.put("ECDH_RSA", KEY_TYPE_EC_RSA); + KEY_TYPES.put("ECDH_ECDSA", KEY_TYPE_EC_EC); + KEY_TYPES.put("DH_RSA", KEY_TYPE_DH_RSA); + } + + private static final Set SUPPORTED_KEY_TYPES = Collections.unmodifiableSet(new LinkedHashSet<>( + Arrays.asList(KEY_TYPE_RSA, + KEY_TYPE_DH_RSA, + KEY_TYPE_EC, + KEY_TYPE_EC_RSA, + KEY_TYPE_EC_EC))); + + // Directly returning this is safe as we never modify it within our JNI code. + private static final long[] NO_KEY_MATERIAL_CLIENT_SIDE = new long[] { 0, 0 }; + + private final QuicheQuicSslEngineMap engineMap; + private final X509ExtendedKeyManager keyManager; + private final String password; + + BoringSSLCertificateCallback(QuicheQuicSslEngineMap engineMap, X509ExtendedKeyManager keyManager, String password) { + this.engineMap = engineMap; + this.keyManager = keyManager; + this.password = password; + } + + @SuppressWarnings("unused") + long[] handle(long ssl, byte[] keyTypeBytes, byte[][] asn1DerEncodedPrincipals, String[] authMethods) { + QuicheQuicSslEngine engine = engineMap.get(ssl); + if (engine == null) { + return null; + } + + try { + if (keyManager == null) { + if (engine.getUseClientMode()) { + return NO_KEY_MATERIAL_CLIENT_SIDE; + } + return null; + } + if (engine.getUseClientMode()) { + final Set keyTypesSet = supportedClientKeyTypes(keyTypeBytes); + final String[] keyTypes = keyTypesSet.toArray(new String[0]); + final X500Principal[] issuers; + if (asn1DerEncodedPrincipals == null) { + issuers = null; + } else { + issuers = new X500Principal[asn1DerEncodedPrincipals.length]; + for (int i = 0; i < asn1DerEncodedPrincipals.length; i++) { + issuers[i] = new X500Principal(asn1DerEncodedPrincipals[i]); + } + } + return removeMappingIfNeeded(ssl, selectKeyMaterialClientSide(ssl, engine, keyTypes, issuers)); + } else { + // For now we just ignore the asn1DerEncodedPrincipals as this is kind of inline with what the + // OpenJDK SSLEngineImpl does. + return removeMappingIfNeeded(ssl, selectKeyMaterialServerSide(ssl, engine, authMethods)); + } + } catch (SSLException e) { + engineMap.remove(ssl); + return null; + } catch (Throwable cause) { + engineMap.remove(ssl); + throw cause; + } + } + + private long[] removeMappingIfNeeded(long ssl, long[] result) { + if (result == null) { + engineMap.remove(ssl); + } + return result; + } + + private long[] selectKeyMaterialServerSide(long ssl, QuicheQuicSslEngine engine, String[] authMethods) + throws SSLException { + if (authMethods.length == 0) { + throw new SSLHandshakeException("Unable to find key material"); + } + + // authMethods may contain duplicates or may result in the same type + // but call chooseServerAlias(...) may be expensive. So let's ensure + // we filter out duplicates. + Set typeSet = new HashSet(KEY_TYPES.size()); + for (String authMethod : authMethods) { + String type = KEY_TYPES.get(authMethod); + if (type != null && typeSet.add(type)) { + String alias = chooseServerAlias(engine, type); + if (alias != null) { + return selectMaterial(ssl, engine, alias) ; + } + } + } + throw new SSLHandshakeException("Unable to find key material for auth method(s): " + + Arrays.toString(authMethods)); + } + + private long[] selectKeyMaterialClientSide(long ssl, QuicheQuicSslEngine engine, String[] keyTypes, + X500Principal[] issuer) { + String alias = chooseClientAlias(engine, keyTypes, issuer); + // Only try to set the keymaterial if we have a match. This is also consistent with what OpenJDK does: + // https://hg.openjdk.java.net/jdk/jdk11/file/76072a077ee1/ + // src/java.base/share/classes/sun/security/ssl/CertificateRequest.java#l362 + if (alias != null) { + return selectMaterial(ssl, engine, alias) ; + } + return NO_KEY_MATERIAL_CLIENT_SIDE; + } + + private long[] selectMaterial(long ssl, QuicheQuicSslEngine engine, String alias) { + X509Certificate[] certificates = keyManager.getCertificateChain(alias); + if (certificates == null || certificates.length == 0) { + return null; + } + byte[][] certs = new byte[certificates.length][]; + + for (int i = 0; i < certificates.length; i++) { + try { + certs[i] = certificates[i].getEncoded(); + } catch (CertificateEncodingException e) { + return null; + } + } + + final long key; + PrivateKey privateKey = keyManager.getPrivateKey(alias); + if (privateKey == BoringSSLKeylessPrivateKey.INSTANCE) { + key = 0; + } else { + byte[] pemKey = toPemEncoded(privateKey); + if (pemKey == null) { + return null; + } + key = BoringSSL.EVP_PKEY_parse(pemKey, password); + } + long chain = BoringSSL.CRYPTO_BUFFER_stack_new(ssl, certs); + engine.setLocalCertificateChain(certificates); + + // Return and signal that the key and chain should be released as well. + return new long[] { key, chain }; + } + + private static byte[] toPemEncoded(PrivateKey key) { + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + out.write(BEGIN_PRIVATE_KEY); + out.write(Base64.getEncoder().encode(key.getEncoded())); + out.write(END_PRIVATE_KEY); + return out.toByteArray(); + } catch (IOException e) { + return null; + } + } + private String chooseClientAlias(QuicheQuicSslEngine engine, + String[] keyTypes, X500Principal[] issuer) { + return keyManager.chooseEngineClientAlias(keyTypes, issuer, engine); + } + + private String chooseServerAlias(QuicheQuicSslEngine engine, String type) { + return keyManager.chooseEngineServerAlias(type, null, engine); + } + + /** + * Gets the supported key types for client certificates. + * + * @param clientCertificateTypes {@code ClientCertificateType} values provided by the server. + * See https://www.ietf.org/assignments/tls-parameters/tls-parameters.xml. + * @return supported key types that can be used in {@code X509KeyManager.chooseClientAlias} and + * {@code X509ExtendedKeyManager.chooseEngineClientAlias}. + */ + private static Set supportedClientKeyTypes(byte[] clientCertificateTypes) { + if (clientCertificateTypes == null) { + // Try all of the supported key types. + return SUPPORTED_KEY_TYPES; + } + Set result = new HashSet<>(clientCertificateTypes.length); + for (byte keyTypeCode : clientCertificateTypes) { + String keyType = clientKeyType(keyTypeCode); + if (keyType == null) { + // Unsupported client key type -- ignore + continue; + } + result.add(keyType); + } + return result; + } + + private static String clientKeyType(byte clientCertificateType) { + // See also https://www.ietf.org/assignments/tls-parameters/tls-parameters.xml + switch (clientCertificateType) { + case TLS_CT_RSA_SIGN: + return KEY_TYPE_RSA; // RFC rsa_sign + case TLS_CT_RSA_FIXED_DH: + return KEY_TYPE_DH_RSA; // RFC rsa_fixed_dh + case TLS_CT_ECDSA_SIGN: + return KEY_TYPE_EC; // RFC ecdsa_sign + case TLS_CT_RSA_FIXED_ECDH: + return KEY_TYPE_EC_RSA; // RFC rsa_fixed_ecdh + case TLS_CT_ECDSA_FIXED_ECDH: + return KEY_TYPE_EC_EC; // RFC ecdsa_fixed_ecdh + default: + return null; + } + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLCertificateCallbackTask.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLCertificateCallbackTask.java new file mode 100644 index 0000000..7c57f6a --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLCertificateCallbackTask.java @@ -0,0 +1,73 @@ +/* + * Copyright 2022 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +/** + * Execute {@link BoringSSLCertificateCallback#handle(long, byte[], byte[][], String[])}. + */ +final class BoringSSLCertificateCallbackTask extends BoringSSLTask { + private final byte[] keyTypeBytes; + private final byte[][] asn1DerEncodedPrincipals; + private final String[] authMethods; + private final BoringSSLCertificateCallback callback; + + // Accessed via JNI. + private long key; + private long chain; + + BoringSSLCertificateCallbackTask(long ssl, byte[] keyTypeBytes, byte[][] asn1DerEncodedPrincipals, String[] authMethods, + BoringSSLCertificateCallback callback) { + // It is important that this constructor never throws. Be sure to not change this! + super(ssl); + // It's ok to not clone the arrays as we create these in JNI and not-reuse. + this.keyTypeBytes = keyTypeBytes; + this.asn1DerEncodedPrincipals = asn1DerEncodedPrincipals; + this.authMethods = authMethods; + this.callback = callback; + } + + // See https://www.openssl.org/docs/man1.0.2/man3/SSL_set_cert_cb.html. + @Override + protected void runTask(long ssl, TaskCallback taskCallback) { + try { + long[] result = callback.handle(ssl, keyTypeBytes, asn1DerEncodedPrincipals, authMethods); + if (result == null) { + taskCallback.onResult(ssl, 0); + } else { + this.key = result[0]; + this.chain = result[1]; + taskCallback.onResult(ssl, 1); + } + } catch (Exception e) { + // Just catch the exception and return 0 to fail the handshake. + // The problem is that rethrowing here is really "useless" as we will process it as part of an openssl + // c callback which needs to return 0 for an error to abort the handshake. + taskCallback.onResult(ssl, 0); + } + } + + @Override + protected void destroy() { + if (key != 0) { + BoringSSL.EVP_PKEY_free(key); + key = 0; + } + if (chain != 0) { + BoringSSL.CRYPTO_BUFFER_stack_free(chain); + chain = 0; + } + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLCertificateVerifyCallback.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLCertificateVerifyCallback.java new file mode 100644 index 0000000..509ba35 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLCertificateVerifyCallback.java @@ -0,0 +1,124 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.handler.ssl.OpenSslCertificateException; + +import javax.net.ssl.X509ExtendedTrustManager; +import javax.net.ssl.X509TrustManager; +import java.security.cert.CertPathValidatorException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.CertificateRevokedException; +import java.security.cert.X509Certificate; + +final class BoringSSLCertificateVerifyCallback { + + private static final boolean TRY_USING_EXTENDED_TRUST_MANAGER; + static { + boolean tryUsingExtendedTrustManager; + try { + Class.forName(X509ExtendedTrustManager.class.getName()); + tryUsingExtendedTrustManager = true; + } catch (Throwable cause) { + tryUsingExtendedTrustManager = false; + } + TRY_USING_EXTENDED_TRUST_MANAGER = tryUsingExtendedTrustManager; + } + + private final QuicheQuicSslEngineMap engineMap; + private final X509TrustManager manager; + + BoringSSLCertificateVerifyCallback(QuicheQuicSslEngineMap engineMap, X509TrustManager manager) { + this.engineMap = engineMap; + this.manager = manager; + } + + @SuppressWarnings("unused") + int verify(long ssl, byte[][] x509, String authAlgorithm) { + final QuicheQuicSslEngine engine = engineMap.get(ssl); + if (engine == null) { + // May be null if it was destroyed in the meantime. + return BoringSSL.X509_V_ERR_UNSPECIFIED; + } + + if (manager == null) { + engineMap.remove(ssl); + return BoringSSL.X509_V_ERR_UNSPECIFIED; + } + + X509Certificate[] peerCerts = BoringSSL.certificates(x509); + try { + if (engine.getUseClientMode()) { + if (TRY_USING_EXTENDED_TRUST_MANAGER && manager instanceof X509ExtendedTrustManager) { + ((X509ExtendedTrustManager) manager).checkServerTrusted(peerCerts, authAlgorithm, engine); + } else { + manager.checkServerTrusted(peerCerts, authAlgorithm); + } + } else { + if (TRY_USING_EXTENDED_TRUST_MANAGER && manager instanceof X509ExtendedTrustManager) { + ((X509ExtendedTrustManager) manager).checkClientTrusted(peerCerts, authAlgorithm, engine); + } else { + manager.checkClientTrusted(peerCerts, authAlgorithm); + } + } + return BoringSSL.X509_V_OK; + } catch (Throwable cause) { + engineMap.remove(ssl); + // Try to extract the correct error code that should be used. + if (cause instanceof OpenSslCertificateException) { + // This will never return a negative error code as its validated when constructing the + // OpenSslCertificateException. + return ((OpenSslCertificateException) cause).errorCode(); + } + if (cause instanceof CertificateExpiredException) { + return BoringSSL.X509_V_ERR_CERT_HAS_EXPIRED; + } + if (cause instanceof CertificateNotYetValidException) { + return BoringSSL.X509_V_ERR_CERT_NOT_YET_VALID; + } + return translateToError(cause); + } + } + + private static int translateToError(Throwable cause) { + if (cause instanceof CertificateRevokedException) { + return BoringSSL.X509_V_ERR_CERT_REVOKED; + } + + // The X509TrustManagerImpl uses a Validator which wraps a CertPathValidatorException into + // an CertificateException. So we need to handle the wrapped CertPathValidatorException to be + // able to send the correct alert. + Throwable wrapped = cause.getCause(); + while (wrapped != null) { + if (wrapped instanceof CertPathValidatorException) { + CertPathValidatorException ex = (CertPathValidatorException) wrapped; + CertPathValidatorException.Reason reason = ex.getReason(); + if (reason == CertPathValidatorException.BasicReason.EXPIRED) { + return BoringSSL.X509_V_ERR_CERT_HAS_EXPIRED; + } + if (reason == CertPathValidatorException.BasicReason.NOT_YET_VALID) { + return BoringSSL.X509_V_ERR_CERT_NOT_YET_VALID; + } + if (reason == CertPathValidatorException.BasicReason.REVOKED) { + return BoringSSL.X509_V_ERR_CERT_REVOKED; + } + } + wrapped = wrapped.getCause(); + } + return BoringSSL.X509_V_ERR_UNSPECIFIED; + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLCertificateVerifyCallbackTask.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLCertificateVerifyCallbackTask.java new file mode 100644 index 0000000..5a45dc0 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLCertificateVerifyCallbackTask.java @@ -0,0 +1,40 @@ +/* + * Copyright 2022 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + + +/** + * Execute {@link BoringSSLCertificateVerifyCallback#verify(long, byte[][], String)}. + */ +final class BoringSSLCertificateVerifyCallbackTask extends BoringSSLTask { + private final byte[][] x509; + private final String authAlgorithm; + private final BoringSSLCertificateVerifyCallback verifier; + + BoringSSLCertificateVerifyCallbackTask(long ssl, byte[][] x509, String authAlgorithm, + BoringSSLCertificateVerifyCallback verifier) { + super(ssl); + this.x509 = x509; + this.authAlgorithm = authAlgorithm; + this.verifier = verifier; + } + + @Override + protected void runTask(long ssl, TaskCallback callback) { + int result = verifier.verify(ssl, x509, authAlgorithm); + callback.onResult(ssl, result); + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLHandshakeCompleteCallback.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLHandshakeCompleteCallback.java new file mode 100644 index 0000000..b44ef71 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLHandshakeCompleteCallback.java @@ -0,0 +1,36 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +final class BoringSSLHandshakeCompleteCallback { + + private final QuicheQuicSslEngineMap map; + + BoringSSLHandshakeCompleteCallback(QuicheQuicSslEngineMap map) { + this.map = map; + } + + @SuppressWarnings("unused") + void handshakeComplete(long ssl, byte[] id, String cipher, String protocol, byte[] peerCertificate, + byte[][] peerCertificateChain, long creationTime, long timeout, byte[] applicationProtocol, + boolean sessionReused) { + QuicheQuicSslEngine engine = map.get(ssl); + if (engine != null) { + engine.handshakeFinished(id, cipher, protocol, peerCertificate, peerCertificateChain, creationTime, + timeout, applicationProtocol, sessionReused); + } + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLKeylessManagerFactory.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLKeylessManagerFactory.java new file mode 100644 index 0000000..65cb430 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLKeylessManagerFactory.java @@ -0,0 +1,245 @@ +/* + * Copyright 2022 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.KeyManagerFactorySpi; +import javax.net.ssl.ManagerFactoryParameters; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.security.Key; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.KeyStoreSpi; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.Date; +import java.util.Enumeration; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static java.util.Objects.requireNonNull; + +/** + * {@link KeyManagerFactory} that can be used to support custom key signing via {@link BoringSSLAsyncPrivateKeyMethod}. + */ +public final class BoringSSLKeylessManagerFactory extends KeyManagerFactory { + + final BoringSSLAsyncPrivateKeyMethod privateKeyMethod; + + private BoringSSLKeylessManagerFactory(KeyManagerFactory keyManagerFactory, + BoringSSLAsyncPrivateKeyMethod privateKeyMethod) { + super(new KeylessManagerFactorySpi(keyManagerFactory), + keyManagerFactory.getProvider(), keyManagerFactory.getAlgorithm()); + this.privateKeyMethod = requireNonNull(privateKeyMethod, "privateKeyMethod"); + } + + /** + * Creates a new factory instance. + * + * @param privateKeyMethod the {@link BoringSSLAsyncPrivateKeyMethod} that is used for key signing. + * @param chain the {@link File} that contains the {@link X509Certificate} chain. + * @return a new factory instance. + * @throws CertificateException on error. + * @throws IOException on error. + * @throws KeyStoreException on error. + * @throws NoSuchAlgorithmException on error. + * @throws UnrecoverableKeyException on error. + */ + public static BoringSSLKeylessManagerFactory newKeyless(BoringSSLAsyncPrivateKeyMethod privateKeyMethod, File chain) + throws CertificateException, IOException, + KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { + return newKeyless(privateKeyMethod, Files.newInputStream(chain.toPath())); + } + + /** + * Creates a new factory instance. + * + * @param privateKeyMethod the {@link BoringSSLAsyncPrivateKeyMethod} that is used for key signing. + * @param chain the {@link InputStream} that contains the {@link X509Certificate} chain. + * @return a new factory instance. + * @throws CertificateException on error. + * @throws IOException on error. + * @throws KeyStoreException on error. + * @throws NoSuchAlgorithmException on error. + * @throws UnrecoverableKeyException on error. + */ + public static BoringSSLKeylessManagerFactory newKeyless(BoringSSLAsyncPrivateKeyMethod privateKeyMethod, + InputStream chain) + throws CertificateException, IOException, + KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { + return newKeyless(privateKeyMethod, QuicSslContext.toX509Certificates0(chain)); + } + + /** + * Creates a new factory instance. + * + * @param privateKeyMethod the {@link BoringSSLAsyncPrivateKeyMethod} that is used for key signing. + * @param certificateChain the {@link X509Certificate} chain. + * @return a new factory instance. + * @throws CertificateException on error. + * @throws IOException on error. + * @throws KeyStoreException on error. + * @throws NoSuchAlgorithmException on error. + * @throws UnrecoverableKeyException on error. + */ + public static BoringSSLKeylessManagerFactory newKeyless(BoringSSLAsyncPrivateKeyMethod privateKeyMethod, + X509Certificate... certificateChain) + throws CertificateException, IOException, + KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { + checkNotNull(certificateChain, "certificateChain"); + KeyStore store = new KeylessKeyStore(certificateChain.clone()); + store.load(null, null); + BoringSSLKeylessManagerFactory factory = new BoringSSLKeylessManagerFactory( + KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()), privateKeyMethod); + factory.init(store, null); + return factory; + } + + private static final class KeylessManagerFactorySpi extends KeyManagerFactorySpi { + + private final KeyManagerFactory keyManagerFactory; + + KeylessManagerFactorySpi(KeyManagerFactory keyManagerFactory) { + this.keyManagerFactory = requireNonNull(keyManagerFactory, "keyManagerFactory"); + } + + @Override + protected void engineInit(KeyStore ks, char[] password) + throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { + keyManagerFactory.init(ks, password); + } + + @Override + protected void engineInit(ManagerFactoryParameters spec) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + protected KeyManager[] engineGetKeyManagers() { + return keyManagerFactory.getKeyManagers(); + } + } + private static final class KeylessKeyStore extends KeyStore { + private static final String ALIAS = "key"; + private KeylessKeyStore(final X509Certificate[] certificateChain) { + super(new KeyStoreSpi() { + + private final Date creationDate = new Date(); + + @Override + public Key engineGetKey(String alias, char[] password) { + if (engineContainsAlias(alias)) { + return BoringSSLKeylessPrivateKey.INSTANCE; + } + return null; + } + + @Override + public Certificate[] engineGetCertificateChain(String alias) { + return engineContainsAlias(alias)? certificateChain.clone() : null; + } + + @Override + public Certificate engineGetCertificate(String alias) { + return engineContainsAlias(alias)? certificateChain[0] : null; + } + + @Override + public Date engineGetCreationDate(String alias) { + return engineContainsAlias(alias)? creationDate : null; + } + + @Override + public void engineSetKeyEntry(String alias, Key key, char[] password, Certificate[] chain) + throws KeyStoreException { + throw new KeyStoreException("Not supported"); + } + + @Override + public void engineSetKeyEntry(String alias, byte[] key, Certificate[] chain) throws KeyStoreException { + throw new KeyStoreException("Not supported"); + } + + @Override + public void engineSetCertificateEntry(String alias, Certificate cert) throws KeyStoreException { + throw new KeyStoreException("Not supported"); + } + + @Override + public void engineDeleteEntry(String alias) throws KeyStoreException { + throw new KeyStoreException("Not supported"); + } + + @Override + public Enumeration engineAliases() { + return Collections.enumeration(Collections.singleton(ALIAS)); + } + + @Override + public boolean engineContainsAlias(String alias) { + return ALIAS.equals(alias); + } + + @Override + public int engineSize() { + return 1; + } + + @Override + public boolean engineIsKeyEntry(String alias) { + return engineContainsAlias(alias); + } + + @Override + public boolean engineIsCertificateEntry(String alias) { + return engineContainsAlias(alias); + } + + @Override + public String engineGetCertificateAlias(Certificate cert) { + if (cert instanceof X509Certificate) { + for (X509Certificate x509Certificate : certificateChain) { + if (x509Certificate.equals(cert)) { + return ALIAS; + } + } + } + return null; + } + + @Override + public void engineStore(OutputStream stream, char[] password) { + throw new UnsupportedOperationException(); + } + + @Override + public void engineLoad(InputStream stream, char[] password) { + if (stream != null && password != null) { + throw new UnsupportedOperationException(); + } + } + }, null, "keyless"); + } + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLKeylessPrivateKey.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLKeylessPrivateKey.java new file mode 100644 index 0000000..2a19eac --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLKeylessPrivateKey.java @@ -0,0 +1,43 @@ +/* + * Copyright 2022 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.util.internal.EmptyArrays; + +import java.security.PrivateKey; + +final class BoringSSLKeylessPrivateKey implements PrivateKey { + + static final BoringSSLKeylessPrivateKey INSTANCE = new BoringSSLKeylessPrivateKey(); + + private BoringSSLKeylessPrivateKey() { + } + + @Override + public String getAlgorithm() { + return "keyless"; + } + + @Override + public String getFormat() { + return "keyless"; + } + + @Override + public byte[] getEncoded() { + return EmptyArrays.EMPTY_BYTES; + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLKeylog.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLKeylog.java new file mode 100644 index 0000000..7d8e356 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLKeylog.java @@ -0,0 +1,35 @@ +/* + * Copyright 2022 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import javax.net.ssl.SSLEngine; + + +/** + * Allow to log keys, logging keys are following + * + * NSS Key Log Format. This is intended for debugging use with tools like Wireshark. + */ +public interface BoringSSLKeylog { + + /** + * Called when a key should be logged. + * + * @param engine the engine. + * @param key the key. + */ + void logKey(SSLEngine engine, String key); +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLKeylogCallback.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLKeylogCallback.java new file mode 100644 index 0000000..6ffdce2 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLKeylogCallback.java @@ -0,0 +1,38 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + + +import javax.net.ssl.SSLEngine; + +final class BoringSSLKeylogCallback { + + private final QuicheQuicSslEngineMap engineMap; + private final BoringSSLKeylog keylog; + + BoringSSLKeylogCallback(QuicheQuicSslEngineMap engineMap, BoringSSLKeylog keylog) { + this.engineMap = engineMap; + this.keylog = keylog; + } + + @SuppressWarnings("unused") + void logKey(long ssl, String key) { + SSLEngine engine = engineMap.get(ssl); + if (engine != null) { + keylog.logKey(engine, key); + } + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLLoggingKeylog.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLLoggingKeylog.java new file mode 100644 index 0000000..3e78dce --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLLoggingKeylog.java @@ -0,0 +1,35 @@ +/* + * Copyright 2022 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import javax.net.ssl.SSLEngine; + +final class BoringSSLLoggingKeylog implements BoringSSLKeylog { + static final BoringSSLLoggingKeylog INSTANCE = new BoringSSLLoggingKeylog(); + + private BoringSSLLoggingKeylog() { + } + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(BoringSSLLoggingKeylog.class); + + @Override + public void logKey(SSLEngine engine, String key) { + logger.debug(key); + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLNativeStaticallyReferencedJniMethods.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLNativeStaticallyReferencedJniMethods.java new file mode 100644 index 0000000..771d0b8 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLNativeStaticallyReferencedJniMethods.java @@ -0,0 +1,43 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +final class BoringSSLNativeStaticallyReferencedJniMethods { + static native int ssl_verify_none(); + static native int ssl_verify_peer(); + static native int ssl_verify_fail_if_no_peer_cert(); + + static native int x509_v_ok(); + static native int x509_v_err_cert_has_expired(); + static native int x509_v_err_cert_not_yet_valid(); + static native int x509_v_err_cert_revoked(); + static native int x509_v_err_unspecified(); + static native int ssl_sign_rsa_pkcs_sha1(); + static native int ssl_sign_rsa_pkcs_sha256(); + static native int ssl_sign_rsa_pkcs_sha384(); + static native int ssl_sign_rsa_pkcs_sha512(); + static native int ssl_sign_ecdsa_pkcs_sha1(); + static native int ssl_sign_ecdsa_secp256r1_sha256(); + static native int ssl_sign_ecdsa_secp384r1_sha384(); + static native int ssl_sign_ecdsa_secp521r1_sha512(); + static native int ssl_sign_rsa_pss_rsae_sha256(); + static native int ssl_sign_rsa_pss_rsae_sha384(); + static native int ssl_sign_rsa_pss_rsae_sha512(); + static native int ssl_sign_ed25519(); + static native int ssl_sign_rsa_pkcs1_md5_sha1(); + + private BoringSSLNativeStaticallyReferencedJniMethods() { } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLPrivateKeyMethod.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLPrivateKeyMethod.java new file mode 100644 index 0000000..691d6fa --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLPrivateKeyMethod.java @@ -0,0 +1,58 @@ +/* + * Copyright 2019 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import java.util.function.BiConsumer; + +/** + * Allows to customize private key signing / decrypt (when using RSA). + */ +interface BoringSSLPrivateKeyMethod { + int SSL_SIGN_RSA_PKCS1_SHA1 = BoringSSLNativeStaticallyReferencedJniMethods.ssl_sign_rsa_pkcs_sha1(); + int SSL_SIGN_RSA_PKCS1_SHA256 = BoringSSLNativeStaticallyReferencedJniMethods.ssl_sign_rsa_pkcs_sha256(); + int SSL_SIGN_RSA_PKCS1_SHA384 = BoringSSLNativeStaticallyReferencedJniMethods.ssl_sign_rsa_pkcs_sha384(); + int SSL_SIGN_RSA_PKCS1_SHA512 = BoringSSLNativeStaticallyReferencedJniMethods.ssl_sign_rsa_pkcs_sha512(); + int SSL_SIGN_ECDSA_SHA1 = BoringSSLNativeStaticallyReferencedJniMethods.ssl_sign_ecdsa_pkcs_sha1(); + int SSL_SIGN_ECDSA_SECP256R1_SHA256 = BoringSSLNativeStaticallyReferencedJniMethods.ssl_sign_ecdsa_secp256r1_sha256(); + int SSL_SIGN_ECDSA_SECP384R1_SHA384 = BoringSSLNativeStaticallyReferencedJniMethods.ssl_sign_ecdsa_secp384r1_sha384(); + int SSL_SIGN_ECDSA_SECP521R1_SHA512 = BoringSSLNativeStaticallyReferencedJniMethods.ssl_sign_ecdsa_secp521r1_sha512(); + int SSL_SIGN_RSA_PSS_RSAE_SHA256 = BoringSSLNativeStaticallyReferencedJniMethods.ssl_sign_rsa_pss_rsae_sha256(); + int SSL_SIGN_RSA_PSS_RSAE_SHA384 = BoringSSLNativeStaticallyReferencedJniMethods.ssl_sign_rsa_pss_rsae_sha384(); + int SSL_SIGN_RSA_PSS_RSAE_SHA512 = BoringSSLNativeStaticallyReferencedJniMethods.ssl_sign_rsa_pss_rsae_sha512(); + int SSL_SIGN_ED25519 = BoringSSLNativeStaticallyReferencedJniMethods.ssl_sign_ed25519(); + int SSL_SIGN_RSA_PKCS1_MD5_SHA1 = BoringSSLNativeStaticallyReferencedJniMethods.ssl_sign_rsa_pkcs1_md5_sha1(); + + /** + * Sign the input with given EC key and returns the signed bytes. + * + * @param ssl the SSL instance + * @param signatureAlgorithm the algorithm to use for signing + * @param input the input itself + * @return the sign + * @throws Exception thrown if an error accours while signing. + */ + void sign(long ssl, int signatureAlgorithm, byte[] input, BiConsumer callback); + + /** + * Decrypts the input with the given RSA key and returns the decrypted bytes. + * + * @param ssl the SSL instance + * @param input the input which should be decrypted + * @return the decrypted data + * @throws Exception thrown if an error accours while decrypting. + */ + void decrypt(long ssl, byte[] input, BiConsumer callback); +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLPrivateKeyMethodDecryptTask.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLPrivateKeyMethodDecryptTask.java new file mode 100644 index 0000000..de50246 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLPrivateKeyMethodDecryptTask.java @@ -0,0 +1,33 @@ +/* + * Copyright 2022 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import java.util.function.BiConsumer; + +final class BoringSSLPrivateKeyMethodDecryptTask extends BoringSSLPrivateKeyMethodTask { + private final byte[] input; + + BoringSSLPrivateKeyMethodDecryptTask(long ssl, byte[] input, BoringSSLPrivateKeyMethod method) { + super(ssl, method); + // It's OK to not clone the arrays as we create these in JNI and not reuse. + this.input = input; + } + + @Override + protected void runMethod(long ssl, BoringSSLPrivateKeyMethod method, BiConsumer consumer) { + method.decrypt(ssl, input, consumer); + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLPrivateKeyMethodSignTask.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLPrivateKeyMethodSignTask.java new file mode 100644 index 0000000..d98952f --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLPrivateKeyMethodSignTask.java @@ -0,0 +1,35 @@ +/* + * Copyright 2022 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import java.util.function.BiConsumer; + +final class BoringSSLPrivateKeyMethodSignTask extends BoringSSLPrivateKeyMethodTask { + private final int signatureAlgorithm; + private final byte[] digest; + + BoringSSLPrivateKeyMethodSignTask(long ssl, int signatureAlgorithm, byte[] digest, BoringSSLPrivateKeyMethod method) { + super(ssl, method); + this.signatureAlgorithm = signatureAlgorithm; + // It's OK to not clone the arrays as we create these in JNI and not reuse. + this.digest = digest; + } + + @Override + protected void runMethod(long ssl, BoringSSLPrivateKeyMethod method, BiConsumer callback) { + method.sign(ssl, signatureAlgorithm, digest, callback); + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLPrivateKeyMethodTask.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLPrivateKeyMethodTask.java new file mode 100644 index 0000000..31ba8ab --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLPrivateKeyMethodTask.java @@ -0,0 +1,32 @@ +package io.netty.handler.codec.quic; + +import java.util.function.BiConsumer; + +abstract class BoringSSLPrivateKeyMethodTask extends BoringSSLTask { + + private final BoringSSLPrivateKeyMethod method; + + // Will be accessed via JNI. + private byte[] resultBytes; + + BoringSSLPrivateKeyMethodTask(long ssl, BoringSSLPrivateKeyMethod method) { + super(ssl); + this.method = method; + } + + + @Override + protected final void runTask(long ssl, TaskCallback callback) { + runMethod(ssl, method, (result, error) -> { + if (result == null || error != null) { + callback.onResult(ssl, -1); + } else { + resultBytes = result; + callback.onResult(ssl, 1); + } + }); + } + + protected abstract void runMethod(long ssl, BoringSSLPrivateKeyMethod method, + BiConsumer callback); +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLSessionCallback.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLSessionCallback.java new file mode 100644 index 0000000..dab5f72 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLSessionCallback.java @@ -0,0 +1,83 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.util.internal.EmptyArrays; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +final class BoringSSLSessionCallback { + private static final InternalLogger logger = InternalLoggerFactory.getInstance(BoringSSLSessionCallback.class); + private final QuicClientSessionCache sessionCache; + private final QuicheQuicSslEngineMap engineMap; + + BoringSSLSessionCallback(QuicheQuicSslEngineMap engineMap, QuicClientSessionCache sessionCache) { + this.engineMap = engineMap; + this.sessionCache = sessionCache; + } + + @SuppressWarnings("unused") + void newSession(long ssl, long creationTime, long timeout, byte[] session, boolean isSingleUse, byte[] peerParams) { + if (sessionCache == null) { + return; + } + + QuicheQuicSslEngine engine = engineMap.get(ssl); + if (engine == null) { + logger.warn("engine is null ssl: {}", ssl); + return; + } + + if (peerParams == null) { + peerParams = EmptyArrays.EMPTY_BYTES; + } + if (logger.isDebugEnabled()) { + logger.debug("ssl: {}, session: {}, peerParams: {}", ssl, Arrays.toString(session), + Arrays.toString(peerParams)); + } + byte[] quicSession = toQuicheQuicSession(session, peerParams); + if (quicSession != null) { + logger.debug("save session host={}, port={}", + engine.getSession().getPeerHost(), engine.getSession().getPeerPort()); + sessionCache.saveSession(engine.getSession().getPeerHost(), engine.getSession().getPeerPort(), + TimeUnit.SECONDS.toMillis(creationTime), TimeUnit.SECONDS.toMillis(timeout), + quicSession, isSingleUse); + } + } + + // Mimic the encoding of quiche: https://github.com/cloudflare/quiche/blob/0.10.0/src/lib.rs#L1668 + private static byte[] toQuicheQuicSession(byte[] sslSession, byte[] peerParams) { + if (sslSession != null && peerParams != null) { + try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos)) { + dos.writeLong(sslSession.length); + dos.write(sslSession); + dos.writeLong(peerParams.length); + dos.write(peerParams); + return bos.toByteArray(); + } catch (IOException e) { + return null; + } + } + return null; + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLSessionTicketCallback.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLSessionTicketCallback.java new file mode 100644 index 0000000..1395f94 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLSessionTicketCallback.java @@ -0,0 +1,65 @@ +/* + * Copyright 2023 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.util.internal.PlatformDependent; + +final class BoringSSLSessionTicketCallback { + + // As we dont assume to have a lot of keys configured we will just use an array for now as a data store. + private volatile byte[][] sessionKeys; + + // Accessed via JNI. + byte[] findSessionTicket(byte[] keyname) { + byte[][] keys = this.sessionKeys; + if (keys == null || keys.length == 0) { + return null; + } + if (keyname == null) { + return keys[0]; + } + + for (int i = 0; i < keys.length; i++) { + byte[] key = keys[i]; + if (PlatformDependent.equals(keyname, 0, key, 1, keyname.length)) { + return key; + } + } + return null; + } + + void setSessionTicketKeys(SslSessionTicketKey[] keys) { + if (keys != null && keys.length != 0) { + byte[][] sessionKeys = new byte[keys.length][]; + for(int i = 0; i < keys.length; ++i) { + SslSessionTicketKey key = keys[i]; + byte[] binaryKey = new byte[49]; + // We mark the first key as preferred by using 1 as byte marker + binaryKey[0] = i == 0 ? (byte) 1 : (byte) 0; + int dstCurPos = 1; + System.arraycopy(key.name, 0, binaryKey, dstCurPos, 16); + dstCurPos += 16; + System.arraycopy(key.hmacKey, 0, binaryKey, dstCurPos, 16); + dstCurPos += 16; + System.arraycopy(key.aesKey, 0, binaryKey, dstCurPos, 16); + sessionKeys[i] = binaryKey; + } + this.sessionKeys = sessionKeys; + } else { + sessionKeys = null; + } + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLTask.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLTask.java new file mode 100644 index 0000000..c739d88 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLTask.java @@ -0,0 +1,60 @@ +/* + * Copyright 2022 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +/** + * A SSL related task that will be returned by {@link BoringSSL#SSL_getTask(long)}. + */ +abstract class BoringSSLTask implements Runnable { + private final long ssl; + protected boolean didRun; + + // These fields are accessed via JNI. + private int returnValue; + private volatile boolean complete; + + protected BoringSSLTask(long ssl) { + // It is important that this constructor never throws. Be sure to not change this! + this.ssl = ssl; + } + + @Override + public final void run() { + if (!didRun) { + didRun = true; + runTask(ssl, (long ssl, int result) -> { + returnValue = result; + complete = true; + }); + } + } + + /** + * Called once the task should be destroyed. + */ + protected void destroy() { + // Noop + } + + /** + * Run the task and return the return value that should be passed back to OpenSSL. + */ + protected abstract void runTask(long ssl, TaskCallback callback); + + interface TaskCallback { + void onResult(long ssl, int result); + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLTlsextServernameCallback.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLTlsextServernameCallback.java new file mode 100644 index 0000000..324ca6d --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/BoringSSLTlsextServernameCallback.java @@ -0,0 +1,45 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.util.Mapping; + +final class BoringSSLTlsextServernameCallback { + + private final QuicheQuicSslEngineMap engineMap; + private final Mapping mapping; + + BoringSSLTlsextServernameCallback(QuicheQuicSslEngineMap engineMap, + Mapping mapping) { + this.engineMap = engineMap; + this.mapping = mapping; + } + + @SuppressWarnings("unused") + long selectCtx(long ssl, String serverName) { + final QuicheQuicSslEngine engine = engineMap.get(ssl); + if (engine == null) { + // May be null if it was destroyed in the meantime. + return -1; + } + + QuicSslContext context = mapping.map(serverName); + if (context == null) { + return -1; + } + return engine.moveTo(serverName, (QuicheQuicSslContext) context); + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/DefaultQuicStreamFrame.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/DefaultQuicStreamFrame.java new file mode 100644 index 0000000..ac78d13 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/DefaultQuicStreamFrame.java @@ -0,0 +1,110 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.DefaultByteBufHolder; + +public final class DefaultQuicStreamFrame extends DefaultByteBufHolder implements QuicStreamFrame { + + private final boolean fin; + + public DefaultQuicStreamFrame(ByteBuf data, boolean fin) { + super(data); + this.fin = fin; + } + + @Override + public boolean hasFin() { + return fin; + } + + @Override + public QuicStreamFrame copy() { + return new DefaultQuicStreamFrame(content().copy(), fin); + } + + @Override + public QuicStreamFrame duplicate() { + return new DefaultQuicStreamFrame(content().duplicate(), fin); + } + + @Override + public QuicStreamFrame retainedDuplicate() { + return new DefaultQuicStreamFrame(content().retainedDuplicate(), fin); + } + + @Override + public QuicStreamFrame replace(ByteBuf content) { + return new DefaultQuicStreamFrame(content, fin); + } + + @Override + public QuicStreamFrame retain() { + super.retain(); + return this; + } + + @Override + public QuicStreamFrame retain(int increment) { + super.retain(increment); + return this; + } + + @Override + public QuicStreamFrame touch() { + super.touch(); + return this; + } + + @Override + public QuicStreamFrame touch(Object hint) { + super.touch(hint); + return this; + } + + @Override + public String toString() { + return "DefaultQuicStreamFrame{" + + "fin=" + fin + + ", content=" + contentToString() + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DefaultQuicStreamFrame that = (DefaultQuicStreamFrame) o; + + if (fin != that.fin) { + return false; + } + + return super.equals(o); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (fin ? 1 : 0); + return result; + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/DirectIoByteBufAllocator.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/DirectIoByteBufAllocator.java new file mode 100644 index 0000000..72a7fe6 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/DirectIoByteBufAllocator.java @@ -0,0 +1,136 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; + +final class DirectIoByteBufAllocator implements ByteBufAllocator { + + private final ByteBufAllocator wrapped; + + DirectIoByteBufAllocator(ByteBufAllocator wrapped) { + if (wrapped instanceof DirectIoByteBufAllocator) { + wrapped = ((DirectIoByteBufAllocator) wrapped).wrapped(); + } + this.wrapped = wrapped; + } + + ByteBufAllocator wrapped() { + return wrapped; + } + + @Override + public ByteBuf buffer() { + return wrapped.buffer(); + } + + @Override + public ByteBuf buffer(int initialCapacity) { + return wrapped.buffer(initialCapacity); + } + + @Override + public ByteBuf buffer(int initialCapacity, int maxCapacity) { + return wrapped.buffer(initialCapacity, maxCapacity); + } + + @Override + public ByteBuf ioBuffer() { + return directBuffer(); + } + + @Override + public ByteBuf ioBuffer(int initialCapacity) { + return directBuffer(initialCapacity); + } + + @Override + public ByteBuf ioBuffer(int initialCapacity, int maxCapacity) { + return directBuffer(initialCapacity, maxCapacity); + } + + @Override + public ByteBuf heapBuffer() { + return wrapped.heapBuffer(); + } + + @Override + public ByteBuf heapBuffer(int initialCapacity) { + return wrapped.heapBuffer(initialCapacity); + } + + @Override + public ByteBuf heapBuffer(int initialCapacity, int maxCapacity) { + return wrapped.heapBuffer(initialCapacity, maxCapacity); + } + + @Override + public ByteBuf directBuffer() { + return wrapped.directBuffer(); + } + + @Override + public ByteBuf directBuffer(int initialCapacity) { + return wrapped.directBuffer(initialCapacity); + } + + @Override + public ByteBuf directBuffer(int initialCapacity, int maxCapacity) { + return wrapped.directBuffer(initialCapacity, maxCapacity); + } + + @Override + public CompositeByteBuf compositeBuffer() { + return wrapped.compositeBuffer(); + } + + @Override + public CompositeByteBuf compositeBuffer(int maxNumComponents) { + return wrapped.compositeBuffer(maxNumComponents); + } + + @Override + public CompositeByteBuf compositeHeapBuffer() { + return wrapped.compositeHeapBuffer(); + } + + @Override + public CompositeByteBuf compositeHeapBuffer(int maxNumComponents) { + return wrapped.compositeHeapBuffer(maxNumComponents); + } + + @Override + public CompositeByteBuf compositeDirectBuffer() { + return wrapped.compositeDirectBuffer(); + } + + @Override + public CompositeByteBuf compositeDirectBuffer(int maxNumComponents) { + return wrapped.compositeDirectBuffer(maxNumComponents); + } + + @Override + public boolean isDirectBufferPooled() { + return wrapped.isDirectBufferPooled(); + } + + @Override + public int calculateNewCapacity(int minNewCapacity, int maxCapacity) { + return wrapped.calculateNewCapacity(minNewCapacity, maxCapacity); + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/EpollQuicUtils.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/EpollQuicUtils.java new file mode 100644 index 0000000..2e8b94f --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/EpollQuicUtils.java @@ -0,0 +1,65 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.epoll.SegmentedDatagramPacket; +import io.netty.channel.socket.DatagramPacket; +import io.netty.util.internal.ObjectUtil; + +import java.net.InetSocketAddress; + +/** + * Class that provides utility methods to setup {@code QUIC} when using the {@code EPOLL} transport. + */ +public final class EpollQuicUtils { + + private EpollQuicUtils() { } + + /** + * Return a new {@link SegmentedDatagramPacketAllocator} that can be used while using + * {@link io.netty.channel.epoll.EpollDatagramChannel}. + * + * @param maxNumSegments the maximum number of segments that we try to send in one packet. + * @return a allocator. + */ + public static SegmentedDatagramPacketAllocator newSegmentedAllocator(int maxNumSegments) { + ObjectUtil.checkInRange(maxNumSegments, 1, 64, "maxNumSegments"); + if (SegmentedDatagramPacket.isSupported()) { + return new EpollSegmentedDatagramPacketAllocator(maxNumSegments); + } + return SegmentedDatagramPacketAllocator.NONE; + } + + private static final class EpollSegmentedDatagramPacketAllocator implements SegmentedDatagramPacketAllocator { + + private final int maxNumSegments; + + EpollSegmentedDatagramPacketAllocator(int maxNumSegments) { + this.maxNumSegments = maxNumSegments; + } + + @Override + public int maxNumSegments() { + return maxNumSegments; + } + + @Override + public DatagramPacket newPacket(ByteBuf buffer, int segmentSize, InetSocketAddress remoteAddress) { + return new io.netty.channel.unix.SegmentedDatagramPacket(buffer, segmentSize, remoteAddress); + } + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/FlushStrategy.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/FlushStrategy.java new file mode 100644 index 0000000..e4a441c --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/FlushStrategy.java @@ -0,0 +1,60 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.util.internal.ObjectUtil; + +/** + * Allows to configure a strategy for when flushes should be happening. + */ +public interface FlushStrategy { + + /** + * Default {@link FlushStrategy} implementation. + */ + FlushStrategy DEFAULT = afterNumBytes(20 * Quic.MAX_DATAGRAM_SIZE); + + /** + * Returns {@code true} if a flush should happen now, {@code false} otherwise. + * + * @param numPackets the number of packets that were written since the last flush. + * @param numBytes the number of bytes that were written since the last flush. + * @return {@code true} if a flush should be done now, {@code false} otherwise. + */ + boolean shouldFlushNow(int numPackets, int numBytes); + + /** + * Implementation that flushes after a number of bytes. + * + * @param bytes the number of bytes after which we should issue a flush. + * @return the {@link FlushStrategy}. + */ + static FlushStrategy afterNumBytes(int bytes) { + ObjectUtil.checkPositive(bytes, "bytes"); + return (numPackets, numBytes) -> numBytes > bytes; + } + + /** + * Implementation that flushes after a number of packets. + * + * @param packets the number of packets after which we should issue a flush. + * @return the {@link FlushStrategy}. + */ + static FlushStrategy afterNumPackets(int packets) { + ObjectUtil.checkPositive(packets, "packets"); + return (numPackets, numBytes) -> numPackets > packets; + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/Hmac.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/Hmac.java new file mode 100644 index 0000000..cad5897 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/Hmac.java @@ -0,0 +1,67 @@ +/* + * Copyright 2023 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.util.concurrent.FastThreadLocal; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.ByteBuffer; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; + +final class Hmac { + + private static final FastThreadLocal MACS = new FastThreadLocal() { + @Override + protected Mac initialValue() { + return newMac(); + } + }; + + private static final String ALGORITM = "HmacSHA256"; + private static final byte[] randomKey = new byte[16]; + + static { + new SecureRandom().nextBytes(randomKey); + } + + private static Mac newMac() { + try { + SecretKeySpec keySpec = new SecretKeySpec(randomKey, ALGORITM); + Mac mac = Mac.getInstance(ALGORITM); + mac.init(keySpec); + return mac; + } catch (NoSuchAlgorithmException | InvalidKeyException exception) { + throw new IllegalStateException(exception); + } + } + + static ByteBuffer sign(ByteBuffer input, int outLength) { + Mac mac = MACS.get(); + mac.reset(); + mac.update(input); + byte[] signBytes = mac.doFinal(); + if (signBytes.length != outLength) { + signBytes = Arrays.copyOf(signBytes, outLength); + } + return ByteBuffer.wrap(signBytes); + } + + private Hmac() { } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/HmacSignQuicConnectionIdGenerator.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/HmacSignQuicConnectionIdGenerator.java new file mode 100644 index 0000000..db7147b --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/HmacSignQuicConnectionIdGenerator.java @@ -0,0 +1,56 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import java.nio.ByteBuffer; + +import io.netty.util.internal.ObjectUtil; + +/** + * A {@link QuicConnectionIdGenerator} which creates new connection id by signing the given input + * using hmac algorithms. + */ +final class HmacSignQuicConnectionIdGenerator implements QuicConnectionIdGenerator { + static final QuicConnectionIdGenerator INSTANCE = new HmacSignQuicConnectionIdGenerator(); + + private HmacSignQuicConnectionIdGenerator() { + } + + @Override + public ByteBuffer newId(int length) { + throw new UnsupportedOperationException( + "HmacSignQuicConnectionIdGenerator should always have an input to sign with"); + } + + @Override + public ByteBuffer newId(ByteBuffer buffer, int length) { + ObjectUtil.checkNotNull(buffer, "buffer"); + ObjectUtil.checkPositive(buffer.remaining(), "buffer"); + ObjectUtil.checkInRange(length, 0, maxConnectionIdLength(), "length"); + + return Hmac.sign(buffer, length); + } + + @Override + public int maxConnectionIdLength() { + return Quiche.QUICHE_MAX_CONN_ID_LEN; + } + + @Override + public boolean isIdempotent() { + return true; + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/HmacSignQuicResetTokenGenerator.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/HmacSignQuicResetTokenGenerator.java new file mode 100644 index 0000000..e52e6e5 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/HmacSignQuicResetTokenGenerator.java @@ -0,0 +1,39 @@ +/* + * Copyright 2023 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.util.internal.ObjectUtil; + +import java.nio.ByteBuffer; + +/** + * A {@link QuicResetTokenGenerator} which creates new reset token by using the connection id by signing the given input + * using HMAC algorithms. + */ +final class HmacSignQuicResetTokenGenerator implements QuicResetTokenGenerator { + static final QuicResetTokenGenerator INSTANCE = new HmacSignQuicResetTokenGenerator(); + + private HmacSignQuicResetTokenGenerator() { + } + + + @Override + public ByteBuffer newResetToken(ByteBuffer cid) { + ObjectUtil.checkNotNull(cid, "cid"); + ObjectUtil.checkPositive(cid.remaining(), "cid"); + return Hmac.sign(cid, Quic.RESET_TOKEN_LEN); + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/InsecureQuicTokenHandler.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/InsecureQuicTokenHandler.java new file mode 100644 index 0000000..278e08c --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/InsecureQuicTokenHandler.java @@ -0,0 +1,84 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import io.netty.util.NetUtil; + +import java.net.InetSocketAddress; + +/** + * Insecure {@link QuicTokenHandler} which only does basic token generation / validation without any + * crypto. + * + * This shouldn't be used in production. + */ +public final class InsecureQuicTokenHandler implements QuicTokenHandler { + + private static final String SERVER_NAME = "netty"; + private static final byte[] SERVER_NAME_BYTES = SERVER_NAME.getBytes(CharsetUtil.US_ASCII); + private static final ByteBuf SERVER_NAME_BUFFER = Unpooled.unreleasableBuffer( + Unpooled.wrappedBuffer(SERVER_NAME_BYTES)).asReadOnly(); + + // Just package-private for unit tests + static final int MAX_TOKEN_LEN = Quiche.QUICHE_MAX_CONN_ID_LEN + + NetUtil.LOCALHOST6.getAddress().length + SERVER_NAME_BYTES.length; + + private InsecureQuicTokenHandler() { + Quic.ensureAvailability(); + } + + public static final InsecureQuicTokenHandler INSTANCE = new InsecureQuicTokenHandler(); + + @Override + public boolean writeToken(ByteBuf out, ByteBuf dcid, InetSocketAddress address) { + byte[] addr = address.getAddress().getAddress(); + out.writeBytes(SERVER_NAME_BYTES) + .writeBytes(addr) + .writeBytes(dcid, dcid.readerIndex(), dcid.readableBytes()); + return true; + } + + @Override + public int validateToken(ByteBuf token, InetSocketAddress address) { + final byte[] addr = address.getAddress().getAddress(); + + int minLength = SERVER_NAME_BYTES.length + address.getAddress().getAddress().length; + if (token.readableBytes() <= SERVER_NAME_BYTES.length + addr.length) { + return -1; + } + + if (!SERVER_NAME_BUFFER.equals(token.slice(0, SERVER_NAME_BYTES.length))) { + return -1; + } + ByteBuf addressBuffer = Unpooled.wrappedBuffer(addr); + try { + if (!addressBuffer.equals(token.slice(SERVER_NAME_BYTES.length, addr.length))) { + return -1; + } + } finally { + addressBuffer.release(); + } + return minLength; + } + + @Override + public int maxTokenLength() { + return MAX_TOKEN_LEN; + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/NoQuicTokenHandler.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/NoQuicTokenHandler.java new file mode 100644 index 0000000..695dd23 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/NoQuicTokenHandler.java @@ -0,0 +1,48 @@ +/* + * Copyright 2023 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBuf; + +import java.net.InetSocketAddress; + +/** + * {@link QuicTokenHandler} which will disable token generation / validation completely. + * This will reduce the round-trip for QUIC connection migration, but will also weaking the + * security during connection establishment. + */ +final class NoQuicTokenHandler implements QuicTokenHandler { + + public final static QuicTokenHandler INSTANCE = new NoQuicTokenHandler(); + + private NoQuicTokenHandler() { + } + + @Override + public boolean writeToken(ByteBuf out, ByteBuf dcid, InetSocketAddress address) { + return false; + } + + @Override + public int validateToken(ByteBuf token, InetSocketAddress address) { + return 0; + } + + @Override + public int maxTokenLength() { + return 0; + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QLogConfiguration.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QLogConfiguration.java new file mode 100644 index 0000000..0c99db8 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QLogConfiguration.java @@ -0,0 +1,70 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import java.util.Objects; + +/** + * Configuration used for setup + * qlog. + */ +public final class QLogConfiguration { + + private final String path; + private final String logTitle; + private final String logDescription; + + /** + * Create a new configuration. + * + * @param path the path to the log file to use. This file must not exist yet. If the path is a + * directory the filename will be generated + * @param logTitle the title to use when logging. + * @param logDescription the description to use when logging. + */ + public QLogConfiguration(String path, String logTitle, String logDescription) { + this.path = Objects.requireNonNull(path, "path"); + this.logTitle = Objects.requireNonNull(logTitle, "logTitle"); + this.logDescription = Objects.requireNonNull(logDescription, "logDescription"); + } + + /** + * Return the path to the log file. + * + * @return the path. + */ + public String path() { + return path; + } + + /** + * Return the title. + * + * @return the title. + */ + public String logTitle() { + return logTitle; + } + + /** + * Return the description. + * + * @return the description. + */ + public String logDescription() { + return logDescription; + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/Quic.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/Quic.java new file mode 100644 index 0000000..0e51d0d --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/Quic.java @@ -0,0 +1,167 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelOption; +import io.netty.util.AttributeKey; +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.logging.InternalLogger; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +public final class Quic { + @SuppressWarnings("unchecked") + static final Map.Entry, Object>[] EMPTY_OPTION_ARRAY = new Map.Entry[0]; + @SuppressWarnings("unchecked") + static final Map.Entry, Object>[] EMPTY_ATTRIBUTE_ARRAY = new Map.Entry[0]; + + static final int MAX_DATAGRAM_SIZE = 1350; + + static final int RESET_TOKEN_LEN = 16; + + private static final Throwable UNAVAILABILITY_CAUSE; + + static { + Throwable cause = null; + + try { + String version = Quiche.quiche_version(); + assert version != null; + } catch (Throwable error) { + cause = error; + } + + UNAVAILABILITY_CAUSE = cause; + } + + /** + * Return if the given QUIC version is supported. + * + * @param version the version. + * @return {@code true} if supported, {@code false} otherwise. + */ + public static boolean isVersionSupported(int version) { + return isAvailable() && Quiche.quiche_version_is_supported(version); + } + + /** + * Returns {@code true} if and only if the QUIC implementation is usable on the running platform is available. + * + * @return {@code true} if this QUIC implementation can be used on the current platform, {@code false} otherwise. + */ + public static boolean isAvailable() { + return UNAVAILABILITY_CAUSE == null; + } + + /** + * Ensure that QUIC implementation is usable on the running platform is available. + * + * @throws UnsatisfiedLinkError if unavailable + */ + public static void ensureAvailability() { + if (UNAVAILABILITY_CAUSE != null) { + throw (Error) new UnsatisfiedLinkError( + "failed to load the required native library").initCause(UNAVAILABILITY_CAUSE); + } + } + + /** + * Returns the cause of unavailability. + * + * @return the cause if unavailable. {@code null} if available. + */ + public static Throwable unavailabilityCause() { + return UNAVAILABILITY_CAUSE; + } + + static Map.Entry, Object>[] toOptionsArray(Map, Object> opts) { + return new HashMap<>(opts).entrySet().toArray(EMPTY_OPTION_ARRAY); + } + + static Map.Entry, Object>[] toAttributesArray(Map, Object> attributes) { + return new LinkedHashMap<>(attributes).entrySet().toArray(EMPTY_ATTRIBUTE_ARRAY); + } + + private static void setAttributes(Channel channel, Map.Entry, Object>[] attrs) { + for (Map.Entry, Object> e: attrs) { + @SuppressWarnings("unchecked") + AttributeKey key = (AttributeKey) e.getKey(); + channel.attr(key).set(e.getValue()); + } + } + + private static void setChannelOptions( + Channel channel, Map.Entry, Object>[] options, InternalLogger logger) { + for (Map.Entry, Object> e: options) { + setChannelOption(channel, e.getKey(), e.getValue(), logger); + } + } + + @SuppressWarnings("unchecked") + private static void setChannelOption( + Channel channel, ChannelOption option, Object value, InternalLogger logger) { + try { + if (!channel.config().setOption((ChannelOption) option, value)) { + logger.warn("Unknown channel option '{}' for channel '{}'", option, channel); + } + } catch (Throwable t) { + logger.warn( + "Failed to set channel option '{}' with value '{}' for channel '{}'", option, value, channel, t); + } + } + + /** + * Allow to specify a {@link ChannelOption} which is used for the {@link QuicStreamChannel} instances once they got + * created. Use a value of {@code null} to remove a previous set {@link ChannelOption}. + */ + static void updateOptions(Map, Object> options, ChannelOption option, T value) { + ObjectUtil.checkNotNull(option, "option"); + if (value == null) { + options.remove(option); + } else { + options.put(option, value); + } + } + + /** + * Allow to specify an initial attribute of the newly created {@link QuicStreamChannel}. If the {@code value} is + * {@code null}, the attribute of the specified {@code key} is removed. + */ + static void updateAttributes(Map, Object> attributes, AttributeKey key, T value) { + ObjectUtil.checkNotNull(key, "key"); + if (value == null) { + attributes.remove(key); + } else { + attributes.put(key, value); + } + } + + static void setupChannel(Channel ch, Map.Entry, Object>[] options, + Map.Entry, Object>[] attrs, ChannelHandler handler, + InternalLogger logger) { + Quic.setChannelOptions(ch, options, logger); + Quic.setAttributes(ch, attrs); + if (handler != null) { + ch.pipeline().addLast(handler); + } + } + + private Quic() { } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicChannel.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicChannel.java new file mode 100644 index 0000000..a8394a1 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicChannel.java @@ -0,0 +1,274 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelProgressivePromise; +import io.netty.channel.ChannelPromise; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.Promise; + +import javax.net.ssl.SSLEngine; +import java.net.SocketAddress; + +/** + * A QUIC {@link Channel}. + */ +public interface QuicChannel extends Channel { + + @Override + default ChannelFuture bind(SocketAddress localAddress) { + return pipeline().bind(localAddress); + } + + @Override + default ChannelFuture connect(SocketAddress remoteAddress) { + return pipeline().connect(remoteAddress); + } + + @Override + default ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress) { + return pipeline().connect(remoteAddress, localAddress); + } + + @Override + default ChannelFuture disconnect() { + return pipeline().disconnect(); + } + + @Override + default ChannelFuture close() { + return pipeline().close(); + } + + @Override + default ChannelFuture deregister() { + return pipeline().deregister(); + } + + @Override + default ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) { + return pipeline().bind(localAddress, promise); + } + + @Override + default ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) { + return pipeline().connect(remoteAddress, promise); + } + + @Override + default ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) { + return pipeline().connect(remoteAddress, localAddress, promise); + } + + @Override + default ChannelFuture disconnect(ChannelPromise promise) { + return pipeline().disconnect(promise); + } + + @Override + default ChannelFuture close(ChannelPromise promise) { + return pipeline().close(promise); + } + + @Override + default ChannelFuture deregister(ChannelPromise promise) { + return pipeline().deregister(promise); + } + + @Override + default ChannelFuture write(Object msg) { + return pipeline().write(msg); + } + + @Override + default ChannelFuture write(Object msg, ChannelPromise promise) { + return pipeline().write(msg, promise); + } + + @Override + default ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) { + return pipeline().writeAndFlush(msg, promise); + } + + @Override + default ChannelFuture writeAndFlush(Object msg) { + return pipeline().writeAndFlush(msg); + } + + @Override + default ChannelPromise newPromise() { + return pipeline().newPromise(); + } + + @Override + default ChannelProgressivePromise newProgressivePromise() { + return pipeline().newProgressivePromise(); + } + + @Override + default ChannelFuture newSucceededFuture() { + return pipeline().newSucceededFuture(); + } + + @Override + default ChannelFuture newFailedFuture(Throwable cause) { + return pipeline().newFailedFuture(cause); + } + + @Override + default ChannelPromise voidPromise() { + return pipeline().voidPromise(); + } + + @Override + QuicChannel read(); + + @Override + QuicChannel flush(); + + /** + * Returns the configuration of this channel. + */ + @Override + QuicChannelConfig config(); + + /** + * Returns the used {@link SSLEngine} or {@code null} if none is used (yet). + * + * @return the engine. + */ + SSLEngine sslEngine(); + + /** + * Returns the number of streams that can be created before stream creation will fail + * with {@link QuicError#STREAM_LIMIT} error. + * + * @param type the stream type. + * @return the number of streams left. + */ + long peerAllowedStreams(QuicStreamType type); + + /** + * Returns {@code true} if the connection was closed because of idle timeout. + * + * @return {@code true} if the connection was closed because of idle timeout, {@code false}. + */ + boolean isTimedOut(); + + /** + * Returns the {@link QuicTransportParameters} of the peer once received, or {@code null} if not known yet. + * + * @return peerTransportParams. + */ + QuicTransportParameters peerTransportParameters(); + + /** + * Creates a stream that is using this {@link QuicChannel} and notifies the {@link Future} once done. + * The {@link ChannelHandler} (if not {@code null}) is added to the {@link io.netty.channel.ChannelPipeline} of the + * {@link QuicStreamChannel} automatically. + * + * @param type the {@link QuicStreamType} of the {@link QuicStreamChannel}. + * @param handler the {@link ChannelHandler} that will be added to the {@link QuicStreamChannel}s + * {@link io.netty.channel.ChannelPipeline} during the stream creation. + * @return the {@link Future} that will be notified once the operation completes. + */ + default Future createStream(QuicStreamType type, ChannelHandler handler) { + return createStream(type, handler, eventLoop().newPromise()); + } + + /** + * Creates a stream that is using this {@link QuicChannel} and notifies the {@link Promise} once done. + * The {@link ChannelHandler} (if not {@code null}) is added to the {@link io.netty.channel.ChannelPipeline} of the + * {@link QuicStreamChannel} automatically. + * + * @param type the {@link QuicStreamType} of the {@link QuicStreamChannel}. + * @param handler the {@link ChannelHandler} that will be added to the {@link QuicStreamChannel}s + * {@link io.netty.channel.ChannelPipeline} during the stream creation. + * @param promise the {@link ChannelPromise} that will be notified once the operation completes. + * @return the {@link Future} that will be notified once the operation completes. + */ + Future createStream(QuicStreamType type, ChannelHandler handler, + Promise promise); + + /** + * Returns a new {@link QuicStreamChannelBootstrap} which makes it easy to bootstrap new {@link QuicStreamChannel}s + * with custom options and attributes. For simpler use-cases you may want to consider using + * {@link #createStream(QuicStreamType, ChannelHandler)} or + * {@link #createStream(QuicStreamType, ChannelHandler, Promise)} directly. + * + * @return {@link QuicStreamChannelBootstrap} that can be used to bootstrap a {@link QuicStreamChannel}. + */ + default QuicStreamChannelBootstrap newStreamBootstrap() { + return new QuicStreamChannelBootstrap(this); + } + + /** + * Close the {@link QuicChannel} + * + * @param applicationClose {@code true} if an application close should be used, + * {@code false} if a normal close should be used. + * @param error the application error number, or {@code 0} if no special error should be signaled. + * @param reason the reason for the closure (which may be an empty {@link ByteBuf}. + * @return the future that is notified. + */ + default ChannelFuture close(boolean applicationClose, int error, ByteBuf reason) { + return close(applicationClose, error, reason, newPromise()); + } + + /** + * Close the {@link QuicChannel} + * + * @param applicationClose {@code true} if an application close should be used, + * {@code false} if a normal close should be used. + * @param error the application error number, or {@code 0} if no special error should be signaled. + * @param reason the reason for the closure (which may be an empty {@link ByteBuf}. + * @param promise the {@link ChannelPromise} that will be notified. + * @return the future that is notified. + */ + ChannelFuture close(boolean applicationClose, int error, ByteBuf reason, ChannelPromise promise); + + /** + * Collects statistics about the connection and notifies the {@link Future} once done. + * + * @return the {@link Future} that is notified once the stats were collected. + */ + default Future collectStats() { + return collectStats(eventLoop().newPromise()); + } + + /** + * Collects statistics about the connection and notifies the {@link Promise} once done. + * + * @param promise the {@link ChannelPromise} that is notified once the stats were collected. + * @return the {@link Future} that is notified once the stats were collected. + */ + Future collectStats(Promise promise); + + /** + * Creates a new {@link QuicChannelBootstrap} that can be used to create and connect new {@link QuicChannel}s to + * endpoints using the given {@link Channel} as transport layer. + * + * @param channel the {@link Channel} that is used as transport layer. + * @return {@link QuicChannelBootstrap} that can be used to bootstrap a client side {@link QuicChannel}. + */ + static QuicChannelBootstrap newBootstrap(Channel channel) { + return new QuicChannelBootstrap(channel); + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicChannelBootstrap.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicChannelBootstrap.java new file mode 100644 index 0000000..6976491 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicChannelBootstrap.java @@ -0,0 +1,244 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoop; +import io.netty.util.AttributeKey; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.Promise; +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Bootstrap that helps to bootstrap {@link QuicChannel}s and connecting these to remote peers. + */ +public final class QuicChannelBootstrap { + private static final InternalLogger logger = InternalLoggerFactory.getInstance(QuicChannelBootstrap.class); + + private final Channel parent; + // The order in which ChannelOptions are applied is important they may depend on each other for validation + // purposes. + private final Map, Object> options = new LinkedHashMap<>(); + private final Map, Object> attrs = new HashMap<>(); + private final Map, Object> streamOptions = new LinkedHashMap<>(); + private final Map, Object> streamAttrs = new HashMap<>(); + + private SocketAddress local; + private SocketAddress remote; + private QuicConnectionAddress connectionAddress = QuicConnectionAddress.EPHEMERAL; + private ChannelHandler handler; + private ChannelHandler streamHandler; + + /** + * Creates a new instance which uses the given {@link Channel} to bootstrap the {@link QuicChannel}. + * This {@link io.netty.channel.ChannelPipeline} of the {@link Channel} needs to have the quic codec in the + * pipeline. + * + * @param parent the {@link Channel} that is used as the transport layer. + */ + QuicChannelBootstrap(Channel parent) { + Quic.ensureAvailability(); + this.parent = ObjectUtil.checkNotNull(parent, "parent"); + } + + /** + * Allow to specify a {@link ChannelOption} which is used for the {@link QuicChannel} instances once they got + * created. Use a value of {@code null} to remove a previous set {@link ChannelOption}. + * + * @param option the {@link ChannelOption} to apply to the {@link QuicChannel}. + * @param value the value of the option. + * @param the type of the value. + * @return this instance. + */ + public QuicChannelBootstrap option(ChannelOption option, T value) { + Quic.updateOptions(options, option, value); + return this; + } + + /** + * Allow to specify an initial attribute of the newly created {@link QuicChannel}. If the {@code value} is + * {@code null}, the attribute of the specified {@code key} is removed. + * + * @param key the {@link AttributeKey} to apply to the {@link QuicChannel}. + * @param value the value of the attribute. + * @param the type of the value. + * @return this instance. + */ + public QuicChannelBootstrap attr(AttributeKey key, T value) { + Quic.updateAttributes(attrs, key, value); + return this; + } + + /** + * Set the {@link ChannelHandler} that is added to the {@link io.netty.channel.ChannelPipeline} of the + * {@link QuicChannel} once created. + * + * @param handler the {@link ChannelHandler} that is added to the {@link QuicChannel}s + * {@link io.netty.channel.ChannelPipeline}. + * @return this instance. + */ + public QuicChannelBootstrap handler(ChannelHandler handler) { + this.handler = ObjectUtil.checkNotNull(handler, "handler"); + return this; + } + + /** + * Allow to specify a {@link ChannelOption} which is used for the {@link QuicStreamChannel} instances once they got + * created. Use a value of {@code null} to remove a previous set {@link ChannelOption}. + * + * @param option the {@link ChannelOption} to apply to the {@link QuicStreamChannel}s. + * @param value the value of the option. + * @param the type of the value. + * @return this instance. + */ + public QuicChannelBootstrap streamOption(ChannelOption option, T value) { + Quic.updateOptions(streamOptions, option, value); + return this; + } + + /** + * Allow to specify an initial attribute of the newly created {@link QuicStreamChannel}. If the {@code value} is + * {@code null}, the attribute of the specified {@code key} is removed. + * + * @param key the {@link AttributeKey} to apply to the {@link QuicStreamChannel}s. + * @param value the value of the attribute. + * @param the type of the value. + * @return this instance. + */ + public QuicChannelBootstrap streamAttr(AttributeKey key, T value) { + Quic.updateAttributes(streamAttrs, key, value); + return this; + } + + /** + * Set the {@link ChannelHandler} that is added to the {@link io.netty.channel.ChannelPipeline} of the + * {@link QuicStreamChannel} once created. + * + * @param streamHandler the {@link ChannelHandler} that is added to the {@link QuicStreamChannel}s + * {@link io.netty.channel.ChannelPipeline}. + * @return this instance. + */ + public QuicChannelBootstrap streamHandler(ChannelHandler streamHandler) { + this.streamHandler = ObjectUtil.checkNotNull(streamHandler, "streamHandler"); + return this; + } + + /** + * Set the local address. + * + * @param local the {@link SocketAddress} of the local peer. + * @return this instance. + */ + public QuicChannelBootstrap localAddress(SocketAddress local) { + this.local = ObjectUtil.checkNotNull(local, "local"); + return this; + } + + /** + * Set the remote address of the host to talk to. + * + * @param remote the {@link SocketAddress} of the remote peer. + * @return this instance. + */ + public QuicChannelBootstrap remoteAddress(SocketAddress remote) { + this.remote = ObjectUtil.checkNotNull(remote, "remote"); + return this; + } + + /** + * Set the {@link QuicConnectionAddress} to use. If none is specified a random address is generated on your + * behalf. + * + * @param connectionAddress the {@link QuicConnectionAddress} to use. + * @return this instance. + */ + public QuicChannelBootstrap connectionAddress(QuicConnectionAddress connectionAddress) { + this.connectionAddress = ObjectUtil.checkNotNull(connectionAddress, "connectionAddress"); + return this; + } + + /** + * Connects a {@link QuicChannel} to the remote peer and notifies the future once done. + * + * @return {@link Future} which is notified once the operation completes. + */ + public Future connect() { + return connect(parent.eventLoop().newPromise()); + } + + /** + * Connects a {@link QuicChannel} to the remote peer and notifies the promise once done. + * + * @param promise the {@link Promise} which is notified once the operations completes. + * @return {@link Future} which is notified once the operation completes. + + */ + public Future connect(Promise promise) { + if (handler == null && streamHandler == null) { + throw new IllegalStateException("handler and streamHandler not set"); + } + SocketAddress local = this.local; + if (local == null) { + local = parent.localAddress(); + } + if (local == null) { + local = new InetSocketAddress(0); + } + + SocketAddress remote = this.remote; + if (remote == null) { + remote = parent.remoteAddress(); + } + if (remote == null) { + throw new IllegalStateException("remote not set"); + } + + final QuicConnectionAddress address = connectionAddress; + QuicChannel channel = QuicheQuicChannel.forClient(parent, (InetSocketAddress) local, + (InetSocketAddress) remote, + streamHandler, Quic.toOptionsArray(streamOptions), Quic.toAttributesArray(streamAttrs)); + + Quic.setupChannel(channel, Quic.toOptionsArray(options), Quic.toAttributesArray(attrs), handler, logger); + EventLoop eventLoop = parent.eventLoop(); + eventLoop.register(channel).addListener((ChannelFuture future) -> { + Throwable cause = future.cause(); + if (cause != null) { + promise.setFailure(cause); + } else { + channel.connect(address).addListener(f -> { + Throwable error = f.cause(); + if (error != null) { + promise.setFailure(error); + } else { + promise.setSuccess(channel); + } + }); + } + }); + return promise; + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicChannelConfig.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicChannelConfig.java new file mode 100644 index 0000000..9e3336f --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicChannelConfig.java @@ -0,0 +1,62 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.ChannelConfig; +import io.netty.channel.MessageSizeEstimator; +import io.netty.channel.RecvByteBufAllocator; +import io.netty.channel.WriteBufferWaterMark; + +/** + * A QUIC {@link ChannelConfig}. + */ +public interface QuicChannelConfig extends ChannelConfig { + + @Override + @Deprecated + QuicChannelConfig setMaxMessagesPerRead(int maxMessagesPerRead); + + @Override + QuicChannelConfig setConnectTimeoutMillis(int connectTimeoutMillis); + + @Override + QuicChannelConfig setWriteSpinCount(int writeSpinCount); + + @Override + QuicChannelConfig setAllocator(ByteBufAllocator allocator); + + @Override + QuicChannelConfig setRecvByteBufAllocator(RecvByteBufAllocator allocator); + + @Override + QuicChannelConfig setAutoRead(boolean autoRead); + + @Override + QuicChannelConfig setAutoClose(boolean autoClose); + + @Override + QuicChannelConfig setWriteBufferHighWaterMark(int writeBufferHighWaterMark); + + @Override + QuicChannelConfig setWriteBufferLowWaterMark(int writeBufferLowWaterMark); + + @Override + QuicChannelConfig setWriteBufferWaterMark(WriteBufferWaterMark writeBufferWaterMark); + + @Override + QuicChannelConfig setMessageSizeEstimator(MessageSizeEstimator estimator); +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicChannelOption.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicChannelOption.java new file mode 100644 index 0000000..41ebe4d --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicChannelOption.java @@ -0,0 +1,50 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.channel.ChannelOption; + +/** + * {@link ChannelOption}s specific to QUIC. + */ +public final class QuicChannelOption extends ChannelOption { + + /** + * If set to {@code true} the {@link QuicStreamChannel} will read {@link QuicStreamFrame}s and fire it through + * the pipeline, if {@code false} it will read {@link io.netty.buffer.ByteBuf} and translate the FIN flag to + * events. + */ + public static final ChannelOption READ_FRAMES = + valueOf(QuicChannelOption.class, "READ_FRAMES"); + + /** + * Enable qlog + * for a {@link QuicChannel}. + */ + public static final ChannelOption QLOG = valueOf(QuicChannelOption.class, "QLOG"); + + /** + * Use GSO + * for QUIC packets if possible. + */ + public static final ChannelOption SEGMENTED_DATAGRAM_PACKET_ALLOCATOR = + valueOf(QuicChannelOption.class, "SEGMENTED_DATAGRAM_PACKET_ALLOCATOR"); + + @SuppressWarnings({ "deprecation" }) + private QuicChannelOption() { + super(null); + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicClientCodecBuilder.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicClientCodecBuilder.java new file mode 100644 index 0000000..89df278 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicClientCodecBuilder.java @@ -0,0 +1,52 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.channel.ChannelHandler; + +import java.util.concurrent.Executor; +import java.util.function.Function; + +/** + * {@link QuicCodecBuilder} that configures and builds a {@link ChannelHandler} that should be added to the + * {@link io.netty.channel.ChannelPipeline} of a {@code QUIC} client. + */ +public final class QuicClientCodecBuilder extends QuicCodecBuilder { + + /** + * Creates a new instance. + */ + public QuicClientCodecBuilder() { + super(false); + } + + private QuicClientCodecBuilder(QuicCodecBuilder builder) { + super(builder); + } + + @Override + public QuicClientCodecBuilder clone() { + return new QuicClientCodecBuilder(this); + } + + @Override + protected ChannelHandler build(QuicheConfig config, + Function sslEngineProvider, + Executor sslTaskExecutor, + int localConnIdLength, FlushStrategy flushStrategy) { + return new QuicheQuicClientCodec(config, sslEngineProvider, sslTaskExecutor, localConnIdLength, flushStrategy); + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicClientSessionCache.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicClientSessionCache.java new file mode 100644 index 0000000..1ff5b91 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicClientSessionCache.java @@ -0,0 +1,244 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.util.AsciiString; +import io.netty.util.internal.SystemPropertyUtil; + +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +final class QuicClientSessionCache { + + private static final int DEFAULT_CACHE_SIZE; + static { + // Respect the same system property as the JDK implementation to make it easy to switch between implementations. + int cacheSize = SystemPropertyUtil.getInt("javax.net.ssl.sessionCacheSize", 20480); + if (cacheSize >= 0) { + DEFAULT_CACHE_SIZE = cacheSize; + } else { + DEFAULT_CACHE_SIZE = 20480; + } + } + + private final AtomicInteger maximumCacheSize = new AtomicInteger(DEFAULT_CACHE_SIZE); + + // Let's use the same default value as OpenSSL does. + // See https://www.openssl.org/docs/man1.1.1/man3/SSL_get_default_timeout.html + private final AtomicInteger sessionTimeout = new AtomicInteger(300); + private int sessionCounter; + + private final Map sessions = + new LinkedHashMap() { + + private static final long serialVersionUID = -7773696788135734448L; + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + int maxSize = maximumCacheSize.get(); + return maxSize >= 0 && size() > maxSize; + } + }; + + void saveSession(String host, int port, long creationTime, long timeout, byte[] session, boolean isSingleUse) { + HostPort hostPort = keyFor(host, port); + if (hostPort != null) { + synchronized (sessions) { + // Mimic what OpenSSL is doing and expunge every 255 new sessions + // See https://www.openssl.org/docs/man1.0.2/man3/SSL_CTX_flush_sessions.html + if (++sessionCounter == 255) { + sessionCounter = 0; + expungeInvalidSessions(); + } + + sessions.put(hostPort, new SessionHolder(creationTime, timeout, session, isSingleUse)); + } + } + } + + // Only used for testing. + boolean hasSession(String host, int port) { + HostPort hostPort = keyFor(host, port); + if (hostPort != null) { + synchronized (sessions) { + return sessions.containsKey(hostPort); + } + } + return false; + } + + byte[] getSession(String host, int port) { + HostPort hostPort = keyFor(host, port); + if (hostPort != null) { + SessionHolder sessionHolder; + synchronized (sessions) { + sessionHolder = sessions.get(hostPort); + if (sessionHolder == null) { + return null; + } + if (sessionHolder.isSingleUse()) { + // Remove session as it should only be re-used once. + sessions.remove(hostPort); + } + } + if (sessionHolder.isValid()) { + return sessionHolder.sessionBytes(); + } + } + return null; + } + + void removeSession(String host, int port) { + HostPort hostPort = keyFor(host, port); + if (hostPort != null) { + synchronized (sessions) { + sessions.remove(hostPort); + } + } + } + + void setSessionTimeout(int seconds) { + int oldTimeout = sessionTimeout.getAndSet(seconds); + if (oldTimeout > seconds) { + // Drain the whole cache as this way we can use the ordering of the LinkedHashMap to detect early + // if there are any other sessions left that are invalid. + clear(); + } + } + + int getSessionTimeout() { + return sessionTimeout.get(); + } + + void setSessionCacheSize(int size) { + long oldSize = maximumCacheSize.getAndSet(size); + if (oldSize > size || size == 0) { + // Just keep it simple for now and drain the whole cache. + clear(); + } + } + + int getSessionCacheSize() { + return maximumCacheSize.get(); + } + + /** + * Clear the cache and free all cached SSL_SESSION*. + */ + void clear() { + synchronized (sessions) { + sessions.clear(); + } + } + + + private void expungeInvalidSessions() { + assert Thread.holdsLock(sessions); + + if (sessions.isEmpty()) { + return; + } + long now = System.currentTimeMillis(); + Iterator> iterator = sessions.entrySet().iterator(); + while (iterator.hasNext()) { + SessionHolder sessionHolder = iterator.next().getValue(); + // As we use a LinkedHashMap we can break the while loop as soon as we find a valid session. + // This is true as we always drain the cache as soon as we change the timeout to a smaller value as + // it was set before. This way its true that the insertion order matches the timeout order. + if (sessionHolder.isValid(now)) { + break; + } + iterator.remove(); + } + } + + private static HostPort keyFor(String host, int port) { + if (host == null && port < 1) { + return null; + } + return new HostPort(host, port); + } + + private static final class SessionHolder { + private final long creationTime; + private final long timeout; + private final byte[] sessionBytes; + private final boolean isSingleUse; + + SessionHolder(long creationTime, long timeout, byte[] session, boolean isSingleUse) { + this.creationTime = creationTime; + this.timeout = timeout; + this.sessionBytes = session; + this.isSingleUse = isSingleUse; + } + + boolean isValid() { + return isValid(System.currentTimeMillis()); + } + + boolean isValid(long current) { + return current <= creationTime + timeout; + } + + boolean isSingleUse() { + return isSingleUse; + } + + byte[] sessionBytes() { + return sessionBytes; + } + } + + /** + * Host / Port tuple used to find a session in the cache. + */ + private static final class HostPort { + private final int hash; + private final String host; + private final int port; + + HostPort(String host, int port) { + this.host = host; + this.port = port; + // Calculate a hashCode that does ignore case. + this.hash = 31 * AsciiString.hashCode(host) + port; + } + + @Override + public int hashCode() { + return hash; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof HostPort)) { + return false; + } + HostPort other = (HostPort) obj; + return port == other.port && host.equalsIgnoreCase(other.host); + } + + @Override + public String toString() { + return "HostPort{" + + "host='" + host + '\'' + + ", port=" + port + + '}'; + } + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicClosedChannelException.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicClosedChannelException.java new file mode 100644 index 0000000..f665b67 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicClosedChannelException.java @@ -0,0 +1,40 @@ +/* + * Copyright 2023 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import java.nio.channels.ClosedChannelException; + +/** + * Special {@link QuicClosedChannelException} which also provides extra info if the close was a result of a + * {@link QuicConnectionCloseEvent} that was triggered by the remote peer. + */ +public final class QuicClosedChannelException extends ClosedChannelException { + + private final QuicConnectionCloseEvent event; + + QuicClosedChannelException(QuicConnectionCloseEvent event) { + this.event = event; + } + + /** + * Returns the {@link QuicConnectionCloseEvent} that caused the closure or {@code null} if none was received. + * + * @return the event. + */ + public QuicConnectionCloseEvent event() { + return event; + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicCodecBuilder.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicCodecBuilder.java new file mode 100644 index 0000000..9c6a764 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicCodecBuilder.java @@ -0,0 +1,508 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.channel.ChannelHandler; + +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +import static io.netty.util.internal.ObjectUtil.checkInRange; +import static io.netty.util.internal.ObjectUtil.checkPositive; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + +/** + * Abstract base class for {@code QUIC} codec builders. + * + * @param the type of the {@link QuicCodecBuilder}. + */ +public abstract class QuicCodecBuilder> { + private final boolean server; + private Boolean grease; + private Long maxIdleTimeout; + private Long maxRecvUdpPayloadSize; + private Long maxSendUdpPayloadSize; + private Long initialMaxData; + private Long initialMaxStreamDataBidiLocal; + private Long initialMaxStreamDataBidiRemote; + private Long initialMaxStreamDataUni; + private Long initialMaxStreamsBidi; + private Long initialMaxStreamsUni; + private Long ackDelayExponent; + private Long maxAckDelay; + private Boolean disableActiveMigration; + private Boolean enableHystart; + private QuicCongestionControlAlgorithm congestionControlAlgorithm; + private int localConnIdLength; + private Function sslEngineProvider; + private FlushStrategy flushStrategy = FlushStrategy.DEFAULT; + private Integer recvQueueLen; + private Integer sendQueueLen; + private Long activeConnectionIdLimit; + private byte[] statelessResetToken; + + private Executor sslTaskExecutor; + + // package-private for testing only + int version; + + QuicCodecBuilder(boolean server) { + Quic.ensureAvailability(); + this.version = Quiche.QUICHE_PROTOCOL_VERSION; + this.localConnIdLength = Quiche.QUICHE_MAX_CONN_ID_LEN; + this.server = server; + } + + QuicCodecBuilder(QuicCodecBuilder builder) { + Quic.ensureAvailability(); + this.server = builder.server; + this.grease = builder.grease; + this.maxIdleTimeout = builder.maxIdleTimeout; + this.maxRecvUdpPayloadSize = builder.maxRecvUdpPayloadSize; + this.maxSendUdpPayloadSize = builder.maxSendUdpPayloadSize; + this.initialMaxData = builder.initialMaxData; + this.initialMaxStreamDataBidiLocal = builder.initialMaxStreamDataBidiLocal; + this.initialMaxStreamDataBidiRemote = builder.initialMaxStreamDataBidiRemote; + this.initialMaxStreamDataUni = builder.initialMaxStreamDataUni; + this.initialMaxStreamsBidi = builder.initialMaxStreamsBidi; + this.initialMaxStreamsUni = builder.initialMaxStreamsUni; + this.ackDelayExponent = builder.ackDelayExponent; + this.maxAckDelay = builder.maxAckDelay; + this.disableActiveMigration = builder.disableActiveMigration; + this.enableHystart = builder.enableHystart; + this.congestionControlAlgorithm = builder.congestionControlAlgorithm; + this.localConnIdLength = builder.localConnIdLength; + this.sslEngineProvider = builder.sslEngineProvider; + this.flushStrategy = builder.flushStrategy; + this.recvQueueLen = builder.recvQueueLen; + this.sendQueueLen = builder.sendQueueLen; + this.activeConnectionIdLimit = builder.activeConnectionIdLimit; + this.statelessResetToken = builder.statelessResetToken; + this.sslTaskExecutor = builder.sslTaskExecutor; + this.version = builder.version; + } + + /** + * Returns itself. + * + * @return itself. + */ + @SuppressWarnings("unchecked") + protected final B self() { + return (B) this; + } + + /** + * Sets the {@link FlushStrategy} that will be used to detect when an automatic flush + * should happen. + * + * @param flushStrategy the strategy to use. + * @return the instance itself. + */ + public final B flushStrategy(FlushStrategy flushStrategy) { + this.flushStrategy = Objects.requireNonNull(flushStrategy, "flushStrategy"); + return self(); + } + + /** + * Sets the congestion control algorithm to use. + * + * The default is {@link QuicCongestionControlAlgorithm#CUBIC}. + * + * @param congestionControlAlgorithm the {@link QuicCongestionControlAlgorithm} to use. + * @return the instance itself. + */ + public final B congestionControlAlgorithm(QuicCongestionControlAlgorithm congestionControlAlgorithm) { + this.congestionControlAlgorithm = congestionControlAlgorithm; + return self(); + } + + /** + * Set if greasing should be enabled + * or not. + * + * The default value is {@code true}. + * + * @param enable {@code true} if enabled, {@code false} otherwise. + * @return the instance itself. + */ + public final B grease(boolean enable) { + grease = enable; + return self(); + } + + /** + * See + * set_max_idle_timeout. + * + * The default value is infinite, that is, no timeout is used. + * + * @param amount the maximum idle timeout. + * @param unit the {@link TimeUnit}. + * @return the instance itself. + */ + public final B maxIdleTimeout(long amount, TimeUnit unit) { + this.maxIdleTimeout = unit.toMillis(checkPositiveOrZero(amount, "amount")); + return self(); + } + + /** + * See + * set_max_send_udp_payload_size. + * + * The default and minimum value is 1200. + * + * @param size the maximum payload size that is advertised to the remote peer. + * @return the instance itself. + */ + public final B maxSendUdpPayloadSize(long size) { + this.maxSendUdpPayloadSize = checkPositiveOrZero(size, "value"); + return self(); + } + + /** + * See + * set_max_recv_udp_payload_size. + * + * The default value is 65527. + * + * @param size the maximum payload size that is advertised to the remote peer. + * @return the instance itself. + */ + public final B maxRecvUdpPayloadSize(long size) { + this.maxRecvUdpPayloadSize = checkPositiveOrZero(size, "value"); + return self(); + } + + /** + * See + * set_initial_max_data. + * + * The default value is 0. + * + * @param value the initial maximum data limit. + * @return the instance itself. + */ + public final B initialMaxData(long value) { + this.initialMaxData = checkPositiveOrZero(value, "value"); + return self(); + } + + /** + * See + * + * set_initial_max_stream_data_bidi_local. + * + * The default value is 0. + * + * @param value the initial maximum data limit for local bidirectional streams. + * @return the instance itself. + */ + public final B initialMaxStreamDataBidirectionalLocal(long value) { + this.initialMaxStreamDataBidiLocal = checkPositiveOrZero(value, "value"); + return self(); + } + + /** + * See + * + * set_initial_max_stream_data_bidi_remote. + * + * The default value is 0. + * + * @param value the initial maximum data limit for remote bidirectional streams. + * @return the instance itself. + */ + public final B initialMaxStreamDataBidirectionalRemote(long value) { + this.initialMaxStreamDataBidiRemote = checkPositiveOrZero(value, "value"); + return self(); + } + + /** + * See + * + * set_initial_max_stream_data_uni. + * + * The default value is 0. + * + * @param value the initial maximum data limit for unidirectional streams. + * @return the instance itself. + */ + public final B initialMaxStreamDataUnidirectional(long value) { + this.initialMaxStreamDataUni = checkPositiveOrZero(value, "value"); + return self(); + } + + /** + * See + * + * set_initial_max_streams_bidi. + * + * The default value is 0. + * + * @param value the initial maximum stream limit for bidirectional streams. + * @return the instance itself. + */ + public final B initialMaxStreamsBidirectional(long value) { + this.initialMaxStreamsBidi = checkPositiveOrZero(value, "value"); + return self(); + } + + /** + * See + * + * set_initial_max_streams_uni. + * + * The default value is 0. + * + * @param value the initial maximum stream limit for unidirectional streams. + * @return the instance itself. + */ + public final B initialMaxStreamsUnidirectional(long value) { + this.initialMaxStreamsUni = checkPositiveOrZero(value, "value"); + return self(); + } + + /** + * See + * + * set_ack_delay_exponent. + * + * The default value is 3. + * + * @param value the delay exponent used for ACKs. + * @return the instance itself. + */ + public final B ackDelayExponent(long value) { + this.ackDelayExponent = checkPositiveOrZero(value, "value"); + return self(); + } + + /** + * See + * + * set_max_ack_delay. + * + * The default value is 25 milliseconds. + * + * @param amount the max ack delay. + * @param unit the {@link TimeUnit}. + * @return the instance itself. + */ + public final B maxAckDelay(long amount, TimeUnit unit) { + this.maxAckDelay = unit.toMillis(checkPositiveOrZero(amount, "amount")); + return self(); + } + + /** + * See + * + * set_disable_active_migration. + * + * The default value is {@code true}. + * + * @param enable {@code true} if migration should be enabled, {@code false} otherwise. + * @return the instance itself. + */ + public final B activeMigration(boolean enable) { + this.disableActiveMigration = !enable; + return self(); + } + + /** + * See + * + * enable_hystart. + * + * The default value is {@code true}. + * + * @param enable {@code true} if Hystart should be enabled. + * @return the instance itself. + */ + public final B hystart(boolean enable) { + this.enableHystart = enable; + return self(); + } + + /** + * Sets the local connection id length that is used. + * + * The default is 20, which is also the maximum that is supported. + * + * @param value the length of local generated connections ids. + * @return the instance itself. + */ + public final B localConnectionIdLength(int value) { + this.localConnIdLength = checkInRange(value, 0, Quiche.QUICHE_MAX_CONN_ID_LEN, "value"); + return self(); + } + + /** + * Allows to configure the {@code QUIC version} that should be used. + * + * The default value is the latest supported version by the underlying library. + * + * @param version the {@code QUIC version} to use. + * @return the instance itself. + */ + public final B version(int version) { + this.version = version; + return self(); + } + + /** + * If configured this will enable + * Datagram support. + * @param recvQueueLen the RECV queue length. + * @param sendQueueLen the SEND queue length. + * @return the instance itself. + */ + public final B datagram(int recvQueueLen, int sendQueueLen) { + checkPositive(recvQueueLen, "recvQueueLen"); + checkPositive(sendQueueLen, "sendQueueLen"); + + this.recvQueueLen = recvQueueLen; + this.sendQueueLen = sendQueueLen; + return self(); + } + + /** + * The {@link QuicSslContext} that will be used to create {@link QuicSslEngine}s for {@link QuicChannel}s. + * + * If you need a more flexible way to provide {@link QuicSslEngine}s use {@link #sslEngineProvider(Function)}. + * + * @param sslContext the context. + * @return the instance itself. + */ + public final B sslContext(QuicSslContext sslContext) { + if (server != sslContext.isServer()) { + throw new IllegalArgumentException("QuicSslContext.isServer() " + sslContext.isServer() + + " isn't supported by this builder"); + } + return sslEngineProvider(q -> sslContext.newEngine(q.alloc())); + } + + /** + * The {@link Function} that will return the {@link QuicSslEngine} that should be used for the + * {@link QuicChannel}. + * + * @param sslEngineProvider the provider. + * @return the instance itself. + */ + public final B sslEngineProvider(Function sslEngineProvider) { + this.sslEngineProvider = sslEngineProvider; + return self(); + } + + /** + * Allow to configure a {@link Executor} that will be used to run expensive SSL operations. + * + * @param sslTaskExecutor the {@link Executor} that will be used to offload expensive SSL operations. + * @return the instance itself. + */ + public final B sslTaskExecutor(Executor sslTaskExecutor) { + this.sslTaskExecutor = sslTaskExecutor; + return self(); + } + + /** + * Allows to configure the {@code active connect id limit} that should be used. + * + * @param limit the limit to use. + * @return the instance itself. + */ + public final B activeConnectionIdLimit(long limit) { + checkPositive(limit, "limit"); + + this.activeConnectionIdLimit = limit; + return self(); + } + + + /** + * Allows to configure the {@code active connect id limit} that should be used. + * + * @param token the token to use. + * @return the instance itself. + */ + public final B statelessResetToken(byte[] token) { + if (token.length != 16) { + throw new IllegalArgumentException("token must be 16 bytes but was " + token.length); + } + + this.statelessResetToken = token.clone(); + return self(); + } + + private QuicheConfig createConfig() { + return new QuicheConfig(version, grease, + maxIdleTimeout, maxSendUdpPayloadSize, maxRecvUdpPayloadSize, initialMaxData, + initialMaxStreamDataBidiLocal, initialMaxStreamDataBidiRemote, + initialMaxStreamDataUni, initialMaxStreamsBidi, initialMaxStreamsUni, + ackDelayExponent, maxAckDelay, disableActiveMigration, enableHystart, + congestionControlAlgorithm, recvQueueLen, sendQueueLen, activeConnectionIdLimit, statelessResetToken); + } + + /** + * Validate the configuration before building the codec. + */ + protected void validate() { + if (sslEngineProvider == null) { + throw new IllegalStateException("sslEngineProvider can't be null"); + } + } + + /** + * Builds the QUIC codec that should be added to the {@link io.netty.channel.ChannelPipeline} of the underlying + * {@link io.netty.channel.Channel} which is used as transport for QUIC. + * + * @return the {@link ChannelHandler} which acts as QUIC codec. + */ + public final ChannelHandler build() { + validate(); + QuicheConfig config = createConfig(); + try { + return build(config, sslEngineProvider, sslTaskExecutor, localConnIdLength, flushStrategy); + } catch (Throwable cause) { + config.free(); + throw cause; + } + } + + /** + * Clone the builder + * + * @return the new instance that is a clone if this instance. + */ + public abstract B clone(); + + /** + * Builds the QUIC codec. + * + * @param config the {@link QuicheConfig} that should be used. + * @param sslContextProvider the context provider + * @param sslTaskExecutor the {@link Executor} to use. + * @param localConnIdLength the local connection id length. + * @param flushStrategy the {@link FlushStrategy} that should be used. + * @return the {@link ChannelHandler} which acts as codec. + */ + protected abstract ChannelHandler build(QuicheConfig config, + Function sslContextProvider, + Executor sslTaskExecutor, + int localConnIdLength, FlushStrategy flushStrategy); +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicCongestionControlAlgorithm.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicCongestionControlAlgorithm.java new file mode 100644 index 0000000..2a4d41c --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicCongestionControlAlgorithm.java @@ -0,0 +1,25 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +/** + * Available congestion control algorithms to use. + */ +public enum QuicCongestionControlAlgorithm { + RENO, + CUBIC, + BBR +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicConnectionAddress.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicConnectionAddress.java new file mode 100644 index 0000000..8cdbc31 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicConnectionAddress.java @@ -0,0 +1,129 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; + +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.Objects; + +/** + * A {@link QuicConnectionAddress} that can be used to connect too. + */ +public final class QuicConnectionAddress extends SocketAddress { + + /** + * Special {@link QuicConnectionAddress} that should be used when the connection address should be generated + * and chosen on the fly. + */ + public static final QuicConnectionAddress EPHEMERAL = new QuicConnectionAddress(null, false); + + private final String toStr; + + // Accessed by QuicheQuicheChannel + final ByteBuffer connId; + + /** + * Create a new instance + * + * @param connId the connection id to use. + */ + public QuicConnectionAddress(byte[] connId) { + this(ByteBuffer.wrap(connId.clone()), true); + } + + /** + * Create a new instance + * + * @param connId the connection id to use. + */ + public QuicConnectionAddress(ByteBuffer connId) { + this(connId, true); + } + + private QuicConnectionAddress(ByteBuffer connId, boolean validate) { + Quic.ensureAvailability(); + if (validate && connId.remaining() > Quiche.QUICHE_MAX_CONN_ID_LEN) { + throw new IllegalArgumentException("Connection ID can only be of max length " + + Quiche.QUICHE_MAX_CONN_ID_LEN); + } + this.connId = connId; + if (connId == null) { + toStr = "QuicConnectionAddress{EPHEMERAL}"; + } else { + ByteBuf buffer = Unpooled.wrappedBuffer(connId); + try { + toStr = "QuicConnectionAddress{" + + "connId=" + ByteBufUtil.hexDump(buffer) + '}'; + } finally { + buffer.release(); + } + } + } + + @Override + public String toString() { + return toStr; + } + + @Override + public int hashCode() { + if (this == EPHEMERAL) { + return System.identityHashCode(EPHEMERAL); + } + return Objects.hash(connId); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof QuicConnectionAddress)) { + return false; + } + QuicConnectionAddress address = (QuicConnectionAddress) obj; + if (obj == this) { + return true; + } + if (connId == null) { + return false; + } + return connId.equals(address.connId); + } + + /** + * Return a random generated {@link QuicConnectionAddress} of a given length + * that can be used to connect a {@link QuicChannel} + * + * @param length the length of the {@link QuicConnectionAddress} to generate. + * @return the generated address. + */ + public static QuicConnectionAddress random(int length) { + return new QuicConnectionAddress(QuicConnectionIdGenerator.randomGenerator().newId(length)); + } + + /** + * Return a random generated {@link QuicConnectionAddress} of maximum size + * that can be used to connect a {@link QuicChannel} + * + * @return the generated address. + */ + public static QuicConnectionAddress random() { + return random(Quiche.QUICHE_MAX_CONN_ID_LEN); + } + +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicConnectionCloseEvent.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicConnectionCloseEvent.java new file mode 100644 index 0000000..c3a9905 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicConnectionCloseEvent.java @@ -0,0 +1,96 @@ +/* + * Copyright 2023 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import java.util.Arrays; + +/** + * Event that is generated if the remote peer sends a + * CLOSE_CONNECTION frame. + * This allows to inspect the various details of the cause of the close. + */ +public final class QuicConnectionCloseEvent implements QuicEvent { + + final boolean applicationClose; + final int error; + final byte[] reason; + + QuicConnectionCloseEvent(boolean applicationClose, int error, byte[] reason) { + this.applicationClose = applicationClose; + this.error = error; + this.reason = reason; + } + + /** + * Return {@code true} if this was an application close, {@code false} otherwise. + * + * @return if this is an application close. + */ + public boolean isApplicationClose() { + return applicationClose; + } + + /** + * Return the error that was provided for the close. + * + * @return the error. + */ + public int error() { + return error; + } + + /** + * Returns {@code true} if a TLS error + * is contained. + * @return {@code true} if this is an {@code TLS error}, {@code false} otherwise. + */ + public boolean isTlsError() { + return !applicationClose && error >= 0x0100; + } + + /** + * Returns the reason for the close, which may be empty if no reason was given as part of the close. + * + * @return the reason. + */ + public byte[] reason() { + return reason.clone(); + } + + @Override + public String toString() { + return "QuicConnectionCloseEvent{" + + "applicationClose=" + applicationClose + + ", error=" + error + + ", reason=" + Arrays.toString(reason) + + '}'; + } + + /** + * Extract the contained {@code TLS error} from the {@code QUIC error}. If the given {@code QUIC error} does not + * contain a {@code TLS error} it will return {@code -1}. + * + * @param error the {@code QUIC error} + * @return the {@code TLS error} or {@code -1} if there was no {@code TLS error} contained. + */ + public static int extractTlsError(int error) { + int tlsError = error - 0x0100; + if (tlsError < 0) { + return -1; + } + return tlsError; + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicConnectionIdGenerator.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicConnectionIdGenerator.java new file mode 100644 index 0000000..fa62816 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicConnectionIdGenerator.java @@ -0,0 +1,77 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import java.nio.ByteBuffer; + +/** + * Creates new connection id instances. + */ +public interface QuicConnectionIdGenerator { + /** + * Creates a new connection id with the given length. This method may not be supported by + * a sign id generator implementation as a sign id generator should always have an input + * to sign with, otherwise this method may generate the same id which may cause some + * unpredictable issues when we use it. + * + * @param length the length of the id. + * @return the id. + */ + ByteBuffer newId(int length); + + /** + * Creates a new connection id with the given length. The given input may be used to sign or + * seed the id, or may be ignored (depending on the implementation). + * + * @param input the input which may be used to generate the id. + * @param length the length of the id. + * @return the id. + */ + ByteBuffer newId(ByteBuffer input, int length); + + /** + * Returns the maximum length of a connection id. + * + * @return the maximum length of a connection id that is supported. + */ + int maxConnectionIdLength(); + + /** + * Returns true if the implementation is idempotent, which means we will get the same id + * with the same input ByteBuffer. Otherwise, returns false. + * + * @return whether the implementation is idempotent. + */ + boolean isIdempotent(); + + /** + * Return a {@link QuicConnectionIdGenerator} which randomly generates new connection ids. + * + * @return a {@link QuicConnectionIdGenerator} which randomly generated ids. + */ + static QuicConnectionIdGenerator randomGenerator() { + return SecureRandomQuicConnectionIdGenerator.INSTANCE; + } + + /** + * Return a {@link QuicConnectionIdGenerator} which generates new connection ids by signing the given input. + * + * @return a {@link QuicConnectionIdGenerator} which generates ids by signing the given input. + */ + static QuicConnectionIdGenerator signGenerator() { + return HmacSignQuicConnectionIdGenerator.INSTANCE; + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicConnectionStats.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicConnectionStats.java new file mode 100644 index 0000000..027d7e9 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicConnectionStats.java @@ -0,0 +1,67 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +/** + * Statistics about the {@code QUIC} connection. If unknown by the implementation it might return {@code -1} values + * for the various methods. + */ +public interface QuicConnectionStats { + /** + * @return The number of QUIC packets received on the connection. + */ + long recv(); + + /** + * @return The number of QUIC packets sent on this connection. + */ + long sent(); + + /** + * @return The number of QUIC packets that were lost. + */ + long lost(); + + /** + * @return The number of sent QUIC packets with retransmitted data. + */ + long retrans(); + + /** + * @return The number of sent bytes. + */ + long sentBytes(); + + /** + * @return The number of received bytes. + */ + long recvBytes(); + + /** + * @return The number of bytes lost. + */ + long lostBytes(); + + /** + * @return The number of stream bytes retransmitted. + */ + long streamRetransBytes(); + + /** + * @return The number of known paths for the connection. + */ + long pathsCount(); +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicDatagramExtensionEvent.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicDatagramExtensionEvent.java new file mode 100644 index 0000000..6ad3300 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicDatagramExtensionEvent.java @@ -0,0 +1,48 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.util.internal.ObjectUtil; + +/** + * Used when the remote peer supports the + * QUIC DATAGRAM extension. + */ +public final class QuicDatagramExtensionEvent implements QuicExtensionEvent { + + private final int maxLength; + + QuicDatagramExtensionEvent(int maxLength) { + this.maxLength = ObjectUtil.checkPositiveOrZero(maxLength, "maxLength"); + } + + /** + * The maximum datagram payload length the peer will accept. If you try to write bigger datagrams the write will + * fail. + * + * @return the max length. + */ + public int maxLength() { + return maxLength; + } + + @Override + public String toString() { + return "QuicDatagramExtensionEvent{" + + "maxLength=" + maxLength + + '}'; + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicError.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicError.java new file mode 100644 index 0000000..0e8aec4 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicError.java @@ -0,0 +1,81 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.util.collection.IntObjectHashMap; +import io.netty.util.collection.IntObjectMap; + +/** + * All QUIC error codes identified by Quiche. + * See Error + */ +public enum QuicError { + BUFFER_TOO_SHORT(Quiche.QUICHE_ERR_BUFFER_TOO_SHORT, "QUICHE_ERR_BUFFER_TOO_SHORT"), + UNKNOWN_VERSION(Quiche.QUICHE_ERR_UNKNOWN_VERSION, "QUICHE_ERR_UNKNOWN_VERSION"), + INVALID_FRAME(Quiche.QUICHE_ERR_INVALID_FRAME, "QUICHE_ERR_INVALID_FRAME"), + INVALID_PACKET(Quiche.QUICHE_ERR_INVALID_PACKET, "QUICHE_ERR_INVALID_PACKET"), + INVALID_STATE(Quiche.QUICHE_ERR_INVALID_STATE, "QUICHE_ERR_INVALID_STATE"), + INVALID_STREAM_STATE(Quiche.QUICHE_ERR_INVALID_STREAM_STATE, "QUICHE_ERR_INVALID_STREAM_STATE"), + INVALID_TRANSPORT_PARAM(Quiche.QUICHE_ERR_INVALID_TRANSPORT_PARAM, "QUICHE_ERR_INVALID_TRANSPORT_PARAM"), + CRYPTO_FAIL(Quiche.QUICHE_ERR_CRYPTO_FAIL, "QUICHE_ERR_CRYPTO_FAIL"), + TLS_FAIL(Quiche.QUICHE_ERR_TLS_FAIL, "QUICHE_ERR_TLS_FAIL"), + FLOW_CONTROL(Quiche.QUICHE_ERR_FLOW_CONTROL, "QUICHE_ERR_FLOW_CONTROL"), + STREAM_LIMIT(Quiche.QUICHE_ERR_STREAM_LIMIT, "QUICHE_ERR_STREAM_LIMIT"), + FINAL_SIZE(Quiche.QUICHE_ERR_FINAL_SIZE, "QUICHE_ERR_FINAL_SIZE"), + CONGESTION_CONTROL(Quiche.QUICHE_ERR_CONGESTION_CONTROL, "QUICHE_ERR_CONGESTION_CONTROL"), + STREAM_RESET(Quiche.QUICHE_ERR_STREAM_RESET, "STREAM_RESET"), + STREAM_STOPPED(Quiche.QUICHE_ERR_STREAM_STOPPED, "STREAM_STOPPED"), + ID_LIMIT(Quiche.QUICHE_ERR_ID_LIMIT, "ID_LIMIT"), + QUT_OF_IDENTIFIERS(Quiche.QUICHE_ERR_OUT_OF_IDENTIFIERS, "OUT_OF_IDENTIFIERS"), + KEY_UPDATE(Quiche.QUICHE_ERR_KEY_UPDATE, "KEY_UPDATE"); + + private static final IntObjectMap ERROR_MAP = new IntObjectHashMap<>(); + + static { + for (QuicError errorCode : QuicError.values()) { + ERROR_MAP.put(errorCode.code(), errorCode); + } + } + + private final int code; + private final String message; + + QuicError(int code, String message) { + this.code = code; + this.message = message; + } + + final int code() { + return code; + } + + final String message() { + return message; + } + + @Override + public final String toString() { + return String.format("QuicError{code=%d, message=%s}", code, message); + } + + static QuicError valueOf(int code) { + final QuicError errorCode = ERROR_MAP.get(code); + if (errorCode == null) { + throw new IllegalArgumentException("unknown " + QuicError.class.getSimpleName() + " code: " + code); + } + return errorCode; + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicEvent.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicEvent.java new file mode 100644 index 0000000..26cbc61 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicEvent.java @@ -0,0 +1,23 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +/** + * Marker interface for events that will be passed through the {@link io.netty.channel.ChannelPipeline} via + * {@link io.netty.channel.ChannelPipeline#fireUserEventTriggered(Object)} to notify the user about {@code QUIC} + * specific events. + */ +public interface QuicEvent { } diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicException.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicException.java new file mode 100644 index 0000000..90534e3 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicException.java @@ -0,0 +1,40 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import java.io.IOException; + +/** + * Exception produced while processing {@code QUIC}. + */ +public final class QuicException extends IOException { + + private final QuicError error; + + QuicException(QuicError error) { + super(error.message()); + this.error = error; + } + + /** + * Returns the {@link QuicError} which was the cause of the {@link QuicException}. + * + * @return the {@link QuicError} that caused this {@link QuicException}. + */ + public QuicError error() { + return error; + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicExtensionEvent.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicExtensionEvent.java new file mode 100644 index 0000000..1112bb4 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicExtensionEvent.java @@ -0,0 +1,23 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +/** + * Marker interface for events that will be passed through the {@link io.netty.channel.ChannelPipeline} via + * {@link io.netty.channel.ChannelPipeline#fireUserEventTriggered(Object)} to notify the user about supported + * QUIC extensions by the remote peer. + */ +public interface QuicExtensionEvent extends QuicEvent { } diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicHeaderParser.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicHeaderParser.java new file mode 100644 index 0000000..f24fa24 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicHeaderParser.java @@ -0,0 +1,163 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +import java.net.InetSocketAddress; + +import static io.netty.handler.codec.quic.Quiche.allocateNativeOrder; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + +/** + * Parses the QUIC packet header and notifies a callback once parsing was successful. + * + * Once the parser is not needed anymore the user needs to call {@link #close()} to ensure all resources are + * released. Failed to do so may lead to memory leaks. + * + * This class can be used for advanced use-cases. Usually you want to just use {@link QuicClientCodecBuilder} or + * {@link QuicServerCodecBuilder}. + */ +public final class QuicHeaderParser implements AutoCloseable { + private final int maxTokenLength; + private final int localConnectionIdLength; + private final ByteBuf versionBuffer; + private final ByteBuf typeBuffer; + private final ByteBuf scidLenBuffer; + private final ByteBuf scidBuffer; + private final ByteBuf dcidLenBuffer; + private final ByteBuf dcidBuffer; + private final ByteBuf tokenBuffer; + private final ByteBuf tokenLenBuffer; + private boolean closed; + + public QuicHeaderParser(int maxTokenLength, int localConnectionIdLength) { + Quic.ensureAvailability(); + this.maxTokenLength = checkPositiveOrZero(maxTokenLength, "maxTokenLength"); + this.localConnectionIdLength = checkPositiveOrZero(localConnectionIdLength, "localConnectionIdLength"); + // Allocate the buffer from which we read primative values like integer/long with native order to ensure + // we read the right value. + versionBuffer = allocateNativeOrder(Integer.BYTES); + typeBuffer = allocateNativeOrder(Byte.BYTES); + scidLenBuffer = allocateNativeOrder(Integer.BYTES); + dcidLenBuffer = allocateNativeOrder(Integer.BYTES); + tokenLenBuffer = allocateNativeOrder(Integer.BYTES); + + // Now allocate the buffers that dont need native ordering and so will be cheaper to access when we slice into + // these or obtain a view into these via internalNioBuffer(...). + scidBuffer = Unpooled.directBuffer(Quiche.QUICHE_MAX_CONN_ID_LEN); + dcidBuffer = Unpooled.directBuffer(Quiche.QUICHE_MAX_CONN_ID_LEN); + tokenBuffer = Unpooled.directBuffer(maxTokenLength); + } + + @Override + public void close() { + if (!closed) { + closed = true; + versionBuffer.release(); + typeBuffer.release(); + scidBuffer.release(); + scidLenBuffer.release(); + dcidBuffer.release(); + dcidLenBuffer.release(); + tokenLenBuffer.release(); + tokenBuffer.release(); + } + } + + /** + * Parses a QUIC packet and extract the header values out of it. This method takes no ownership of the packet itself + * which means the caller of this method is expected to call {@link ByteBuf#release()} once the packet is not needed + * anymore. + * + * @param sender the sender of the packet. This is directly passed to the {@link QuicHeaderProcessor} once + * parsing was successful. + * @param recipient the recipient of the packet.This is directly passed to the {@link QuicHeaderProcessor} once + * parsing was successful. + * @param packet raw QUIC packet itself. The ownership of the packet is not transferred. This is directly + * passed to the {@link QuicHeaderProcessor} once parsing was successful. + * @param callback the {@link QuicHeaderProcessor} that is called once a QUIC packet could be parsed and all + * the header values be extracted. + * @throws Exception thrown if we couldn't parse the header or if the {@link QuicHeaderProcessor} throws an + * exception. + */ + public void parse(InetSocketAddress sender, + InetSocketAddress recipient, ByteBuf packet, QuicHeaderProcessor callback) throws Exception { + if (closed) { + throw new IllegalStateException(QuicHeaderParser.class.getSimpleName() + " is already closed"); + } + + // Set various len values so quiche_header_info can make use of these. + scidLenBuffer.setInt(0, Quiche.QUICHE_MAX_CONN_ID_LEN); + dcidLenBuffer.setInt(0, Quiche.QUICHE_MAX_CONN_ID_LEN); + tokenLenBuffer.setInt(0, maxTokenLength); + + int res = Quiche.quiche_header_info( + Quiche.readerMemoryAddress(packet), packet.readableBytes(), + localConnectionIdLength, + Quiche.memoryAddress(versionBuffer, 0, versionBuffer.capacity()), + Quiche.memoryAddress(typeBuffer, 0, typeBuffer.capacity()), + Quiche.memoryAddress(scidBuffer, 0, scidBuffer.capacity()), + Quiche.memoryAddress(scidLenBuffer, 0, scidLenBuffer.capacity()), + Quiche.memoryAddress(dcidBuffer, 0, dcidBuffer.capacity()), + Quiche.memoryAddress(dcidLenBuffer, 0, dcidLenBuffer.capacity()), + Quiche.memoryAddress(tokenBuffer, 0, tokenBuffer.capacity()), + Quiche.writerMemoryAddress(tokenLenBuffer)); + if (res >= 0) { + int version = versionBuffer.getInt(0); + byte type = typeBuffer.getByte(0); + int scidLen = scidLenBuffer.getInt(0); + int dcidLen = dcidLenBuffer.getInt(0); + int tokenLen = tokenLenBuffer.getInt(0); + + callback.process(sender, recipient, packet, QuicPacketType.of(type), version, + scidBuffer.setIndex(0, scidLen), + dcidBuffer.setIndex(0, dcidLen), + tokenBuffer.setIndex(0, tokenLen)); + } else { + throw Quiche.newException(res); + } + } + + /** + * Called when a QUIC packet and its header could be parsed. + */ + public interface QuicHeaderProcessor { + + /** + * Called when a QUIC packet header was parsed. + * + * @param sender the sender of the QUIC packet. + * @param recipient the recipient of the QUIC packet. + * @param packet the raw QUIC packet. The ownership is not transferred, which means you will need to call + * {@link ByteBuf#retain()} on it if you want to keep a reference after this method + * returns. + * @param type the type of the packet. + * @param version the version of the packet. + * @param scid the source connection id. The ownership is not transferred and its generally not allowed + * to hold any references to this buffer outside of the method as it will be re-used. + * @param dcid the destination connection id. The ownership is not transferred and its generally not + * allowed to hold any references to this buffer outside of the method as it will be + * re-used. + * @param token the token.The ownership is not transferred and its generally not allowed + * to hold any references to this buffer outside of the method as it will be re-used. + * @throws Exception throws if an error happens during processing. + */ + void process(InetSocketAddress sender, InetSocketAddress recipient, ByteBuf packet, + QuicPacketType type, int version, ByteBuf scid, ByteBuf dcid, ByteBuf token) throws Exception; + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicPacketType.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicPacketType.java new file mode 100644 index 0000000..318c617 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicPacketType.java @@ -0,0 +1,83 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +/** + * The type of the + * QUIC packet. + */ +public enum QuicPacketType { + /** + * Initial packet. + */ + INITIAL((byte) 1), + + /** + * Retry packet. + */ + RETRY((byte) 2), + + /** + * Handshake packet. + */ + HANDSHAKE((byte) 3), + + /** + * 0-RTT packet. + */ + ZERO_RTT((byte) 4), + + /** + * 1-RTT short header packet. + */ + SHORT((byte) 5), + + /** + * Version negotiation packet. + */ + VERSION_NEGOTIATION((byte) 6); + + final byte type; + + QuicPacketType(byte type) { + this.type = type; + } + + /** + * Return the {@link QuicPacketType} for the given byte. + * + * @param type the byte that represent the type. + * @return the {@link QuicPacketType}. + */ + static QuicPacketType of(byte type) { + switch(type) { + case 1: + return INITIAL; + case 2: + return RETRY; + case 3: + return HANDSHAKE; + case 4: + return ZERO_RTT; + case 5: + return SHORT; + case 6: + return VERSION_NEGOTIATION; + default: + throw new IllegalArgumentException("Unknown QUIC packet type: " + type); + } + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicPathEvent.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicPathEvent.java new file mode 100644 index 0000000..96f6952 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicPathEvent.java @@ -0,0 +1,297 @@ +/* + * Copyright 2023 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import java.net.InetSocketAddress; +import java.util.Objects; + +import static java.util.Objects.requireNonNull; + +/** + * A network path specific {@link QuicEvent}. + */ +public abstract class QuicPathEvent implements QuicEvent { + + private final InetSocketAddress local; + private final InetSocketAddress remote; + + QuicPathEvent(InetSocketAddress local, InetSocketAddress remote) { + this.local = requireNonNull(local, "local"); + this.remote = requireNonNull(remote, "remote"); + } + + /** + * The local address of the network path. + * + * @return local + */ + public InetSocketAddress local() { + return local; + } + + /** + * The remote address of the network path. + * + * @return local + */ + public InetSocketAddress remote() { + return remote; + } + + @Override + public String toString() { + return "QuicPathEvent{" + + "local=" + local + + ", remote=" + remote + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + QuicPathEvent that = (QuicPathEvent) o; + if (!Objects.equals(local, that.local)) { + return false; + } + return Objects.equals(remote, that.remote); + } + + @Override + public int hashCode() { + int result = local != null ? local.hashCode() : 0; + result = 31 * result + (remote != null ? remote.hashCode() : 0); + return result; + } + + public static final class New extends QuicPathEvent { + /** + * A new network path (local address, remote address) has been seen on a received packet. + * Note that this event is only triggered for servers, as the client is responsible from initiating new paths. + * The application may then probe this new path, if desired. + * + * @param local local address. + * @param remote remote address. + */ + public New(InetSocketAddress local, InetSocketAddress remote) { + super(local, remote); + } + + @Override + public String toString() { + return "QuicPathEvent.New{" + + "local=" + local() + + ", remote=" + remote() + + '}'; + } + } + + public static final class Validated extends QuicPathEvent { + /** + * The related network path between local and remote has been validated. + * + * @param local local address. + * @param remote remote address. + */ + public Validated(InetSocketAddress local, InetSocketAddress remote) { + super(local, remote); + } + + @Override + public String toString() { + return "QuicPathEvent.Validated{" + + "local=" + local() + + ", remote=" + remote() + + '}'; + } + } + + public static final class FailedValidation extends QuicPathEvent { + /** + * The related network path between local and remote failed to be validated. + * This network path will not be used anymore, unless the application requests probing this path again. + * + * @param local local address. + * @param remote remote address. + */ + public FailedValidation(InetSocketAddress local, InetSocketAddress remote) { + super(local, remote); + } + + @Override + public String toString() { + return "QuicPathEvent.FailedValidation{" + + "local=" + local() + + ", remote=" + remote() + + '}'; + } + } + + public static final class Closed extends QuicPathEvent { + + /** + * The related network path between local and remote has been closed and is now unusable on this connection. + * + * @param local local address. + * @param remote remote address. + */ + public Closed(InetSocketAddress local, InetSocketAddress remote) { + super(local, remote); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + return super.equals(o); + } + + @Override + public String toString() { + return "QuicPathEvent.Closed{" + + "local=" + local() + + ", remote=" + remote() + + '}'; + } + } + + public static final class ReusedSourceConnectionId extends QuicPathEvent { + private final long seq; + private final InetSocketAddress oldLocal; + private final InetSocketAddress oldRemote; + + /** + * The stack observes that the Source Connection ID with the given sequence number, + * initially used by the peer over the first pair of addresses, is now reused over + * the second pair of addresses. + * + * @param seq sequence number + * @param oldLocal old local address. + * @param oldRemote old remote address. + * @param local local address. + * @param remote remote address. + */ + public ReusedSourceConnectionId(long seq, InetSocketAddress oldLocal, InetSocketAddress oldRemote, + InetSocketAddress local, InetSocketAddress remote) { + super(local, remote); + this.seq = seq; + this.oldLocal = requireNonNull(oldLocal, "oldLocal"); + this.oldRemote = requireNonNull(oldRemote, "oldRemote"); + } + + /** + * Source connection id sequence number. + * + * @return sequence number + */ + public long seq() { + return seq; + } + + /** + * The old local address of the network path. + * + * @return local + */ + public InetSocketAddress oldLocal() { + return oldLocal; + } + + /** + * The old remote address of the network path. + * + * @return local + */ + public InetSocketAddress oldRemote() { + return oldRemote; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + + ReusedSourceConnectionId that = (ReusedSourceConnectionId) o; + + if (seq != that.seq) { + return false; + } + if (!Objects.equals(oldLocal, that.oldLocal)) { + return false; + } + return Objects.equals(oldRemote, that.oldRemote); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (int) (seq ^ (seq >>> 32)); + result = 31 * result + (oldLocal != null ? oldLocal.hashCode() : 0); + result = 31 * result + (oldRemote != null ? oldRemote.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "QuicPathEvent.ReusedSourceConnectionId{" + + "seq=" + seq + + ", oldLocal=" + oldLocal + + ", oldRemote=" + oldRemote + + ", local=" + local() + + ", remote=" + remote() + + '}'; + } + } + + public static final class PeerMigrated extends QuicPathEvent { + + /** + * The connection observed that the remote migrated over the network path denoted by the pair of addresses, + * i.e., non-probing packets have been received on this network path. This is a server side only event. + * Note that this event is only raised if the path has been validated. + * + * @param local local address. + * @param remote remote address. + */ + public PeerMigrated(InetSocketAddress local, InetSocketAddress remote) { + super(local, remote); + } + + @Override + public String toString() { + return "QuicPathEvent.PeerMigrated{" + + "local=" + local() + + ", remote=" + remote() + + '}'; + } + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicResetTokenGenerator.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicResetTokenGenerator.java new file mode 100644 index 0000000..feb10d4 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicResetTokenGenerator.java @@ -0,0 +1,42 @@ +/* + * Copyright 2023 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import java.nio.ByteBuffer; + +/** + * Generate + * + * stateless reset tokens to use. + */ +public interface QuicResetTokenGenerator { + + /** + * Generate a reset token to use for the given connection id. The returned token MUST be of length 16. + * @param cid + * @return + */ + ByteBuffer newResetToken(ByteBuffer cid); + + /** + * Return a {@link QuicResetTokenGenerator} which generates new reset tokens by signing the given input. + * + * @return a {@link QuicResetTokenGenerator} which generates new reset tokens by signing the given input. + */ + static QuicResetTokenGenerator signGenerator() { + return HmacSignQuicResetTokenGenerator.INSTANCE; + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicServerCodecBuilder.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicServerCodecBuilder.java new file mode 100644 index 0000000..7d6ec22 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicServerCodecBuilder.java @@ -0,0 +1,221 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelOption; +import io.netty.util.AttributeKey; +import io.netty.util.internal.ObjectUtil; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.function.Function; + +/** + * {@link QuicCodecBuilder} that configures and builds a {@link ChannelHandler} that should be added to the + * {@link io.netty.channel.ChannelPipeline} of a {@code QUIC} server. + */ +public final class QuicServerCodecBuilder extends QuicCodecBuilder { + // The order in which ChannelOptions are applied is important they may depend on each other for validation + // purposes. + private final Map, Object> options = new LinkedHashMap<>(); + private final Map, Object> attrs = new HashMap<>(); + private final Map, Object> streamOptions = new LinkedHashMap<>(); + private final Map, Object> streamAttrs = new HashMap<>(); + private ChannelHandler handler; + private ChannelHandler streamHandler; + private QuicConnectionIdGenerator connectionIdAddressGenerator; + private QuicTokenHandler tokenHandler; + private QuicResetTokenGenerator resetTokenGenerator; + + /** + * Creates a new instance. + */ + public QuicServerCodecBuilder() { + super(true); + } + + private QuicServerCodecBuilder(QuicServerCodecBuilder builder) { + super(builder); + options.putAll(builder.options); + attrs.putAll(builder.attrs); + streamOptions.putAll(builder.streamOptions); + streamAttrs.putAll(builder.streamAttrs); + handler = builder.handler; + streamHandler = builder.streamHandler; + connectionIdAddressGenerator = builder.connectionIdAddressGenerator; + tokenHandler = builder.tokenHandler; + } + + @Override + public QuicServerCodecBuilder clone() { + return new QuicServerCodecBuilder(this); + } + + /** + * Allow to specify a {@link ChannelOption} which is used for the {@link QuicChannel} instances once they got + * created. Use a value of {@code null} to remove a previous set {@link ChannelOption}. + * + * @param option the {@link ChannelOption} to apply to the {@link QuicChannel}. + * @param value the value of the option. + * @param the type of the value. + * @return this instance. + */ + public QuicServerCodecBuilder option(ChannelOption option, T value) { + Quic.updateOptions(options, option, value); + return self(); + } + + /** + * Allow to specify an initial attribute of the newly created {@link QuicChannel}. If the {@code value} is + * {@code null}, the attribute of the specified {@code key} is removed. + * + * @param key the {@link AttributeKey} to apply to the {@link QuicChannel}. + * @param value the value of the attribute. + * @param the type of the value. + * @return this instance. + */ + public QuicServerCodecBuilder attr(AttributeKey key, T value) { + Quic.updateAttributes(attrs, key, value); + return self(); + } + + /** + * Set the {@link ChannelHandler} that is added to the {@link io.netty.channel.ChannelPipeline} of the + * {@link QuicChannel} once created. + * + * @param handler the {@link ChannelHandler} that is added to the {@link QuicChannel}s + * {@link io.netty.channel.ChannelPipeline}. + * @return this instance. + */ + public QuicServerCodecBuilder handler(ChannelHandler handler) { + this.handler = ObjectUtil.checkNotNull(handler, "handler"); + return self(); + } + + /** + * Allow to specify a {@link ChannelOption} which is used for the {@link QuicStreamChannel} instances once they got + * created. Use a value of {@code null} to remove a previous set {@link ChannelOption}. + * + * @param option the {@link ChannelOption} to apply to the {@link QuicStreamChannel}s. + * @param value the value of the option. + * @param the type of the value. + * @return this instance. + */ + public QuicServerCodecBuilder streamOption(ChannelOption option, T value) { + Quic.updateOptions(streamOptions, option, value); + return self(); + } + + /** + * Allow to specify an initial attribute of the newly created {@link QuicStreamChannel}. If the {@code value} is + * {@code null}, the attribute of the specified {@code key} is removed. + * + * @param key the {@link AttributeKey} to apply to the {@link QuicStreamChannel}s. + * @param value the value of the attribute. + * @param the type of the value. + * @return this instance. + */ + public QuicServerCodecBuilder streamAttr(AttributeKey key, T value) { + Quic.updateAttributes(streamAttrs, key, value); + return self(); + } + + /** + * Set the {@link ChannelHandler} that is added to the {@link io.netty.channel.ChannelPipeline} of the + * {@link QuicStreamChannel} once created. + * + * @param streamHandler the {@link ChannelHandler} that is added to the {@link QuicStreamChannel}s + * {@link io.netty.channel.ChannelPipeline}. + * @return this instance. + */ + public QuicServerCodecBuilder streamHandler(ChannelHandler streamHandler) { + this.streamHandler = ObjectUtil.checkNotNull(streamHandler, "streamHandler"); + return self(); + } + + /** + * Sets the {@link QuicConnectionIdGenerator} to use. + * + * @param connectionIdAddressGenerator the {@link QuicConnectionIdGenerator} to use. + * @return this instance. + */ + public QuicServerCodecBuilder connectionIdAddressGenerator( + QuicConnectionIdGenerator connectionIdAddressGenerator) { + this.connectionIdAddressGenerator = connectionIdAddressGenerator; + return this; + } + + /** + * Set the {@link QuicTokenHandler} that is used to generate and validate tokens or + * {@code null} if no tokens should be used at all. + * + * @param tokenHandler the {@link QuicTokenHandler} to use. + * @return this instance. + */ + public QuicServerCodecBuilder tokenHandler(QuicTokenHandler tokenHandler) { + this.tokenHandler = tokenHandler; + return self(); + } + + /** + * Set the {@link QuicResetTokenGenerator} that is used to generate stateless reset tokens or + * {@code null} if the default should be used. + * + * @param resetTokenGenerator the {@link QuicResetTokenGenerator} to use. + * @return this instance. + */ + public QuicServerCodecBuilder resetTokenGenerator(QuicResetTokenGenerator resetTokenGenerator) { + this.resetTokenGenerator = resetTokenGenerator; + return self(); + } + + @Override + protected void validate() { + super.validate(); + if (handler == null && streamHandler == null) { + throw new IllegalStateException("handler and streamHandler not set"); + } + } + + @Override + protected ChannelHandler build(QuicheConfig config, + Function sslEngineProvider, + Executor sslTaskExecutor, + int localConnIdLength, FlushStrategy flushStrategy) { + validate(); + QuicTokenHandler tokenHandler = this.tokenHandler; + if (tokenHandler == null) { + tokenHandler = NoQuicTokenHandler.INSTANCE; + } + QuicConnectionIdGenerator generator = connectionIdAddressGenerator; + if (generator == null) { + generator = QuicConnectionIdGenerator.signGenerator(); + } + QuicResetTokenGenerator resetTokenGenerator = this.resetTokenGenerator; + if (resetTokenGenerator == null) { + resetTokenGenerator = QuicResetTokenGenerator.signGenerator(); + } + ChannelHandler handler = this.handler; + ChannelHandler streamHandler = this.streamHandler; + return new QuicheQuicServerCodec(config, localConnIdLength, tokenHandler, generator, resetTokenGenerator, + flushStrategy, sslEngineProvider, sslTaskExecutor, handler, + Quic.toOptionsArray(options), Quic.toAttributesArray(attrs), + streamHandler, Quic.toOptionsArray(streamOptions), Quic.toAttributesArray(streamAttrs)); + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicSslContext.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicSslContext.java new file mode 100644 index 0000000..e367771 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicSslContext.java @@ -0,0 +1,43 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.ssl.SslContext; + +import java.io.InputStream; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * Special {@link SslContext} that can be used for {@code QUIC}. + */ +public abstract class QuicSslContext extends SslContext { + + @Override + public abstract QuicSslEngine newEngine(ByteBufAllocator alloc); + + @Override + public abstract QuicSslEngine newEngine(ByteBufAllocator alloc, String peerHost, int peerPort); + + @Override + public abstract QuicSslSessionContext sessionContext(); + + static X509Certificate[] toX509Certificates0(InputStream stream) + throws CertificateException { + return SslContext.toX509Certificates(stream); + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicSslContextBuilder.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicSslContextBuilder.java new file mode 100644 index 0000000..365da0e --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicSslContextBuilder.java @@ -0,0 +1,381 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.netty.handler.codec.quic; + +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.util.KeyManagerFactoryWrapper; +import io.netty.handler.ssl.util.TrustManagerFactoryWrapper; +import io.netty.util.Mapping; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedKeyManager; +import java.io.File; +import java.net.Socket; +import java.security.KeyStore; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +/** + * Builder for configuring a new SslContext for creation. + */ +public final class QuicSslContextBuilder { + + /** + * Special {@link X509ExtendedKeyManager} implementation which will just fail the certificate selection. + * This is used as a "dummy" implementation when SNI is used as we should always select an other + * {@link QuicSslContext} based on the provided hostname. + */ + private static final X509ExtendedKeyManager SNI_KEYMANAGER = new X509ExtendedKeyManager() { + private final X509Certificate[] emptyCerts = new X509Certificate[0]; + private final String[] emptyStrings = new String[0]; + + @Override + public String[] getClientAliases(String keyType, Principal[] issuers) { + return emptyStrings; + } + + @Override + public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { + return null; + } + + @Override + public String[] getServerAliases(String keyType, Principal[] issuers) { + return emptyStrings; + } + + @Override + public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { + return null; + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + return emptyCerts; + } + + @Override + public PrivateKey getPrivateKey(String alias) { + return null; + } + }; + + /** + * Creates a builder for new client-side {@link QuicSslContext} that can be used for {@code QUIC}. + */ + public static QuicSslContextBuilder forClient() { + return new QuicSslContextBuilder(false); + } + + /** + * Creates a builder for new server-side {@link QuicSslContext} that can be used for {@code QUIC}. + * + * @param keyFile a PKCS#8 private key file in PEM format + * @param keyPassword the password of the {@code keyFile}, or {@code null} if it's not + * password-protected + * @param certChainFile an X.509 certificate chain file in PEM format + * @see #keyManager(File, String, File) + */ + public static QuicSslContextBuilder forServer( + File keyFile, String keyPassword, File certChainFile) { + return new QuicSslContextBuilder(true).keyManager(keyFile, keyPassword, certChainFile); + } + + /** + * Creates a builder for new server-side {@link QuicSslContext} that can be used for {@code QUIC}. + * + * @param key a PKCS#8 private key + * @param keyPassword the password of the {@code keyFile}, or {@code null} if it's not + * password-protected + * @param certChain the X.509 certificate chain + * @see #keyManager(File, String, File) + */ + public static QuicSslContextBuilder forServer( + PrivateKey key, String keyPassword, X509Certificate... certChain) { + return new QuicSslContextBuilder(true).keyManager(key, keyPassword, certChain); + } + + /** + * Creates a builder for new server-side {@link QuicSslContext} that can be used for {@code QUIC}. + * + * @param keyManagerFactory non-{@code null} factory for server's private key + * @see #keyManager(KeyManagerFactory, String) + */ + public static QuicSslContextBuilder forServer(KeyManagerFactory keyManagerFactory, String password) { + return new QuicSslContextBuilder(true).keyManager(keyManagerFactory, password); + } + + /** + * Creates a builder for new server-side {@link QuicSslContext} with {@link KeyManager} that can be used for + * {@code QUIC}. + * + * @param keyManager non-{@code null} KeyManager for server's private key + * @param keyPassword the password of the {@code keyFile}, or {@code null} if it's not + * password-protected + */ + public static QuicSslContextBuilder forServer(KeyManager keyManager, String keyPassword) { + return new QuicSslContextBuilder(true).keyManager(keyManager, keyPassword); + } + + /** + * Enables support for + * + * SNI on the server side. + * + * @param mapping the {@link Mapping} that is used to map names to the {@link QuicSslContext} to use. + * Usually using {@link io.netty.util.DomainWildcardMappingBuilder} should be used + * to create the {@link Mapping}. + */ + public static QuicSslContext buildForServerWithSni(Mapping mapping) { + return forServer(SNI_KEYMANAGER, null).sni(mapping).build(); + } + + private final boolean forServer; + private TrustManagerFactory trustManagerFactory; + private String keyPassword; + private KeyManagerFactory keyManagerFactory; + private long sessionCacheSize = 20480; + private long sessionTimeout = 300; + private ClientAuth clientAuth = ClientAuth.NONE; + private String[] applicationProtocols; + private Boolean earlyData; + private BoringSSLKeylog keylog; + private Mapping mapping; + + private QuicSslContextBuilder(boolean forServer) { + this.forServer = forServer; + } + + private QuicSslContextBuilder sni(Mapping mapping) { + this.mapping = checkNotNull(mapping, "mapping"); + return this; + } + + /** + * Enable / disable the usage of early data. + */ + public QuicSslContextBuilder earlyData(boolean enabled) { + this.earlyData = enabled; + return this; + } + + /** + * Enable / disable keylog. When enabled, TLS keys are logged to an internal logger named + * "io.netty.handler.codec.quic.BoringSSLLogginKeylog" with DEBUG level, see + * {@link BoringSSLKeylog} for detail, logging keys are following + * + * NSS Key Log Format. This is intended for debugging use with tools like Wireshark. + */ + public QuicSslContextBuilder keylog(boolean enabled) { + keylog(enabled ? BoringSSLLoggingKeylog.INSTANCE : null); + return this; + } + + /** + * Enable / disable keylog. When enabled, TLS keys are logged to {@link BoringSSLKeylog#logKey(SSLEngine, String)} + * logging keys are following + * + * NSS Key Log Format. This is intended for debugging use with tools like Wireshark. + */ + public QuicSslContextBuilder keylog(BoringSSLKeylog keylog) { + this.keylog = keylog; + return this; + } + + /** + * Trusted certificates for verifying the remote endpoint's certificate. The file should + * contain an X.509 certificate collection in PEM format. {@code null} uses the system default + * which only works with Java 8u261 and later as these versions support TLS1.3, + * see + * JDK 8u261 Update Release Notes + */ + public QuicSslContextBuilder trustManager(File trustCertCollectionFile) { + try { + return trustManager(QuicheQuicSslContext.toX509Certificates0(trustCertCollectionFile)); + } catch (Exception e) { + throw new IllegalArgumentException("File does not contain valid certificates: " + + trustCertCollectionFile, e); + } + } + + /** + * Trusted certificates for verifying the remote endpoint's certificate. {@code null} uses the system default + * which only works with Java 8u261 and later as these versions support TLS1.3, + * see + * JDK 8u261 Update Release Notes + */ + public QuicSslContextBuilder trustManager(X509Certificate... trustCertCollection) { + try { + return trustManager(QuicheQuicSslContext.buildTrustManagerFactory0(trustCertCollection)); + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Trusted manager for verifying the remote endpoint's certificate. {@code null} uses the system default + * which only works with Java 8u261 and later as these versions support TLS1.3, + * see + * JDK 8u261 Update Release Notes + */ + public QuicSslContextBuilder trustManager(TrustManagerFactory trustManagerFactory) { + this.trustManagerFactory = trustManagerFactory; + return this; + } + + /** + * A single trusted manager for verifying the remote endpoint's certificate. + * This is helpful when custom implementation of {@link TrustManager} is needed. + * Internally, a simple wrapper of {@link TrustManagerFactory} that only produces this + * specified {@link TrustManager} will be created, thus all the requirements specified in + * {@link #trustManager(TrustManagerFactory trustManagerFactory)} also apply here. + */ + public QuicSslContextBuilder trustManager(TrustManager trustManager) { + return trustManager(new TrustManagerFactoryWrapper(trustManager)); + } + + /** + * Identifying certificate for this host. {@code keyCertChainFile} and {@code keyFile} may + * be {@code null} for client contexts, which disables mutual authentication. + * + * @param keyFile a PKCS#8 private key file in PEM format + * @param keyPassword the password of the {@code keyFile}, or {@code null} if it's not + * password-protected + * @param keyCertChainFile an X.509 certificate chain file in PEM format + */ + public QuicSslContextBuilder keyManager(File keyFile, String keyPassword, File keyCertChainFile) { + X509Certificate[] keyCertChain; + PrivateKey key; + try { + keyCertChain = QuicheQuicSslContext.toX509Certificates0(keyCertChainFile); + } catch (Exception e) { + throw new IllegalArgumentException("File does not contain valid certificates: " + keyCertChainFile, e); + } + try { + key = QuicheQuicSslContext.toPrivateKey0(keyFile, keyPassword); + } catch (Exception e) { + throw new IllegalArgumentException("File does not contain valid private key: " + keyFile, e); + } + return keyManager(key, keyPassword, keyCertChain); + } + + /** + * Identifying certificate for this host. {@code keyCertChain} and {@code key} may + * be {@code null} for client contexts, which disables mutual authentication. + * + * @param key a PKCS#8 private key file + * @param keyPassword the password of the {@code key}, or {@code null} if it's not + * password-protected + * @param certChain an X.509 certificate chain + */ + public QuicSslContextBuilder keyManager(PrivateKey key, String keyPassword, X509Certificate... certChain) { + try { + java.security.KeyStore ks = java.security.KeyStore.getInstance(KeyStore.getDefaultType()); + ks.load(null); + char[] pass = keyPassword == null ? new char[0]: keyPassword.toCharArray(); + ks.setKeyEntry("alias", key, pass, certChain); + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance( + KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(ks, pass); + return keyManager(keyManagerFactory, keyPassword); + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Identifying manager for this host. {@code keyManagerFactory} may be {@code null} for + * client contexts, which disables mutual authentication. + */ + public QuicSslContextBuilder keyManager(KeyManagerFactory keyManagerFactory, String keyPassword) { + this.keyPassword = keyPassword; + this.keyManagerFactory = keyManagerFactory; + return this; + } + + /** + * A single key manager managing the identity information of this host. + * This is helpful when custom implementation of {@link KeyManager} is needed. + * Internally, a wrapper of {@link KeyManagerFactory} that only produces this specified + * {@link KeyManager} will be created, thus all the requirements specified in + * {@link #keyManager(KeyManagerFactory, String)} also apply here. + */ + public QuicSslContextBuilder keyManager(KeyManager keyManager, String password) { + return keyManager(new KeyManagerFactoryWrapper(keyManager), password); + } + + /** + * Application protocol negotiation configuration. {@code null} disables support. + */ + public QuicSslContextBuilder applicationProtocols(String... applicationProtocols) { + this.applicationProtocols = applicationProtocols; + return this; + } + + /** + * Set the size of the cache used for storing SSL session objects. {@code 0} to use the + * default value. + */ + public QuicSslContextBuilder sessionCacheSize(long sessionCacheSize) { + this.sessionCacheSize = sessionCacheSize; + return this; + } + + /** + * Set the timeout for the cached SSL session objects, in seconds. {@code 0} to use the + * default value. + */ + public QuicSslContextBuilder sessionTimeout(long sessionTimeout) { + this.sessionTimeout = sessionTimeout; + return this; + } + + /** + * Sets the client authentication mode. + */ + public QuicSslContextBuilder clientAuth(ClientAuth clientAuth) { + if (!forServer) { + throw new UnsupportedOperationException("Only supported for server"); + } + this.clientAuth = checkNotNull(clientAuth, "clientAuth"); + return this; + } + + /** + * Create new {@link QuicSslContext} instance with configured settings that can be used for {@code QUIC}. + * + */ + public QuicSslContext build() { + if (forServer) { + return new QuicheQuicSslContext(true, sessionTimeout, sessionCacheSize, clientAuth, trustManagerFactory, + keyManagerFactory, keyPassword, mapping, earlyData, keylog, applicationProtocols); + } else { + return new QuicheQuicSslContext(false, sessionTimeout, sessionCacheSize, clientAuth, trustManagerFactory, + keyManagerFactory, keyPassword, mapping, earlyData, keylog, + applicationProtocols); + } + } + + +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicSslEngine.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicSslEngine.java new file mode 100644 index 0000000..c2bd694 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicSslEngine.java @@ -0,0 +1,23 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import javax.net.ssl.SSLEngine; + +/** + * An {@link SSLEngine} that can be used for QUIC. + */ +public abstract class QuicSslEngine extends SSLEngine { } diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicSslSessionContext.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicSslSessionContext.java new file mode 100644 index 0000000..08909b1 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicSslSessionContext.java @@ -0,0 +1,34 @@ +/* + * Copyright 2023 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import javax.net.ssl.SSLSessionContext; + +/** + * {@link SSLSessionContext} which also supports advanced operations. + */ +public interface QuicSslSessionContext extends SSLSessionContext { + + /** + * Sets the {@link SslSessionTicketKey}s that should be used. The first key of the array is used for encryption + * and decryption while the rest of the array is only used for decryption. This allows you to better handling + * rotating of the keys. The rotating is the responsibility of the user. + * If {@code null} is used for {@code keys} a key will automatically generated by the library and also rotated. + * + * @param keys the tickets to use. + */ + void setTicketKeys(SslSessionTicketKey... keys); +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamAddress.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamAddress.java new file mode 100644 index 0000000..529a54a --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamAddress.java @@ -0,0 +1,61 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import java.net.SocketAddress; +import java.util.Objects; + +/** + * A {@link SocketAddress} for QUIC stream. + */ +public final class QuicStreamAddress extends SocketAddress { + + private final long streamId; + + public QuicStreamAddress(long streamId) { + this.streamId = streamId; + } + + /** + * Return the id of the stream. + * + * @return the id. + */ + public long streamId() { + return streamId; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof QuicStreamAddress)) { + return false; + } + QuicStreamAddress that = (QuicStreamAddress) o; + return streamId == that.streamId; + } + + @Override + public int hashCode() { + return Objects.hash(streamId); + } + + @Override + public String toString() { + return "QuicStreamAddress{" + + "streamId=" + streamId + + '}'; + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamChannel.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamChannel.java new file mode 100644 index 0000000..dc5d682 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamChannel.java @@ -0,0 +1,299 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelProgressivePromise; +import io.netty.channel.ChannelPromise; +import io.netty.channel.socket.DuplexChannel; + +import java.net.SocketAddress; + +/** + * A QUIC stream. + */ +public interface QuicStreamChannel extends DuplexChannel { + + /** + * Should be added to a {@link ChannelFuture} when the output should be cleanly shutdown via a {@code FIN}. No more + * writes will be allowed after this point. + */ + ChannelFutureListener SHUTDOWN_OUTPUT = f -> ((QuicStreamChannel) f.channel()).shutdownOutput(); + + @Override + default ChannelFuture bind(SocketAddress socketAddress) { + return pipeline().bind(socketAddress); + } + + @Override + default ChannelFuture connect(SocketAddress remoteAddress) { + return pipeline().connect(remoteAddress); + } + + @Override + default ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress) { + return pipeline().connect(remoteAddress, localAddress); + } + + @Override + default ChannelFuture disconnect() { + return pipeline().disconnect(); + } + + @Override + default ChannelFuture close() { + return pipeline().close(); + } + + @Override + default ChannelFuture deregister() { + return pipeline().deregister(); + } + + @Override + default ChannelFuture bind(SocketAddress localAddress, ChannelPromise channelPromise) { + return pipeline().bind(localAddress, channelPromise); + } + + @Override + default ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise channelPromise) { + return pipeline().connect(remoteAddress, channelPromise); + } + + @Override + default ChannelFuture connect( + SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise channelPromise) { + return pipeline().connect(remoteAddress, localAddress, channelPromise); + } + + @Override + default ChannelFuture disconnect(ChannelPromise channelPromise) { + return pipeline().disconnect(channelPromise); + } + + @Override + default ChannelFuture close(ChannelPromise channelPromise) { + return pipeline().close(channelPromise); + } + + @Override + default ChannelFuture deregister(ChannelPromise channelPromise) { + return pipeline().deregister(channelPromise); + } + + @Override + default ChannelFuture write(Object msg) { + return pipeline().write(msg); + } + + @Override + default ChannelFuture write(Object msg, ChannelPromise channelPromise) { + return pipeline().write(msg, channelPromise); + } + + @Override + default ChannelFuture writeAndFlush(Object msg, ChannelPromise channelPromise) { + return pipeline().writeAndFlush(msg, channelPromise); + } + + @Override + default ChannelFuture writeAndFlush(Object msg) { + return pipeline().writeAndFlush(msg); + } + + @Override + default ChannelPromise newPromise() { + return pipeline().newPromise(); + } + + @Override + default ChannelProgressivePromise newProgressivePromise() { + return pipeline().newProgressivePromise(); + } + + @Override + default ChannelFuture newSucceededFuture() { + return pipeline().newSucceededFuture(); + } + + @Override + default ChannelFuture newFailedFuture(Throwable cause) { + return pipeline().newFailedFuture(cause); + } + + @Override + default ChannelPromise voidPromise() { + return pipeline().voidPromise(); + } + + @Override + default ChannelFuture shutdownInput() { + return shutdownInput(newPromise()); + } + + @Override + default ChannelFuture shutdownInput(ChannelPromise promise) { + return shutdownInput(0, promise); + } + + @Override + default ChannelFuture shutdownOutput() { + return shutdownOutput(newPromise()); + } + + @Override + default ChannelFuture shutdown() { + return shutdown(newPromise()); + } + + /** + * Shortcut for calling {@link #shutdownInput(int)} and {@link #shutdownInput(int)}. + * + * @param error the error to send. + * @return the future that is notified on completion. + */ + default ChannelFuture shutdown(int error) { + return shutdown(error, newPromise()); + } + + /** + * Shortcut for calling {@link #shutdownInput(int, ChannelPromise)} and {@link #shutdownInput(int, ChannelPromise)}. + * + * @param error the error to send. + * @param promise will be notified on completion. + * @return the future that is notified on completion. + */ + ChannelFuture shutdown(int error, ChannelPromise promise); + + /** + * Shutdown the input of the stream with the given error code. This means a {@code STOP_SENDING} frame will + * be send to the remote peer and all data received will be discarded. + * + * @param error the error to send as part of the {@code STOP_SENDING} frame. + * @return the future that is notified on completion. + */ + default ChannelFuture shutdownInput(int error) { + return shutdownInput(error, newPromise()); + } + + /** + * Shutdown the input of the stream with the given error code. This means a {@code STOP_SENDING} frame will + * be send to the remote peer and all data received will be discarded. + * + * @param error the error to send as part of the {@code STOP_SENDING} frame. + * @param promise will be notified on completion. + * @return the future that is notified on completion. + */ + ChannelFuture shutdownInput(int error, ChannelPromise promise); + + /** + * Shutdown the output of the stream with the given error code. This means a {@code RESET_STREAM} frame will + * be send to the remote peer and all data that is not sent yet will be discarded. + * + * Important:If you want to shutdown the output without sending a {@code RESET_STREAM} frame you + * should use {@link #shutdownOutput()} which will shutdown the output by sending a {@code FIN} and so signal + * a clean shutdown. + * + * @param error the error to send as part of the {@code RESET_STREAM} frame. + * @return the future that is notified on completion. + */ + default ChannelFuture shutdownOutput(int error) { + return shutdownOutput(error, newPromise()); + } + + /** + * Shutdown the output of the stream with the given error code. This means a {@code RESET_STREAM} frame will + * be send to the remote peer and all data that is not sent yet will be discarded. + * + * Important:If you want to shutdown the output without sending a {@code RESET_STREAM} frame you + * should use {@link #shutdownOutput(ChannelPromise)} which will shutdown the output by sending a {@code FIN} + * and so signal a clean shutdown. + * + * @param error the error to send as part of the {@code RESET_STREAM} frame. + * @param promise will be notified on completion. + * @return the future that is notified on completion. + */ + ChannelFuture shutdownOutput(int error, ChannelPromise promise); + + @Override + QuicStreamAddress localAddress(); + + @Override + QuicStreamAddress remoteAddress(); + + /** + * Returns {@code true} if the stream was created locally. + * + * @return {@code true} if created locally, {@code false} otherwise. + */ + boolean isLocalCreated(); + + /** + * Returns the {@link QuicStreamType} of the stream. + * + * @return {@link QuicStreamType} of this stream. + */ + QuicStreamType type(); + + /** + * The id of the stream. + * + * @return the stream id of this {@link QuicStreamChannel}. + */ + long streamId(); + + /** + * The {@link QuicStreamPriority} if explicit set for the stream via {@link #updatePriority(QuicStreamPriority)} or + * {@link #updatePriority(QuicStreamPriority, ChannelPromise)}. Otherwise {@code null}. + * + * @return the priority if any was set. + */ + QuicStreamPriority priority(); + + /** + * Update the priority of the stream. A stream's priority determines the order in which stream data is sent + * on the wire (streams with lower priority are sent first). + * + * @param priority the priority. + * @return future that is notified once the operation completes. + */ + default ChannelFuture updatePriority(QuicStreamPriority priority) { + return updatePriority(priority, newPromise()); + } + + /** + * Update the priority of the stream. A stream's priority determines the order in which stream data is sent + * on the wire (streams with lower priority are sent first). + * + * @param priority the priority. + * @param promise notified once operations completes. + * @return future that is notified once the operation completes. + */ + ChannelFuture updatePriority(QuicStreamPriority priority, ChannelPromise promise); + + @Override + QuicChannel parent(); + + @Override + QuicStreamChannel read(); + + @Override + QuicStreamChannel flush(); + + @Override + QuicStreamChannelConfig config(); +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamChannelBootstrap.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamChannelBootstrap.java new file mode 100644 index 0000000..cc59c00 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamChannelBootstrap.java @@ -0,0 +1,148 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.util.AttributeKey; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.Promise; +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Allows to bootstrap outgoing {@link QuicStreamChannel}s. + */ +public final class QuicStreamChannelBootstrap { + private static final InternalLogger logger = InternalLoggerFactory.getInstance(QuicStreamChannelBootstrap.class); + + private final QuicChannel parent; + private final Map, Object> options = new LinkedHashMap<>(); + private final Map, Object> attrs = new HashMap<>(); + private ChannelHandler handler; + private QuicStreamType type = QuicStreamType.BIDIRECTIONAL; + + /** + * Creates a new instance which uses the given {@link QuicChannel} to bootstrap {@link QuicStreamChannel}s. + * + * @param parent the {@link QuicChannel} that is used. + + */ + QuicStreamChannelBootstrap(QuicChannel parent) { + this.parent = ObjectUtil.checkNotNull(parent, "parent"); + } + + /** + * Allow to specify a {@link ChannelOption} which is used for the {@link QuicStreamChannel} instances once they got + * created. Use a value of {@code null} to remove a previous set {@link ChannelOption}. + * + * @param option the {@link ChannelOption} to apply to the {@link QuicStreamChannel}. + * @param value the value of the option. + * @param the type of the value. + * @return this instance. + */ + public QuicStreamChannelBootstrap option(ChannelOption option, T value) { + Quic.updateOptions(options, option, value); + return this; + } + + /** + * Allow to specify an initial attribute of the newly created {@link QuicStreamChannel}. If the {@code value} is + * {@code null}, the attribute of the specified {@code key} is removed. + * + * @param key the {@link AttributeKey} to apply to the {@link QuicChannel}. + * @param value the value of the attribute. + * @param the type of the value. + * @return this instance. + */ + public QuicStreamChannelBootstrap attr(AttributeKey key, T value) { + Quic.updateAttributes(attrs, key, value); + return this; + } + + /** + * Set the {@link ChannelHandler} that is added to the {@link io.netty.channel.ChannelPipeline} of the + * {@link QuicStreamChannel} once created. + * + * @param streamHandler the {@link ChannelHandler} that is added to the {@link QuicStreamChannel}s + * {@link io.netty.channel.ChannelPipeline}. + * @return this instance. + */ + public QuicStreamChannelBootstrap handler(ChannelHandler streamHandler) { + this.handler = ObjectUtil.checkNotNull(streamHandler, "streamHandler"); + return this; + } + + /** + * Set the {@link QuicStreamType} to use for the {@link QuicStreamChannel}, default is + * {@link QuicStreamType#BIDIRECTIONAL}. + * + * @param type the {@link QuicStreamType} of the {@link QuicStreamChannel}. + * @return this instance. + */ + public QuicStreamChannelBootstrap type(QuicStreamType type) { + this.type = ObjectUtil.checkNotNull(type, "type"); + return this; + } + + /** + * Creates a new {@link QuicStreamChannel} and notifies the {@link Future}. + * + * @return the {@link Future} that is notified once the operation completes. + */ + public Future create() { + return create(parent.eventLoop().newPromise()); + } + + /** + * Creates a new {@link QuicStreamChannel} and notifies the {@link Future}. + * + * @param promise the {@link Promise} that is notified once the operation completes. + * @return the {@link Future} that is notified once the operation completes. + */ + public Future create(Promise promise) { + if (handler == null) { + throw new IllegalStateException("streamHandler not set"); + } + + return parent.createStream(type, new QuicStreamChannelBootstrapHandler(handler, + Quic.toOptionsArray(options), Quic.toAttributesArray(attrs)), promise); + } + + private static final class QuicStreamChannelBootstrapHandler extends ChannelInitializer { + private final ChannelHandler streamHandler; + private final Map.Entry, Object>[] streamOptions; + private final Map.Entry, Object>[] streamAttrs; + + QuicStreamChannelBootstrapHandler(ChannelHandler streamHandler, + Map.Entry, Object>[] streamOptions, + Map.Entry, Object>[] streamAttrs) { + this.streamHandler = streamHandler; + this.streamOptions = streamOptions; + this.streamAttrs = streamAttrs; + } + @Override + protected void initChannel(QuicStreamChannel ch) { + Quic.setupChannel(ch, streamOptions, streamAttrs, streamHandler, logger); + } + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamChannelConfig.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamChannelConfig.java new file mode 100644 index 0000000..951fa4a --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamChannelConfig.java @@ -0,0 +1,83 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.MessageSizeEstimator; +import io.netty.channel.RecvByteBufAllocator; +import io.netty.channel.WriteBufferWaterMark; +import io.netty.channel.socket.DuplexChannelConfig; + +/** + * {@link DuplexChannelConfig} for QUIC streams. + */ +public interface QuicStreamChannelConfig extends DuplexChannelConfig { + /** + * Set this to {@code true} if the {@link QuicStreamChannel} should read {@link QuicStreamFrame}s and fire these + * through the {@link io.netty.channel.ChannelPipeline}, {@code false} if it uses {@link io.netty.buffer.ByteBuf}. + * + * @param readFrames {@code true} if {@link QuicStreamFrame}s should be used, {@code false} if + * {@link io.netty.buffer.ByteBuf} should be used. + * @return this instance itself. + * + */ + QuicStreamChannelConfig setReadFrames(boolean readFrames); + + /** + * Returns {@code true} if the {@link QuicStreamChannel} will read {@link QuicStreamFrame}s and fire these through + * the {@link io.netty.channel.ChannelPipeline}, {@code false} if it uses {@link io.netty.buffer.ByteBuf}. + * + * @return {@code true} if {@link QuicStreamFrame}s should be used, {@code false} if + * {@link io.netty.buffer.ByteBuf} should be used. + */ + boolean isReadFrames(); + + @Override + QuicStreamChannelConfig setAllowHalfClosure(boolean allowHalfClosure); + + @Override + QuicStreamChannelConfig setMaxMessagesPerRead(int maxMessagesPerRead); + + @Override + QuicStreamChannelConfig setWriteSpinCount(int writeSpinCount); + + @Override + QuicStreamChannelConfig setAllocator(ByteBufAllocator allocator); + + @Override + QuicStreamChannelConfig setRecvByteBufAllocator(RecvByteBufAllocator allocator); + + @Override + QuicStreamChannelConfig setAutoRead(boolean autoRead); + + @Override + QuicStreamChannelConfig setAutoClose(boolean autoClose); + + @Override + QuicStreamChannelConfig setMessageSizeEstimator(MessageSizeEstimator estimator); + + @Override + QuicStreamChannelConfig setWriteBufferWaterMark(WriteBufferWaterMark writeBufferWaterMark); + + @Override + QuicStreamChannelConfig setConnectTimeoutMillis(int connectTimeoutMillis); + + @Override + QuicStreamChannelConfig setWriteBufferHighWaterMark(int writeBufferHighWaterMark); + + @Override + QuicStreamChannelConfig setWriteBufferLowWaterMark(int writeBufferLowWaterMark); +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamFrame.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamFrame.java new file mode 100644 index 0000000..1fcdd2c --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamFrame.java @@ -0,0 +1,128 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufHolder; +import io.netty.buffer.Unpooled; + +/** + * A QUIC STREAM_FRAME. + */ +public interface QuicStreamFrame extends ByteBufHolder { + + /** + * An empty {@link QuicStreamFrame} that has the {@code FIN} flag set. + */ + QuicStreamFrame EMPTY_FIN = new QuicStreamFrame() { + @Override + public boolean hasFin() { + return true; + } + + @Override + public QuicStreamFrame copy() { + return this; + } + + @Override + public QuicStreamFrame duplicate() { + return this; + } + + @Override + public QuicStreamFrame retainedDuplicate() { + return this; + } + + @Override + public QuicStreamFrame replace(ByteBuf content) { + return new DefaultQuicStreamFrame(content, hasFin()); + } + + @Override + public QuicStreamFrame retain() { + return this; + } + + @Override + public QuicStreamFrame retain(int increment) { + return this; + } + + @Override + public QuicStreamFrame touch() { + return this; + } + + @Override + public QuicStreamFrame touch(Object hint) { + return this; + } + + @Override + public ByteBuf content() { + return Unpooled.EMPTY_BUFFER; + } + + @Override + public int refCnt() { + return 1; + } + + @Override + public boolean release() { + return false; + } + + @Override + public boolean release(int decrement) { + return false; + } + }; + + /** + * Returns {@code true} if the frame has the FIN set, which means it notifies the remote peer that + * there will be no more writing happen. {@code false} otherwise. + * + * @return {@code true} if the FIN flag should be set, {@code false} otherwise. + */ + boolean hasFin(); + + @Override + QuicStreamFrame copy(); + + @Override + QuicStreamFrame duplicate(); + + @Override + QuicStreamFrame retainedDuplicate(); + + @Override + QuicStreamFrame replace(ByteBuf content); + + @Override + QuicStreamFrame retain(); + + @Override + QuicStreamFrame retain(int increment); + + @Override + QuicStreamFrame touch(); + + @Override + QuicStreamFrame touch(Object hint); +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamIdGenerator.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamIdGenerator.java new file mode 100644 index 0000000..7e3e3c3 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamIdGenerator.java @@ -0,0 +1,41 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +/** + * Generates and hands over the next stream id to use for a QUIC stream. + */ +final class QuicStreamIdGenerator { + private long nextBidirectionalStreamId; + private long nextUnidirectionalStreamId; + + QuicStreamIdGenerator(boolean server) { + // See https://quicwg.org/base-drafts/rfc9000.html#name-stream-types-and-identifier + nextBidirectionalStreamId = server ? 1 : 0; + nextUnidirectionalStreamId = server ? 3 : 2; + } + + long nextStreamId(boolean bidirectional) { + if (bidirectional) { + long stream = nextBidirectionalStreamId; + nextBidirectionalStreamId += 4; + return stream; + } + long stream = nextUnidirectionalStreamId; + nextUnidirectionalStreamId += 4; + return stream; + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamLimitChangedEvent.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamLimitChangedEvent.java new file mode 100644 index 0000000..8cd4063 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamLimitChangedEvent.java @@ -0,0 +1,26 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +/** + * Event fired once the stream limit of a {@link QuicChannel} changes. + */ +public final class QuicStreamLimitChangedEvent implements QuicEvent { + + static final QuicStreamLimitChangedEvent INSTANCE = new QuicStreamLimitChangedEvent(); + + private QuicStreamLimitChangedEvent() { } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamPriority.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamPriority.java new file mode 100644 index 0000000..570ca78 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamPriority.java @@ -0,0 +1,83 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.util.internal.ObjectUtil; + +import java.util.Objects; + +/** + * The priority of a {@link QuicStreamChannel}. + */ +public final class QuicStreamPriority { + + private final int urgency; + private final boolean incremental; + + /** + * Create a new instance + * + * @param urgency the urgency of the stream. + * @param incremental {@code true} if incremental. + */ + public QuicStreamPriority(int urgency, boolean incremental) { + this.urgency = ObjectUtil.checkInRange(urgency, 0, Byte.MAX_VALUE, "urgency"); + this.incremental = incremental; + } + + /** + * The urgency of the stream. Smaller number means more urgent and so data will be send earlier. + * + * @return the urgency. + */ + public int urgency() { + return urgency; + } + + /** + * {@code true} if incremental, {@code false} otherwise. + * + * @return if incremental. + */ + public boolean isIncremental() { + return incremental; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + QuicStreamPriority that = (QuicStreamPriority) o; + return urgency == that.urgency && incremental == that.incremental; + } + + @Override + public int hashCode() { + return Objects.hash(urgency, incremental); + } + + @Override + public String toString() { + return "QuicStreamPriority{" + + "urgency=" + urgency + + ", incremental=" + incremental + + '}'; + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamType.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamType.java new file mode 100644 index 0000000..268c5d8 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicStreamType.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +/** + * The type of a {@link QuicStreamChannel}. + */ +public enum QuicStreamType { + + /** + * An unidirectional stream. + */ + UNIDIRECTIONAL, + /** + * A bidirectional stream. + */ + BIDIRECTIONAL +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicTokenHandler.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicTokenHandler.java new file mode 100644 index 0000000..729d83b --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicTokenHandler.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBuf; + +import java.net.InetSocketAddress; + +/** + * Handle token related operations. + */ +public interface QuicTokenHandler { + + /** + * Generate a new token for the given destination connection id and address. This token is written to {@code out}. + * If no token should be generated and so no token validation should take place at all this method should return + * {@code false}. + * + * @param out {@link ByteBuf} into which the token will be written. + * @param dcid the destination connection id. + * @param address the {@link InetSocketAddress} of the sender. + * @return {@code true} if a token was written and so validation should happen, {@code false} otherwise. + */ + boolean writeToken(ByteBuf out, ByteBuf dcid, InetSocketAddress address); + + /** + * Validate the token and return the offset, {@code -1} is returned if the token is not valid. + * + * @param token the {@link ByteBuf} that contains the token. The ownership is not transferred. + * @param address the {@link InetSocketAddress} of the sender. + * @return the start index after the token or {@code -1} if the token was not valid. + */ + int validateToken(ByteBuf token, InetSocketAddress address); + + /** + * Return the maximal token length. + * + * @return the maximal supported token length. + */ + int maxTokenLength(); +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicTransportParameters.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicTransportParameters.java new file mode 100644 index 0000000..9bf1cc2 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicTransportParameters.java @@ -0,0 +1,113 @@ +/* + * Copyright 2023 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +/** + * Transport parameters for QUIC. + */ +public interface QuicTransportParameters { + + /** + * The maximum idle timeout. + * @return timeout. + */ + long maxIdleTimeout(); + + /** + * The maximum UDP payload size. + * + * @return maximum payload size. + */ + long maxUdpPayloadSize(); + + /** + * The initial flow control maximum data for the connection. + * + * @return flowcontrol. + */ + long initialMaxData(); + + /** + * The initial flow control maximum data for local bidirectional streams. + * + * @return flowcontrol. + */ + long initialMaxStreamDataBidiLocal(); + + /** + * The initial flow control maximum data for remote bidirectional streams. + * + * @return flowcontrol. + */ + long initialMaxStreamDataBidiRemote(); + + /** + * The initial flow control maximum data for unidirectional streams. + * + * @return flowcontrol. + */ + long initialMaxStreamDataUni(); + + + /** + * The initial maximum bidirectional streams. + * + * @return streams. + */ + long initialMaxStreamsBidi(); + + /** + * The initial maximum unidirectional streams. + * + * @return streams. + */ + long initialMaxStreamsUni(); + + /** + * The ACK delay exponent + * + * @return exponent. + */ + long ackDelayExponent(); + + /** + * The max ACK delay. + * + * @return delay. + */ + long maxAckDelay(); + + /** + * Whether active migration is disabled. + * + * @return disabled. + */ + boolean disableActiveMigration(); + + /** + * The active connection ID limit. + * + * @return limit. + */ + long activeConnIdLimit(); + + /** + * DATAGRAM frame extension parameter, if any. + * + * @return param. + */ + long maxDatagramFrameSize(); +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/Quiche.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/Quiche.java new file mode 100644 index 0000000..e8a1ce7 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/Quiche.java @@ -0,0 +1,870 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelPromise; +import io.netty.util.internal.ClassInitializerUtil; +import io.netty.util.internal.NativeLibraryLoader; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLHandshakeException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +final class Quiche { + private static final InternalLogger logger = InternalLoggerFactory.getInstance(Quiche.class); + private static final boolean DEBUG_LOGGING_ENABLED = logger.isDebugEnabled(); + + static { + // Preload all classes that will be used in the OnLoad(...) function of JNI to eliminate the possiblity of a + // class-loader deadlock. This is a workaround for https://github.com/netty/netty/issues/11209. + + // This needs to match all the classes that are loaded via NETTY_JNI_UTIL_LOAD_CLASS or looked up via + // NETTY_JNI_UTIL_FIND_CLASS. + ClassInitializerUtil.tryLoadClasses(Quiche.class, + // netty_quic_boringssl + byte[].class, String.class, BoringSSLCertificateCallback.class, + BoringSSLCertificateVerifyCallback.class, BoringSSLHandshakeCompleteCallback.class, + + //netty_quic_quiche + QuicheLogger.class + ); + + try { + // First, try calling a side-effect free JNI method to see if the library was already + // loaded by the application. + quiche_version(); + } catch (UnsatisfiedLinkError ignore) { + // The library was not previously loaded, load it now. + loadNativeLibrary(); + } + + // Let's enable debug logging for quiche if its enabled in our logger. + if (DEBUG_LOGGING_ENABLED) { + quiche_enable_debug_logging(new QuicheLogger(logger)); + } + } + + private static void loadNativeLibrary() { + // This needs to be kept in sync with what is defined in netty_quic_quiche.c + String libName = "netty_quiche"; + ClassLoader cl = PlatformDependent.getClassLoader(Quiche.class); + + if (!PlatformDependent.isAndroid()) { + libName += '_' + PlatformDependent.normalizedOs() + + '_' + PlatformDependent.normalizedArch(); + } + + try { + NativeLibraryLoader.load(libName, cl); + } catch (UnsatisfiedLinkError e) { + logger.debug("Failed to load {}", libName, e); + throw e; + } + } + + static final short AF_INET = (short) QuicheNativeStaticallyReferencedJniMethods.afInet(); + static final short AF_INET6 = (short) QuicheNativeStaticallyReferencedJniMethods.afInet6(); + static final int SIZEOF_SOCKADDR_STORAGE = QuicheNativeStaticallyReferencedJniMethods.sizeofSockaddrStorage(); + static final int SIZEOF_SOCKADDR_IN = QuicheNativeStaticallyReferencedJniMethods.sizeofSockaddrIn(); + static final int SIZEOF_SOCKADDR_IN6 = QuicheNativeStaticallyReferencedJniMethods.sizeofSockaddrIn6(); + static final int SOCKADDR_IN_OFFSETOF_SIN_FAMILY = + QuicheNativeStaticallyReferencedJniMethods.sockaddrInOffsetofSinFamily(); + static final int SOCKADDR_IN_OFFSETOF_SIN_PORT = + QuicheNativeStaticallyReferencedJniMethods.sockaddrInOffsetofSinPort(); + static final int SOCKADDR_IN_OFFSETOF_SIN_ADDR = + QuicheNativeStaticallyReferencedJniMethods.sockaddrInOffsetofSinAddr(); + static final int IN_ADDRESS_OFFSETOF_S_ADDR = QuicheNativeStaticallyReferencedJniMethods.inAddressOffsetofSAddr(); + static final int SOCKADDR_IN6_OFFSETOF_SIN6_FAMILY = + QuicheNativeStaticallyReferencedJniMethods.sockaddrIn6OffsetofSin6Family(); + static final int SOCKADDR_IN6_OFFSETOF_SIN6_PORT = + QuicheNativeStaticallyReferencedJniMethods.sockaddrIn6OffsetofSin6Port(); + static final int SOCKADDR_IN6_OFFSETOF_SIN6_FLOWINFO = + QuicheNativeStaticallyReferencedJniMethods.sockaddrIn6OffsetofSin6Flowinfo(); + static final int SOCKADDR_IN6_OFFSETOF_SIN6_ADDR = + QuicheNativeStaticallyReferencedJniMethods.sockaddrIn6OffsetofSin6Addr(); + static final int SOCKADDR_IN6_OFFSETOF_SIN6_SCOPE_ID = + QuicheNativeStaticallyReferencedJniMethods.sockaddrIn6OffsetofSin6ScopeId(); + static final int IN6_ADDRESS_OFFSETOF_S6_ADDR = + QuicheNativeStaticallyReferencedJniMethods.in6AddressOffsetofS6Addr(); + static final int SIZEOF_SOCKLEN_T = QuicheNativeStaticallyReferencedJniMethods.sizeofSocklenT(); + static final int SIZEOF_SIZE_T = QuicheNativeStaticallyReferencedJniMethods.sizeofSizeT(); + + static final int SIZEOF_TIMESPEC = QuicheNativeStaticallyReferencedJniMethods.sizeofTimespec(); + + static final int SIZEOF_TIME_T = QuicheNativeStaticallyReferencedJniMethods.sizeofTimeT(); + static final int SIZEOF_LONG = QuicheNativeStaticallyReferencedJniMethods.sizeofLong(); + + static final int TIMESPEC_OFFSETOF_TV_SEC = + QuicheNativeStaticallyReferencedJniMethods.timespecOffsetofTvSec(); + + static final int TIMESPEC_OFFSETOF_TV_NSEC = + QuicheNativeStaticallyReferencedJniMethods.timespecOffsetofTvNsec(); + + static final int QUICHE_RECV_INFO_OFFSETOF_FROM = + QuicheNativeStaticallyReferencedJniMethods.quicheRecvInfoOffsetofFrom(); + static final int QUICHE_RECV_INFO_OFFSETOF_FROM_LEN = + QuicheNativeStaticallyReferencedJniMethods.quicheRecvInfoOffsetofFromLen(); + + static final int QUICHE_RECV_INFO_OFFSETOF_TO = + QuicheNativeStaticallyReferencedJniMethods.quicheRecvInfoOffsetofTo(); + static final int QUICHE_RECV_INFO_OFFSETOF_TO_LEN = + QuicheNativeStaticallyReferencedJniMethods.quicheRecvInfoOffsetofToLen(); + + static final int SIZEOF_QUICHE_RECV_INFO = QuicheNativeStaticallyReferencedJniMethods.sizeofQuicheRecvInfo(); + static final int QUICHE_SEND_INFO_OFFSETOF_TO = + QuicheNativeStaticallyReferencedJniMethods.quicheSendInfoOffsetofTo(); + static final int QUICHE_SEND_INFO_OFFSETOF_TO_LEN = + QuicheNativeStaticallyReferencedJniMethods.quicheSendInfoOffsetofToLen(); + + static final int QUICHE_SEND_INFO_OFFSETOF_FROM = + QuicheNativeStaticallyReferencedJniMethods.quicheSendInfoOffsetofFrom(); + static final int QUICHE_SEND_INFO_OFFSETOF_FROM_LEN = + QuicheNativeStaticallyReferencedJniMethods.quicheSendInfoOffsetofFromLen(); + + static final int QUICHE_SEND_INFO_OFFSETOF_AT = + QuicheNativeStaticallyReferencedJniMethods.quicheSendInfoOffsetofAt(); + static final int SIZEOF_QUICHE_SEND_INFO = QuicheNativeStaticallyReferencedJniMethods.sizeofQuicheSendInfo(); + + static final int QUICHE_PROTOCOL_VERSION = QuicheNativeStaticallyReferencedJniMethods.quiche_protocol_version(); + static final int QUICHE_MAX_CONN_ID_LEN = QuicheNativeStaticallyReferencedJniMethods.quiche_max_conn_id_len(); + + /** + * See QUICHE_SHUTDOWN_READ. + */ + static final int QUICHE_SHUTDOWN_READ = QuicheNativeStaticallyReferencedJniMethods.quiche_shutdown_read(); + + /** + * See QUICHE_SHUTDOWN_WRITE. + */ + static final int QUICHE_SHUTDOWN_WRITE = QuicheNativeStaticallyReferencedJniMethods.quiche_shutdown_write(); + + /** + * See QUICHE_ERR_DONE. + */ + static final int QUICHE_ERR_DONE = QuicheNativeStaticallyReferencedJniMethods.quiche_err_done(); + + /** + * See + * QUICHE_ERR_BUFFER_TOO_SHORT. + */ + static final int QUICHE_ERR_BUFFER_TOO_SHORT = + QuicheNativeStaticallyReferencedJniMethods.quiche_err_buffer_too_short(); + + /** + * See + * QUICHE_ERR_UNKNOWN_VERSION. + */ + static final int QUICHE_ERR_UNKNOWN_VERSION = + QuicheNativeStaticallyReferencedJniMethods.quiche_err_unknown_version(); + + /** + * See + * QUICHE_ERR_INVALID_FRAME. + */ + static final int QUICHE_ERR_INVALID_FRAME = QuicheNativeStaticallyReferencedJniMethods.quiche_err_invalid_frame(); + + /** + * See + * QUICHE_ERR_INVALID_PACKET. + */ + static final int QUICHE_ERR_INVALID_PACKET = QuicheNativeStaticallyReferencedJniMethods.quiche_err_invalid_packet(); + + /** + * See + * QUICHE_ERR_INVALID_STATE. + */ + static final int QUICHE_ERR_INVALID_STATE = QuicheNativeStaticallyReferencedJniMethods.quiche_err_invalid_state(); + + /** + * See + * QUICHE_ERR_INVALID_STREAM_STATE. + */ + static final int QUICHE_ERR_INVALID_STREAM_STATE = + QuicheNativeStaticallyReferencedJniMethods.quiche_err_invalid_stream_state(); + + /** + * See + * QUICHE_ERR_INVALID_TRANSPORT_PARAM. + */ + static final int QUICHE_ERR_INVALID_TRANSPORT_PARAM = + QuicheNativeStaticallyReferencedJniMethods.quiche_err_invalid_transport_param(); + + /** + * See + * QUICHE_ERR_CRYPTO_FAIL. + */ + static final int QUICHE_ERR_CRYPTO_FAIL = QuicheNativeStaticallyReferencedJniMethods.quiche_err_crypto_fail(); + + /** + * See + * QUICHE_ERR_TLS_FAIL. + */ + static final int QUICHE_ERR_TLS_FAIL = QuicheNativeStaticallyReferencedJniMethods.quiche_err_tls_fail(); + + /** + * See + * QUICHE_ERR_FLOW_CONTROL. + */ + static final int QUICHE_ERR_FLOW_CONTROL = QuicheNativeStaticallyReferencedJniMethods.quiche_err_flow_control(); + + /** + * See + * QUICHE_ERR_STREAM_LIMIT. + */ + static final int QUICHE_ERR_STREAM_LIMIT = QuicheNativeStaticallyReferencedJniMethods.quiche_err_stream_limit(); + + /** + * See + * QUICHE_ERR_FINAL_SIZE. + */ + static final int QUICHE_ERR_FINAL_SIZE = QuicheNativeStaticallyReferencedJniMethods.quiche_err_final_size(); + + /** + * See + * QUICHE_ERR_CONGESTION_CONTROL. + */ + static final int QUICHE_ERR_CONGESTION_CONTROL = + QuicheNativeStaticallyReferencedJniMethods.quiche_err_congestion_control(); + + /** + * See QUICHE_ERR_STREAM_STOPPED. + */ + static final int QUICHE_ERR_STREAM_RESET = + QuicheNativeStaticallyReferencedJniMethods.quiche_err_stream_reset(); + + /** + * See + * QUICHE_ERR_STREAM_STOPPED. + */ + static final int QUICHE_ERR_STREAM_STOPPED = + QuicheNativeStaticallyReferencedJniMethods.quiche_err_stream_stopped(); + + + // Too many identifiers were provided. + static final int QUICHE_ERR_ID_LIMIT = + QuicheNativeStaticallyReferencedJniMethods.quiche_err_id_limit(); + + // Not enough available identifiers. + static final int QUICHE_ERR_OUT_OF_IDENTIFIERS = + QuicheNativeStaticallyReferencedJniMethods.quiche_err_out_of_identifiers(); + + // Error in key update. + static final int QUICHE_ERR_KEY_UPDATE = + QuicheNativeStaticallyReferencedJniMethods.quiche_err_key_update(); + + /** + * See + * QUICHE_CC_RENO. + */ + static final int QUICHE_CC_RENO = QuicheNativeStaticallyReferencedJniMethods.quiche_cc_reno(); + + /** + * See + * QUICHE_CC_CUBIC. + */ + static final int QUICHE_CC_CUBIC = QuicheNativeStaticallyReferencedJniMethods.quiche_cc_cubic(); + + /** + * See + * QUICHE_CC_BBR. + */ + static final int QUICHE_CC_BBR = QuicheNativeStaticallyReferencedJniMethods.quiche_cc_bbr(); + + + static final int QUICHE_PATH_EVENT_NEW = QuicheNativeStaticallyReferencedJniMethods.quiche_path_event_new(); + static final int QUICHE_PATH_EVENT_VALIDATED = QuicheNativeStaticallyReferencedJniMethods.quiche_path_event_validated(); + static final int QUICHE_PATH_EVENT_FAILED_VALIDATION = QuicheNativeStaticallyReferencedJniMethods.quiche_path_event_failed_validation(); + static final int QUICHE_PATH_EVENT_CLOSED = QuicheNativeStaticallyReferencedJniMethods.quiche_path_event_closed(); + static final int QUICHE_PATH_EVENT_REUSED_SOURCE_CONNECTION_ID = QuicheNativeStaticallyReferencedJniMethods.quiche_path_event_reused_source_connection_id(); + static final int QUICHE_PATH_EVENT_PEER_MIGRATED = QuicheNativeStaticallyReferencedJniMethods.quiche_path_event_peer_migrated(); + + /** + * See quiche_version. + */ + static native String quiche_version(); + + /** + * See + * quiche_version_is_supported. + */ + static native boolean quiche_version_is_supported(int version); + + /** + * See quiche_header_info. + */ + static native int quiche_header_info(long bufAddr, int bufLength, int dcil, long versionAddr, long typeAddr, + long scidAddr, long scidLenAddr, long dcidAddr, long dcidLenAddr, + long tokenAddr, long tokenLenAddr); + /** + * See quiche_negotiate_version. + */ + static native int quiche_negotiate_version( + long scidAddr, int scidLen, long dcidAddr, int dcidLen, long outAddr, int outLen); + + /** + * See quiche_retry. + */ + static native int quiche_retry(long scidAddr, int scidLen, long dcidAddr, int dcidLen, long newScidAddr, + int newScidLen, long tokenAddr, int tokenLen, int version, long outAddr, int outLen); + + /** + * See quiche_conn_new_with_tls. + */ + static native long quiche_conn_new_with_tls(long scidAddr, int scidLen, long odcidAddr, int odcidLen, + long localAddr, int localLen, + long peerAddr, int peerLen, + long configAddr, long ssl, boolean isServer); + + /** + * See + * quiche_conn_set_qlog_path. + */ + static native boolean quiche_conn_set_qlog_path(long connAddr, String path, String logTitle, String logDescription); + + /** + * See quiche_conn_recv. + */ + static native int quiche_conn_recv(long connAddr, long bufAddr, int bufLen, long infoAddr); + + /** + * See quiche_conn_send. + */ + static native int quiche_conn_send(long connAddr, long outAddr, int outLen, long infoAddr); + + /** + * See quiche_conn_free. + */ + static native void quiche_conn_free(long connAddr); + + static QuicConnectionCloseEvent quiche_conn_peer_error(long connAddr) { + Object[] error = quiche_conn_peer_error0(connAddr); + if (error == null) { + return null; + } + return new QuicConnectionCloseEvent((Boolean) error[0], (Integer) error[1], (byte[]) error[2]); + } + + private static native Object[] quiche_conn_peer_error0(long connAddr); + + /** + * See + * quiche_conn_peer_streams_left_bidi. + */ + static native long quiche_conn_peer_streams_left_bidi(long connAddr); + + /** + * See + * quiche_conn_peer_streams_left_uni. + */ + static native long quiche_conn_peer_streams_left_uni(long connAddr); + + /** + * See + * quiche_conn_stream_priority. + */ + static native int quiche_conn_stream_priority( + long connAddr, long streamId, byte urgency, boolean incremental); + + static native int quiche_conn_send_quantum(long connAddr); + + /** + * See quiche_conn_trace_id. + */ + static native byte[] quiche_conn_trace_id(long connAddr); + + static native byte[] quiche_conn_source_id(long connAddr); + + static native byte[] quiche_conn_destination_id(long connAddr); + + /** + * See quiche_conn_stream_recv. + */ + static native int quiche_conn_stream_recv(long connAddr, long streamId, long outAddr, int bufLen, long finAddr); + + /** + * See quiche_conn_stream_send. + */ + static native int quiche_conn_stream_send(long connAddr, long streamId, long bufAddr, int bufLen, boolean fin); + + /** + * See + * quiche_conn_stream_shutdown. + */ + static native int quiche_conn_stream_shutdown(long connAddr, long streamId, int direction, long err); + + /** + * See + * quiche_conn_stream_capacity. + */ + static native int quiche_conn_stream_capacity(long connAddr, long streamId); + + /** + * See + * quiche_conn_stream_finished. + */ + static native boolean quiche_conn_stream_finished(long connAddr, long streamId); + + /** + * See + * quiche_conn_close. + */ + static native int quiche_conn_close(long connAddr, boolean app, long err, long reasonAddr, int reasonLen); + + /** + * See + * quiche_conn_is_established. + */ + static native boolean quiche_conn_is_established(long connAddr); + + /** + * See + * quiche_conn_is_in_early_data. + */ + static native boolean quiche_conn_is_in_early_data(long connAddr); + + /** + * See + * quiche_conn_is_closed. + */ + static native boolean quiche_conn_is_closed(long connAddr); + + /** + * See + * quiche_conn_is_timed_out. + */ + static native boolean quiche_conn_is_timed_out(long connAddr); + + /** + * See + * quiche_conn_stats. + * The implementation relies on all fields of + * quiche_stats being numerical. + * The assumption made allows passing primitive array rather than dealing with objects. + */ + static native long[] quiche_conn_stats(long connAddr); + + /** + * See + * + * quiche_conn_stats. + */ + static native long[] quiche_conn_peer_transport_params(long connAddr); + + /** + * See + * quiche_conn_timeout_as_nanos. + */ + static native long quiche_conn_timeout_as_nanos(long connAddr); + + /** + * See + * quiche_conn_on_timeout. + */ + static native void quiche_conn_on_timeout(long connAddr); + + /** + * See + * quiche_conn_readable. + */ + static native long quiche_conn_readable(long connAddr); + + /** + * See + * quiche_conn_writable. + */ + static native long quiche_conn_writable(long connAddr); + + /** + * See + * quiche_stream_iter_next. + * + * This method will fill the {@code streamIds} array and return the number of streams that were filled into + * the array. If the number is the same as the length of the array you should call it again until it returns + * less to ensure you process all the streams later on. + */ + static native int quiche_stream_iter_next(long iterAddr, long[] streamIds); + + /** + * See + * quiche_stream_iter_free. + * + */ + static native void quiche_stream_iter_free(long iterAddr); + + /** + * See + * + * quiche_conn_dgram_max_writable_len. + */ + static native int quiche_conn_dgram_max_writable_len(long connAddr); + + /** + * See + * + * quiche_conn_dgram_recv_front_len. + */ + static native int quiche_conn_dgram_recv_front_len(long connAddr); + + /** + * See + * + * quiche_conn_dgram_recv. + */ + static native int quiche_conn_dgram_recv(long connAddr, long buf, int size); + + /** + * See + * + * quiche_conn_dgram_send. + */ + static native int quiche_conn_dgram_send(long connAddr, long buf, int size); + + /** + * See + * + * quiche_conn_set_session. + */ + static native int quiche_conn_set_session(long connAddr, byte[] sessionBytes); + + /** + * See + * + * quiche_conn_max_send_udp_payload_size. + */ + static native int quiche_conn_max_send_udp_payload_size(long connAddr); + + static native int quiche_conn_scids_left(long connAddr); + + static native long quiche_conn_new_scid(long connAddr, long scidAddr, int scidLen, byte[] resetToken, boolean retire_if_needed, long seq); + + static native byte[] quiche_conn_retired_scid_next(long connAddr); + + static native long quiche_conn_path_event_next(long connAddr); + static native int quiche_path_event_type(long pathEvent); + static native void quiche_path_event_free(long pathEvent); + static native Object[] quiche_path_event_new(long pathEvent); + static native Object[] quiche_path_event_validated(long pathEvent); + static native Object[] quiche_path_event_failed_validation(long pathEvent); + static native Object[] quiche_path_event_closed(long pathEvent); + static native Object[] quiche_path_event_reused_source_connection_id(long pathEvent); + static native Object[] quiche_path_event_peer_migrated(long pathEvent); + + /** + * See + * quiche_config_new. + */ + static native long quiche_config_new(int version); + + /** + * See + * + * quiche_config_grease. + */ + static native void quiche_config_grease(long configAddr, boolean value); + + /** + * See + * + * quiche_config_set_max_idle_timeout. + */ + static native void quiche_config_set_max_idle_timeout(long configAddr, long value); + + /** + * See + * + * quiche_config_set_max_recv_udp_payload_size. + */ + static native void quiche_config_set_max_recv_udp_payload_size(long configAddr, long value); + + /** + * See + * + * quiche_config_set_max_recv_udp_payload_size. + */ + static native void quiche_config_set_max_send_udp_payload_size(long configAddr, long value); + + /** + * See + * + * quiche_config_set_initial_max_data. + */ + static native void quiche_config_set_initial_max_data(long configAddr, long value); + + /** + * See + * + * quiche_config_set_initial_max_stream_data_bidi_local. + */ + static native void quiche_config_set_initial_max_stream_data_bidi_local(long configAddr, long value); + + /** + * See + * + * quiche_config_set_initial_max_stream_data_bidi_remote. + */ + static native void quiche_config_set_initial_max_stream_data_bidi_remote(long configAddr, long value); + + /** + * See + * + * quiche_config_set_initial_max_stream_data_uni. + */ + static native void quiche_config_set_initial_max_stream_data_uni(long configAddr, long value); + + /** + * See + * + * quiche_config_set_initial_max_streams_bidi. + */ + static native void quiche_config_set_initial_max_streams_bidi(long configAddr, long value); + + /** + * See + * + * quiche_config_set_initial_max_streams_uni. + */ + static native void quiche_config_set_initial_max_streams_uni(long configAddr, long value); + + /** + * See + * + * quiche_config_set_ack_delay_exponent. + */ + static native void quiche_config_set_ack_delay_exponent(long configAddr, long value); + + /** + * See + * + * quiche_config_set_max_ack_delay. + */ + static native void quiche_config_set_max_ack_delay(long configAddr, long value); + + /** + * See + * + * quiche_config_set_disable_active_migration. + */ + static native void quiche_config_set_disable_active_migration(long configAddr, boolean value); + + /** + * See + * + * quiche_config_set_cc_algorithm. + */ + static native void quiche_config_set_cc_algorithm(long configAddr, int algo); + + /** + * See + * + * quiche_config_enable_hystart. + */ + static native void quiche_config_enable_hystart(long configAddr, boolean value); + + /** + * See + * + * quiche_config_enable_dgram. + */ + static native void quiche_config_enable_dgram(long configAddr, boolean enable, + int recv_queue_len, int send_queue_len); + + // Sets the limit of active connection IDs. + static native void quiche_config_set_active_connection_id_limit(long configAddr, long value); + + // Sets the initial stateless reset token. + static native void quiche_config_set_stateless_reset_token(long configAddr, byte[] token); + + /** + * See + * + * quiche_config_free. + */ + static native void quiche_config_free(long configAddr); + + /** + * See + * quiche_config_new. + */ + private static native void quiche_enable_debug_logging(QuicheLogger logger); + + private static native long buffer_memory_address(ByteBuffer buffer); + + static native int sockaddr_cmp(long addr, long addr2); + + /** + * Returns the memory address if the {@link ByteBuf} taking the readerIndex into account. + * + * @param buf the {@link ByteBuf} of which we want to obtain the memory address + * (taking its {@link ByteBuf#readerIndex()} into account). + * @return the memory address of this {@link ByteBuf}s readerIndex. + */ + static long readerMemoryAddress(ByteBuf buf) { + return memoryAddress(buf, buf.readerIndex(), buf.readableBytes()); + } + + /** + * Returns the memory address if the {@link ByteBuf} taking the writerIndex into account. + * + * @param buf the {@link ByteBuf} of which we want to obtain the memory address + * (taking its {@link ByteBuf#writerIndex()} into account). + * @return the memory address of this {@link ByteBuf}s writerIndex. + */ + static long writerMemoryAddress(ByteBuf buf) { + return memoryAddress(buf, buf.writerIndex(), buf.writableBytes()); + } + + /** + * Returns the memory address if the {@link ByteBuf} taking the offset into account. + * + * @param buf the {@link ByteBuf} of which we want to obtain the memory address + * (taking the {@code offset} into account). + * @param offset the offset of the memory address. + * @param len the length of the {@link ByteBuf}. + * @return the memory address of this {@link ByteBuf}s offset. + */ + static long memoryAddress(ByteBuf buf, int offset, int len) { + assert buf.isDirect(); + if (buf.hasMemoryAddress()) { + return buf.memoryAddress() + offset; + } + return memoryAddressWithPosition(buf.internalNioBuffer(offset, len)); + } + + /** + * Returns the memory address of the given {@link ByteBuffer} taking its current {@link ByteBuffer#position()} into + * account. + * + * @param buf the {@link ByteBuffer} of which we want to obtain the memory address + * (taking its {@link ByteBuffer#position()} into account). + * @return the memory address of this {@link ByteBuffer}s position. + */ + static long memoryAddressWithPosition(ByteBuffer buf) { + assert buf.isDirect(); + return buffer_memory_address(buf) + buf.position(); + } + + @SuppressWarnings("deprecation") + static ByteBuf allocateNativeOrder(int capacity) { + // Just use Unpooled as the life-time of these buffers is long. + ByteBuf buffer = Unpooled.directBuffer(capacity); + + // As we use the buffers as pointers to int etc we need to ensure we use the right oder so we will + // see the right value when we read primitive values. + return PlatformDependent.BIG_ENDIAN_NATIVE_ORDER ? buffer : buffer.order(ByteOrder.LITTLE_ENDIAN); + } + + static Exception newException(int err) { + final QuicError error = QuicError.valueOf(err); + final QuicException reason = new QuicException(error); + if (err == QUICHE_ERR_TLS_FAIL) { + String lastSslError = BoringSSL.ERR_last_error(); + final String message; + if (lastSslError != null) { + message = error.message() + ": " + lastSslError; + } else { + message = error.message(); + } + final SSLHandshakeException sslExc = new SSLHandshakeException(message); + sslExc.initCause(reason); + return sslExc; + } + if (err == QUICHE_ERR_CRYPTO_FAIL) { + return new SSLException(error.message(), reason); + } + return reason; + } + + static boolean shouldClose(int res) { + return res == Quiche.QUICHE_ERR_CRYPTO_FAIL || res == Quiche.QUICHE_ERR_TLS_FAIL; + } + + static boolean throwIfError(int res) throws Exception { + if (res < 0) { + if (res == Quiche.QUICHE_ERR_DONE) { + return true; + } + throw Quiche.newException(res); + } + return false; + } + + static void notifyPromise(int res, ChannelPromise promise) { + if (res < 0 && res != Quiche.QUICHE_ERR_DONE) { + promise.setFailure(Quiche.newException(res)); + } else { + promise.setSuccess(); + } + } + + /** + * Returns {@code true} if both {@link ByteBuffer}s have the same {@code sock_addr} stored. + * + * @param memory the first {@link ByteBuffer} which holds a {@code quiche_recv_info}. + * @param memory2 the second {@link ByteBuffer} which holds a {@code quiche_recv_info}. + * @return {@code true} if both {@link ByteBuffer}s have the same {@code sock_addr} stored, {@code false} + * otherwise. + */ + static boolean isSameAddress(ByteBuffer memory, ByteBuffer memory2, int addressOffset) { + long address1 = Quiche.memoryAddressWithPosition(memory) + addressOffset; + long address2 = Quiche.memoryAddressWithPosition(memory2) + addressOffset; + return SockaddrIn.cmp(address1, address2) == 0; + } + + static void setPrimitiveValue(ByteBuffer memory, int offset, int valueType, long value) { + switch (valueType) { + case 1: + memory.put(offset, (byte) value); + break; + case 2: + memory.putShort(offset, (short) value); + break; + case 4: + memory.putInt(offset, (int) value); + break; + case 8: + memory.putLong(offset, value); + break; + default: + throw new IllegalStateException(); + } + } + + static long getPrimitiveValue(ByteBuffer memory, int offset, int valueType) { + switch (valueType) { + case 1: + return memory.get(offset); + case 2: + return memory.getShort(offset); + case 4: + return memory.getInt(offset); + case 8: + return memory.getLong(offset); + default: + throw new IllegalStateException(); + } + } + + private Quiche() { } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheConfig.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheConfig.java new file mode 100644 index 0000000..c5866dc --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheConfig.java @@ -0,0 +1,137 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +final class QuicheConfig { + private final boolean isDatagramSupported; + private long config = -1; + + QuicheConfig(int version, Boolean grease, Long maxIdleTimeout, Long maxSendUdpPayloadSize, + Long maxRecvUdpPayloadSize, Long initialMaxData, + Long initialMaxStreamDataBidiLocal, Long initialMaxStreamDataBidiRemote, + Long initialMaxStreamDataUni, Long initialMaxStreamsBidi, Long initialMaxStreamsUni, + Long ackDelayExponent, Long maxAckDelay, Boolean disableActiveMigration, Boolean enableHystart, + QuicCongestionControlAlgorithm congestionControlAlgorithm, + Integer recvQueueLen, Integer sendQueueLen, + Long activeConnectionIdLimit, byte[] statelessResetToken) { + long config = Quiche.quiche_config_new(version); + try { + if (grease != null) { + Quiche.quiche_config_grease(config, grease); + } + if (maxIdleTimeout != null) { + Quiche.quiche_config_set_max_idle_timeout(config, maxIdleTimeout); + } + if (maxSendUdpPayloadSize != null) { + Quiche.quiche_config_set_max_send_udp_payload_size(config, maxSendUdpPayloadSize); + } + if (maxRecvUdpPayloadSize != null) { + Quiche.quiche_config_set_max_recv_udp_payload_size(config, maxRecvUdpPayloadSize); + } + if (initialMaxData != null) { + Quiche.quiche_config_set_initial_max_data(config, initialMaxData); + } + if (initialMaxStreamDataBidiLocal != null) { + Quiche.quiche_config_set_initial_max_stream_data_bidi_local(config, initialMaxStreamDataBidiLocal); + } + if (initialMaxStreamDataBidiRemote != null) { + Quiche.quiche_config_set_initial_max_stream_data_bidi_remote(config, initialMaxStreamDataBidiRemote); + } + if (initialMaxStreamDataUni != null) { + Quiche.quiche_config_set_initial_max_stream_data_uni(config, initialMaxStreamDataUni); + } + if (initialMaxStreamsBidi != null) { + Quiche.quiche_config_set_initial_max_streams_bidi(config, initialMaxStreamsBidi); + } + if (initialMaxStreamsUni != null) { + Quiche.quiche_config_set_initial_max_streams_uni(config, initialMaxStreamsUni); + } + if (ackDelayExponent != null) { + Quiche.quiche_config_set_ack_delay_exponent(config, ackDelayExponent); + } + if (maxAckDelay != null) { + Quiche.quiche_config_set_max_ack_delay(config, maxAckDelay); + } + if (disableActiveMigration != null) { + Quiche.quiche_config_set_disable_active_migration(config, disableActiveMigration); + } + if (enableHystart != null) { + Quiche.quiche_config_enable_hystart(config, enableHystart); + } + if (congestionControlAlgorithm != null) { + switch (congestionControlAlgorithm) { + case RENO: + Quiche.quiche_config_set_cc_algorithm(config, Quiche.QUICHE_CC_RENO); + break; + case CUBIC: + Quiche.quiche_config_set_cc_algorithm(config, Quiche.QUICHE_CC_CUBIC); + break; + case BBR: + Quiche.quiche_config_set_cc_algorithm(config, Quiche.QUICHE_CC_BBR); + break; + default: + throw new IllegalArgumentException( + "Unknown congestionControlAlgorithm: " + congestionControlAlgorithm); + } + } + if (recvQueueLen != null && sendQueueLen != null) { + isDatagramSupported = true; + Quiche.quiche_config_enable_dgram(config, true, recvQueueLen, sendQueueLen); + } else { + isDatagramSupported = false; + } + if (activeConnectionIdLimit != null) { + Quiche.quiche_config_set_active_connection_id_limit(config, activeConnectionIdLimit); + } + if (statelessResetToken != null) { + Quiche.quiche_config_set_stateless_reset_token(config, statelessResetToken); + } + this.config = config; + } catch (Throwable cause) { + Quiche.quiche_config_free(config); + throw cause; + } + } + + boolean isDatagramSupported() { + return isDatagramSupported; + } + + long nativeAddress() { + return config; + } + + // Let's override finalize() as we want to ensure we never leak memory even if the user will miss to close + // Channel that uses this handler that used the config and just let it get GC'ed. + @Override + protected void finalize() throws Throwable { + try { + free(); + } finally { + super.finalize(); + } + } + + void free() { + if (config != -1) { + try { + Quiche.quiche_config_free(config); + } finally { + config = -1; + } + } + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheLogger.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheLogger.java new file mode 100644 index 0000000..5e8ca06 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheLogger.java @@ -0,0 +1,35 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.util.internal.logging.InternalLogger; + +/** + * Delegates QUICHE logging to {@link InternalLogger}. + */ +final class QuicheLogger { + private final InternalLogger logger; + + QuicheLogger(InternalLogger logger) { + this.logger = logger; + } + + // Called from JNI. + @SuppressWarnings("unused") + void log(String msg) { + logger.debug(msg); + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheNativeStaticallyReferencedJniMethods.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheNativeStaticallyReferencedJniMethods.java new file mode 100644 index 0000000..4183e29 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheNativeStaticallyReferencedJniMethods.java @@ -0,0 +1,96 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +final class QuicheNativeStaticallyReferencedJniMethods { + + static native int quiche_protocol_version(); + static native int quiche_max_conn_id_len(); + static native int quiche_shutdown_read(); + static native int quiche_shutdown_write(); + + static native int quiche_err_done(); + static native int quiche_err_buffer_too_short(); + static native int quiche_err_unknown_version(); + static native int quiche_err_invalid_frame(); + static native int quiche_err_invalid_packet(); + static native int quiche_err_invalid_state(); + static native int quiche_err_invalid_stream_state(); + static native int quiche_err_invalid_transport_param(); + static native int quiche_err_crypto_fail(); + static native int quiche_err_tls_fail(); + static native int quiche_err_flow_control(); + static native int quiche_err_stream_limit(); + static native int quiche_err_final_size(); + static native int quiche_err_stream_stopped(); + static native int quiche_err_stream_reset(); + static native int quiche_err_congestion_control(); + static native int quiche_err_id_limit(); + static native int quiche_err_out_of_identifiers(); + static native int quiche_err_key_update(); + + static native int quiche_cc_reno(); + static native int quiche_cc_cubic(); + static native int quiche_cc_bbr(); + + static native int quicheRecvInfoOffsetofFrom(); + static native int quicheRecvInfoOffsetofFromLen(); + static native int quicheRecvInfoOffsetofTo(); + static native int quicheRecvInfoOffsetofToLen(); + + static native int sizeofQuicheRecvInfo(); + static native int quicheSendInfoOffsetofTo(); + static native int quicheSendInfoOffsetofToLen(); + static native int quicheSendInfoOffsetofFrom(); + static native int quicheSendInfoOffsetofFromLen(); + + static native int quicheSendInfoOffsetofAt(); + + static native int sizeofQuicheSendInfo(); + + static native int afInet(); + static native int afInet6(); + static native int sizeofSockaddrIn(); + static native int sizeofSockaddrIn6(); + static native int sockaddrInOffsetofSinFamily(); + static native int sockaddrInOffsetofSinPort(); + static native int sockaddrInOffsetofSinAddr(); + static native int inAddressOffsetofSAddr(); + static native int sockaddrIn6OffsetofSin6Family(); + static native int sockaddrIn6OffsetofSin6Port(); + static native int sockaddrIn6OffsetofSin6Flowinfo(); + static native int sockaddrIn6OffsetofSin6Addr(); + static native int sockaddrIn6OffsetofSin6ScopeId(); + static native int in6AddressOffsetofS6Addr(); + static native int sizeofSockaddrStorage(); + static native int sizeofSocklenT(); + static native int sizeofSizeT(); + + static native int sizeofTimespec(); + static native int timespecOffsetofTvSec(); + static native int timespecOffsetofTvNsec(); + static native int sizeofTimeT(); + static native int sizeofLong(); + + static native int quiche_path_event_new(); + static native int quiche_path_event_validated(); + static native int quiche_path_event_failed_validation(); + static native int quiche_path_event_closed(); + static native int quiche_path_event_reused_source_connection_id(); + static native int quiche_path_event_peer_migrated(); + + private QuicheNativeStaticallyReferencedJniMethods() { } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicChannel.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicChannel.java new file mode 100644 index 0000000..4713c13 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicChannel.java @@ -0,0 +1,1969 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.AbstractChannel; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelMetadata; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelOutboundBuffer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelPromise; +import io.netty.channel.ConnectTimeoutException; +import io.netty.channel.DefaultChannelPipeline; +import io.netty.channel.EventLoop; +import io.netty.channel.RecvByteBufAllocator; +import io.netty.channel.socket.DatagramPacket; +import io.netty.handler.ssl.SniCompletionEvent; +import io.netty.handler.ssl.SslHandshakeCompletionEvent; +import io.netty.util.AttributeKey; +import io.netty.util.collection.LongObjectHashMap; +import io.netty.util.collection.LongObjectMap; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.ImmediateEventExecutor; +import io.netty.util.concurrent.ImmediateExecutor; +import io.netty.util.concurrent.Promise; +import io.netty.util.internal.StringUtil; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLHandshakeException; +import java.io.File; +import java.net.ConnectException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.AlreadyConnectedException; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.ConnectionPendingException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * {@link QuicChannel} implementation that uses quiche. + */ +final class QuicheQuicChannel extends AbstractChannel implements QuicChannel { + private static final InternalLogger logger = InternalLoggerFactory.getInstance(QuicheQuicChannel.class); + private static final String QLOG_FILE_EXTENSION = ".qlog"; + + enum StreamRecvResult { + /** + * Nothing more to read from the stream. + */ + DONE, + /** + * FIN flag received. + */ + FIN, + /** + * Normal read without FIN flag. + */ + OK + } + + private static final class CloseData implements ChannelFutureListener { + final boolean applicationClose; + final int err; + final ByteBuf reason; + + CloseData(boolean applicationClose, int err, ByteBuf reason) { + this.applicationClose = applicationClose; + this.err = err; + this.reason = reason; + } + + @Override + public void operationComplete(ChannelFuture future) { + reason.release(); + } + } + + private final ChannelFutureListener continueSendingListener = new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture channelFuture) { + if (connectionSend()) { + flushParent(); + } + } + }; + + private static final ChannelMetadata METADATA = new ChannelMetadata(false, 16); + private final long[] readableStreams = new long[128]; + private final long[] writableStreams = new long[128]; + + private final LongObjectMap streams = new LongObjectHashMap<>(); + private final QuicheQuicChannelConfig config; + private final boolean server; + private final QuicStreamIdGenerator idGenerator; + private final ChannelHandler streamHandler; + private final Map.Entry, Object>[] streamOptionsArray; + private final Map.Entry, Object>[] streamAttrsArray; + private final TimeoutHandler timeoutHandler; + private Executor sslTaskExecutor; + + private boolean inFireChannelReadCompleteQueue; + private boolean fireChannelReadCompletePending; + private ByteBuf finBuffer; + private ChannelPromise connectPromise; + private ScheduledFuture connectTimeoutFuture; + private QuicConnectionAddress connectAddress; + private final Set sourceConnectionIds = new HashSet<>(); + private CloseData closeData; + private QuicConnectionCloseEvent connectionCloseEvent; + private QuicConnectionStats statsAtClose; + + private InetSocketAddress local; + private InetSocketAddress remote; + private boolean supportsDatagram; + private boolean recvDatagramPending; + private boolean datagramReadable; + + private boolean recvStreamPending; + private boolean streamReadable; + private boolean handshakeCompletionNotified; + private boolean earlyDataReadyNotified; + + private int reantranceGuard = 0; + private static final int IN_RECV = 1 << 1; + private static final int IN_CONNECTION_SEND = 1 << 2; + private static final int IN_HANDLE_WRITABLE_STREAMS = 1 << 3; + private static final int IN_FORCE_CLOSE = 1 << 4; + + private static final int CLOSED = 0; + private static final int OPEN = 1; + private static final int ACTIVE = 2; + private volatile int state; + private volatile boolean timedOut; + private volatile String traceId; + private volatile QuicheQuicConnection connection; + + private static final AtomicLongFieldUpdater UNI_STREAMS_LEFT_UPDATER = + AtomicLongFieldUpdater.newUpdater(QuicheQuicChannel.class, "uniStreamsLeft"); + private volatile long uniStreamsLeft; + + private static final AtomicLongFieldUpdater BIDI_STREAMS_LEFT_UPDATER = + AtomicLongFieldUpdater.newUpdater(QuicheQuicChannel.class, "bidiStreamsLeft"); + private volatile long bidiStreamsLeft; + + private QuicheQuicChannel(Channel parent, boolean server, ByteBuffer key, InetSocketAddress local, + InetSocketAddress remote, boolean supportsDatagram, ChannelHandler streamHandler, + Map.Entry, Object>[] streamOptionsArray, + Map.Entry, Object>[] streamAttrsArray, + Consumer timeoutTask, + Executor sslTaskExecutor) { + super(parent); + config = new QuicheQuicChannelConfig(this); + this.server = server; + this.idGenerator = new QuicStreamIdGenerator(server); + if (key != null) { + this.sourceConnectionIds.add(key); + } + state = OPEN; + + this.supportsDatagram = supportsDatagram; + this.local = local; + this.remote = remote; + + this.streamHandler = streamHandler; + this.streamOptionsArray = streamOptionsArray; + this.streamAttrsArray = streamAttrsArray; + timeoutHandler = new TimeoutHandler(timeoutTask); + this.sslTaskExecutor = sslTaskExecutor == null ? ImmediateExecutor.INSTANCE : sslTaskExecutor; + } + + static QuicheQuicChannel forClient(Channel parent, InetSocketAddress local, InetSocketAddress remote, + ChannelHandler streamHandler, + Map.Entry, Object>[] streamOptionsArray, + Map.Entry, Object>[] streamAttrsArray) { + return new QuicheQuicChannel(parent, false, null, local, remote, false, streamHandler, + streamOptionsArray, streamAttrsArray, null, null); + } + + static QuicheQuicChannel forServer(Channel parent, ByteBuffer key, InetSocketAddress local, + InetSocketAddress remote, + boolean supportsDatagram, ChannelHandler streamHandler, + Map.Entry, Object>[] streamOptionsArray, + Map.Entry, Object>[] streamAttrsArray, + Consumer timeoutTask, Executor sslTaskExecutor) { + return new QuicheQuicChannel(parent, true, key, local, remote, supportsDatagram, + streamHandler, streamOptionsArray, streamAttrsArray, timeoutTask, + sslTaskExecutor); + } + + @Override + public boolean isTimedOut() { + return timedOut; + } + + @Override + public SSLEngine sslEngine() { + QuicheQuicConnection connection = this.connection; + return connection == null ? null : connection.engine(); + } + + private void notifyAboutHandshakeCompletionIfNeeded(SSLHandshakeException cause) { + if (handshakeCompletionNotified) { + return; + } + if (cause != null) { + pipeline().fireUserEventTriggered(new SslHandshakeCompletionEvent(cause)); + return; + } + QuicheQuicConnection connection = this.connection; + if (connection == null) { + return; + } + switch (connection.engine().getHandshakeStatus()) { + case NOT_HANDSHAKING: + case FINISHED: + handshakeCompletionNotified = true; + String sniHostname = connection.engine().sniHostname; + if (sniHostname != null) { + connection.engine().sniHostname = null; + pipeline().fireUserEventTriggered(new SniCompletionEvent(sniHostname)); + } + pipeline().fireUserEventTriggered(SslHandshakeCompletionEvent.SUCCESS); + break; + default: + break; + } + } + + @Override + public long peerAllowedStreams(QuicStreamType type) { + switch (type) { + case BIDIRECTIONAL: + return bidiStreamsLeft; + case UNIDIRECTIONAL: + return uniStreamsLeft; + default: + return 0; + } + } + + void attachQuicheConnection(QuicheQuicConnection connection) { + this.connection = connection; + + byte[] traceId = Quiche.quiche_conn_trace_id(connection.address()); + if (traceId != null) { + this.traceId = new String(traceId); + } + + connection.initInfo(local, remote); + + // Setup QLOG if needed. + QLogConfiguration configuration = config.getQLogConfiguration(); + if (configuration != null) { + final String fileName; + File file = new File(configuration.path()); + if (file.isDirectory()) { + // Create directory if needed. + file.mkdir(); + if (this.traceId != null) { + fileName = configuration.path() + File.separatorChar + this.traceId + "-" + + id().asShortText() + QLOG_FILE_EXTENSION; + } else { + fileName = configuration.path() + File.separatorChar + id().asShortText() + QLOG_FILE_EXTENSION; + } + } else { + fileName = configuration.path(); + } + + if (!Quiche.quiche_conn_set_qlog_path(connection.address(), fileName, + configuration.logTitle(), configuration.logDescription())) { + logger.info("Unable to create qlog file: {} ", fileName); + } + } + } + + private void connect(Function engineProvider, Executor sslTaskExecutor, + long configAddr, int localConnIdLength, + boolean supportsDatagram, ByteBuffer fromSockaddrMemory, ByteBuffer toSockaddrMemory) + throws Exception { + assert this.connection == null; + assert this.traceId == null; + assert this.sourceConnectionIds.isEmpty(); + + this.sslTaskExecutor = sslTaskExecutor; + + QuicConnectionAddress address = this.connectAddress; + if (address == QuicConnectionAddress.EPHEMERAL) { + address = QuicConnectionAddress.random(localConnIdLength); + } else { + if (address.connId.remaining() != localConnIdLength) { + failConnectPromiseAndThrow(new IllegalArgumentException("connectionAddress has length " + + address.connId.remaining() + + " instead of " + localConnIdLength)); + } + } + QuicSslEngine engine = engineProvider.apply(this); + if (!(engine instanceof QuicheQuicSslEngine)) { + failConnectPromiseAndThrow(new IllegalArgumentException("QuicSslEngine is not of type " + + QuicheQuicSslEngine.class.getSimpleName())); + return; + } + if (!engine.getUseClientMode()) { + failConnectPromiseAndThrow(new IllegalArgumentException("QuicSslEngine is not create in client mode")); + } + QuicheQuicSslEngine quicheEngine = (QuicheQuicSslEngine) engine; + ByteBuffer connectId = address.connId.duplicate(); + ByteBuf idBuffer = alloc().directBuffer(connectId.remaining()).writeBytes(connectId.duplicate()); + try { + int fromSockaddrLen = SockaddrIn.setAddress(fromSockaddrMemory, local); + int toSockaddrLen = SockaddrIn.setAddress(toSockaddrMemory, remote); + QuicheQuicConnection connection = quicheEngine.createConnection(ssl -> + Quiche.quiche_conn_new_with_tls(Quiche.readerMemoryAddress(idBuffer), + idBuffer.readableBytes(), -1, -1, + Quiche.memoryAddressWithPosition(fromSockaddrMemory), fromSockaddrLen, + Quiche.memoryAddressWithPosition(toSockaddrMemory), toSockaddrLen, + configAddr, ssl, false)); + if (connection == null) { + failConnectPromiseAndThrow(new ConnectException()); + return; + } + attachQuicheConnection(connection); + QuicClientSessionCache sessionCache = quicheEngine.ctx.getSessionCache(); + if (sessionCache != null) { + byte[] sessionBytes = sessionCache + .getSession(quicheEngine.getSession().getPeerHost(), quicheEngine.getSession().getPeerPort()); + if (sessionBytes != null) { + Quiche.quiche_conn_set_session(connection.address(), sessionBytes); + } + } + this.supportsDatagram = supportsDatagram; + sourceConnectionIds.add(connectId); + } finally { + idBuffer.release(); + } + } + + private void failConnectPromiseAndThrow(Exception e) throws Exception { + tryFailConnectPromise(e); + throw e; + } + + private boolean tryFailConnectPromise(Exception e) { + ChannelPromise promise = connectPromise; + if (promise != null) { + connectPromise = null; + promise.tryFailure(e); + return true; + } + return false; + } + + Set sourceConnectionIds() { + return sourceConnectionIds; + } + + private boolean closeAllIfConnectionClosed() { + if (connection.isClosed()) { + forceClose(); + return true; + } + return false; + } + + boolean markInFireChannelReadCompleteQueue() { + if (inFireChannelReadCompleteQueue) { + return false; + } + inFireChannelReadCompleteQueue = true; + return true; + } + + private void failPendingConnectPromise() { + ChannelPromise promise = QuicheQuicChannel.this.connectPromise; + if (promise != null) { + QuicheQuicChannel.this.connectPromise = null; + promise.tryFailure(new QuicClosedChannelException(this.connectionCloseEvent)); + } + } + + void forceClose() { + if (isConnDestroyed() || (reantranceGuard & IN_FORCE_CLOSE) != 0) { + // Just return if we already destroyed the underlying connection. + return; + } + reantranceGuard |= IN_FORCE_CLOSE; + + QuicheQuicConnection conn = connection; + + unsafe().close(voidPromise()); + // making sure that connection statistics is avaliable + // even after channel is closed + statsAtClose = collectStats0(conn, eventLoop().newPromise()); + try { + failPendingConnectPromise(); + state = CLOSED; + timedOut = Quiche.quiche_conn_is_timed_out(conn.address()); + + closeStreams(); + + if (finBuffer != null) { + finBuffer.release(); + finBuffer = null; + } + state = CLOSED; + + timeoutHandler.cancel(); + } finally { + flushParent(); + connection = null; + conn.free(); + } + } + + @Override + protected DefaultChannelPipeline newChannelPipeline() { + return new DefaultChannelPipeline(this) { + @Override + protected void onUnhandledInboundMessage(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof QuicStreamChannel) { + QuicStreamChannel channel = (QuicStreamChannel) msg; + Quic.setupChannel(channel, streamOptionsArray, streamAttrsArray, streamHandler, logger); + ctx.channel().eventLoop().register(channel); + } else { + super.onUnhandledInboundMessage(ctx, msg); + } + } + }; + } + + @Override + public QuicChannel flush() { + super.flush(); + return this; + } + + @Override + public QuicChannel read() { + super.read(); + return this; + } + + @Override + public Future createStream(QuicStreamType type, ChannelHandler handler, + Promise promise) { + if (eventLoop().inEventLoop()) { + ((QuicChannelUnsafe) unsafe()).connectStream(type, handler, promise); + } else { + eventLoop().execute(() -> ((QuicChannelUnsafe) unsafe()).connectStream(type, handler, promise)); + } + return promise; + } + + @Override + public ChannelFuture close(boolean applicationClose, int error, ByteBuf reason, ChannelPromise promise) { + if (eventLoop().inEventLoop()) { + close0(applicationClose, error, reason, promise); + } else { + eventLoop().execute(() -> close0(applicationClose, error, reason, promise)); + } + return promise; + } + + private void close0(boolean applicationClose, int error, ByteBuf reason, ChannelPromise promise) { + if (closeData == null) { + if (!reason.hasMemoryAddress()) { + // Copy to direct buffer as that's what we need. + ByteBuf copy = alloc().directBuffer(reason.readableBytes()).writeBytes(reason); + reason.release(); + reason = copy; + } + closeData = new CloseData(applicationClose, error, reason); + promise.addListener(closeData); + } else { + // We already have a close scheduled that uses a close data. Lets release the buffer early. + reason.release(); + } + close(promise); + } + + @Override + public String toString() { + String traceId = this.traceId; + if (traceId == null) { + return "()" + super.toString(); + } else { + return '(' + traceId + ')' + super.toString(); + } + } + + @Override + protected AbstractUnsafe newUnsafe() { + return new QuicChannelUnsafe(); + } + + @Override + protected boolean isCompatible(EventLoop eventLoop) { + return parent().eventLoop() == eventLoop; + } + + @Override + protected SocketAddress localAddress0() { + QuicheQuicConnection connection = this.connection; + return connection == null ? null : connection.sourceId(); + } + + @Override + protected SocketAddress remoteAddress0() { + QuicheQuicConnection connection = this.connection; + return connection == null ? null : connection.destinationId(); + } + + @Override + protected void doBind(SocketAddress socketAddress) { + throw new UnsupportedOperationException(); + } + + @Override + protected void doDisconnect() throws Exception { + doClose(); + } + + @Override + protected void doClose() throws Exception { + state = CLOSED; + + final boolean app; + final int err; + final ByteBuf reason; + if (closeData == null) { + app = false; + err = 0; + reason = Unpooled.EMPTY_BUFFER; + } else { + app = closeData.applicationClose; + err = closeData.err; + reason = closeData.reason; + closeData = null; + } + + // Call connectionSend() so we ensure we send all that is queued before we close the channel + boolean written = connectionSend(); + + failPendingConnectPromise(); + Quiche.throwIfError(Quiche.quiche_conn_close(connectionAddressChecked(), app, err, + Quiche.readerMemoryAddress(reason), reason.readableBytes())); + + // As we called quiche_conn_close(...) we need to ensure we will call quiche_conn_send(...) either + // now or we will do so once we see the channelReadComplete event. + // + // See https://docs.rs/quiche/0.6.0/quiche/struct.Connection.html#method.close + written |= connectionSend(); + if (written) { + // As this is the close let us flush it asap. + forceFlushParent(); + } + } + + @Override + protected void doBeginRead() { + recvDatagramPending = true; + recvStreamPending = true; + if (datagramReadable || streamReadable) { + ((QuicChannelUnsafe) unsafe()).recv(); + } + } + + @Override + protected Object filterOutboundMessage(Object msg) { + if (msg instanceof ByteBuf) { + return msg; + } + throw new UnsupportedOperationException("Unsupported message type: " + StringUtil.simpleClassName(msg)); + } + + @Override + protected void doWrite(ChannelOutboundBuffer channelOutboundBuffer) throws Exception { + if (!supportsDatagram) { + throw new UnsupportedOperationException("Datagram extension is not supported"); + } + boolean sendSomething = false; + boolean retry = false; + try { + for (;;) { + ByteBuf buffer = (ByteBuf) channelOutboundBuffer.current(); + if (buffer == null) { + break; + } + + int readable = buffer.readableBytes(); + if (readable == 0) { + // Skip empty buffers. + channelOutboundBuffer.remove(); + continue; + } + + final int res; + if (!buffer.isDirect() || buffer.nioBufferCount() > 1) { + ByteBuf tmpBuffer = alloc().directBuffer(readable); + try { + tmpBuffer.writeBytes(buffer, buffer.readerIndex(), readable); + res = sendDatagram(tmpBuffer); + } finally { + tmpBuffer.release(); + } + } else { + res = sendDatagram(buffer); + } + if (res >= 0) { + channelOutboundBuffer.remove(); + sendSomething = true; + retry = false; + } else { + if (res == Quiche.QUICHE_ERR_BUFFER_TOO_SHORT) { + retry = false; + channelOutboundBuffer.remove(Quiche.newException(res)); + } else if (res == Quiche.QUICHE_ERR_INVALID_STATE) { + throw new UnsupportedOperationException("Remote peer does not support Datagram extension", + Quiche.newException(res)); + } else if (Quiche.throwIfError(res)) { + if (retry) { + // We already retried and it didn't work. Let's drop the datagrams on the floor. + for (;;) { + if (!channelOutboundBuffer.remove()) { + // The buffer is empty now. + return; + } + } + } + // Set sendSomething to false a we will call connectionSend() now. + sendSomething = false; + // If this returned DONE we couldn't write anymore. This happens if the internal queue + // is full. In this case we should call quiche_conn_send(...) and so make space again. + if (connectionSend()) { + forceFlushParent(); + } + // Let's try again to write the message. + retry = true; + } + } + } + } finally { + if (sendSomething && connectionSend()) { + flushParent(); + } + } + } + + private int sendDatagram(ByteBuf buf) throws ClosedChannelException { + return Quiche.quiche_conn_dgram_send(connectionAddressChecked(), + Quiche.readerMemoryAddress(buf), buf.readableBytes()); + } + + @Override + public QuicChannelConfig config() { + return config; + } + + @Override + public boolean isOpen() { + return state >= OPEN; + } + + @Override + public boolean isActive() { + return state == ACTIVE; + } + + @Override + public ChannelMetadata metadata() { + return METADATA; + } + + /** + * This may call {@link #flush()} on the parent channel if needed. The flush may delayed until the read loop + * is over. + */ + private void flushParent() { + if (!inFireChannelReadCompleteQueue) { + forceFlushParent(); + } + } + + /** + * Call @link #flush()} on the parent channel. + */ + private void forceFlushParent() { + parent().flush(); + } + + private long connectionAddressChecked() throws ClosedChannelException { + if (isConnDestroyed()) { + throw new ClosedChannelException(); + } + return connection.address(); + } + + boolean freeIfClosed() { + if (isConnDestroyed()) { + return true; + } + return closeAllIfConnectionClosed(); + } + + private void closeStreams() { + // Make a copy to ensure we not run into a situation when we change the underlying iterator from + // another method and so run in an assert error. + for (QuicheQuicStreamChannel stream: streams.values().toArray(new QuicheQuicStreamChannel[0])) { + stream.unsafe().close(voidPromise()); + } + streams.clear(); + } + + void streamPriority(long streamId, byte priority, boolean incremental) throws Exception { + Quiche.throwIfError(Quiche.quiche_conn_stream_priority(connectionAddressChecked(), streamId, + priority, incremental)); + } + + void streamClosed(long streamId) { + streams.remove(streamId); + } + + boolean isStreamLocalCreated(long streamId) { + return (streamId & 0x1) == (server ? 1 : 0); + } + + QuicStreamType streamType(long streamId) { + return (streamId & 0x2) == 0 ? QuicStreamType.BIDIRECTIONAL : QuicStreamType.UNIDIRECTIONAL; + } + + void streamShutdown(long streamId, boolean read, boolean write, int err, ChannelPromise promise) { + final long connectionAddress; + try { + connectionAddress = connectionAddressChecked(); + } catch (ClosedChannelException e) { + promise.setFailure(e); + return; + } + int res = 0; + if (read) { + res |= Quiche.quiche_conn_stream_shutdown(connectionAddress, streamId, Quiche.QUICHE_SHUTDOWN_READ, err); + } + if (write) { + res |= Quiche.quiche_conn_stream_shutdown(connectionAddress, streamId, Quiche.QUICHE_SHUTDOWN_WRITE, err); + } + + // As we called quiche_conn_stream_shutdown(...) we need to ensure we will call quiche_conn_send(...) either + // now or we will do so once we see the channelReadComplete event. + // + // See https://docs.rs/quiche/0.6.0/quiche/struct.Connection.html#method.send + if (connectionSend()) { + // Force the flush so the shutdown can be seen asap. + forceFlushParent(); + } + Quiche.notifyPromise(res, promise); + } + + void streamSendFin(long streamId) throws Exception { + try { + // Just write an empty buffer and set fin to true. + Quiche.throwIfError(streamSend0(streamId, Unpooled.EMPTY_BUFFER, true)); + } finally { + // As we called quiche_conn_stream_send(...) we need to ensure we will call quiche_conn_send(...) either + // now or we will do so once we see the channelReadComplete event. + // + // See https://docs.rs/quiche/0.6.0/quiche/struct.Connection.html#method.send + if (connectionSend()) { + flushParent(); + } + } + } + + int streamSend(long streamId, ByteBuf buffer, boolean fin) throws ClosedChannelException { + if (buffer.nioBufferCount() == 1) { + return streamSend0(streamId, buffer, fin); + } + ByteBuffer[] nioBuffers = buffer.nioBuffers(); + int lastIdx = nioBuffers.length - 1; + int res = 0; + for (int i = 0; i < lastIdx; i++) { + ByteBuffer nioBuffer = nioBuffers[i]; + while (nioBuffer.hasRemaining()) { + int localRes = streamSend(streamId, nioBuffer, false); + if (localRes <= 0) { + return res; + } + res += localRes; + + nioBuffer.position(nioBuffer.position() + localRes); + } + } + int localRes = streamSend(streamId, nioBuffers[lastIdx], fin); + if (localRes > 0) { + res += localRes; + } + return res; + } + + void connectionSendAndFlush() { + if (inFireChannelReadCompleteQueue || (reantranceGuard & IN_HANDLE_WRITABLE_STREAMS) != 0) { + return; + } + if (connectionSend()) { + flushParent(); + } + } + + private int streamSend0(long streamId, ByteBuf buffer, boolean fin) throws ClosedChannelException { + return Quiche.quiche_conn_stream_send(connectionAddressChecked(), streamId, + Quiche.readerMemoryAddress(buffer), buffer.readableBytes(), fin); + } + + private int streamSend(long streamId, ByteBuffer buffer, boolean fin) throws ClosedChannelException { + return Quiche.quiche_conn_stream_send(connectionAddressChecked(), streamId, + Quiche.memoryAddressWithPosition(buffer), buffer.remaining(), fin); + } + + StreamRecvResult streamRecv(long streamId, ByteBuf buffer) throws Exception { + if (finBuffer == null) { + finBuffer = alloc().directBuffer(1); + } + int writerIndex = buffer.writerIndex(); + int recvLen = Quiche.quiche_conn_stream_recv(connectionAddressChecked(), streamId, + Quiche.writerMemoryAddress(buffer), buffer.writableBytes(), Quiche.writerMemoryAddress(finBuffer)); + if (Quiche.throwIfError(recvLen)) { + return StreamRecvResult.DONE; + } + + buffer.writerIndex(writerIndex + recvLen); + return finBuffer.getBoolean(0) ? StreamRecvResult.FIN : StreamRecvResult.OK; + } + + /** + * Receive some data on a QUIC connection. + */ + void recv(InetSocketAddress sender, InetSocketAddress recipient, ByteBuf buffer) { + ((QuicChannelUnsafe) unsafe()).connectionRecv(sender, recipient, buffer); + } + + /** + * Return all source connection ids that are retired and so should be removed to map to the channel. + * + * @return retired ids. + */ + List retiredSourceConnectionId() { + QuicheQuicConnection connection = this.connection; + if (connection == null || connection.isClosed()) { + return Collections.emptyList(); + } + long connAddr = connection.address(); + assert connAddr != -1; + List retiredSourceIds = null; + for (;;) { + byte[] retired = Quiche.quiche_conn_retired_scid_next(connAddr); + if (retired == null) { + break; + } + if (retiredSourceIds == null) { + retiredSourceIds = new ArrayList<>(); + } + ByteBuffer retiredId = ByteBuffer.wrap(retired); + retiredSourceIds.add(retiredId); + sourceConnectionIds.remove(retiredId); + } + if (retiredSourceIds == null) { + return Collections.emptyList(); + } + return retiredSourceIds; + } + + List newSourceConnectionIds( + QuicConnectionIdGenerator connectionIdGenerator, QuicResetTokenGenerator resetTokenGenerator) { + if (server) { + QuicheQuicConnection connection = this.connection; + if (connection == null || connection.isClosed()) { + return Collections.emptyList(); + } + long connAddr = connection.address(); + // Generate all extra source ids that we can provide. This will cause frames that need to be send. Which + // is the reason why we might need to call connectionSendAndFlush(). + int left = Quiche.quiche_conn_scids_left(connAddr); + if (left > 0) { + QuicConnectionAddress sourceAddr = connection.sourceId(); + if (sourceAddr == null) { + return Collections.emptyList(); + } + List generatedIds = new ArrayList<>(left); + boolean sendAndFlush = false; + ByteBuffer key = sourceAddr.connId.duplicate(); + ByteBuf connIdBuffer = alloc().directBuffer(key.remaining()); + + byte[] resetTokenArray = new byte[Quic.RESET_TOKEN_LEN]; + try { + do { + ByteBuffer srcId = connectionIdGenerator.newId(key, key.remaining()); + connIdBuffer.clear(); + connIdBuffer.writeBytes(srcId.duplicate()); + ByteBuffer resetToken = resetTokenGenerator.newResetToken(srcId.duplicate()); + resetToken.get(resetTokenArray); + long result = Quiche.quiche_conn_new_scid( + connAddr, Quiche.memoryAddress(connIdBuffer, 0, connIdBuffer.readableBytes()), + connIdBuffer.readableBytes(), resetTokenArray, false, -1); + if (result < 0) { + break; + } + sendAndFlush = true; + generatedIds.add(srcId); + sourceConnectionIds.add(srcId); + } while (--left > 0); + } finally { + connIdBuffer.release(); + } + + if (sendAndFlush) { + connectionSendAndFlush(); + } + return generatedIds; + } + } + return Collections.emptyList(); + } + + void writable() { + boolean written = connectionSend(); + handleWritableStreams(); + written |= connectionSend(); + + if (written) { + // The writability changed so lets flush as fast as possible. + forceFlushParent(); + } + } + + int streamCapacity(long streamId) { + if (connection.isClosed()) { + return 0; + } + return Quiche.quiche_conn_stream_capacity(connection.address(), streamId); + } + + private boolean handleWritableStreams() { + if (isConnDestroyed()) { + return false; + } + reantranceGuard |= IN_HANDLE_WRITABLE_STREAMS; + try { + long connAddr = connection.address(); + boolean mayNeedWrite = false; + + if (Quiche.quiche_conn_is_established(connAddr) || + Quiche.quiche_conn_is_in_early_data(connAddr)) { + long writableIterator = Quiche.quiche_conn_writable(connAddr); + + try { + // For streams we always process all streams when at least on read was requested. + for (;;) { + int writable = Quiche.quiche_stream_iter_next( + writableIterator, writableStreams); + for (int i = 0; i < writable; i++) { + long streamId = writableStreams[i]; + QuicheQuicStreamChannel streamChannel = streams.get(streamId); + if (streamChannel != null) { + int capacity = Quiche.quiche_conn_stream_capacity(connAddr, streamId); + if (capacity < 0) { + // Let's close the channel if quiche_conn_stream_capacity(...) returns an error. + streamChannel.forceClose(); + } else if (streamChannel.writable(capacity)) { + mayNeedWrite = true; + } + } + } + if (writable < writableStreams.length) { + // We did handle all writable streams. + break; + } + } + } finally { + Quiche.quiche_stream_iter_free(writableIterator); + } + } + return mayNeedWrite; + } finally { + reantranceGuard &= ~IN_HANDLE_WRITABLE_STREAMS; + } + } + + /** + * Called once we receive a channelReadComplete event. This method will take care of calling + * {@link ChannelPipeline#fireChannelReadComplete()} if needed and also to handle pending flushes of + * writable {@link QuicheQuicStreamChannel}s. + */ + void recvComplete() { + try { + if (isConnDestroyed()) { + // Ensure we flush all pending writes. + forceFlushParent(); + return; + } + fireChannelReadCompleteIfNeeded(); + + // If we had called recv we need to ensure we call send as well. + // See https://docs.rs/quiche/0.6.0/quiche/struct.Connection.html#method.send + connectionSend(); + + // We are done with the read loop, flush all pending writes now. + forceFlushParent(); + } finally { + inFireChannelReadCompleteQueue = false; + } + } + + private void fireChannelReadCompleteIfNeeded() { + if (fireChannelReadCompletePending) { + fireChannelReadCompletePending = false; + pipeline().fireChannelReadComplete(); + } + } + + private boolean isConnDestroyed() { + return connection == null; + } + + private void fireExceptionEvents(Throwable cause) { + if (cause instanceof SSLHandshakeException) { + notifyAboutHandshakeCompletionIfNeeded((SSLHandshakeException) cause); + } + pipeline().fireExceptionCaught(cause); + } + + private boolean runTasksDirectly() { + return sslTaskExecutor == null || sslTaskExecutor == ImmediateExecutor.INSTANCE || + sslTaskExecutor == ImmediateEventExecutor.INSTANCE; + } + + private void runAllTaskSend(Runnable task) { + sslTaskExecutor.execute(decorateTaskSend(task)); + } + + private void runAll(Runnable task) { + do { + task.run(); + } while ((task = connection.sslTask()) != null); + } + + private Runnable decorateTaskSend(Runnable task) { + return () -> { + try { + runAll(task); + } finally { + // Move back to the EventLoop. + eventLoop().execute(() -> { + // Call connection send to continue handshake if needed. + if (connectionSend()) { + forceFlushParent(); + } + }); + } + }; + } + + private boolean connectionSendSegments(SegmentedDatagramPacketAllocator segmentedDatagramPacketAllocator) { + List bufferList = new ArrayList<>(segmentedDatagramPacketAllocator.maxNumSegments()); + long connAddr = connection.address(); + int maxDatagramSize = Quiche.quiche_conn_max_send_udp_payload_size(connAddr); + boolean packetWasWritten = false; + boolean close = false; + try { + for (;;) { + int len = calculateSendBufferLength(connAddr, maxDatagramSize); + ByteBuf out = alloc().directBuffer(len); + + ByteBuffer sendInfo = connection.nextSendInfo(); + InetSocketAddress sendToAddress = this.remote; + + boolean done; + int writerIndex = out.writerIndex(); + int written = Quiche.quiche_conn_send( + connAddr, Quiche.writerMemoryAddress(out), out.writableBytes(), + Quiche.memoryAddressWithPosition(sendInfo)); + if (written == 0) { + out.release(); + // No need to create a new datagram packet. Just try again. + continue; + } + + try { + done = Quiche.throwIfError(written); + } catch (Exception e) { + done = true; + close = Quiche.shouldClose(written); + if (!tryFailConnectPromise(e)) { + // Only fire through the pipeline if this does not fail the connect promise. + fireExceptionEvents(e); + } + } + int size = bufferList.size(); + if (done) { + // We are done, release the buffer and send what we did build up so far. + out.release(); + + switch (size) { + case 0: + // Nothing more to write. + break; + case 1: + // We can write a normal datagram packet. + parent().write(new DatagramPacket(bufferList.get(0), sendToAddress)); + packetWasWritten = true; + break; + default: + int segmentSize = segmentSize(bufferList); + ByteBuf compositeBuffer = Unpooled.wrappedBuffer(bufferList.toArray(new ByteBuf[0])); + // We had more than one buffer, create a segmented packet. + parent().write(segmentedDatagramPacketAllocator.newPacket( + compositeBuffer, segmentSize, sendToAddress)); + packetWasWritten = true; + break; + } + bufferList.clear(); + return packetWasWritten; + } + out.writerIndex(writerIndex + written); + + int segmentSize = -1; + if (connection.isSendInfoChanged()) { + // Change the cached address and let the user know there was a connection migration. + InetSocketAddress oldRemote = remote; + remote = QuicheSendInfo.getToAddress(sendInfo); + local = QuicheSendInfo.getFromAddress(sendInfo); + + if (size > 0) { + // We have something in the out list already, we need to send this now and so we set the + // segmentSize. + segmentSize = segmentSize(bufferList); + } + } else if (size > 0) { + int lastReadable = segmentSize(bufferList); + // Check if we either need to send now because the last buffer we added has a smaller size then this + // one or if we reached the maximum number of segments that we can send. + if (lastReadable != out.readableBytes() || + size == segmentedDatagramPacketAllocator.maxNumSegments()) { + segmentSize = lastReadable; + } + } + + // If the segmentSize is not -1 we know we need to send now what was in the out list. + if (segmentSize != -1) { + final boolean stop; + if (size == 1) { + // Only one buffer in the out list, there is no need to use segments. + stop = writePacket(new DatagramPacket( + bufferList.get(0), sendToAddress), maxDatagramSize, len); + } else { + // Create a packet with segments in. + ByteBuf compositeBuffer = Unpooled.wrappedBuffer(bufferList.toArray(new ByteBuf[0])); + stop = writePacket(segmentedDatagramPacketAllocator.newPacket( + compositeBuffer, segmentSize, sendToAddress), maxDatagramSize, len); + } + bufferList.clear(); + packetWasWritten = true; + + if (stop) { + // Nothing left in the window, continue later. That said we still need to also + // write the previous filled out buffer as otherwise we would either leak or need + // to drop it and so produce some loss. + if (out.isReadable()) { + parent().write(new DatagramPacket(out, sendToAddress)); + } else { + out.release(); + } + return true; + } + } + // Let's add a touch with the bufferList as a hint. This will help us to debug leaks if there + // are any. + out.touch(bufferList); + // store for later, so we can make use of segments. + bufferList.add(out); + } + } finally { + if (close) { + // Close now... now way to recover. + unsafe().close(newPromise()); + } + } + } + + private static int segmentSize(List bufferList) { + assert !bufferList.isEmpty(); + int size = bufferList.size(); + return bufferList.get(size - 1).readableBytes(); + } + + private boolean connectionSendSimple() { + long connAddr = connection.address(); + boolean packetWasWritten = false; + boolean close = false; + int maxDatagramSize = Quiche.quiche_conn_max_send_udp_payload_size(connAddr); + for (;;) { + ByteBuffer sendInfo = connection.nextSendInfo(); + + int len = calculateSendBufferLength(connAddr, maxDatagramSize); + ByteBuf out = alloc().directBuffer(len); + int writerIndex = out.writerIndex(); + + int written = Quiche.quiche_conn_send( + connAddr, Quiche.writerMemoryAddress(out), out.writableBytes(), + Quiche.memoryAddressWithPosition(sendInfo)); + + try { + if (Quiche.throwIfError(written)) { + out.release(); + break; + } + } catch (Exception e) { + close = Quiche.shouldClose(written); + out.release(); + if (!tryFailConnectPromise(e)) { + fireExceptionEvents(e); + } + break; + } + + if (written == 0) { + // No need to create a new datagram packet. Just release and try again. + out.release(); + continue; + } + if (connection.isSendInfoChanged()) { + // Change the cached address + remote = QuicheSendInfo.getToAddress(sendInfo); + local = QuicheSendInfo.getFromAddress(sendInfo); + } + out.writerIndex(writerIndex + written); + boolean stop = writePacket(new DatagramPacket(out, remote), maxDatagramSize, len); + packetWasWritten = true; + if (stop) { + // Nothing left in the window, continue later + break; + } + } + if (close) { + // Close now... now way to recover. + unsafe().close(newPromise()); + } + return packetWasWritten; + } + + private boolean writePacket(DatagramPacket packet, int maxDatagramSize, int len) { + ChannelFuture future = parent().write(packet); + if (isSendWindowUsed(maxDatagramSize, len)) { + // Nothing left in the window, continue later + future.addListener(continueSendingListener); + return true; + } + return false; + } + + private static boolean isSendWindowUsed(int maxDatagramSize, int len) { + return len < maxDatagramSize; + } + + private static int calculateSendBufferLength(long connAddr, int maxDatagramSize) { + int len = Math.min(maxDatagramSize, Quiche.quiche_conn_send_quantum(connAddr)); + if (len <= 0) { + // If there is no room left we just return some small number to reduce the risk of packet drop + // while still be able to attach the listener to the write future. + // We use the value of 8 because such an allocation will be cheap to serve from the + // PooledByteBufAllocator while still serve our need. + return 8; + } + return len; + } + + /** + * Write datagrams if needed and return {@code true} if something was written and we need to call + * {@link Channel#flush()} at some point. + */ + private boolean connectionSend() { + if (isConnDestroyed()) { + return false; + } + if ((reantranceGuard & IN_CONNECTION_SEND) != 0) { + // Let's notify about early data if needed. + notifyEarlyDataReadyIfNeeded(); + return false; + } + + reantranceGuard |= IN_CONNECTION_SEND; + try { + boolean packetWasWritten; + SegmentedDatagramPacketAllocator segmentedDatagramPacketAllocator = + config.getSegmentedDatagramPacketAllocator(); + if (segmentedDatagramPacketAllocator.maxNumSegments() > 0) { + packetWasWritten = connectionSendSegments(segmentedDatagramPacketAllocator); + } else { + packetWasWritten = connectionSendSimple(); + } + + // Process / schedule all tasks that were created. + Runnable task = connection.sslTask(); + if (task != null) { + if (runTasksDirectly()) { + // Consume all tasks + do { + task.run(); + // Notify about early data ready if needed. + notifyEarlyDataReadyIfNeeded(); + } while ((task = connection.sslTask()) != null); + + // Let's try again sending after we did process all tasks. + return packetWasWritten | connectionSend(); + } else { + runAllTaskSend(task); + } + } else { + // Notify about early data ready if needed. + notifyEarlyDataReadyIfNeeded(); + } + + if (packetWasWritten) { + timeoutHandler.scheduleTimeout(); + } + return packetWasWritten; + } finally { + reantranceGuard &= ~IN_CONNECTION_SEND; + } + } + + private final class QuicChannelUnsafe extends AbstractChannel.AbstractUnsafe { + + void connectStream(QuicStreamType type, ChannelHandler handler, + Promise promise) { + long streamId = idGenerator.nextStreamId(type == QuicStreamType.BIDIRECTIONAL); + try { + Quiche.throwIfError(streamSend0(streamId, Unpooled.EMPTY_BUFFER, false)); + } catch (Exception e) { + promise.setFailure(e); + return; + } + if (type == QuicStreamType.UNIDIRECTIONAL) { + UNI_STREAMS_LEFT_UPDATER.decrementAndGet(QuicheQuicChannel.this); + } else { + BIDI_STREAMS_LEFT_UPDATER.decrementAndGet(QuicheQuicChannel.this); + } + QuicheQuicStreamChannel streamChannel = addNewStreamChannel(streamId); + if (handler != null) { + streamChannel.pipeline().addLast(handler); + } + eventLoop().register(streamChannel).addListener((ChannelFuture f) -> { + if (f.isSuccess()) { + promise.setSuccess(streamChannel); + } else { + promise.setFailure(f.cause()); + streams.remove(streamId); + } + }); + } + + @Override + public void connect(SocketAddress remote, SocketAddress local, ChannelPromise channelPromise) { + assert eventLoop().inEventLoop(); + if (server) { + channelPromise.setFailure(new UnsupportedOperationException()); + return; + } + + if (connectPromise != null) { + channelPromise.setFailure(new ConnectionPendingException()); + return; + } + + if (remote instanceof QuicConnectionAddress) { + if (!sourceConnectionIds.isEmpty()) { + // If a key is assigned we know this channel was already connected. + channelPromise.setFailure(new AlreadyConnectedException()); + return; + } + + QuicConnectionAddress address = (QuicConnectionAddress) remote; + connectPromise = channelPromise; + connectAddress = address; + + // Schedule connect timeout. + int connectTimeoutMillis = config().getConnectTimeoutMillis(); + if (connectTimeoutMillis > 0) { + connectTimeoutFuture = eventLoop().schedule(() -> { + ChannelPromise connectPromise = QuicheQuicChannel.this.connectPromise; + if (connectPromise != null && !connectPromise.isDone() + && connectPromise.tryFailure(new ConnectTimeoutException( + "connection timed out: " + remote))) { + close(voidPromise()); + } + }, connectTimeoutMillis, TimeUnit.MILLISECONDS); + } + + connectPromise.addListener((ChannelFuture future) -> { + if (future.isCancelled()) { + if (connectTimeoutFuture != null) { + connectTimeoutFuture.cancel(false); + } + connectPromise = null; + close(voidPromise()); + } + }); + + parent().connect(new QuicheQuicChannelAddress(QuicheQuicChannel.this)); + return; + } + + channelPromise.setFailure(new UnsupportedOperationException()); + } + + private void fireConnectCloseEventIfNeeded(long connAddr) { + if (connectionCloseEvent == null) { + connectionCloseEvent = Quiche.quiche_conn_peer_error(connAddr); + if (connectionCloseEvent != null) { + pipeline().fireUserEventTriggered(connectionCloseEvent); + } + } + } + + void connectionRecv(InetSocketAddress sender, InetSocketAddress recipient, ByteBuf buffer) { + if (isConnDestroyed()) { + return; + } + int bufferReadable = buffer.readableBytes(); + if (bufferReadable == 0) { + // Nothing to do here. Just return... + // See also https://github.com/cloudflare/quiche/issues/817 + return; + } + + reantranceGuard |= IN_RECV; + boolean close = false; + try { + ByteBuf tmpBuffer = null; + // We need to make a copy if the buffer is read only as recv(...) may modify the input buffer as well. + // See https://docs.rs/quiche/0.6.0/quiche/struct.Connection.html#method.recv + if (buffer.isReadOnly()) { + tmpBuffer = alloc().directBuffer(buffer.readableBytes()); + tmpBuffer.writeBytes(buffer); + buffer = tmpBuffer; + } + long memoryAddress = Quiche.readerMemoryAddress(buffer); + + ByteBuffer recvInfo = connection.nextRecvInfo(); + QuicheRecvInfo.setRecvInfo(recvInfo, sender, recipient); + + SocketAddress oldRemote = remote; + + if (connection.isRecvInfoChanged()) { + // Update the cached address + remote = sender; + } + local = recipient; + + long connAddr = connection.address(); + try { + do { + // Call quiche_conn_recv(...) until we consumed all bytes or we did receive some error. + int res = Quiche.quiche_conn_recv(connAddr, memoryAddress, bufferReadable, + Quiche.memoryAddressWithPosition(recvInfo)); + boolean done; + try { + done = Quiche.throwIfError(res); + } catch (Exception e) { + done = true; + close = Quiche.shouldClose(res); + if (tryFailConnectPromise(e)) { + break; + } + fireExceptionEvents(e); + } + + // Process / schedule all tasks that were created. + Runnable task = connection.sslTask(); + if (task != null) { + if (runTasksDirectly()) { + // Consume all tasks + do { + task.run(); + } while ((task = connection.sslTask()) != null); + processReceived(connAddr); + } else { + runAllTaskRecv(task); + } + } else { + processReceived(connAddr); + } + + if (done) { + break; + } + memoryAddress += res; + bufferReadable -= res; + } while (bufferReadable > 0); + } finally { + buffer.skipBytes((int) (memoryAddress - Quiche.readerMemoryAddress(buffer))); + if (tmpBuffer != null) { + tmpBuffer.release(); + } + } + if (close) { + // Let's close now as there is no way to recover + unsafe().close(newPromise()); + } + } finally { + reantranceGuard &= ~IN_RECV; + } + } + + private void processReceived(long connAddr) { + // Handle pending channelActive if needed. + if (handlePendingChannelActive()) { + // Connection was closed right away. + return; + } + + notifyAboutHandshakeCompletionIfNeeded(null); + + fireConnectCloseEventIfNeeded(connAddr); + + if (Quiche.quiche_conn_is_established(connAddr) || + Quiche.quiche_conn_is_in_early_data(connAddr)) { + long uniLeftOld = uniStreamsLeft; + long bidiLeftOld = bidiStreamsLeft; + // Only fetch new stream info when we used all our credits + if (uniLeftOld == 0 || bidiLeftOld == 0) { + long uniLeft = Quiche.quiche_conn_peer_streams_left_uni(connAddr); + long bidiLeft = Quiche.quiche_conn_peer_streams_left_bidi(connAddr); + uniStreamsLeft = uniLeft; + bidiStreamsLeft = bidiLeft; + if (uniLeftOld != uniLeft || bidiLeftOld != bidiLeft) { + pipeline().fireUserEventTriggered(QuicStreamLimitChangedEvent.INSTANCE); + } + } + + handlePathEvents(connAddr); + + if (handleWritableStreams()) { + // Some data was produced, let's flush. + flushParent(); + } + + datagramReadable = true; + streamReadable = true; + + recvDatagram(); + recvStream(); + } + } + + private void handlePathEvents(long addr) { + long event; + while ((event = Quiche.quiche_conn_path_event_next(addr)) > 0) { + try { + int type = Quiche.quiche_path_event_type(event); + + if (type == Quiche.QUICHE_PATH_EVENT_NEW) { + Object[] ret = Quiche.quiche_path_event_new(event); + InetSocketAddress local = (InetSocketAddress) ret[0]; + InetSocketAddress peer = (InetSocketAddress) ret[1]; + pipeline().fireUserEventTriggered(new QuicPathEvent.New(local, peer)); + } else if (type == Quiche.QUICHE_PATH_EVENT_VALIDATED) { + Object[] ret = Quiche.quiche_path_event_validated(event); + InetSocketAddress local = (InetSocketAddress) ret[0]; + InetSocketAddress peer = (InetSocketAddress) ret[1]; + pipeline().fireUserEventTriggered(new QuicPathEvent.Validated(local, peer)); + } else if (type == Quiche.QUICHE_PATH_EVENT_FAILED_VALIDATION) { + Object[] ret = Quiche.quiche_path_event_failed_validation(event); + InetSocketAddress local = (InetSocketAddress) ret[0]; + InetSocketAddress peer = (InetSocketAddress) ret[1]; + pipeline().fireUserEventTriggered(new QuicPathEvent.FailedValidation(local, peer)); + } else if (type == Quiche.QUICHE_PATH_EVENT_CLOSED) { + Object[] ret = Quiche.quiche_path_event_closed(event); + InetSocketAddress local = (InetSocketAddress) ret[0]; + InetSocketAddress peer = (InetSocketAddress) ret[1]; + pipeline().fireUserEventTriggered(new QuicPathEvent.Closed(local, peer)); + } else if (type == Quiche.QUICHE_PATH_EVENT_REUSED_SOURCE_CONNECTION_ID) { + Object[] ret = Quiche.quiche_path_event_reused_source_connection_id(event); + Long seq = (Long) ret[0]; + InetSocketAddress localOld = (InetSocketAddress) ret[1]; + InetSocketAddress peerOld = (InetSocketAddress) ret[2]; + InetSocketAddress local = (InetSocketAddress) ret[3]; + InetSocketAddress peer = (InetSocketAddress) ret[4]; + pipeline().fireUserEventTriggered( + new QuicPathEvent.ReusedSourceConnectionId(seq, localOld, peerOld, local, peer)); + } else if (type == Quiche.QUICHE_PATH_EVENT_PEER_MIGRATED) { + Object[] ret = Quiche.quiche_path_event_peer_migrated(event); + InetSocketAddress local = (InetSocketAddress) ret[0]; + InetSocketAddress peer = (InetSocketAddress) ret[1]; + pipeline().fireUserEventTriggered(new QuicPathEvent.PeerMigrated(local, peer)); + } + } finally { + Quiche.quiche_path_event_free(event); + } + } + } + + private void runAllTaskRecv(Runnable task) { + sslTaskExecutor.execute(decorateTaskRecv(task)); + } + + private Runnable decorateTaskRecv(Runnable task) { + return () -> { + try { + runAll(task); + } finally { + // Move back to the EventLoop. + eventLoop().execute(() -> { + if (connection != null) { + processReceived(connection.address()); + + // Call connection send to continue handshake if needed. + if (connectionSend()) { + forceFlushParent(); + } + } + }); + } + }; + } + void recv() { + if ((reantranceGuard & IN_RECV) != 0 || isConnDestroyed()) { + return; + } + + long connAddr = connection.address(); + // Check if we can read anything yet. + if (!Quiche.quiche_conn_is_established(connAddr) && + !Quiche.quiche_conn_is_in_early_data(connAddr)) { + return; + } + + reantranceGuard |= IN_RECV; + try { + recvDatagram(); + recvStream(); + } finally { + fireChannelReadCompleteIfNeeded(); + reantranceGuard &= ~IN_RECV; + } + } + + private void recvStream() { + long connAddr = connection.address(); + long readableIterator = Quiche.quiche_conn_readable(connAddr); + if (readableIterator != -1) { + try { + // For streams we always process all streams when at least on read was requested. + if (recvStreamPending && streamReadable) { + for (;;) { + int readable = Quiche.quiche_stream_iter_next( + readableIterator, readableStreams); + for (int i = 0; i < readable; i++) { + long streamId = readableStreams[i]; + QuicheQuicStreamChannel streamChannel = streams.get(streamId); + if (streamChannel == null) { + recvStreamPending = false; + fireChannelReadCompletePending = true; + streamChannel = addNewStreamChannel(streamId); + streamChannel.readable(); + pipeline().fireChannelRead(streamChannel); + } else { + streamChannel.readable(); + } + } + if (readable < readableStreams.length) { + // We did consume all readable streams. + streamReadable = false; + break; + } + } + } + } finally { + Quiche.quiche_stream_iter_free(readableIterator); + } + } + } + + private void recvDatagram() { + if (!supportsDatagram) { + return; + } + long connAddr = connection.address(); + while (recvDatagramPending && datagramReadable) { + @SuppressWarnings("deprecation") + RecvByteBufAllocator.Handle recvHandle = recvBufAllocHandle(); + recvHandle.reset(config()); + + int numMessagesRead = 0; + do { + int len = Quiche.quiche_conn_dgram_recv_front_len(connAddr); + if (len == Quiche.QUICHE_ERR_DONE) { + datagramReadable = false; + return; + } + + ByteBuf datagramBuffer = alloc().directBuffer(len); + recvHandle.attemptedBytesRead(datagramBuffer.writableBytes()); + int writerIndex = datagramBuffer.writerIndex(); + long memoryAddress = Quiche.writerMemoryAddress(datagramBuffer); + + int written = Quiche.quiche_conn_dgram_recv(connAddr, + memoryAddress, datagramBuffer.writableBytes()); + try { + if (Quiche.throwIfError(written)) { + datagramBuffer.release(); + // We did consume all datagram packets. + datagramReadable = false; + break; + } + } catch (Exception e) { + datagramBuffer.release(); + pipeline().fireExceptionCaught(e); + } + recvHandle.lastBytesRead(written); + recvHandle.incMessagesRead(1); + numMessagesRead++; + datagramBuffer.writerIndex(writerIndex + written); + recvDatagramPending = false; + fireChannelReadCompletePending = true; + + pipeline().fireChannelRead(datagramBuffer); + } while (recvHandle.continueReading()); + recvHandle.readComplete(); + + // Check if we produced any messages. + if (numMessagesRead > 0) { + fireChannelReadCompleteIfNeeded(); + } + } + } + + private boolean handlePendingChannelActive() { + long connAddr = connection.address(); + if (server) { + if (state == OPEN && Quiche.quiche_conn_is_established(connAddr)) { + // We didn't notify before about channelActive... Update state and fire the event. + state = ACTIVE; + + pipeline().fireChannelActive(); + notifyAboutHandshakeCompletionIfNeeded(null); + fireDatagramExtensionEvent(); + } + } else if (connectPromise != null && Quiche.quiche_conn_is_established(connAddr)) { + ChannelPromise promise = connectPromise; + connectPromise = null; + state = ACTIVE; + + boolean promiseSet = promise.trySuccess(); + pipeline().fireChannelActive(); + notifyAboutHandshakeCompletionIfNeeded(null); + fireDatagramExtensionEvent(); + if (!promiseSet) { + fireConnectCloseEventIfNeeded(connAddr); + this.close(this.voidPromise()); + return true; + } + } + return false; + } + + private void fireDatagramExtensionEvent() { + long connAddr = connection.address(); + int len = Quiche.quiche_conn_dgram_max_writable_len(connAddr); + // QUICHE_ERR_DONE means the remote peer does not support the extension. + if (len != Quiche.QUICHE_ERR_DONE) { + pipeline().fireUserEventTriggered(new QuicDatagramExtensionEvent(len)); + } + } + + private QuicheQuicStreamChannel addNewStreamChannel(long streamId) { + QuicheQuicStreamChannel streamChannel = new QuicheQuicStreamChannel( + QuicheQuicChannel.this, streamId); + QuicheQuicStreamChannel old = streams.put(streamId, streamChannel); + assert old == null; + streamChannel.writable(streamCapacity(streamId)); + return streamChannel; + } + } + + /** + * Finish the connect of a client channel. + */ + void finishConnect() { + assert !server; + if (connectionSend()) { + flushParent(); + } + } + + private void notifyEarlyDataReadyIfNeeded() { + if (!server && !earlyDataReadyNotified && + !isConnDestroyed() && Quiche.quiche_conn_is_in_early_data(connection.address())) { + earlyDataReadyNotified = true; + pipeline().fireUserEventTriggered(SslEarlyDataReadyEvent.INSTANCE); + } + } + + // TODO: Come up with something better. + static QuicheQuicChannel handleConnect(Function sslEngineProvider, + Executor sslTaskExecutor, + SocketAddress address, long config, int localConnIdLength, + boolean supportsDatagram, ByteBuffer fromSockaddrMemory, + ByteBuffer toSockaddrMemory) throws Exception { + if (address instanceof QuicheQuicChannel.QuicheQuicChannelAddress) { + QuicheQuicChannel.QuicheQuicChannelAddress addr = (QuicheQuicChannel.QuicheQuicChannelAddress) address; + QuicheQuicChannel channel = addr.channel; + channel.connect(sslEngineProvider, sslTaskExecutor, config, localConnIdLength, supportsDatagram, + fromSockaddrMemory, toSockaddrMemory); + return channel; + } + return null; + } + + /** + * Just a container to pass the {@link QuicheQuicChannel} to {@link QuicheQuicClientCodec}. + */ + private static final class QuicheQuicChannelAddress extends SocketAddress { + + final QuicheQuicChannel channel; + + QuicheQuicChannelAddress(QuicheQuicChannel channel) { + this.channel = channel; + } + } + + private final class TimeoutHandler implements Runnable { + private ScheduledFuture timeoutFuture; + private final Consumer timeoutTask; + + TimeoutHandler(Consumer timeoutTask) { + this.timeoutTask = timeoutTask; + } + + @Override + public void run() { + if (!isConnDestroyed()) { + long connAddr = connection.address(); + timeoutFuture = null; + // Notify quiche there was a timeout. + Quiche.quiche_conn_on_timeout(connAddr); + + if (Quiche.quiche_conn_is_closed(connAddr)) { + forceClose(); + if (timeoutTask != null){ + timeoutTask.accept(QuicheQuicChannel.this); + } + } else { + // We need to call connectionSend when a timeout was triggered. + // See https://docs.rs/quiche/0.6.0/quiche/struct.Connection.html#method.send. + boolean send = connectionSend(); + if (send) { + flushParent(); + } + if (!closeAllIfConnectionClosed()) { + // The connection is alive, reschedule. + scheduleTimeout(); + } + } + } + } + + // Schedule timeout. + // See https://docs.rs/quiche/0.6.0/quiche/#generating-outgoing-packets + void scheduleTimeout() { + if (isConnDestroyed()) { + cancel(); + return; + } + long nanos = Quiche.quiche_conn_timeout_as_nanos(connection.address()); + if (timeoutFuture == null) { + timeoutFuture = eventLoop().schedule(this, + nanos, TimeUnit.NANOSECONDS); + } else { + long remaining = timeoutFuture.getDelay(TimeUnit.NANOSECONDS); + if (remaining <= 0) { + // This means the timer already elapsed. In this case just cancel the future and call run() + // directly. This will ensure we correctly call quiche_conn_on_timeout() etc. + cancel(); + run(); + } else if (remaining > nanos) { + // The new timeout is smaller then what was scheduled before. Let's cancel the old timeout + // and schedule a new one. + cancel(); + timeoutFuture = eventLoop().schedule(this, nanos, TimeUnit.NANOSECONDS); + } + } + } + + void cancel() { + if (timeoutFuture != null) { + timeoutFuture.cancel(false); + timeoutFuture = null; + } + } + } + + @Override + public Future collectStats(Promise promise) { + if (eventLoop().inEventLoop()) { + collectStats0(promise); + } else { + eventLoop().execute(() -> collectStats0(promise)); + } + return promise; + } + + private void collectStats0(Promise promise) { + if (isConnDestroyed()) { + promise.setSuccess(statsAtClose); + return; + } + + collectStats0(connection, promise); + } + + private QuicConnectionStats collectStats0(QuicheQuicConnection connection, Promise promise) { + final long[] stats = Quiche.quiche_conn_stats(connection.address()); + if (stats == null) { + promise.setFailure(new IllegalStateException("native quiche_conn_stats(...) failed")); + return null; + } + + final QuicheQuicConnectionStats connStats = + new QuicheQuicConnectionStats(stats); + promise.setSuccess(connStats); + return connStats; + } + + @Override + public QuicTransportParameters peerTransportParameters() { + return connection.peerParameters(); + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicChannelConfig.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicChannelConfig.java new file mode 100644 index 0000000..67988c1 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicChannelConfig.java @@ -0,0 +1,158 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.Channel; +import io.netty.channel.ChannelOption; +import io.netty.channel.DefaultChannelConfig; +import io.netty.channel.MessageSizeEstimator; +import io.netty.channel.RecvByteBufAllocator; +import io.netty.channel.WriteBufferWaterMark; + +import java.util.Map; + +/** + * Default {@link QuicChannelConfig} implementation. + */ +final class QuicheQuicChannelConfig extends DefaultChannelConfig implements QuicChannelConfig { + + private volatile QLogConfiguration qLogConfiguration; + private volatile SegmentedDatagramPacketAllocator segmentedDatagramPacketAllocator = + SegmentedDatagramPacketAllocator.NONE; + + QuicheQuicChannelConfig(Channel channel) { + super(channel); + } + + @Override + public Map, Object> getOptions() { + return getOptions(super.getOptions(), + QuicChannelOption.QLOG, QuicChannelOption.SEGMENTED_DATAGRAM_PACKET_ALLOCATOR); + } + + @SuppressWarnings("unchecked") + @Override + public T getOption(ChannelOption option) { + if (option == QuicChannelOption.QLOG) { + return (T) getQLogConfiguration(); + } + if (option == QuicChannelOption.SEGMENTED_DATAGRAM_PACKET_ALLOCATOR) { + return (T) getSegmentedDatagramPacketAllocator(); + } + return super.getOption(option); + } + + @Override + public boolean setOption(ChannelOption option, T value) { + if (option == QuicChannelOption.QLOG) { + setQLogConfiguration((QLogConfiguration) value); + return true; + } + if (option == QuicChannelOption.SEGMENTED_DATAGRAM_PACKET_ALLOCATOR) { + setSegmentedDatagramPacketAllocator((SegmentedDatagramPacketAllocator) value); + return true; + } + return super.setOption(option, value); + } + + @Override + public QuicChannelConfig setConnectTimeoutMillis(int connectTimeoutMillis) { + super.setConnectTimeoutMillis(connectTimeoutMillis); + return this; + } + + @Override + @Deprecated + public QuicChannelConfig setMaxMessagesPerRead(int maxMessagesPerRead) { + super.setMaxMessagesPerRead(maxMessagesPerRead); + return this; + } + + @Override + public QuicChannelConfig setWriteSpinCount(int writeSpinCount) { + super.setWriteSpinCount(writeSpinCount); + return this; + } + + @Override + public QuicChannelConfig setAllocator(ByteBufAllocator allocator) { + super.setAllocator(allocator); + return this; + } + + @Override + public QuicChannelConfig setRecvByteBufAllocator(RecvByteBufAllocator allocator) { + super.setRecvByteBufAllocator(allocator); + return this; + } + + @Override + public QuicChannelConfig setAutoRead(boolean autoRead) { + super.setAutoRead(autoRead); + return this; + } + + @Override + public QuicChannelConfig setAutoClose(boolean autoClose) { + super.setAutoClose(autoClose); + return this; + } + + @Override + public QuicChannelConfig setWriteBufferHighWaterMark(int writeBufferHighWaterMark) { + super.setWriteBufferHighWaterMark(writeBufferHighWaterMark); + return this; + } + + @Override + public QuicChannelConfig setWriteBufferLowWaterMark(int writeBufferLowWaterMark) { + super.setWriteBufferLowWaterMark(writeBufferLowWaterMark); + return this; + } + + @Override + public QuicChannelConfig setWriteBufferWaterMark(WriteBufferWaterMark writeBufferWaterMark) { + super.setWriteBufferWaterMark(writeBufferWaterMark); + return this; + } + + @Override + public QuicChannelConfig setMessageSizeEstimator(MessageSizeEstimator estimator) { + super.setMessageSizeEstimator(estimator); + return this; + } + + QLogConfiguration getQLogConfiguration() { + return qLogConfiguration; + } + + private void setQLogConfiguration(QLogConfiguration qLogConfiguration) { + if (channel.isRegistered()) { + throw new IllegalStateException("QLOG can only be enabled before the Channel was registered"); + } + this.qLogConfiguration = qLogConfiguration; + } + + SegmentedDatagramPacketAllocator getSegmentedDatagramPacketAllocator() { + return segmentedDatagramPacketAllocator; + } + + private void setSegmentedDatagramPacketAllocator( + SegmentedDatagramPacketAllocator segmentedDatagramPacketAllocator) { + this.segmentedDatagramPacketAllocator = segmentedDatagramPacketAllocator; + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicClientCodec.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicClientCodec.java new file mode 100644 index 0000000..939266c --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicClientCodec.java @@ -0,0 +1,76 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.concurrent.Executor; +import java.util.function.Function; + +/** + * {@link QuicheQuicCodec} for QUIC clients. + */ +final class QuicheQuicClientCodec extends QuicheQuicCodec { + + private final Function sslEngineProvider; + private final Executor sslTaskExecutor; + + QuicheQuicClientCodec(QuicheConfig config, Function sslEngineProvider, + Executor sslTaskExecutor, int localConnIdLength, FlushStrategy flushStrategy) { + // Let's just use Quic.MAX_DATAGRAM_SIZE as the maximum size for a token on the client side. This should be + // safe enough and as we not have too many codecs at the same time this should be ok. + super(config, localConnIdLength, Quic.MAX_DATAGRAM_SIZE, flushStrategy); + this.sslEngineProvider = sslEngineProvider; + this.sslTaskExecutor = sslTaskExecutor; + } + + @Override + protected QuicheQuicChannel quicPacketRead( + ChannelHandlerContext ctx, InetSocketAddress sender, InetSocketAddress recipient, + QuicPacketType type, int version, ByteBuf scid, ByteBuf dcid, + ByteBuf token) { + ByteBuffer key = dcid.internalNioBuffer(dcid.readerIndex(), dcid.readableBytes()); + return getChannel(key); + } + + @Override + public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, + SocketAddress localAddress, ChannelPromise promise) { + final QuicheQuicChannel channel; + try { + channel = QuicheQuicChannel.handleConnect(sslEngineProvider, sslTaskExecutor, remoteAddress, config.nativeAddress(), + localConnIdLength, config.isDatagramSupported(), + senderSockaddrMemory.internalNioBuffer(0, senderSockaddrMemory.capacity()), + recipientSockaddrMemory.internalNioBuffer(0, recipientSockaddrMemory.capacity())); + } catch (Exception e) { + promise.setFailure(e); + return; + } + if (channel != null) { + addChannel(channel); + + channel.finishConnect(); + promise.setSuccess(); + return; + } + ctx.connect(remoteAddress, localAddress, promise); + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicCodec.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicCodec.java new file mode 100644 index 0000000..478f9f3 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicCodec.java @@ -0,0 +1,315 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.channel.MessageSizeEstimator; +import io.netty.channel.socket.DatagramPacket; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; + +import static io.netty.handler.codec.quic.Quiche.allocateNativeOrder; + +/** + * Abstract base class for QUIC codecs. + */ +abstract class QuicheQuicCodec extends ChannelDuplexHandler { + private static final InternalLogger LOGGER = InternalLoggerFactory.getInstance(QuicheQuicCodec.class); + + private final Map connectionIdToChannel = new HashMap<>(); + private final Set channels = new HashSet<>(); + private final Queue needsFireChannelReadComplete = new ArrayDeque<>(); + private final int maxTokenLength; + private final FlushStrategy flushStrategy; + + private MessageSizeEstimator.Handle estimatorHandle; + private QuicHeaderParser headerParser; + private QuicHeaderParser.QuicHeaderProcessor parserCallback; + private int pendingBytes; + private int pendingPackets; + private boolean inChannelReadComplete; + + protected final QuicheConfig config; + protected final int localConnIdLength; + // This buffer is used to copy InetSocketAddress to sockaddr_storage and so pass it down the JNI layer. + protected ByteBuf senderSockaddrMemory; + protected ByteBuf recipientSockaddrMemory; + + QuicheQuicCodec(QuicheConfig config, int localConnIdLength, int maxTokenLength, FlushStrategy flushStrategy) { + this.config = config; + this.localConnIdLength = localConnIdLength; + this.maxTokenLength = maxTokenLength; + this.flushStrategy = flushStrategy; + } + + protected final QuicheQuicChannel getChannel(ByteBuffer key) { + return connectionIdToChannel.get(key); + } + + protected final void addMapping(ByteBuffer key, QuicheQuicChannel channel) { + connectionIdToChannel.put(key, channel); + } + + protected final void removeMapping(ByteBuffer key) { + connectionIdToChannel.remove(key); + } + + protected final void removeChannel(QuicheQuicChannel channel) { + boolean removed = channels.remove(channel); + assert removed; + for (ByteBuffer id : channel.sourceConnectionIds()) { + connectionIdToChannel.remove(id); + } + } + + protected final void addChannel(QuicheQuicChannel channel) { + boolean added = channels.add(channel); + assert added; + for (ByteBuffer id : channel.sourceConnectionIds()) { + connectionIdToChannel.put(id, channel); + } + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) { + senderSockaddrMemory = allocateNativeOrder(Quiche.SIZEOF_SOCKADDR_STORAGE); + recipientSockaddrMemory = allocateNativeOrder(Quiche.SIZEOF_SOCKADDR_STORAGE); + headerParser = new QuicHeaderParser(maxTokenLength, localConnIdLength); + parserCallback = new QuicCodecHeaderProcessor(ctx); + estimatorHandle = ctx.channel().config().getMessageSizeEstimator().newHandle(); + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) { + try { + // Use a copy of the array as closing the channel may cause an unwritable event that could also + // remove channels. + for (QuicheQuicChannel ch : channels.toArray(new QuicheQuicChannel[0])) { + ch.forceClose(); + } + channels.clear(); + connectionIdToChannel.clear(); + + needsFireChannelReadComplete.clear(); + if (pendingPackets > 0) { + flushNow(ctx); + } + } finally { + config.free(); + if (senderSockaddrMemory != null) { + senderSockaddrMemory.release(); + } + if (recipientSockaddrMemory != null) { + recipientSockaddrMemory.release(); + } + if (headerParser != null) { + headerParser.close(); + headerParser = null; + } + } + } + + @Override + public final void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + DatagramPacket packet = (DatagramPacket) msg; + try { + ByteBuf buffer = ((DatagramPacket) msg).content(); + if (!buffer.isDirect()) { + // We need a direct buffer as otherwise we can not access the memoryAddress. + // Let's do a copy to direct memory. + ByteBuf direct = ctx.alloc().directBuffer(buffer.readableBytes()); + try { + direct.writeBytes(buffer, buffer.readerIndex(), buffer.readableBytes()); + handleQuicPacket(packet.sender(), packet.recipient(), direct); + } finally { + direct.release(); + } + } else { + handleQuicPacket(packet.sender(), packet.recipient(), buffer); + } + } finally { + packet.release(); + } + } + + private void handleQuicPacket(InetSocketAddress sender, InetSocketAddress recipient, ByteBuf buffer) { + try { + headerParser.parse(sender, recipient, buffer, parserCallback); + } catch (Exception e) { + LOGGER.debug("Error while processing QUIC packet", e); + } + } + + /** + * Handle a QUIC packet and return {@code true} if we need to call {@link ChannelHandlerContext#flush()}. + * + * @param ctx the {@link ChannelHandlerContext}. + * @param sender the {@link InetSocketAddress} of the sender of the QUIC packet + * @param recipient the {@link InetSocketAddress} of the recipient of the QUIC packet + * @param type the type of the packet. + * @param version the QUIC version + * @param scid the source connection id. + * @param dcid the destination connection id + * @param token the token + * @return {@code true} if we need to call {@link ChannelHandlerContext#flush()} before there is no new events + * for this handler in the current eventloop run. + * @throws Exception thrown if there is an error during processing. + */ + protected abstract QuicheQuicChannel quicPacketRead(ChannelHandlerContext ctx, InetSocketAddress sender, + InetSocketAddress recipient, QuicPacketType type, int version, + ByteBuf scid, ByteBuf dcid, ByteBuf token) throws Exception; + + @Override + public final void channelReadComplete(ChannelHandlerContext ctx) { + inChannelReadComplete = true; + try { + for (;;) { + QuicheQuicChannel channel = needsFireChannelReadComplete.poll(); + if (channel == null) { + break; + } + channel.recvComplete(); + if (channel.freeIfClosed()) { + removeChannel(channel); + } + } + } finally { + inChannelReadComplete = false; + if (pendingPackets > 0) { + flushNow(ctx); + } + } + } + + @Override + public final void channelWritabilityChanged(ChannelHandlerContext ctx) { + if (ctx.channel().isWritable()) { + List closed = null; + for (QuicheQuicChannel channel : channels) { + // TODO: Be a bit smarter about this. + channel.writable(); + if (channel.freeIfClosed()) { + if (closed == null) { + closed = new ArrayList<>(); + } + closed.add(channel); + } + } + if (closed != null) { + for (QuicheQuicChannel ch: closed) { + removeChannel(ch); + } + } + } else { + // As we batch flushes we need to ensure we at least try to flush a batch once the channel becomes + // unwritable. Otherwise we may end up with buffering too much writes and so waste memory. + ctx.flush(); + } + + ctx.fireChannelWritabilityChanged(); + } + + @Override + public final void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { + pendingPackets ++; + int size = estimatorHandle.size(msg); + if (size > 0) { + pendingBytes += size; + } + try { + ctx.write(msg, promise); + } finally { + flushIfNeeded(ctx); + } + } + + @Override + public final void flush(ChannelHandlerContext ctx) { + // If we are in the channelReadComplete(...) method we might be able to delay the flush(...) until we finish + // processing all channels. + if (inChannelReadComplete) { + flushIfNeeded(ctx); + } else if (pendingPackets > 0) { + flushNow(ctx); + } + } + + private void flushIfNeeded(ChannelHandlerContext ctx) { + // Check if we should force a flush() and so ensure the packets are delivered in a timely + // manner and also make room in the outboundbuffer again that belongs to the underlying channel. + if (flushStrategy.shouldFlushNow(pendingPackets, pendingBytes)) { + flushNow(ctx); + } + } + + private void flushNow(ChannelHandlerContext ctx) { + pendingBytes = 0; + pendingPackets = 0; + ctx.flush(); + } + + private final class QuicCodecHeaderProcessor implements QuicHeaderParser.QuicHeaderProcessor { + + private final ChannelHandlerContext ctx; + + QuicCodecHeaderProcessor(ChannelHandlerContext ctx) { + this.ctx = ctx; + } + + @Override + public void process(InetSocketAddress sender, InetSocketAddress recipient, ByteBuf buffer, QuicPacketType type, + int version, ByteBuf scid, ByteBuf dcid, ByteBuf token) throws Exception { + QuicheQuicChannel channel = quicPacketRead(ctx, sender, recipient, + type, version, scid, + dcid, token); + if (channel != null) { + channelRecv(channel, sender, recipient, buffer); + } + } + } + + /** + * Called once something was received for a {@link QuicheQuicChannel}. + * + * @param channel the channel for which the data was received + * @param sender the sender + * @param recipient the recipient + * @param buffer the acutal data. + */ + protected void channelRecv(QuicheQuicChannel channel, InetSocketAddress sender, + InetSocketAddress recipient, ByteBuf buffer) { + // Add to queue first, we might be able to safe some flushes and consolidate them + // in channelReadComplete(...) this way. + if (channel.markInFireChannelReadCompleteQueue()) { + needsFireChannelReadComplete.add(channel); + } + channel.recv(sender, recipient, buffer); + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicConnection.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicConnection.java new file mode 100644 index 0000000..c561c7c --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicConnection.java @@ -0,0 +1,232 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.util.ReferenceCounted; +import io.netty.util.ResourceLeakDetector; +import io.netty.util.ResourceLeakDetectorFactory; +import io.netty.util.ResourceLeakTracker; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.function.Supplier; + +final class QuicheQuicConnection { + private static final int TOTAL_RECV_INFO_SIZE = Quiche.SIZEOF_QUICHE_RECV_INFO + + Quiche.SIZEOF_SOCKADDR_STORAGE + Quiche.SIZEOF_SOCKADDR_STORAGE; + private static final ResourceLeakDetector leakDetector = + ResourceLeakDetectorFactory.instance().newResourceLeakDetector(QuicheQuicConnection.class); + private final QuicheQuicSslEngine engine; + + private final ResourceLeakTracker leakTracker; + + final long ssl; + private ReferenceCounted refCnt; + + // This block of memory is used to store the following structs (in this order): + // - quiche_recv_info + // - sockaddr_storage + // - quiche_recv_info + // - sockaddr_storage + // - quiche_send_info + // - quiche_send_info + // + // We need to have every stored 2 times as we need to check if the last sockaddr has changed between + // quiche_conn_recv and quiche_conn_send calls. If this happens we know a QUIC connection migration did happen. + private final ByteBuf recvInfoBuffer; + private final ByteBuf sendInfoBuffer; + + private boolean recvInfoFirst = true; + private boolean sendInfoFirst = true; + private final ByteBuffer recvInfoBuffer1; + private final ByteBuffer recvInfoBuffer2; + private final ByteBuffer sendInfoBuffer1; + private final ByteBuffer sendInfoBuffer2; + + private long connection; + + QuicheQuicConnection(long connection, long ssl, QuicheQuicSslEngine engine, ReferenceCounted refCnt) { + this.connection = connection; + this.ssl = ssl; + this.engine = engine; + this.refCnt = refCnt; + // TODO: Maybe cache these per thread as we only use them temporary within a limited scope. + recvInfoBuffer = Quiche.allocateNativeOrder(2 * TOTAL_RECV_INFO_SIZE); + sendInfoBuffer = Quiche.allocateNativeOrder(2 * Quiche.SIZEOF_QUICHE_SEND_INFO); + + // Let's memset the memory. + recvInfoBuffer.setZero(0, recvInfoBuffer.capacity()); + sendInfoBuffer.setZero(0, sendInfoBuffer.capacity()); + + recvInfoBuffer1 = recvInfoBuffer.nioBuffer(0, TOTAL_RECV_INFO_SIZE); + recvInfoBuffer2 = recvInfoBuffer.nioBuffer(TOTAL_RECV_INFO_SIZE, TOTAL_RECV_INFO_SIZE); + + sendInfoBuffer1 = sendInfoBuffer.nioBuffer(0, Quiche.SIZEOF_QUICHE_SEND_INFO); + sendInfoBuffer2 = sendInfoBuffer.nioBuffer(Quiche.SIZEOF_QUICHE_SEND_INFO, Quiche.SIZEOF_QUICHE_SEND_INFO); + this.engine.connection = this; + leakTracker = leakDetector.track(this); + } + + synchronized void reattach(ReferenceCounted refCnt) { + this.refCnt.release(); + this.refCnt = refCnt; + } + + void free() { + free(true); + } + + private void free(boolean closeLeakTracker) { + boolean release = false; + synchronized (this) { + if (connection != -1) { + try { + BoringSSL.SSL_cleanup(ssl); + Quiche.quiche_conn_free(connection); + engine.ctx.remove(engine); + release = true; + refCnt.release(); + } finally { + connection = -1; + } + } + } + if (release) { + recvInfoBuffer.release(); + sendInfoBuffer.release(); + if (closeLeakTracker && leakTracker != null) { + leakTracker.close(this); + } + } + } + + Runnable sslTask() { + final Runnable task; + synchronized (this) { + if (connection != -1) { + task = BoringSSL.SSL_getTask(ssl); + } else { + task = null; + } + } + if (task == null) { + return null; + } + + return () -> { + if (connection == -1) { + return; + } + + task.run(); + }; + } + + QuicConnectionAddress sourceId() { + return connectionId(() -> Quiche.quiche_conn_source_id(connection)); + } + + QuicConnectionAddress destinationId() { + return connectionId(() -> Quiche.quiche_conn_destination_id(connection)); + } + + QuicConnectionAddress connectionId(Supplier idSupplier) { + final byte[] id; + synchronized (this) { + if (connection == -1) { + return null; + } + id = idSupplier.get(); + } + return id == null ? null : new QuicConnectionAddress(id); + } + + QuicheQuicTransportParameters peerParameters() { + final long[] ret; + synchronized (this) { + if (connection == -1) { + return null; + } + ret = Quiche.quiche_conn_peer_transport_params(connection); + } + if (ret == null) { + return null; + } + return new QuicheQuicTransportParameters(ret); + } + + QuicheQuicSslEngine engine() { + return engine; + } + + long address() { + assert connection != -1; + return connection; + } + + void initInfo(InetSocketAddress local, InetSocketAddress remote) { + assert connection != -1; + assert recvInfoBuffer.refCnt() != 0; + assert sendInfoBuffer.refCnt() != 0; + + // Fill both quiche_recv_info structs with the same address. + QuicheRecvInfo.setRecvInfo(recvInfoBuffer1, remote, local); + QuicheRecvInfo.setRecvInfo(recvInfoBuffer2, remote, local); + + // Fill both quiche_send_info structs with the same address. + QuicheSendInfo.setSendInfo(sendInfoBuffer1, local, remote); + QuicheSendInfo.setSendInfo(sendInfoBuffer2, local, remote); + } + + ByteBuffer nextRecvInfo() { + assert recvInfoBuffer.refCnt() != 0; + recvInfoFirst = !recvInfoFirst; + return recvInfoFirst ? recvInfoBuffer1 : recvInfoBuffer2; + } + + ByteBuffer nextSendInfo() { + assert sendInfoBuffer.refCnt() != 0; + sendInfoFirst = !sendInfoFirst; + return sendInfoFirst ? sendInfoBuffer1 : sendInfoBuffer2; + } + + boolean isSendInfoChanged() { + assert sendInfoBuffer.refCnt() != 0; + return !QuicheSendInfo.isSameAddress(sendInfoBuffer1, sendInfoBuffer2); + } + + boolean isRecvInfoChanged() { + assert recvInfoBuffer.refCnt() != 0; + return !QuicheRecvInfo.isSameAddress(recvInfoBuffer1, recvInfoBuffer2); + } + + boolean isClosed() { + assert connection != -1; + return Quiche.quiche_conn_is_closed(connection); + } + + // Let's override finalize() as we want to ensure we never leak memory even if the user will miss to close + // Channel that uses this connection and just let it get GC'ed + @Override + protected void finalize() throws Throwable { + try { + free(false); + } finally { + super.finalize(); + } + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicConnectionStats.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicConnectionStats.java new file mode 100644 index 0000000..87cf1ab --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicConnectionStats.java @@ -0,0 +1,91 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.util.internal.StringUtil; + +final class QuicheQuicConnectionStats implements QuicConnectionStats { + + private final long[] values; + + QuicheQuicConnectionStats(long[] values) { + this.values = values; + } + + @Override + public long recv() { + return values[0]; + } + + @Override + public long sent() { + return values[1]; + } + + @Override + public long lost() { + return values[2]; + } + + @Override + public long retrans() { + return values[3]; + } + + @Override + public long sentBytes() { + return values[4]; + } + + @Override + public long recvBytes() { + return values[5]; + } + + @Override + public long lostBytes() { + return values[6]; + } + + @Override + public long streamRetransBytes() { + return values[7]; + } + + @Override + public long pathsCount() { + return values[8]; + } + + /** + * Returns the {@link String} representation of stats. + */ + @Override + public String toString() { + return StringUtil.simpleClassName(this) + "[" + + "recv=" + recv() + + ", sent=" + sent() + + ", lost=" + lost() + + ", retrans=" + retrans() + + ", sentBytes=" + sentBytes() + + ", recvBytes=" + recvBytes() + + ", lostBytes=" + lostBytes() + + ", streamRetransBytes=" + streamRetransBytes() + + ", pathsCount=" + pathsCount() + + "]"; + } + +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicServerCodec.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicServerCodec.java new file mode 100644 index 0000000..edd6e71 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicServerCodec.java @@ -0,0 +1,268 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOption; +import io.netty.channel.socket.DatagramPacket; +import io.netty.util.AttributeKey; +import io.netty.util.CharsetUtil; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.function.Function; + +import static io.netty.handler.codec.quic.Quiche.allocateNativeOrder; + +/** + * {@link QuicheQuicCodec} for QUIC servers. + */ +final class QuicheQuicServerCodec extends QuicheQuicCodec { + private static final InternalLogger LOGGER = InternalLoggerFactory.getInstance(QuicheQuicServerCodec.class); + + private final Function sslEngineProvider; + private final Executor sslTaskExecutor; + private final QuicConnectionIdGenerator connectionIdAddressGenerator; + private final QuicResetTokenGenerator resetTokenGenerator; + private final QuicTokenHandler tokenHandler; + private final ChannelHandler handler; + private final Map.Entry, Object>[] optionsArray; + private final Map.Entry, Object>[] attrsArray; + private final ChannelHandler streamHandler; + private final Map.Entry, Object>[] streamOptionsArray; + private final Map.Entry, Object>[] streamAttrsArray; + private ByteBuf mintTokenBuffer; + private ByteBuf connIdBuffer; + + QuicheQuicServerCodec(QuicheConfig config, + int localConnIdLength, + QuicTokenHandler tokenHandler, + QuicConnectionIdGenerator connectionIdAddressGenerator, + QuicResetTokenGenerator resetTokenGenerator, + FlushStrategy flushStrategy, + Function sslEngineProvider, + Executor sslTaskExecutor, + ChannelHandler handler, + Map.Entry, Object>[] optionsArray, + Map.Entry, Object>[] attrsArray, + ChannelHandler streamHandler, + Map.Entry, Object>[] streamOptionsArray, + Map.Entry, Object>[] streamAttrsArray) { + super(config, localConnIdLength, tokenHandler.maxTokenLength(), flushStrategy); + this.tokenHandler = tokenHandler; + this.connectionIdAddressGenerator = connectionIdAddressGenerator; + this.resetTokenGenerator = resetTokenGenerator; + this.sslEngineProvider = sslEngineProvider; + this.sslTaskExecutor = sslTaskExecutor; + this.handler = handler; + this.optionsArray = optionsArray; + this.attrsArray = attrsArray; + this.streamHandler = streamHandler; + this.streamOptionsArray = streamOptionsArray; + this.streamAttrsArray = streamAttrsArray; + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) { + super.handlerAdded(ctx); + connIdBuffer = Quiche.allocateNativeOrder(localConnIdLength); + mintTokenBuffer = allocateNativeOrder(tokenHandler.maxTokenLength()); + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) { + super.handlerRemoved(ctx); + if (connIdBuffer != null) { + connIdBuffer.release(); + } + if (mintTokenBuffer != null) { + mintTokenBuffer.release(); + } + } + + @Override + protected void channelRecv(QuicheQuicChannel channel, InetSocketAddress sender, + InetSocketAddress recipient, ByteBuf buffer) { + super.channelRecv(channel, sender, recipient, buffer); + for (ByteBuffer retiredSourceConnectionId : channel.retiredSourceConnectionId()) { + removeMapping(retiredSourceConnectionId); + } + for (ByteBuffer newSourceConnectionId : + channel.newSourceConnectionIds(connectionIdAddressGenerator, resetTokenGenerator)) { + addMapping(newSourceConnectionId, channel); + } + } + + @Override + protected QuicheQuicChannel quicPacketRead(ChannelHandlerContext ctx, InetSocketAddress sender, + InetSocketAddress recipient, QuicPacketType type, int version, + ByteBuf scid, ByteBuf dcid, ByteBuf token) throws Exception { + ByteBuffer dcidByteBuffer = dcid.internalNioBuffer(dcid.readerIndex(), dcid.readableBytes()); + QuicheQuicChannel channel = getChannel(dcidByteBuffer); + if (channel == null && type == QuicPacketType.ZERO_RTT && connectionIdAddressGenerator.isIdempotent()) { + // 0 rtt packet should obtain the server generated dcid + channel = getChannel(connectionIdAddressGenerator.newId(dcidByteBuffer, localConnIdLength)); + } + if (channel == null) { + return handleServer(ctx, sender, recipient, type, version, scid, dcid, token); + } + + return channel; + } + + private QuicheQuicChannel handleServer(ChannelHandlerContext ctx, InetSocketAddress sender, + InetSocketAddress recipient, + @SuppressWarnings("unused") QuicPacketType type, int version, + ByteBuf scid, ByteBuf dcid, ByteBuf token) throws Exception { + if (!Quiche.quiche_version_is_supported(version)) { + // Version is not supported, try to negotiate it. + ByteBuf out = ctx.alloc().directBuffer(Quic.MAX_DATAGRAM_SIZE); + int outWriterIndex = out.writerIndex(); + + int res = Quiche.quiche_negotiate_version( + Quiche.readerMemoryAddress(scid), scid.readableBytes(), + Quiche.readerMemoryAddress(dcid), dcid.readableBytes(), + Quiche.writerMemoryAddress(out), out.writableBytes()); + if (res < 0) { + out.release(); + Quiche.throwIfError(res); + } else { + ctx.writeAndFlush(new DatagramPacket(out.writerIndex(outWriterIndex + res), sender)); + } + return null; + } + + final int offset; + boolean noToken = false; + if (!token.isReadable()) { + // Clear buffers so we can reuse these. + mintTokenBuffer.clear(); + connIdBuffer.clear(); + + // The remote peer did not send a token. + if (tokenHandler.writeToken(mintTokenBuffer, dcid, sender)) { + ByteBuffer connId = connectionIdAddressGenerator.newId( + dcid.internalNioBuffer(dcid.readerIndex(), dcid.readableBytes()), localConnIdLength); + connIdBuffer.writeBytes(connId); + + ByteBuf out = ctx.alloc().directBuffer(Quic.MAX_DATAGRAM_SIZE); + int outWriterIndex = out.writerIndex(); + int written = Quiche.quiche_retry( + Quiche.readerMemoryAddress(scid), scid.readableBytes(), + Quiche.readerMemoryAddress(dcid), dcid.readableBytes(), + Quiche.readerMemoryAddress(connIdBuffer), connIdBuffer.readableBytes(), + Quiche.readerMemoryAddress(mintTokenBuffer), mintTokenBuffer.readableBytes(), + version, + Quiche.writerMemoryAddress(out), out.writableBytes()); + + if (written < 0) { + out.release(); + Quiche.throwIfError(written); + } else { + ctx.writeAndFlush(new DatagramPacket(out.writerIndex(outWriterIndex + written), sender)); + } + return null; + } + offset = 0; + noToken = true; + } else { + offset = tokenHandler.validateToken(token, sender); + if (offset == -1) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("invalid token: {}", token.toString(CharsetUtil.US_ASCII)); + } + return null; + } + } + + final ByteBuffer key; + final long scidAddr; + final int scidLen; + final long ocidAddr; + final int ocidLen; + + if (noToken) { + connIdBuffer.clear(); + key = connectionIdAddressGenerator.newId( + dcid.internalNioBuffer(dcid.readerIndex(), dcid.readableBytes()), localConnIdLength); + connIdBuffer.writeBytes(key.duplicate()); + scidAddr = Quiche.readerMemoryAddress(connIdBuffer); + scidLen = localConnIdLength; + ocidAddr = -1; + ocidLen = -1; + + QuicheQuicChannel existingChannel = getChannel(key); + if (existingChannel != null) { + return existingChannel; + } + } else { + scidAddr = Quiche.readerMemoryAddress(dcid); + scidLen = localConnIdLength; + ocidAddr = Quiche.memoryAddress(token, offset, token.readableBytes()); + ocidLen = token.readableBytes() - offset; + // Now create the key to store the channel in the map. + byte[] bytes = new byte[localConnIdLength]; + dcid.getBytes(dcid.readerIndex(), bytes); + key = ByteBuffer.wrap(bytes); + } + QuicheQuicChannel channel = QuicheQuicChannel.forServer( + ctx.channel(), key, recipient, sender, config.isDatagramSupported(), + streamHandler, streamOptionsArray, streamAttrsArray, this::removeChannel, sslTaskExecutor); + + Quic.setupChannel(channel, optionsArray, attrsArray, handler, LOGGER); + QuicSslEngine engine = sslEngineProvider.apply(channel); + if (!(engine instanceof QuicheQuicSslEngine)) { + channel.unsafe().closeForcibly(); + throw new IllegalArgumentException("QuicSslEngine is not of type " + + QuicheQuicSslEngine.class.getSimpleName()); + } + if (engine.getUseClientMode()) { + channel.unsafe().closeForcibly(); + throw new IllegalArgumentException("QuicSslEngine is not created in server mode"); + } + + QuicheQuicSslEngine quicSslEngine = (QuicheQuicSslEngine) engine; + QuicheQuicConnection connection = quicSslEngine.createConnection(ssl -> { + ByteBuffer localAddrMemory = recipientSockaddrMemory.internalNioBuffer(0, recipientSockaddrMemory.capacity()); + int localLen = SockaddrIn.setAddress(localAddrMemory, recipient); + + ByteBuffer peerAddrMemory = senderSockaddrMemory.internalNioBuffer(0, senderSockaddrMemory.capacity()); + int peerLen = SockaddrIn.setAddress(peerAddrMemory, sender); + return Quiche.quiche_conn_new_with_tls(scidAddr, scidLen, ocidAddr, ocidLen, + Quiche.memoryAddressWithPosition(localAddrMemory), localLen, + Quiche.memoryAddressWithPosition(peerAddrMemory), peerLen, + config.nativeAddress(), ssl, true); + }); + if (connection == null) { + channel.unsafe().closeForcibly(); + LOGGER.debug("quiche_accept failed"); + return null; + } + + channel.attachQuicheConnection(connection); + + addChannel(channel); + + ctx.channel().eventLoop().register(channel); + return channel; + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicSslContext.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicSslContext.java new file mode 100644 index 0000000..9b031dd --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicSslContext.java @@ -0,0 +1,497 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.ssl.ApplicationProtocolNegotiator; +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.SslHandler; +import io.netty.util.AbstractReferenceCounted; +import io.netty.util.Mapping; +import io.netty.util.ReferenceCounted; + +import javax.crypto.NoSuchPaddingException; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509TrustManager; +import java.io.File; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.concurrent.Executor; +import java.util.function.BiConsumer; +import java.util.function.LongFunction; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static java.util.Objects.requireNonNull; + +final class QuicheQuicSslContext extends QuicSslContext { + final ClientAuth clientAuth; + private final boolean server; + @SuppressWarnings("deprecation") + private final ApplicationProtocolNegotiator apn; + private long sessionCacheSize; + private long sessionTimeout; + private final QuicheQuicSslSessionContext sessionCtx; + private final QuicheQuicSslEngineMap engineMap = new QuicheQuicSslEngineMap(); + private final QuicClientSessionCache sessionCache; + + private final BoringSSLSessionTicketCallback sessionTicketCallback = new BoringSSLSessionTicketCallback(); + + final NativeSslContext nativeSslContext; + + QuicheQuicSslContext(boolean server, long sessionTimeout, long sessionCacheSize, + ClientAuth clientAuth, TrustManagerFactory trustManagerFactory, + KeyManagerFactory keyManagerFactory, String password, + Mapping mapping, + Boolean earlyData, BoringSSLKeylog keylog, + String... applicationProtocols) { + Quic.ensureAvailability(); + this.server = server; + this.clientAuth = server ? checkNotNull(clientAuth, "clientAuth") : ClientAuth.NONE; + final X509TrustManager trustManager; + if (trustManagerFactory == null) { + try { + trustManagerFactory = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init((KeyStore) null); + trustManager = chooseTrustManager(trustManagerFactory); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } else { + trustManager = chooseTrustManager(trustManagerFactory); + } + final X509ExtendedKeyManager keyManager; + if (keyManagerFactory == null) { + if (server) { + throw new IllegalArgumentException("No KeyManagerFactory"); + } + keyManager = null; + } else { + keyManager = chooseKeyManager(keyManagerFactory); + } + final BoringSSLPrivateKeyMethod privateKeyMethod; + if (keyManagerFactory instanceof BoringSSLKeylessManagerFactory) { + privateKeyMethod = new BoringSSLAsyncPrivateKeyMethodAdapter(engineMap, + ((BoringSSLKeylessManagerFactory) keyManagerFactory).privateKeyMethod); + } else { + privateKeyMethod = null; + } + sessionCache = server ? null : new QuicClientSessionCache(); + int verifyMode = server ? boringSSLVerifyModeForServer(this.clientAuth) : BoringSSL.SSL_VERIFY_PEER; + nativeSslContext = new NativeSslContext(BoringSSL.SSLContext_new(server, applicationProtocols, + new BoringSSLHandshakeCompleteCallback(engineMap), + new BoringSSLCertificateCallback(engineMap, keyManager, password), + new BoringSSLCertificateVerifyCallback(engineMap, trustManager), + mapping == null ? null : new BoringSSLTlsextServernameCallback(engineMap, mapping), + keylog == null ? null : new BoringSSLKeylogCallback(engineMap, keylog), + server ? null : new BoringSSLSessionCallback(engineMap, sessionCache), privateKeyMethod, + sessionTicketCallback, verifyMode, + BoringSSL.subjectNames(trustManager.getAcceptedIssuers()))); + apn = new QuicheQuicApplicationProtocolNegotiator(applicationProtocols); + if (this.sessionCache != null) { + // Cache is handled via our own implementation. + this.sessionCache.setSessionCacheSize((int) sessionCacheSize); + this.sessionCache.setSessionTimeout((int) sessionTimeout); + } else { + // Cache is handled by BoringSSL internally + BoringSSL.SSLContext_setSessionCacheSize( + nativeSslContext.address(), sessionCacheSize); + this.sessionCacheSize = sessionCacheSize; + + BoringSSL.SSLContext_setSessionCacheTimeout( + nativeSslContext.address(), sessionTimeout); + this.sessionTimeout = sessionTimeout; + } + if (earlyData != null) { + BoringSSL.SSLContext_set_early_data_enabled(nativeSslContext.address(), earlyData); + } + sessionCtx = new QuicheQuicSslSessionContext(this); + } + + private X509ExtendedKeyManager chooseKeyManager(KeyManagerFactory keyManagerFactory) { + for (KeyManager manager: keyManagerFactory.getKeyManagers()) { + if (manager instanceof X509ExtendedKeyManager) { + return (X509ExtendedKeyManager) manager; + } + } + throw new IllegalArgumentException("No X509ExtendedKeyManager included"); + } + + private static X509TrustManager chooseTrustManager(TrustManagerFactory trustManagerFactory) { + for (TrustManager manager: trustManagerFactory.getTrustManagers()) { + if (manager instanceof X509TrustManager) { + return (X509TrustManager) manager; + } + } + throw new IllegalArgumentException("No X509TrustManager included"); + } + + static X509Certificate[] toX509Certificates0(File file) throws CertificateException { + return toX509Certificates(file); + } + + static PrivateKey toPrivateKey0(File keyFile, String keyPassword) throws NoSuchAlgorithmException, + NoSuchPaddingException, InvalidKeySpecException, + InvalidAlgorithmParameterException, + KeyException, IOException { + return toPrivateKey(keyFile, keyPassword); + } + + static TrustManagerFactory buildTrustManagerFactory0( + X509Certificate[] certCollection) + throws NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException { + return buildTrustManagerFactory(certCollection, null, null); + } + + private static int boringSSLVerifyModeForServer(ClientAuth mode) { + switch (mode) { + case NONE: + return BoringSSL.SSL_VERIFY_NONE; + case REQUIRE: + return BoringSSL.SSL_VERIFY_PEER | BoringSSL.SSL_VERIFY_FAIL_IF_NO_PEER_CERT; + case OPTIONAL: + return BoringSSL.SSL_VERIFY_PEER; + default: + throw new Error(mode.toString()); + } + } + + QuicheQuicConnection createConnection(LongFunction connectionCreator, QuicheQuicSslEngine engine) { + nativeSslContext.retain(); + long ssl = BoringSSL.SSL_new(nativeSslContext.address(), isServer(), engine.tlsHostName); + engineMap.put(ssl, engine); + long connection = connectionCreator.apply(ssl); + if (connection == -1) { + engineMap.remove(ssl); + // We retained before but as we don't create a QuicheQuicConnection and transfer ownership we need to + // explict call release again here. + nativeSslContext.release(); + return null; + } + // The connection will call nativeSslContext.release() once it is freed. + return new QuicheQuicConnection(connection, ssl, engine, nativeSslContext); + } + + /** + * Add the given engine to this context + * + * @param engine the engine to add. + * @return the pointer address of this context. + */ + long add(QuicheQuicSslEngine engine) { + nativeSslContext.retain(); + engine.connection.reattach(nativeSslContext); + engineMap.put(engine.connection.ssl, engine); + return nativeSslContext.address(); + } + + /** + * Remove the given engine from this context. + * + * @param engine the engine to remove. + */ + void remove(QuicheQuicSslEngine engine) { + QuicheQuicSslEngine removed = engineMap.remove(engine.connection.ssl); + assert removed == null || removed == engine; + engine.removeSessionFromCacheIfInvalid(); + } + + QuicClientSessionCache getSessionCache() { + return sessionCache; + } + + @Override + public boolean isClient() { + return !server; + } + + @Override + public List cipherSuites() { + return Arrays.asList("TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384"); + } + + @Override + public long sessionCacheSize() { + if (sessionCache != null) { + return sessionCache.getSessionCacheSize(); + } else { + synchronized (this) { + return sessionCacheSize; + } + } + } + + @Override + public long sessionTimeout() { + if (sessionCache != null) { + return sessionCache.getSessionTimeout(); + } else { + synchronized (this) { + return sessionTimeout; + } + } + } + + @Override + public ApplicationProtocolNegotiator applicationProtocolNegotiator() { + return apn; + } + + @Override + public QuicSslEngine newEngine(ByteBufAllocator alloc) { + return new QuicheQuicSslEngine(this, null, -1); + } + + @Override + public QuicSslEngine newEngine(ByteBufAllocator alloc, String peerHost, int peerPort) { + return new QuicheQuicSslEngine(this, peerHost, peerPort); + } + + @Override + public QuicSslSessionContext sessionContext() { + return sessionCtx; + } + + @Override + protected SslHandler newHandler(ByteBufAllocator alloc, boolean startTls) { + throw new UnsupportedOperationException(); + } + + @Override + public SslHandler newHandler(ByteBufAllocator alloc, Executor delegatedTaskExecutor) { + throw new UnsupportedOperationException(); + } + + @Override + protected SslHandler newHandler(ByteBufAllocator alloc, boolean startTls, Executor executor) { + throw new UnsupportedOperationException(); + } + + @Override + protected SslHandler newHandler(ByteBufAllocator alloc, String peerHost, int peerPort, boolean startTls) { + throw new UnsupportedOperationException(); + } + + @Override + public SslHandler newHandler(ByteBufAllocator alloc, String peerHost, int peerPort, + Executor delegatedTaskExecutor) { + throw new UnsupportedOperationException(); + } + + @Override + protected SslHandler newHandler(ByteBufAllocator alloc, String peerHost, int peerPort, + boolean startTls, Executor delegatedTaskExecutor) { + throw new UnsupportedOperationException(); + } + + @Override + protected void finalize() throws Throwable { + try { + nativeSslContext.release(); + } finally { + super.finalize(); + } + } + + void setSessionTimeout(int seconds) throws IllegalArgumentException { + if (sessionCache != null) { + sessionCache.setSessionTimeout(seconds); + } else { + BoringSSL.SSLContext_setSessionCacheTimeout(nativeSslContext.address(), seconds); + this.sessionTimeout = seconds; + } + } + + void setSessionCacheSize(int size) throws IllegalArgumentException { + if (sessionCache != null) { + sessionCache.setSessionCacheSize(size); + } else { + BoringSSL.SSLContext_setSessionCacheSize(nativeSslContext.address(), size); + sessionCacheSize = size; + } + } + + void setSessionTicketKeys(SslSessionTicketKey[] ticketKeys) { + sessionTicketCallback.setSessionTicketKeys(ticketKeys); + BoringSSL.SSLContext_setSessionTicketKeys( + nativeSslContext.address(), ticketKeys != null && ticketKeys.length != 0); + } + + @SuppressWarnings("deprecation") + private static final class QuicheQuicApplicationProtocolNegotiator implements ApplicationProtocolNegotiator { + private final List protocols; + + QuicheQuicApplicationProtocolNegotiator(String... protocols) { + if (protocols == null) { + this.protocols = Collections.emptyList(); + } else { + this.protocols = Collections.unmodifiableList(Arrays.asList(protocols)); + } + } + + @Override + public List protocols() { + return protocols; + } + } + + private static final class QuicheQuicSslSessionContext implements QuicSslSessionContext { + private final QuicheQuicSslContext context; + + QuicheQuicSslSessionContext(QuicheQuicSslContext context) { + this.context = context; + } + + @Override + public SSLSession getSession(byte[] sessionId) { + return null; + } + + @Override + public Enumeration getIds() { + return new Enumeration() { + @Override + public boolean hasMoreElements() { + return false; + } + + @Override + public byte[] nextElement() { + throw new NoSuchElementException(); + } + }; + } + + @Override + public void setSessionTimeout(int seconds) throws IllegalArgumentException { + context.setSessionTimeout(seconds); + } + + @Override + public int getSessionTimeout() { + return (int) context.sessionTimeout(); + } + + @Override + public void setSessionCacheSize(int size) throws IllegalArgumentException { + context.setSessionCacheSize(size); + } + + @Override + public int getSessionCacheSize() { + return (int) context.sessionCacheSize(); + } + + @Override + public void setTicketKeys(SslSessionTicketKey... keys) { + context.setSessionTicketKeys(keys); + } + } + + static final class NativeSslContext extends AbstractReferenceCounted { + private final long ctx; + + NativeSslContext(long ctx) { + this.ctx = ctx; + } + + long address() { + return ctx; + } + + @Override + protected void deallocate() { + BoringSSL.SSLContext_free(ctx); + } + + @Override + public ReferenceCounted touch(Object hint) { + return this; + } + + @Override + public String toString() { + return "NativeSslContext{" + + "ctx=" + ctx + + '}'; + } + } + + private static final class BoringSSLAsyncPrivateKeyMethodAdapter implements BoringSSLPrivateKeyMethod { + private final QuicheQuicSslEngineMap engineMap; + private final BoringSSLAsyncPrivateKeyMethod privateKeyMethod; + + BoringSSLAsyncPrivateKeyMethodAdapter(QuicheQuicSslEngineMap engineMap, + BoringSSLAsyncPrivateKeyMethod privateKeyMethod) { + this.engineMap = engineMap; + this.privateKeyMethod = privateKeyMethod; + } + + @Override + public void sign(long ssl, int signatureAlgorithm, byte[] input, BiConsumer callback) { + final QuicheQuicSslEngine engine = engineMap.get(ssl); + if (engine == null) { + // May be null if it was destroyed in the meantime. + callback.accept(null, null); + } else { + privateKeyMethod.sign(engine, signatureAlgorithm, input).addListener(f -> { + Throwable cause = f.cause(); + if (cause != null) { + callback.accept(null, cause); + } else { + callback.accept((byte[]) f.getNow(), null); + } + }); + } + } + + @Override + public void decrypt(long ssl, byte[] input, BiConsumer callback) { + final QuicheQuicSslEngine engine = engineMap.get(ssl); + if (engine == null) { + // May be null if it was destroyed in the meantime. + callback.accept(null, null); + } else { + privateKeyMethod.decrypt(engine, input).addListener(f -> { + Throwable cause = f.cause(); + if (cause != null) { + callback.accept(null, cause); + } else { + callback.accept((byte[]) f.getNow(), null); + } + }); + } + } + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicSslEngine.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicSslEngine.java new file mode 100644 index 0000000..f0f0636 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicSslEngine.java @@ -0,0 +1,565 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.util.LazyJavaxX509Certificate; +import io.netty.handler.ssl.util.LazyX509Certificate; +import io.netty.util.NetUtil; +import io.netty.util.internal.EmptyArrays; +import io.netty.util.internal.ObjectUtil; + +import javax.net.ssl.SNIHostName; +import javax.net.ssl.SNIServerName; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSessionBindingEvent; +import javax.net.ssl.SSLSessionBindingListener; +import javax.net.ssl.SSLSessionContext; +import javax.security.cert.X509Certificate; +import java.nio.ByteBuffer; +import java.security.Principal; +import java.security.cert.Certificate; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.LongFunction; + +final class QuicheQuicSslEngine extends QuicSslEngine { + QuicheQuicSslContext ctx; + private final String peerHost; + private final int peerPort; + private final QuicheQuicSslSession session = new QuicheQuicSslSession(); + private volatile Certificate[] localCertificateChain; + private List sniHostNames; + private boolean handshakeFinished; + private String applicationProtocol; + private boolean sessionReused; + final String tlsHostName; + volatile QuicheQuicConnection connection; + + String sniHostname; + + QuicheQuicSslEngine(QuicheQuicSslContext ctx, String peerHost, int peerPort) { + this.ctx = ctx; + this.peerHost = peerHost; + this.peerPort = peerPort; + // Use SNI if peerHost was specified and a valid hostname + // See https://github.com/netty/netty/issues/4746 + if (ctx.isClient() && isValidHostNameForSNI(peerHost)) { + tlsHostName = peerHost; + sniHostNames = Collections.singletonList(new SNIHostName(tlsHostName)); + } else { + tlsHostName = null; + } + } + + long moveTo(String hostname, QuicheQuicSslContext ctx) { + // First of remove the engine from its previous QuicheQuicSslContext. + this.ctx.remove(this); + this.ctx = ctx; + long added = ctx.add(this); + sniHostname = hostname; + return added; + } + + QuicheQuicConnection createConnection(LongFunction connectionCreator) { + return ctx.createConnection(connectionCreator, this); + } + + void setLocalCertificateChain(Certificate[] localCertificateChain) { + this.localCertificateChain = localCertificateChain; + } + + /** + * Validate that the given hostname can be used in SNI extension. + */ + static boolean isValidHostNameForSNI(String hostname) { + return hostname != null && + hostname.indexOf('.') > 0 && + !hostname.endsWith(".") && + !NetUtil.isValidIpV4Address(hostname) && + !NetUtil.isValidIpV6Address(hostname); + } + + @Override + public SSLParameters getSSLParameters() { + SSLParameters parameters = super.getSSLParameters(); + parameters.setServerNames(sniHostNames); + return parameters; + } + + // These method will override the method defined by Java 8u251 and later. As we may compile with an earlier + // java8 version we don't use @Override annotations here. + public synchronized String getApplicationProtocol() { + return applicationProtocol; + } + + // These method will override the method defined by Java 8u251 and later. As we may compile with an earlier + // java8 version we don't use @Override annotations here. + public synchronized String getHandshakeApplicationProtocol() { + return applicationProtocol; + } + + @Override + public SSLEngineResult wrap(ByteBuffer[] srcs, int offset, int length, ByteBuffer dst) { + throw new UnsupportedOperationException(); + } + + @Override + public SSLEngineResult unwrap(ByteBuffer src, ByteBuffer[] dsts, int offset, int length) { + throw new UnsupportedOperationException(); + } + + @Override + public Runnable getDelegatedTask() { + return null; + } + + @Override + public void closeInbound() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isInboundDone() { + return false; + } + + @Override + public void closeOutbound() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isOutboundDone() { + return false; + } + + @Override + public String[] getSupportedCipherSuites() { + return ctx.cipherSuites().toArray(new String[0]); + } + + @Override + public String[] getEnabledCipherSuites() { + return getSupportedCipherSuites(); + } + + @Override + public void setEnabledCipherSuites(String[] suites) { + throw new UnsupportedOperationException(); + } + + @Override + public String[] getSupportedProtocols() { + // QUIC only supports TLSv1.3 + return new String[] { "TLSv1.3" }; + } + + @Override + public String[] getEnabledProtocols() { + return getSupportedProtocols(); + } + + @Override + public void setEnabledProtocols(String[] protocols) { + throw new UnsupportedOperationException(); + } + + @Override + public SSLSession getSession() { + return session; + } + + @Override + public SSLSession getHandshakeSession() { + if (handshakeFinished) { + return null; + } + return session; + } + + @Override + public void beginHandshake() { + // NOOP + } + + @Override + public SSLEngineResult.HandshakeStatus getHandshakeStatus() { + if (handshakeFinished) { + return SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING; + } + return SSLEngineResult.HandshakeStatus.NEED_WRAP; + } + + @Override + public void setUseClientMode(boolean clientMode) { + if (clientMode != ctx.isClient()) { + throw new UnsupportedOperationException(); + } + } + + @Override + public boolean getUseClientMode() { + return ctx.isClient(); + } + + @Override + public void setNeedClientAuth(boolean b) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean getNeedClientAuth() { + return ctx.clientAuth == ClientAuth.REQUIRE; + } + + @Override + public void setWantClientAuth(boolean b) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean getWantClientAuth() { + return ctx.clientAuth == ClientAuth.OPTIONAL; + } + + @Override + public void setEnableSessionCreation(boolean flag) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean getEnableSessionCreation() { + return false; + } + + synchronized void handshakeFinished(byte[] id, String cipher, String protocol, byte[] peerCertificate, + byte[][] peerCertificateChain, + long creationTime, long timeout, + byte[] applicationProtocol, boolean sessionReused) { + if (applicationProtocol == null) { + this.applicationProtocol = null; + } else { + this.applicationProtocol = new String(applicationProtocol); + } + session.handshakeFinished(id, cipher, protocol, peerCertificate, peerCertificateChain, creationTime, timeout); + this.sessionReused = sessionReused; + handshakeFinished = true; + } + + void removeSessionFromCacheIfInvalid() { + session.removeFromCacheIfInvalid(); + } + + synchronized boolean isSessionReused() { + return sessionReused; + } + + private final class QuicheQuicSslSession implements SSLSession { + private X509Certificate[] x509PeerCerts; + private Certificate[] peerCerts; + private String protocol; + private String cipher; + private byte[] id; + private long creationTime = -1; + private long timeout = -1; + private boolean invalid; + private long lastAccessedTime = -1; + + // lazy init for memory reasons + private Map values; + + private boolean isEmpty(Object[] arr) { + return arr == null || arr.length == 0; + } + private boolean isEmpty(byte[] arr) { + return arr == null || arr.length == 0; + } + + void handshakeFinished(byte[] id, String cipher, String protocol, byte[] peerCertificate, + byte[][] peerCertificateChain, long creationTime, long timeout) { + synchronized (QuicheQuicSslEngine.this) { + initPeerCerts(peerCertificateChain, peerCertificate); + this.id = id; + this.cipher = cipher; + this.protocol = protocol; + this.creationTime = creationTime * 1000L; + this.timeout = timeout * 1000L; + lastAccessedTime = System.currentTimeMillis(); + } + } + + void removeFromCacheIfInvalid() { + if (!isValid()) { + // Shouldn't be re-used again + removeFromCache(); + } + } + + private void removeFromCache() { + // Shouldn't be re-used again + QuicClientSessionCache cache = ctx.getSessionCache(); + if (cache != null) { + cache.removeSession(getPeerHost(), getPeerPort()); + } + } + + /** + * Init peer certificates that can be obtained via {@link #getPeerCertificateChain()} + * and {@link #getPeerCertificates()}. + */ + private void initPeerCerts(byte[][] chain, byte[] clientCert) { + // Return the full chain from the JNI layer. + if (getUseClientMode()) { + if (isEmpty(chain)) { + peerCerts = EmptyArrays.EMPTY_CERTIFICATES; + x509PeerCerts = EmptyArrays.EMPTY_JAVAX_X509_CERTIFICATES; + } else { + peerCerts = new Certificate[chain.length]; + x509PeerCerts = new X509Certificate[chain.length]; + initCerts(chain, 0); + } + } else { + // if used on the server side SSL_get_peer_cert_chain(...) will not include the remote peer + // certificate. We use SSL_get_peer_certificate to get it in this case and add it to our + // array later. + // + // See https://www.openssl.org/docs/ssl/SSL_get_peer_cert_chain.html + if (isEmpty(clientCert)) { + peerCerts = EmptyArrays.EMPTY_CERTIFICATES; + x509PeerCerts = EmptyArrays.EMPTY_JAVAX_X509_CERTIFICATES; + } else { + if (isEmpty(chain)) { + peerCerts = new Certificate[] {new LazyX509Certificate(clientCert)}; + x509PeerCerts = new X509Certificate[] {new LazyJavaxX509Certificate(clientCert)}; + } else { + peerCerts = new Certificate[chain.length + 1]; + x509PeerCerts = new X509Certificate[chain.length + 1]; + peerCerts[0] = new LazyX509Certificate(clientCert); + x509PeerCerts[0] = new LazyJavaxX509Certificate(clientCert); + initCerts(chain, 1); + } + } + } + } + + private void initCerts(byte[][] chain, int startPos) { + for (int i = 0; i < chain.length; i++) { + int certPos = startPos + i; + peerCerts[certPos] = new LazyX509Certificate(chain[i]); + x509PeerCerts[certPos] = new LazyJavaxX509Certificate(chain[i]); + } + } + + @Override + public byte[] getId() { + synchronized (QuicheQuicSslSession.this) { + if (id == null) { + return EmptyArrays.EMPTY_BYTES; + } + return id.clone(); + } + } + + @Override + public SSLSessionContext getSessionContext() { + return ctx.sessionContext(); + } + + @Override + public long getCreationTime() { + synchronized (QuicheQuicSslEngine.this) { + return creationTime; + } + } + + @Override + public long getLastAccessedTime() { + return lastAccessedTime; + } + + @Override + public void invalidate() { + boolean removeFromCache; + synchronized (this) { + removeFromCache = !invalid; + invalid = true; + } + if (removeFromCache) { + removeFromCache(); + } + } + + @Override + public boolean isValid() { + synchronized (QuicheQuicSslEngine.this) { + return !invalid && System.currentTimeMillis() - timeout < creationTime; + } + } + + @Override + public void putValue(String name, Object value) { + ObjectUtil.checkNotNull(name, "name"); + ObjectUtil.checkNotNull(value, "value"); + + final Object old; + synchronized (this) { + Map values = this.values; + if (values == null) { + // Use size of 2 to keep the memory overhead small + values = this.values = new HashMap<>(2); + } + old = values.put(name, value); + } + + if (value instanceof SSLSessionBindingListener) { + // Use newSSLSessionBindingEvent so we alway use the wrapper if needed. + ((SSLSessionBindingListener) value).valueBound(newSSLSessionBindingEvent(name)); + } + notifyUnbound(old, name); + } + + @Override + public Object getValue(String name) { + ObjectUtil.checkNotNull(name, "name"); + synchronized (this) { + if (values == null) { + return null; + } + return values.get(name); + } + } + + @Override + public void removeValue(String name) { + ObjectUtil.checkNotNull(name, "name"); + + final Object old; + synchronized (this) { + Map values = this.values; + if (values == null) { + return; + } + old = values.remove(name); + } + + notifyUnbound(old, name); + } + + @Override + public String[] getValueNames() { + synchronized (this) { + Map values = this.values; + if (values == null || values.isEmpty()) { + return EmptyArrays.EMPTY_STRINGS; + } + return values.keySet().toArray(new String[0]); + } + } + + private SSLSessionBindingEvent newSSLSessionBindingEvent(String name) { + return new SSLSessionBindingEvent(session, name); + } + + private void notifyUnbound(Object value, String name) { + if (value instanceof SSLSessionBindingListener) { + // Use newSSLSessionBindingEvent so we alway use the wrapper if needed. + ((SSLSessionBindingListener) value).valueUnbound(newSSLSessionBindingEvent(name)); + } + } + + @Override + public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException { + synchronized (QuicheQuicSslEngine.this) { + if (isEmpty(peerCerts)) { + throw new SSLPeerUnverifiedException("peer not verified"); + } + return peerCerts.clone(); + } + } + + @Override + public Certificate[] getLocalCertificates() { + Certificate[] localCerts = localCertificateChain; + if (localCerts == null) { + return null; + } + return localCerts.clone(); + } + + @Override + public X509Certificate[] getPeerCertificateChain() throws SSLPeerUnverifiedException { + synchronized (QuicheQuicSslEngine.this) { + if (isEmpty(x509PeerCerts)) { + throw new SSLPeerUnverifiedException("peer not verified"); + } + return x509PeerCerts.clone(); + } + } + + @Override + public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { + Certificate[] peer = getPeerCertificates(); + // No need for null or length > 0 is needed as this is done in getPeerCertificates() + // already. + return ((java.security.cert.X509Certificate) peer[0]).getSubjectX500Principal(); + } + + @Override + public Principal getLocalPrincipal() { + Certificate[] local = localCertificateChain; + if (local == null || local.length == 0) { + return null; + } + return ((java.security.cert.X509Certificate) local[0]).getIssuerX500Principal(); + } + + @Override + public String getCipherSuite() { + return cipher; + } + + @Override + public String getProtocol() { + return protocol; + } + + @Override + public String getPeerHost() { + return peerHost; + } + + @Override + public int getPeerPort() { + return peerPort; + } + + @Override + public int getPacketBufferSize() { + return -1; + } + + @Override + public int getApplicationBufferSize() { + return -1; + } + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicSslEngineMap.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicSslEngineMap.java new file mode 100644 index 0000000..4ce18f5 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicSslEngineMap.java @@ -0,0 +1,37 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +final class QuicheQuicSslEngineMap { + + private final ConcurrentMap engines = new ConcurrentHashMap<>(); + + QuicheQuicSslEngine get(long ssl) { + return engines.get(ssl); + } + + QuicheQuicSslEngine remove(long ssl) { + return engines.remove(ssl); + } + + void put(long ssl, QuicheQuicSslEngine engine) { + engines.put(ssl, engine); + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicStreamChannel.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicStreamChannel.java new file mode 100644 index 0000000..fb34ea0 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicStreamChannel.java @@ -0,0 +1,956 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelId; +import io.netty.channel.ChannelMetadata; +import io.netty.channel.ChannelOutboundBuffer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelPromise; +import io.netty.channel.DefaultChannelId; +import io.netty.channel.DefaultChannelPipeline; +import io.netty.channel.EventLoop; +import io.netty.channel.PendingWriteQueue; +import io.netty.channel.RecvByteBufAllocator; +import io.netty.channel.VoidChannelPromise; +import io.netty.channel.socket.ChannelInputShutdownEvent; +import io.netty.channel.socket.ChannelInputShutdownReadComplete; +import io.netty.channel.socket.ChannelOutputShutdownException; +import io.netty.util.DefaultAttributeMap; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.concurrent.PromiseNotifier; +import io.netty.util.internal.StringUtil; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.net.SocketAddress; +import java.nio.channels.ClosedChannelException; +import java.util.concurrent.RejectedExecutionException; + +/** + * {@link QuicStreamChannel} implementation that uses quiche. + */ +final class QuicheQuicStreamChannel extends DefaultAttributeMap implements QuicStreamChannel { + private static final ChannelMetadata METADATA = new ChannelMetadata(false, 16); + private static final InternalLogger LOGGER = InternalLoggerFactory.getInstance(QuicheQuicStreamChannel.class); + private final QuicheQuicChannel parent; + private final ChannelId id; + private final ChannelPipeline pipeline; + private final QuicStreamChannelUnsafe unsafe; + private final ChannelPromise closePromise; + private final PendingWriteQueue queue; + + private final QuicStreamChannelConfig config; + private final QuicStreamAddress address; + + private boolean readable; + private boolean readPending; + private boolean inRecv; + private boolean inWriteQueued; + private boolean finReceived; + private boolean finSent; + + private volatile boolean registered; + private volatile boolean writable = true; + private volatile boolean active = true; + private volatile boolean inputShutdown; + private volatile boolean outputShutdown; + private volatile QuicStreamPriority priority; + private volatile int capacity; + + QuicheQuicStreamChannel(QuicheQuicChannel parent, long streamId) { + this.parent = parent; + this.id = DefaultChannelId.newInstance(); + unsafe = new QuicStreamChannelUnsafe(); + this.pipeline = new DefaultChannelPipeline(this) { + // TODO: add some overrides maybe ? + }; + config = new QuicheQuicStreamChannelConfig(this); + this.address = new QuicStreamAddress(streamId); + this.closePromise = newPromise(); + queue = new PendingWriteQueue(this); + // Local created unidirectional streams have the input shutdown by spec. There will never be any data for + // these to be read. + if (parent.streamType(streamId) == QuicStreamType.UNIDIRECTIONAL && parent.isStreamLocalCreated(streamId)) { + inputShutdown = true; + } + } + + @Override + public QuicStreamAddress localAddress() { + return address; + } + + @Override + public QuicStreamAddress remoteAddress() { + return address; + } + + @Override + public boolean isLocalCreated() { + return parent().isStreamLocalCreated(streamId()); + } + + @Override + public QuicStreamType type() { + return parent().streamType(streamId()); + } + + @Override + public long streamId() { + return address.streamId(); + } + + @Override + public QuicStreamPriority priority() { + return priority; + } + + @Override + public ChannelFuture updatePriority(QuicStreamPriority priority, ChannelPromise promise) { + if (eventLoop().inEventLoop()) { + updatePriority0(priority, promise); + } else { + eventLoop().execute(() -> updatePriority0(priority, promise)); + } + return promise; + } + + private void updatePriority0(QuicStreamPriority priority, ChannelPromise promise) { + assert eventLoop().inEventLoop(); + try { + parent().streamPriority(streamId(), (byte) priority.urgency(), priority.isIncremental()); + } catch (Throwable cause) { + promise.setFailure(cause); + return; + } + this.priority = priority; + promise.setSuccess(); + } + + @Override + public boolean isInputShutdown() { + return inputShutdown; + } + + @Override + public ChannelFuture shutdownOutput(ChannelPromise promise) { + if (eventLoop().inEventLoop()) { + shutdownOutput0(promise); + } else { + eventLoop().execute(() -> shutdownOutput0(promise)); + } + return promise; + } + + private void shutdownOutput0(ChannelPromise promise) { + assert eventLoop().inEventLoop(); + outputShutdown = true; + unsafe.writeWithoutCheckChannelState(QuicStreamFrame.EMPTY_FIN, promise); + unsafe.flush(); + } + + @Override + public ChannelFuture shutdownInput(int error, ChannelPromise promise) { + if (eventLoop().inEventLoop()) { + shutdownInput0(error, promise); + } else { + eventLoop().execute(() -> shutdownInput0(error, promise)); + } + return promise; + } + + @Override + public ChannelFuture shutdownOutput(int error, ChannelPromise promise) { + if (eventLoop().inEventLoop()) { + shutdownOutput0(error, promise); + } else { + eventLoop().execute(() -> shutdownOutput0(error, promise)); + } + return promise; + } + + @Override + public QuicheQuicChannel parent() { + return parent; + } + + private void shutdownInput0(int err, ChannelPromise channelPromise) { + assert eventLoop().inEventLoop(); + inputShutdown = true; + parent().streamShutdown(streamId(), true, false, err, channelPromise); + closeIfDone(); + } + + @Override + public boolean isOutputShutdown() { + return outputShutdown; + } + + private void shutdownOutput0(int error, ChannelPromise channelPromise) { + assert eventLoop().inEventLoop(); + parent().streamShutdown(streamId(), false, true, error, channelPromise); + outputShutdown = true; + closeIfDone(); + } + + @Override + public boolean isShutdown() { + return outputShutdown && inputShutdown; + } + + @Override + public ChannelFuture shutdown(ChannelPromise channelPromise) { + if (eventLoop().inEventLoop()) { + shutdown0(channelPromise); + } else { + eventLoop().execute(() -> shutdown0(channelPromise)); + } + return channelPromise; + } + + private void shutdown0(ChannelPromise promise) { + assert eventLoop().inEventLoop(); + inputShutdown = true; + outputShutdown = true; + unsafe.writeWithoutCheckChannelState(QuicStreamFrame.EMPTY_FIN, unsafe.voidPromise()); + unsafe.flush(); + parent().streamShutdown(streamId(), true, false, 0, promise); + closeIfDone(); + } + + @Override + public ChannelFuture shutdown(int error, ChannelPromise promise) { + if (eventLoop().inEventLoop()) { + shutdown0(error, promise); + } else { + eventLoop().execute(() -> shutdown0(error, promise)); + } + return promise; + } + + private void shutdown0(int error, ChannelPromise channelPromise) { + assert eventLoop().inEventLoop(); + inputShutdown = true; + outputShutdown = true; + parent().streamShutdown(streamId(), true, true, error, channelPromise); + closeIfDone(); + } + + private void sendFinIfNeeded() throws Exception { + if (!finSent) { + finSent = true; + parent().streamSendFin(streamId()); + } + } + + private void closeIfDone() { + if (finSent && (finReceived || type() == QuicStreamType.UNIDIRECTIONAL && isLocalCreated())) { + unsafe().close(unsafe().voidPromise()); + } + } + + private void removeStreamFromParent() { + if (!active && finReceived) { + parent().streamClosed(streamId()); + inputShutdown = true; + outputShutdown = true; + } + } + + @Override + public QuicStreamChannel flush() { + pipeline.flush(); + return this; + } + + @Override + public QuicStreamChannel read() { + pipeline.read(); + return this; + } + + @Override + public QuicStreamChannelConfig config() { + return config; + } + + @Override + public boolean isOpen() { + return active; + } + + @Override + public boolean isActive() { + return isOpen(); + } + + @Override + public ChannelMetadata metadata() { + return METADATA; + } + + @Override + public ChannelId id() { + return id; + } + + @Override + public EventLoop eventLoop() { + return parent.eventLoop(); + } + + @Override + public boolean isRegistered() { + return registered; + } + + @Override + public ChannelFuture closeFuture() { + return closePromise; + } + + @Override + public boolean isWritable() { + return writable; + } + + @Override + public long bytesBeforeUnwritable() { + // Capacity might be negative if the stream was closed. + return Math.max(capacity, 0); + } + + @Override + public long bytesBeforeWritable() { + if (writable) { + return 0; + } + // Just return something positive for now + return 8; + } + + @Override + public Unsafe unsafe() { + return unsafe; + } + + @Override + public ChannelPipeline pipeline() { + return pipeline; + } + + @Override + public ByteBufAllocator alloc() { + return config.getAllocator(); + } + + @Override + public int compareTo(Channel o) { + return id.compareTo(o.id()); + } + + /** + * Returns the ID of this channel. + */ + @Override + public int hashCode() { + return id.hashCode(); + } + + /** + * Returns {@code true} if and only if the specified object is identical + * with this channel (i.e: {@code this == o}). + */ + @Override + public boolean equals(Object o) { + return this == o; + } + + @Override + public String toString() { + return "[id: 0x" + id.asShortText() + ", " + address + "]"; + } + + /** + * Stream is writable. + */ + boolean writable(@SuppressWarnings("unused") int capacity) { + assert eventLoop().inEventLoop(); + this.capacity = capacity; + boolean mayNeedWrite = ((QuicStreamChannelUnsafe) unsafe()).writeQueued(); + // we need to re-read this.capacity as writeQueued() may update the capacity. + updateWritabilityIfNeeded(this.capacity > 0); + return mayNeedWrite; + } + + private void updateWritabilityIfNeeded(boolean newWritable) { + if (writable != newWritable) { + writable = newWritable; + pipeline.fireChannelWritabilityChanged(); + } + } + + /** + * Stream is readable. + */ + void readable() { + assert eventLoop().inEventLoop(); + // Mark as readable and if a read is pending execute it. + readable = true; + if (readPending) { + ((QuicStreamChannelUnsafe) unsafe()).recv(); + } + } + + void forceClose() { + assert eventLoop().inEventLoop(); + // Set received to true to ensure we will remove it from the internal map once we send the fin. + finSent = true; + unsafe().close(unsafe().voidPromise()); + } + + private final class QuicStreamChannelUnsafe implements Unsafe { + private RecvByteBufAllocator.Handle recvHandle; + + private final ChannelPromise voidPromise = new VoidChannelPromise( + QuicheQuicStreamChannel.this, false); + @Override + public void connect(SocketAddress remote, SocketAddress local, ChannelPromise promise) { + assert eventLoop().inEventLoop(); + promise.setFailure(new UnsupportedOperationException()); + } + + @SuppressWarnings("deprecation") + @Override + public RecvByteBufAllocator.Handle recvBufAllocHandle() { + if (recvHandle == null) { + recvHandle = config.getRecvByteBufAllocator().newHandle(); + } + return recvHandle; + } + + @Override + public SocketAddress localAddress() { + return address; + } + + @Override + public SocketAddress remoteAddress() { + return address; + } + + @Override + public void register(EventLoop eventLoop, ChannelPromise promise) { + assert eventLoop.inEventLoop(); + if (registered) { + promise.setFailure(new IllegalStateException()); + return; + } + if (eventLoop != parent.eventLoop()) { + promise.setFailure(new IllegalArgumentException()); + return; + } + registered = true; + promise.setSuccess(); + pipeline.fireChannelRegistered(); + pipeline.fireChannelActive(); + } + + @Override + public void bind(SocketAddress localAddress, ChannelPromise promise) { + assert eventLoop().inEventLoop(); + promise.setFailure(new UnsupportedOperationException()); + } + + @Override + public void disconnect(ChannelPromise promise) { + assert eventLoop().inEventLoop(); + close(promise); + } + + @Override + public void close(ChannelPromise promise) { + assert eventLoop().inEventLoop(); + if (!active || closePromise.isDone()) { + if (promise.isVoid()) { + return; + } + closePromise.addListener(new PromiseNotifier<>(promise)); + return; + } + active = false; + try { + // Close the channel and fail the queued messages in all cases. + sendFinIfNeeded(); + } catch (Exception ignore) { + // Just ignore + } finally { + if (!queue.isEmpty()) { + // Only fail if the queue is non-empty. + queue.removeAndFailAll(new ClosedChannelException()); + } + + promise.trySuccess(); + closePromise.trySuccess(); + if (type() == QuicStreamType.UNIDIRECTIONAL && isLocalCreated()) { + inputShutdown = true; + outputShutdown = true; + // If its an unidirectional stream and was created locally it is safe to close the stream now as + // we will never receive data from the other side. + parent().streamClosed(streamId()); + } else { + removeStreamFromParent(); + } + } + if (inWriteQueued) { + invokeLater(() -> deregister(voidPromise(), true)); + } else { + deregister(voidPromise(), true); + } + } + + private void deregister(final ChannelPromise promise, final boolean fireChannelInactive) { + assert eventLoop().inEventLoop(); + if (!promise.setUncancellable()) { + return; + } + + if (!registered) { + promise.trySuccess(); + return; + } + + // As a user may call deregister() from within any method while doing processing in the ChannelPipeline, + // we need to ensure we do the actual deregister operation later. This is needed as for example, + // we may be in the ByteToMessageDecoder.callDecode(...) method and so still try to do processing in + // the old EventLoop while the user already registered the Channel to a new EventLoop. Without delay, + // the deregister operation this could lead to have a handler invoked by different EventLoop and so + // threads. + // + // See: + // https://github.com/netty/netty/issues/4435 + invokeLater(() -> { + if (fireChannelInactive) { + pipeline.fireChannelInactive(); + } + // Some transports like local and AIO does not allow the deregistration of + // an open channel. Their doDeregister() calls close(). Consequently, + // close() calls deregister() again - no need to fire channelUnregistered, so check + // if it was registered. + if (registered) { + registered = false; + pipeline.fireChannelUnregistered(); + } + promise.setSuccess(); + }); + } + + private void invokeLater(Runnable task) { + try { + // This method is used by outbound operation implementations to trigger an inbound event later. + // They do not trigger an inbound event immediately because an outbound operation might have been + // triggered by another inbound event handler method. If fired immediately, the call stack + // will look like this for example: + // + // handlerA.inboundBufferUpdated() - (1) an inbound handler method closes a connection. + // -> handlerA.ctx.close() + // -> channel.unsafe.close() + // -> handlerA.channelInactive() - (2) another inbound handler method called while in (1) yet + // + // which means the execution of two inbound handler methods of the same handler overlap undesirably. + eventLoop().execute(task); + } catch (RejectedExecutionException e) { + LOGGER.warn("Can't invoke task later as EventLoop rejected it", e); + } + } + + @Override + public void closeForcibly() { + assert eventLoop().inEventLoop(); + close(unsafe().voidPromise()); + } + + @Override + public void deregister(ChannelPromise promise) { + assert eventLoop().inEventLoop(); + deregister(promise, false); + } + + @Override + public void beginRead() { + assert eventLoop().inEventLoop(); + readPending = true; + if (readable) { + ((QuicStreamChannelUnsafe) unsafe()).recv(); + + // As the stream was readable, and we called recv() ourselves we also need to call + // connectionSendAndFlush(). This is needed as recv() might consume data and so a window update + // frame might be produced. If we miss to call connectionSendAndFlush() we might never send the update + // to the remote peer and so the remote peer might never attempt to send more data. + // See also https://docs.rs/quiche/latest/quiche/struct.Connection.html#method.send. + parent().connectionSendAndFlush(); + } + } + + private void closeIfNeeded(boolean wasFinSent) { + // Let's check if we should close the channel now. + // If it's a unidirectional channel we can close it as there will be no fin that we can read + // from the remote peer. If its an bidirectional channel we should only close the channel if we + // also received the fin from the remote peer. + if (!wasFinSent && QuicheQuicStreamChannel.this.finSent + && (type() == QuicStreamType.UNIDIRECTIONAL || finReceived)) { + // close the channel now + close(voidPromise()); + } + } + + boolean writeQueued() { + assert eventLoop().inEventLoop(); + boolean wasFinSent = QuicheQuicStreamChannel.this.finSent; + inWriteQueued = true; + try { + if (queue.isEmpty()) { + return false; + } + boolean written = false; + for (;;) { + Object msg = queue.current(); + if (msg == null) { + break; + } + try { + if (!write0(msg)) { + return written; + } + } catch (Exception e) { + if (e instanceof QuicException && ( + (QuicException) e).error() == QuicError.STREAM_STOPPED) { + // Once its signaled that the stream is stopped we can just fail everything. + queue.removeAndFailAll(e); + forceClose(); + break; + } + queue.remove().setFailure(e); + continue; + } + queue.remove().setSuccess(); + written = true; + } + updateWritabilityIfNeeded(true); + return written; + } finally { + closeIfNeeded(wasFinSent); + inWriteQueued = false; + } + } + + @Override + public void write(Object msg, ChannelPromise promise) { + assert eventLoop().inEventLoop(); + + // Check first if the Channel is in a state in which it will accept writes, if not fail everything + // with the right exception + if (!isOpen()) { + queueAndFailAll(msg, promise, new ClosedChannelException()); + } else if (finSent) { + queueAndFailAll(msg, promise, new ChannelOutputShutdownException("Fin was sent already")); + } else { + writeWithoutCheckChannelState(msg, promise); + } + } + + private void queueAndFailAll(Object msg, ChannelPromise promise, Throwable cause) { + queue.add(msg, promise); + queue.removeAndFailAll(cause); + } + + void writeWithoutCheckChannelState(Object msg, ChannelPromise promise) { + if (msg instanceof ByteBuf) { + ByteBuf buffer = (ByteBuf) msg; + if (!buffer.isDirect()) { + ByteBuf tmpBuffer = alloc().directBuffer(buffer.readableBytes()); + tmpBuffer.writeBytes(buffer, buffer.readerIndex(), buffer.readableBytes()); + buffer.release(); + msg = tmpBuffer; + } + } else if (msg instanceof QuicStreamFrame) { + QuicStreamFrame frame = (QuicStreamFrame) msg; + ByteBuf buffer = frame.content(); + if (!buffer.isDirect()) { + ByteBuf tmpBuffer = alloc().directBuffer(buffer.readableBytes()); + tmpBuffer.writeBytes(buffer, buffer.readerIndex(), buffer.readableBytes()); + buffer.release(); + msg = frame.replace(tmpBuffer); + } + } else { + ReferenceCountUtil.release(msg); + promise.setFailure(new UnsupportedOperationException( + "unsupported message type: " + StringUtil.simpleClassName(msg))); + return; + } + + boolean wasFinSent = QuicheQuicStreamChannel.this.finSent; + boolean mayNeedWritabilityUpdate = false; + try { + if (write0(msg)) { + ReferenceCountUtil.release(msg); + promise.setSuccess(); + mayNeedWritabilityUpdate = capacity == 0; + } else { + queue.add(msg, promise); + mayNeedWritabilityUpdate = true; + } + } catch (Exception e) { + ReferenceCountUtil.release(msg); + promise.setFailure(e); + mayNeedWritabilityUpdate = capacity == 0; + } finally { + if (mayNeedWritabilityUpdate) { + updateWritabilityIfNeeded(false); + } + closeIfNeeded(wasFinSent); + } + } + + private boolean write0(Object msg) throws Exception { + if (type() == QuicStreamType.UNIDIRECTIONAL && !isLocalCreated()) { + throw new UnsupportedOperationException( + "Writes on non-local created streams that are unidirectional are not supported"); + } + if (finSent) { + throw new ChannelOutputShutdownException("Fin was sent already"); + } + + final boolean fin; + ByteBuf buffer; + if (msg instanceof ByteBuf) { + fin = false; + buffer = (ByteBuf) msg; + } else { + QuicStreamFrame frame = (QuicStreamFrame) msg; + fin = frame.hasFin(); + buffer = frame.content(); + } + + boolean readable = buffer.isReadable(); + if (!fin && !readable) { + return true; + } + + boolean sendSomething = false; + try { + do { + int res = parent().streamSend(streamId(), buffer, fin); + + // Update the capacity as well. + int cap = parent.streamCapacity(streamId()); + if (cap >= 0) { + capacity = cap; + } + if (Quiche.throwIfError(res) || (readable && res == 0)) { + return false; + } + sendSomething = true; + buffer.skipBytes(res); + } while (buffer.isReadable()); + + if (fin) { + finSent = true; + outputShutdown = true; + } + return true; + } finally { + // As we called quiche_conn_stream_send(...) we need to ensure we will call quiche_conn_send(...) either + // now or we will do so once we see the channelReadComplete event. + // + // See https://docs.rs/quiche/0.6.0/quiche/struct.Connection.html#method.send + if (sendSomething) { + parent.connectionSendAndFlush(); + } + } + } + + @Override + public void flush() { + assert eventLoop().inEventLoop(); + // NOOP. + } + + @Override + public ChannelPromise voidPromise() { + assert eventLoop().inEventLoop(); + return voidPromise; + } + + @Override + public ChannelOutboundBuffer outboundBuffer() { + return null; + } + + private void closeOnRead(ChannelPipeline pipeline, boolean readFrames) { + if (readFrames && finReceived && finSent) { + close(voidPromise()); + } else if (config.isAllowHalfClosure()) { + if (finReceived) { + // If we receive a fin there will be no more data to read so we need to fire both events + // to be consistent with other transports. + pipeline.fireUserEventTriggered(ChannelInputShutdownEvent.INSTANCE); + pipeline.fireUserEventTriggered(ChannelInputShutdownReadComplete.INSTANCE); + if (finSent) { + // This was an unidirectional stream which means as soon as we received FIN and sent a FIN + // we need close the connection. + close(voidPromise()); + } + } + } else { + // This was an unidirectional stream which means as soon as we received FIN we need + // close the connection. + close(voidPromise()); + } + } + + private void handleReadException(ChannelPipeline pipeline, ByteBuf byteBuf, Throwable cause, + @SuppressWarnings("deprecation") RecvByteBufAllocator.Handle allocHandle, + boolean readFrames) { + if (byteBuf != null) { + if (byteBuf.isReadable()) { + pipeline.fireChannelRead(byteBuf); + } else { + byteBuf.release(); + } + } + + readComplete(allocHandle, pipeline); + pipeline.fireExceptionCaught(cause); + if (finReceived) { + closeOnRead(pipeline, readFrames); + } + } + + void recv() { + assert eventLoop().inEventLoop(); + if (inRecv) { + // As the use may call read() we need to guard against re-entrancy here as otherwise it could + // be possible that we re-enter this method while still processing it. + return; + } + + inRecv = true; + try { + ChannelPipeline pipeline = pipeline(); + QuicheQuicStreamChannelConfig config = (QuicheQuicStreamChannelConfig) config(); + // Directly access the DirectIoByteBufAllocator as we need an direct buffer to read into in all cases + // even if there is no Unsafe present and the direct buffer is not pooled. + DirectIoByteBufAllocator allocator = config.allocator; + @SuppressWarnings("deprecation") + RecvByteBufAllocator.Handle allocHandle = this.recvBufAllocHandle(); + boolean readFrames = config.isReadFrames(); + + // We should loop as long as a read() was requested and there is anything left to read, which means the + // stream was marked as readable before. + while (active && readPending && readable) { + allocHandle.reset(config); + ByteBuf byteBuf = null; + QuicheQuicChannel parent = parent(); + // It's possible that the stream was marked as finish while we iterated over the readable streams + // or while we did have auto read disabled. If so we need to ensure we not try to read from it as it + // would produce an error. + boolean readCompleteNeeded = false; + boolean continueReading = true; + try { + while (!finReceived && continueReading) { + byteBuf = allocHandle.allocate(allocator); + allocHandle.attemptedBytesRead(byteBuf.writableBytes()); + switch (parent.streamRecv(streamId(), byteBuf)) { + case DONE: + // Nothing left to read; + readable = false; + break; + case FIN: + // If we received a FIN we also should mark the channel as non readable as + // there is nothing left to read really. + readable = false; + finReceived = true; + inputShutdown = true; + break; + case OK: + break; + default: + throw new Error(); + } + allocHandle.lastBytesRead(byteBuf.readableBytes()); + if (allocHandle.lastBytesRead() <= 0) { + byteBuf.release(); + if (finReceived && readFrames) { + // If we read QuicStreamFrames we should fire an frame through the pipeline + // with an empty buffer but the fin flag set to true. + byteBuf = Unpooled.EMPTY_BUFFER; + } else { + byteBuf = null; + break; + } + } + // We did read one message. + allocHandle.incMessagesRead(1); + readCompleteNeeded = true; + + // It's important that we reset this to false before we call fireChannelRead(...) + // as the user may request another read() from channelRead(...) callback. + readPending = false; + + if (readFrames) { + pipeline.fireChannelRead(new DefaultQuicStreamFrame(byteBuf, finReceived)); + } else { + pipeline.fireChannelRead(byteBuf); + } + byteBuf = null; + continueReading = allocHandle.continueReading(); + } + + if (readCompleteNeeded) { + readComplete(allocHandle, pipeline); + } + if (finReceived) { + readable = false; + closeOnRead(pipeline, readFrames); + } + } catch (Throwable cause) { + readable = false; + handleReadException(pipeline, byteBuf, cause, allocHandle, readFrames); + } + } + } finally { + // About to leave the method lets reset so we can enter it again. + inRecv = false; + removeStreamFromParent(); + } + } + + // Read was complete and something was read, so we we need to reset the readPending flags, the allocHandle + // and call fireChannelReadComplete(). The user may schedule another read now. + private void readComplete(@SuppressWarnings("deprecation") RecvByteBufAllocator.Handle allocHandle, + ChannelPipeline pipeline) { + allocHandle.readComplete(); + pipeline.fireChannelReadComplete(); + } + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicStreamChannelConfig.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicStreamChannelConfig.java new file mode 100644 index 0000000..917a730 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicStreamChannelConfig.java @@ -0,0 +1,175 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.ChannelOption; +import io.netty.channel.DefaultChannelConfig; +import io.netty.channel.MessageSizeEstimator; +import io.netty.channel.RecvByteBufAllocator; +import io.netty.channel.WriteBufferWaterMark; + +import java.util.Map; + +final class QuicheQuicStreamChannelConfig extends DefaultChannelConfig implements QuicStreamChannelConfig { + // We should use half-closure sementatics by default as this is what QUIC does by default. + // If you receive a FIN you should still keep the stream open until you write a FIN as well. + private volatile boolean allowHalfClosure = true; + private volatile boolean readFrames; + volatile DirectIoByteBufAllocator allocator; + + QuicheQuicStreamChannelConfig(QuicStreamChannel channel) { + super(channel); + allocator = new DirectIoByteBufAllocator(super.getAllocator()); + } + + @Override + public Map, Object> getOptions() { + if (isHalfClosureSupported()) { + return getOptions(super.getOptions(), ChannelOption.ALLOW_HALF_CLOSURE, QuicChannelOption.READ_FRAMES); + } + return super.getOptions(); + } + + @SuppressWarnings("unchecked") + @Override + public T getOption(ChannelOption option) { + if (option == ChannelOption.ALLOW_HALF_CLOSURE) { + return (T) Boolean.valueOf(isAllowHalfClosure()); + } + if (option == QuicChannelOption.READ_FRAMES) { + return (T) Boolean.valueOf(isReadFrames()); + } + return super.getOption(option); + } + + @Override + public boolean setOption(ChannelOption option, T value) { + validate(option, value); + + if (option == ChannelOption.ALLOW_HALF_CLOSURE) { + if (isHalfClosureSupported()) { + setAllowHalfClosure((Boolean) value); + return true; + } + return false; + } + if (option == QuicChannelOption.READ_FRAMES) { + setReadFrames((Boolean) value); + } + return super.setOption(option, value); + } + + @Override + public QuicStreamChannelConfig setReadFrames(boolean readFrames) { + this.readFrames = readFrames; + return this; + } + + @Override + public boolean isReadFrames() { + return readFrames; + } + + @Override + public QuicStreamChannelConfig setConnectTimeoutMillis(int connectTimeoutMillis) { + super.setConnectTimeoutMillis(connectTimeoutMillis); + return this; + } + + @Override + public QuicStreamChannelConfig setMaxMessagesPerRead(int maxMessagesPerRead) { + super.setMaxMessagesPerRead(maxMessagesPerRead); + return this; + } + + @Override + public QuicStreamChannelConfig setWriteSpinCount(int writeSpinCount) { + super.setWriteSpinCount(writeSpinCount); + return this; + } + + @Override + public QuicStreamChannelConfig setAllocator(ByteBufAllocator allocator) { + this.allocator = new DirectIoByteBufAllocator(allocator); + return this; + } + + @Override + public QuicStreamChannelConfig setRecvByteBufAllocator(RecvByteBufAllocator allocator) { + super.setRecvByteBufAllocator(allocator); + return this; + } + + @Override + public QuicStreamChannelConfig setAutoRead(boolean autoRead) { + super.setAutoRead(autoRead); + return this; + } + + @Override + public QuicStreamChannelConfig setAutoClose(boolean autoClose) { + super.setAutoClose(autoClose); + return this; + } + + @Override + public QuicStreamChannelConfig setWriteBufferHighWaterMark(int writeBufferHighWaterMark) { + super.setWriteBufferHighWaterMark(writeBufferHighWaterMark); + return this; + } + + @Override + public QuicStreamChannelConfig setWriteBufferLowWaterMark(int writeBufferLowWaterMark) { + super.setWriteBufferLowWaterMark(writeBufferLowWaterMark); + return this; + } + + @Override + public QuicStreamChannelConfig setWriteBufferWaterMark(WriteBufferWaterMark writeBufferWaterMark) { + super.setWriteBufferWaterMark(writeBufferWaterMark); + return this; + } + + @Override + public QuicStreamChannelConfig setMessageSizeEstimator(MessageSizeEstimator estimator) { + super.setMessageSizeEstimator(estimator); + return this; + } + + @Override + public QuicStreamChannelConfig setAllowHalfClosure(boolean allowHalfClosure) { + if (!isHalfClosureSupported()) { + throw new UnsupportedOperationException("Undirectional streams don't support half-closure"); + } + this.allowHalfClosure = allowHalfClosure; + return this; + } + + @Override + public ByteBufAllocator getAllocator() { + return allocator.wrapped(); + } + + @Override + public boolean isAllowHalfClosure() { + return allowHalfClosure; + } + + private boolean isHalfClosureSupported() { + return ((QuicStreamChannel) channel).type() == QuicStreamType.BIDIRECTIONAL; + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicTransportParameters.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicTransportParameters.java new file mode 100644 index 0000000..2546db6 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheQuicTransportParameters.java @@ -0,0 +1,110 @@ +/* + * Copyright 2023 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.util.internal.StringUtil; + +final class QuicheQuicTransportParameters implements QuicTransportParameters { + private final long[] values; + + QuicheQuicTransportParameters(long[] values) { + this.values = values; + } + + @Override + public long maxIdleTimeout() { + return values[0]; + } + + @Override + public long maxUdpPayloadSize() { + return values[1]; + } + + @Override + public long initialMaxData() { + return values[2]; + } + + @Override + public long initialMaxStreamDataBidiLocal() { + return values[3]; + } + + @Override + public long initialMaxStreamDataBidiRemote() { + return values[4]; + } + + @Override + public long initialMaxStreamDataUni() { + return values[5]; + } + + @Override + public long initialMaxStreamsBidi() { + return values[6]; + } + + @Override + public long initialMaxStreamsUni() { + return values[7]; + } + + @Override + public long ackDelayExponent() { + return values[8]; + } + + @Override + public long maxAckDelay() { + return values[9]; + } + + @Override + public boolean disableActiveMigration() { + return values[10] == 1; + } + + @Override + public long activeConnIdLimit() { + return values[11]; + } + + @Override + public long maxDatagramFrameSize() { + return values[12]; + } + + @Override + public String toString() { + return StringUtil.simpleClassName(this) + "[" + + "maxIdleTimeout=" + maxIdleTimeout() + + ", maxUdpPayloadSize=" + maxUdpPayloadSize() + + ", initialMaxData=" + initialMaxData() + + ", initialMaxStreamDataBidiLocal=" + initialMaxStreamDataBidiLocal() + + ", initialMaxStreamDataBidiRemote=" + initialMaxStreamDataBidiRemote() + + ", initialMaxStreamDataUni=" + initialMaxStreamDataUni() + + ", initialMaxStreamsBidi=" + initialMaxStreamsBidi() + + ", initialMaxStreamsUni=" + initialMaxStreamsUni() + + ", ackDelayExponent=" + ackDelayExponent() + + ", maxAckDelay=" + maxAckDelay() + + ", disableActiveMigration=" + disableActiveMigration() + + ", activeConnIdLimit=" + activeConnIdLimit() + + ", maxDatagramFrameSize=" + maxDatagramFrameSize() + + "]"; + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheRecvInfo.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheRecvInfo.java new file mode 100644 index 0000000..40d7935 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheRecvInfo.java @@ -0,0 +1,85 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; + + +/** + * Utility class to handle access to {@code quiche_recv_info}. + */ +final class QuicheRecvInfo { + + private QuicheRecvInfo() { } + + /** + * Set the {@link InetSocketAddress} into the {@code quiche_recv_info} struct. + * + *
+     * typedef struct {
+     *     struct sockaddr *from;
+     *     socklen_t from_len;
+     *     struct sockaddr *to;
+     *     socklen_t to_len;
+     * } quiche_recv_info;
+     * 
+ * + * @param memory the memory of {@code quiche_recv_info}. + * @param from the {@link InetSocketAddress} to write into {@code quiche_recv_info}. + * @param to the {@link InetSocketAddress} to write into {@code quiche_recv_info}. + */ + static void setRecvInfo(ByteBuffer memory, InetSocketAddress from, InetSocketAddress to) { + int position = memory.position(); + try { + setAddress(memory, Quiche.SIZEOF_QUICHE_RECV_INFO, Quiche.QUICHE_RECV_INFO_OFFSETOF_FROM, Quiche.QUICHE_RECV_INFO_OFFSETOF_FROM_LEN, from); + setAddress(memory, Quiche.SIZEOF_QUICHE_RECV_INFO + Quiche.SIZEOF_SOCKADDR_STORAGE, + Quiche.QUICHE_RECV_INFO_OFFSETOF_TO, Quiche.QUICHE_RECV_INFO_OFFSETOF_TO_LEN, to); + } finally { + memory.position(position); + } + } + + private static void setAddress(ByteBuffer memory, int socketAddressOffset, int addrOffset, int lenOffset, InetSocketAddress address) { + int position = memory.position(); + try { + int sockaddrPosition = position +socketAddressOffset; + memory.position(sockaddrPosition); + long sockaddrMemoryAddress = Quiche.memoryAddressWithPosition(memory); + int len = SockaddrIn.setAddress(memory, address); + if (Quiche.SIZEOF_SIZE_T == 4) { + memory.putInt(position + addrOffset, (int) sockaddrMemoryAddress); + } else { + memory.putLong(position + addrOffset, sockaddrMemoryAddress); + } + Quiche.setPrimitiveValue(memory, position + lenOffset, Quiche.SIZEOF_SOCKLEN_T, len); + } finally { + memory.position(position); + } + } + + /** + * Returns {@code true} if both {@link ByteBuffer}s have the same {@code sock_addr} stored. + * + * @param memory the first {@link ByteBuffer} which holds a {@code quiche_recv_info}. + * @param memory2 the second {@link ByteBuffer} which holds a {@code quiche_recv_info}. + * @return {@code true} if both {@link ByteBuffer}s have the same {@code sock_addr} stored, {@code false} + * otherwise. + */ + static boolean isSameAddress(ByteBuffer memory, ByteBuffer memory2) { + return Quiche.isSameAddress(memory, memory2, Quiche.SIZEOF_QUICHE_RECV_INFO); + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheSendInfo.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheSendInfo.java new file mode 100644 index 0000000..b371680 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/QuicheSendInfo.java @@ -0,0 +1,167 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.util.concurrent.FastThreadLocal; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; + +/** + * Utility class to handle access to {@code quiche_send_info}. + */ +final class QuicheSendInfo { + + private static final FastThreadLocal IPV4_ARRAYS = new FastThreadLocal() { + @Override + protected byte[] initialValue() { + return new byte[SockaddrIn.IPV4_ADDRESS_LENGTH]; + } + }; + + private static final FastThreadLocal IPV6_ARRAYS = new FastThreadLocal() { + @Override + protected byte[] initialValue() { + return new byte[SockaddrIn.IPV6_ADDRESS_LENGTH]; + } + }; + + private static final byte[] TIMESPEC_ZEROOUT = new byte[Quiche.SIZEOF_TIMESPEC]; + + private QuicheSendInfo() { } + + /** + * Get the {@link InetSocketAddress} out of the {@code quiche_send_info} struct. + * + * @param memory the memory of {@code quiche_send_info}. + * @return the address that was read. + */ + static InetSocketAddress getToAddress(ByteBuffer memory) { + return getAddress(memory, Quiche.QUICHE_SEND_INFO_OFFSETOF_TO_LEN, Quiche.QUICHE_SEND_INFO_OFFSETOF_TO); + } + + static InetSocketAddress getFromAddress(ByteBuffer memory) { + return getAddress(memory, Quiche.QUICHE_SEND_INFO_OFFSETOF_FROM_LEN, Quiche.QUICHE_SEND_INFO_OFFSETOF_FROM); + } + + private static InetSocketAddress getAddress(ByteBuffer memory, int lenOffset, int addressOffset) { + int position = memory.position(); + try { + long len = getLen(memory, position + lenOffset); + + memory.position(position + addressOffset); + + if (len == Quiche.SIZEOF_SOCKADDR_IN) { + return SockaddrIn.getIPv4(memory, IPV4_ARRAYS.get()); + } + assert len == Quiche.SIZEOF_SOCKADDR_IN6; + return SockaddrIn.getIPv6(memory, IPV6_ARRAYS.get(), IPV4_ARRAYS.get()); + } finally { + memory.position(position); + } + } + + private static long getLen(ByteBuffer memory, int index) { + return Quiche.getPrimitiveValue(memory, index, Quiche.SIZEOF_SOCKLEN_T); + } + + /** + * Set the {@link InetSocketAddress} into the {@code quiche_send_info} struct. + *
+     *
+     * typedef struct {
+     *     // The local address the packet should be sent from.
+     *     struct sockaddr_storage from;
+     *     socklen_t from_len;
+     *
+     *     // The address the packet should be sent to.
+     *     struct sockaddr_storage to;
+     *     socklen_t to_len;
+     *
+     *     // The time to send the packet out.
+     *     struct timespec at;
+     * } quiche_send_info;
+     * 
+ * + * @param memory the memory of {@code quiche_send_info}. + * @param from the {@link InetSocketAddress} to write into {@code quiche_send_info}. + * @param to the {@link InetSocketAddress} to write into {@code quiche_send_info}. + */ + static void setSendInfo(ByteBuffer memory, InetSocketAddress from, InetSocketAddress to) { + int position = memory.position(); + try { + setAddress(memory, Quiche.QUICHE_SEND_INFO_OFFSETOF_FROM, Quiche.QUICHE_SEND_INFO_OFFSETOF_FROM_LEN, from); + setAddress(memory, Quiche.QUICHE_SEND_INFO_OFFSETOF_TO, Quiche.QUICHE_SEND_INFO_OFFSETOF_TO_LEN, to); + // Zero out the timespec. + memory.position(position + Quiche.QUICHE_SEND_INFO_OFFSETOF_AT); + memory.put(TIMESPEC_ZEROOUT); + } finally { + memory.position(position); + } + } + + private static void setAddress(ByteBuffer memory, int addrOffset, int lenOffset, InetSocketAddress addr) { + int position = memory.position(); + try { + memory.position(position + addrOffset); + int len = SockaddrIn.setAddress(memory, addr); + Quiche.setPrimitiveValue(memory, position + lenOffset, Quiche.SIZEOF_SOCKLEN_T, len); + } finally { + memory.position(position); + } + } + + /** + * Get the {@code timespec} from the {@code quiche_send_info} struct in nanos. + *
+     *
+     * typedef struct {
+     *     // The local address the packet should be sent from.
+     *     struct sockaddr_storage from;
+     *     socklen_t from_len;
+     *
+     *     // The address the packet should be sent to.
+     *     struct sockaddr_storage to;
+     *     socklen_t to_len;
+     *
+     *     // The time to send the packet out.
+     *     struct timespec at;
+     * } quiche_send_info;
+     * 
+ * + * @param memory the memory of {@code quiche_send_info}. + */ + static long getAtNanos(ByteBuffer memory) { + long sec = Quiche.getPrimitiveValue(memory, Quiche.QUICHE_SEND_INFO_OFFSETOF_AT + + Quiche.TIMESPEC_OFFSETOF_TV_SEC, Quiche.SIZEOF_TIME_T); + long nsec = Quiche.getPrimitiveValue(memory, Quiche.QUICHE_SEND_INFO_OFFSETOF_AT + + Quiche.TIMESPEC_OFFSETOF_TV_SEC, Quiche.SIZEOF_LONG); + return TimeUnit.SECONDS.toNanos(sec) + nsec; + } + + /** + * Returns {@code true} if both {@link ByteBuffer}s have the same {@code sockaddr_storage} stored. + * + * @param memory the first {@link ByteBuffer} which holds a {@code quiche_send_info}. + * @param memory2 the second {@link ByteBuffer} which holds a {@code quiche_send_info}. + * @return {@code true} if both {@link ByteBuffer}s have the same {@code sockaddr_storage} stored, + * {@code false} otherwise. + */ + static boolean isSameAddress(ByteBuffer memory, ByteBuffer memory2) { + return Quiche.isSameAddress(memory, memory2, Quiche.QUICHE_SEND_INFO_OFFSETOF_TO); + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/SecureRandomQuicConnectionIdGenerator.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/SecureRandomQuicConnectionIdGenerator.java new file mode 100644 index 0000000..622d798 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/SecureRandomQuicConnectionIdGenerator.java @@ -0,0 +1,53 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.util.internal.ObjectUtil; + +import java.nio.ByteBuffer; +import java.security.SecureRandom; + +final class SecureRandomQuicConnectionIdGenerator implements QuicConnectionIdGenerator { + private static final SecureRandom RANDOM = new SecureRandom(); + + static final QuicConnectionIdGenerator INSTANCE = new SecureRandomQuicConnectionIdGenerator(); + + private SecureRandomQuicConnectionIdGenerator() { + } + + @Override + public ByteBuffer newId(int length) { + ObjectUtil.checkInRange(length, 0, maxConnectionIdLength(), "length"); + byte[] bytes = new byte[length]; + RANDOM.nextBytes(bytes); + return ByteBuffer.wrap(bytes); + } + + @Override + public ByteBuffer newId(ByteBuffer buffer, int length) { + return newId(length); + } + + @Override + public int maxConnectionIdLength() { + return Quiche.QUICHE_MAX_CONN_ID_LEN; + } + + @Override + public boolean isIdempotent() { + return false; + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/SegmentedDatagramPacketAllocator.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/SegmentedDatagramPacketAllocator.java new file mode 100644 index 0000000..0784bf7 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/SegmentedDatagramPacketAllocator.java @@ -0,0 +1,63 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.socket.DatagramPacket; + +import java.net.InetSocketAddress; + +/** + * Used to allocate datagram packets that use UDP_SEGMENT (GSO). + */ +@FunctionalInterface +public interface SegmentedDatagramPacketAllocator { + + /** + * {@link SegmentedDatagramPacketAllocator} which should be used if no UDP_SEGMENT is supported and used. + */ + SegmentedDatagramPacketAllocator NONE = new SegmentedDatagramPacketAllocator() { + @Override + public int maxNumSegments() { + return 0; + } + + @Override + public DatagramPacket newPacket(ByteBuf buffer, int segmentSize, InetSocketAddress remoteAddress) { + throw new UnsupportedOperationException(); + } + }; + + /** + * The maximum number of segments to use per packet. By default this is {@code 10} but this may be overridden by + * the implementation of the interface. + * + * @return the segments. + */ + default int maxNumSegments() { + return 10; + } + + /** + * Return a new segmented {@link DatagramPacket}. + * + * @param buffer the {@link ByteBuf} that is used as content. + * @param segmentSize the size of each segment. + * @param remoteAddress the remote address to send to. + * @return the packet. + */ + DatagramPacket newPacket(ByteBuf buffer, int segmentSize, InetSocketAddress remoteAddress); +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/SockaddrIn.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/SockaddrIn.java new file mode 100644 index 0000000..9025c95 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/SockaddrIn.java @@ -0,0 +1,187 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.util.internal.PlatformDependent; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; + +final class SockaddrIn { + static final byte[] IPV4_MAPPED_IPV6_PREFIX = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xff, (byte) 0xff }; + static final int IPV4_ADDRESS_LENGTH = 4; + static final int IPV6_ADDRESS_LENGTH = 16; + static final byte[] SOCKADDR_IN6_EMPTY_ARRAY = new byte[Quiche.SIZEOF_SOCKADDR_IN6]; + static final byte[] SOCKADDR_IN_EMPTY_ARRAY = new byte[Quiche.SIZEOF_SOCKADDR_IN]; + + private SockaddrIn() { } + + static int cmp(long memory, long memory2) { + return Quiche.sockaddr_cmp(memory, memory2); + } + + static int setAddress(ByteBuffer memory, InetSocketAddress address) { + InetAddress addr = address.getAddress(); + return setAddress(addr instanceof Inet6Address, memory, address); + } + + static int setAddress(boolean ipv6, ByteBuffer memory, InetSocketAddress address) { + if (ipv6) { + return SockaddrIn.setIPv6(memory, address.getAddress(), address.getPort()); + } else { + return SockaddrIn.setIPv4(memory, address.getAddress(), address.getPort()); + } + } + + /** + * + * struct sockaddr_in { + * sa_family_t sin_family; // address family: AF_INET + * in_port_t sin_port; // port in network byte order + * struct in_addr sin_addr; // internet address + * }; + * + * // Internet address. + * struct in_addr { + * uint32_t s_addr; // address in network byte order + * }; + * + */ + static int setIPv4(ByteBuffer memory, InetAddress address, int port) { + int position = memory.position(); + try { + // memset + memory.put(SOCKADDR_IN_EMPTY_ARRAY); + + memory.putShort(position + Quiche.SOCKADDR_IN_OFFSETOF_SIN_FAMILY, Quiche.AF_INET); + memory.putShort(position + Quiche.SOCKADDR_IN_OFFSETOF_SIN_PORT, (short) port); + + byte[] bytes = address.getAddress(); + int offset = 0; + if (bytes.length == IPV6_ADDRESS_LENGTH) { + // IPV6 mapped IPV4 address, we only need the last 4 bytes. + offset = IPV4_MAPPED_IPV6_PREFIX.length; + } + assert bytes.length == offset + IPV4_ADDRESS_LENGTH; + memory.position(position + Quiche.SOCKADDR_IN_OFFSETOF_SIN_ADDR + Quiche.IN_ADDRESS_OFFSETOF_S_ADDR); + memory.put(bytes, offset, IPV4_ADDRESS_LENGTH); + return Quiche.SIZEOF_SOCKADDR_IN; + } finally { + memory.position(position); + } + } + + /** + * struct sockaddr_in6 { + * sa_family_t sin6_family; // AF_INET6 + * in_port_t sin6_port; // port number + * uint32_t sin6_flowinfo; // IPv6 flow information + * struct in6_addr sin6_addr; // IPv6 address + * uint32_t sin6_scope_id; /* Scope ID (new in 2.4) + * }; + * + * struct in6_addr { + * unsigned char s6_addr[16]; // IPv6 address + * }; + */ + static int setIPv6(ByteBuffer memory, InetAddress address, int port) { + int position = memory.position(); + try { + // memset + memory.put(SOCKADDR_IN6_EMPTY_ARRAY); + + memory.putShort(position + Quiche.SOCKADDR_IN6_OFFSETOF_SIN6_FAMILY, Quiche.AF_INET6); + memory.putShort(position + Quiche.SOCKADDR_IN6_OFFSETOF_SIN6_PORT, (short) port); + + // Skip sin6_flowinfo as we did memset before + byte[] bytes = address.getAddress(); + int offset = Quiche.SOCKADDR_IN6_OFFSETOF_SIN6_ADDR + Quiche.IN6_ADDRESS_OFFSETOF_S6_ADDR; + + if (bytes.length == IPV4_ADDRESS_LENGTH) { + memory.position(position + offset); + memory.put(IPV4_MAPPED_IPV6_PREFIX); + + memory.position(position + offset + IPV4_MAPPED_IPV6_PREFIX.length); + memory.put(bytes, 0, IPV4_ADDRESS_LENGTH); + + // Skip sin6_scope_id as we did memset before + } else { + memory.position(position + offset); + memory.put(bytes, 0, IPV6_ADDRESS_LENGTH); + + memory.putInt(position + Quiche.SOCKADDR_IN6_OFFSETOF_SIN6_SCOPE_ID, + ((Inet6Address) address).getScopeId()); + } + return Quiche.SIZEOF_SOCKADDR_IN6; + } finally { + memory.position(position); + } + } + + static InetSocketAddress getIPv4(ByteBuffer memory, byte[] tmpArray) { + assert tmpArray.length == IPV4_ADDRESS_LENGTH; + int position = memory.position(); + + try { + int port = memory.getShort(position + Quiche.SOCKADDR_IN_OFFSETOF_SIN_PORT) & 0xFFFF; + memory.position(position + Quiche.SOCKADDR_IN_OFFSETOF_SIN_ADDR + Quiche.IN_ADDRESS_OFFSETOF_S_ADDR); + memory.get(tmpArray); + try { + return new InetSocketAddress(InetAddress.getByAddress(tmpArray), port); + } catch (UnknownHostException ignore) { + return null; + } + } finally { + memory.position(position); + } + } + + static InetSocketAddress getIPv6(ByteBuffer memory, byte[] ipv6Array, byte[] ipv4Array) { + assert ipv6Array.length == IPV6_ADDRESS_LENGTH; + assert ipv4Array.length == IPV4_ADDRESS_LENGTH; + int position = memory.position(); + + try { + int port = memory.getShort( + position + Quiche.SOCKADDR_IN6_OFFSETOF_SIN6_PORT) & 0xFFFF; + memory.position(position + Quiche.SOCKADDR_IN6_OFFSETOF_SIN6_ADDR + Quiche.IN6_ADDRESS_OFFSETOF_S6_ADDR); + memory.get(ipv6Array); + if (PlatformDependent.equals( + ipv6Array, 0, IPV4_MAPPED_IPV6_PREFIX, 0, IPV4_MAPPED_IPV6_PREFIX.length)) { + System.arraycopy(ipv6Array, IPV4_MAPPED_IPV6_PREFIX.length, ipv4Array, 0, IPV4_ADDRESS_LENGTH); + try { + return new InetSocketAddress(Inet4Address.getByAddress(ipv4Array), port); + } catch (UnknownHostException ignore) { + return null; + } + } else { + int scopeId = memory.getInt(position + Quiche.SOCKADDR_IN6_OFFSETOF_SIN6_SCOPE_ID); + try { + return new InetSocketAddress(Inet6Address.getByAddress(null, ipv6Array, scopeId), port); + } catch (UnknownHostException ignore) { + return null; + } + } + } finally { + memory.position(position); + } + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/SslEarlyDataReadyEvent.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/SslEarlyDataReadyEvent.java new file mode 100644 index 0000000..be76406 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/SslEarlyDataReadyEvent.java @@ -0,0 +1,32 @@ +/* + * Copyright 2023 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + + +/** + * Event which is fired once it's possible to send early data on the client-side. + * See RFC8446 4.2.10 Early Data Indication. + *

+ * Users might call {@link io.netty.channel.Channel#writeAndFlush(Object)} or + * {@link io.netty.channel.ChannelHandlerContext#writeAndFlush(Object)} to send early data. + * Please be aware that early data may be replay-able and so may have other security concerns then other data. + */ +public final class SslEarlyDataReadyEvent { + + static final SslEarlyDataReadyEvent INSTANCE = new SslEarlyDataReadyEvent(); + + private SslEarlyDataReadyEvent() { } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/SslSessionTicketKey.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/SslSessionTicketKey.java new file mode 100644 index 0000000..ce1a42d --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/SslSessionTicketKey.java @@ -0,0 +1,129 @@ +/* + * Copyright 2023 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.netty.handler.codec.quic; + +import java.util.Arrays; + +/** + * Session Ticket Key + */ +public final class SslSessionTicketKey { + /** + * Size of session ticket key name + */ + public static final int NAME_SIZE = 16; + /** + * Size of session ticket key HMAC key + */ + public static final int HMAC_KEY_SIZE = 16; + /** + * Size of session ticket key AES key + */ + public static final int AES_KEY_SIZE = 16; + /** + * Size of session ticket key + */ + public static final int TICKET_KEY_SIZE = NAME_SIZE + HMAC_KEY_SIZE + AES_KEY_SIZE; + + // package private so we can access these in BoringSSLSessionTicketCallback without calling clone() on the byte[]. + final byte[] name; + final byte[] hmacKey; + final byte[] aesKey; + + /** + * Construct SessionTicketKey. + * @param name the name of the session ticket key + * @param hmacKey the HMAC key of the session ticket key + * @param aesKey the AES key of the session ticket key + */ + public SslSessionTicketKey(byte[] name, byte[] hmacKey, byte[] aesKey) { + if (name == null || name.length != NAME_SIZE) { + throw new IllegalArgumentException("Length of name must be " + NAME_SIZE); + } + if (hmacKey == null || hmacKey.length != HMAC_KEY_SIZE) { + throw new IllegalArgumentException("Length of hmacKey must be " + HMAC_KEY_SIZE); + } + if (aesKey == null || aesKey.length != AES_KEY_SIZE) { + throw new IllegalArgumentException("Length of aesKey must be " + AES_KEY_SIZE); + } + this.name = name.clone(); + this.hmacKey = hmacKey.clone(); + this.aesKey = aesKey.clone(); + } + + /** + * Get name. + * + * @return the name of the session ticket key + */ + public byte[] name() { + return name.clone(); + } + + /** + * Get HMAC key. + * @return the HMAC key of the session ticket key + */ + public byte[] hmacKey() { + return hmacKey.clone(); + } + + /** + * Get AES Key. + * @return the AES key of the session ticket key + */ + public byte[] aesKey() { + return aesKey.clone(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + SslSessionTicketKey that = (SslSessionTicketKey) o; + + if (!Arrays.equals(name, that.name)) { + return false; + } + if (!Arrays.equals(hmacKey, that.hmacKey)) { + return false; + } + return Arrays.equals(aesKey, that.aesKey); + } + + @Override + public int hashCode() { + int result = Arrays.hashCode(name); + result = 31 * result + Arrays.hashCode(hmacKey); + result = 31 * result + Arrays.hashCode(aesKey); + return result; + } + + @Override + public String toString() { + return "SessionTicketKey{" + + "name=" + Arrays.toString(name) + + ", hmacKey=" + Arrays.toString(hmacKey) + + ", aesKey=" + Arrays.toString(aesKey) + + '}'; + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/package-info.java b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/package-info.java new file mode 100644 index 0000000..07a45ed --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/handler/codec/quic/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +/** + * QUIC implementation + */ +package io.netty.handler.codec.quic; diff --git a/codec-native-quic/pom.xml b/codec-native-quic/pom.xml new file mode 100644 index 0000000..32438cf --- /dev/null +++ b/codec-native-quic/pom.xml @@ -0,0 +1,1239 @@ + + + + 4.0.0 + + org.xbib.netty + netty-handler-codec-parent-quic + 0.0.56.Final-SNAPSHOT + + + netty-handler-codec-native-quic + 0.0.56.Final-SNAPSHOT + Netty/Handler/Codec/Native/Quic + ${packaging.type} + + + ${os.detected.name}.${os.detected.arch} + ${javaModuleName}.${javaModuleNameClassifier} + io.netty.handler.codec.quic + io.netty.handler.netty-handler-codec-classes-quic + ${project.basedir}/src/main/c + ${project.build.directory}/native-lib-only + false + jar + ${os.detected.name}-${os.detected.arch} + netty_quiche_${os.detected.name}_${os.detected.arch} + ${project.build.directory}/netty-jni-util/ + ${project.build.directory}/boringssl-source + ${boringsslSourceDir}/build-target + ${project.build.directory}/boringssl + ${boringsslHomeDir}/build + ${boringsslHomeDir}/include + https://boringssl.googlesource.com/boringssl + + chromium-stable + dd5219451c3ce26221762a15d867edf43b463bb2 + + ${project.build.directory}/quiche-source + ${quicheSourceDir}/target/release + ${project.build.directory}/quiche + ${quicheHomeDir}/build + ${quicheHomeDir}/quiche/include + https://github.com/cloudflare/quiche + master + 5b1e3d286411e2cc411f8dc6d6ff1ee40f1bd026 + ${project.build.directory}/generated-sources + ${project.build.directory}/template + + + + + + + + + + + + + + + + + windows + + + windows + + + + ${boringsslHomeDir}/build/RelWithDebInfo + x86_64-pc-windows-msvc + --target=${quicheTarget} + ${quicheSourceDir}/target/${quicheTarget}/release + + + /MT + + /MT /wd4091 + ssl.lib + crypto.lib + quiche.lib + META-INF/native/${jniLibName}.dll;osname=win32;processor=${os.detected.arch} + + + + mac + + + mac + + + + + 10.12 + + -Wa,--noexecstack -mmacosx-version-min=${macosxDeploymentTarget} + -mmacosx-version-min=${macosxDeploymentTarget} + ${extraCflags} -O3 -fno-omit-frame-pointer + + ${extraCxxflags} -O3 -fno-omit-frame-pointer -Wno-error=range-loop-analysis + libssl.a + libcrypto.a + libquiche.a + -platform_version,macos,${macosxDeploymentTarget},${macosxDeploymentTarget} -mmacosx-version-min=${macosxDeploymentTarget} + MACOSX_DEPLOYMENT_TARGET=${macosxDeploymentTarget} + META-INF/native/lib${jniLibName}.jnilib;osname=macos;osname=macosx;processor=${os.detected.arch} + + + + mac-m1-cross-compile + + netty_quiche_osx_aarch_64 + osx-aarch_64 + osx.aarch_64 + 11.0 + -target arm64-apple-macos11 + -target arm64-apple-macos11 + + -Wa,--noexecstack -target arm64-apple-macos11 + -DCMAKE_SYSTEM_PROCESSOR=arm64 -DCMAKE_OSX_ARCHITECTURES=arm64 + ${extraCflags} -O3 -fno-omit-frame-pointer + + ${extraCxxflags} -O3 -fno-omit-frame-pointer -Wno-error=range-loop-analysis + libssl.a + libcrypto.a + libquiche.a + -arch arm64 -platform_version,macos,${macosxDeploymentTarget},${macosxDeploymentTarget} + --host=aarch64-apple-darwin + MACOSX_DEPLOYMENT_TARGET=${macosxDeploymentTarget} + META-INF/native/lib${jniLibName}.jnilib;osname=macos;osname=macosx;processor=aarch64 + + true + mac + aarch64-apple-darwin + --target=${quicheTarget} + ${quicheSourceDir}/target/${quicheTarget}/release + + + + mac-intel-cross-compile + + netty_quiche_osx_x86_64 + osx-x86_64 + osx.x86_64 + 10.12 + -target x86_64-apple-macos10.12 -mmacosx-version-min=${macosxDeploymentTarget} + -target x86_64-apple-macos10.12 + + -Wa,--noexecstack -target x86_64-apple-macos10.12 -mmacosx-version-min=${macosxDeploymentTarget} + -DCMAKE_SYSTEM_PROCESSOR=x86_64 -DCMAKE_OSX_ARCHITECTURES=x86_64 + ${extraCflags} -O3 -fno-omit-frame-pointer + + ${extraCxxflags} -O3 -fno-omit-frame-pointer -Wno-error=range-loop-analysis + libssl.a + libcrypto.a + libquiche.a + -arch x86_64 -platform_version,macos,${macosxDeploymentTarget},${macosxDeploymentTarget} -mmacosx-version-min=${macosxDeploymentTarget} + --host=x86_64-apple-darwin + MACOSX_DEPLOYMENT_TARGET=${macosxDeploymentTarget} + META-INF/native/lib${jniLibName}.jnilib;osname=macos;osname=macosx;processor=x86_64 + + true + mac + x86_64-apple-darwin + --target=${quicheTarget} + ${quicheSourceDir}/target/${quicheTarget}/release + + + + linux + + + linux + + + !android + + + + -O3 -fno-omit-frame-pointer + -O3 -fno-omit-frame-pointer + + -Wa,--noexecstack + ${extraCflags} + + ${extraCxxflags} -Wno-error=maybe-uninitialized -Wno-error=shadow -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS + libssl.a + libcrypto.a + libquiche.a + -Wl,--strip-debug -Wl,--exclude-libs,ALL -Wl,-lrt + META-INF/native/lib${jniLibName}.so;osname=linux;processor=${os.detected.arch} + + + + android-armeabi-v7a + + + linux + + + android + + + + + + armeabi-v7a + armv7a-linux-androideabi + armv7-linux-androideabi + + + + android-arm64-v8a + + arm64-v8a + aarch64-linux-android + aarch64-linux-android + + + + android-x86 + + x86 + i686-linux-android + i686-linux-android + + + + android-x86_64 + + x86_64 + x86_64-linux-android + x86_64-linux-android + + + + android + + + linux + + + android + + + + aar + true + android + 21 + 21 + --target=${quicheTarget} + false + ${project.build.directory}/native-lib-only/${androidAbi} + ${quicheSourceDir}/target/${quicheTarget}/release + -O3 -fno-omit-frame-pointer + -O3 -fno-omit-frame-pointer + + -Wa,--noexecstack + ${extraCflags} + + ${extraCxxflags} -Wno-error=shadow -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS + libssl.a + libcrypto.a + libquiche.a + + + netty_quiche + ${platform} + android + -std=c99 -Werror -fno-omit-frame-pointer -fvisibility=hidden -Wunused -Wno-unused-value -O3 -I${quicheHomeIncludeDir} -I${boringsslHomeIncludeDir} + -L${quicheHomeBuildDir} -lquiche -L${boringsslHomeBuildDir} -lssl -lcrypto + META-INF/native/${jniLibName}.dll;osname=android;processor=${androidAbi} + ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64 + -Wl,-soname=${jniLibName}.so -Wl,--strip-debug -Wl,--exclude-libs,ALL -lm + + --host=${androidTriple} + + ${boringsslSourceDir}/build/${androidAbi} + ${project.build.directory}/boringssl/${androidAbi} + ${project.build.directory}/quiche/${androidAbi} + ${project.build.directory}/native-build/${androidAbi} + + + + + com.soebes.maven.plugins + iterator-maven-plugin + + 0.5.0 + + + package + + invoker + + + + + arm64-v8a + + true + + + + x86 + + true + + + + x86_64 + + true + + + + ${project.basedir} + + package + + android-@item@,android + ${skipIteration} + + + + + + org.fusesource.hawtjni + hawtjni-maven-plugin + + + generate-native-lib + + + ${extraConfigureArg} + ${extraConfigureArg2} + CFLAGS=${cflags} + LDFLAGS=${ldflags} ${extraLdflags} + --libdir=${hawtjniBuildDir}/native-build/target/lib + + TOOLCHAIN=${ndkToolchain} + TARGET=${androidTriple} + API=${androidMinSdkVersion} + AR=${ndkToolchain}/bin/llvm-ar + CC=${ndkToolchain}/bin/${androidTriple}${androidMinSdkVersion}-clang + AS=${ndkToolchain}/bin/${androidTriple}${androidMinSdkVersion}-clang + CXX=${ndkToolchain}/bin/${androidTriple}${androidMinSdkVersion}-clang++ + LD=${ndkToolchain}/bin/ld + RANLIB=${ndkToolchain}/bin/llvm-ranlib + STRIP=${ndkToolchain}/bin/llvm-strip + + ${hawtjniBuildDir} + + + generate + build + + + + + + maven-jar-plugin + + + native-jar + none + + + + + org.apache.felix + maven-bundle-plugin + + + generate-manifest + none + + + + + maven-antrun-plugin + + + + copy-android-native-lib + initialize + + run + + + + + + + + + + + + + + + + + + copy-native-lib-and-license + none + + + + copy-android-native-libs + process-test-resources + + run + + + + + + + + + + + + + + + + + + + + + linux-aarch64 + + -O3 -fno-omit-frame-pointer + -O3 -fno-omit-frame-pointer + + -Wa,--noexecstack + ${extraCflags} + + ${extraCxxflags} -Wno-error=maybe-uninitialized -Wno-error=shadow -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS + libssl.a + libcrypto.a + libquiche.a + -Wl,--strip-debug -Wl,--exclude-libs,ALL + META-INF/native/lib${jniLibName}.so;osname=linux;processor=aarch64 + netty_quiche_linux_aarch_64 + linux-aarch_64 + linux.aarch_64 + --host=aarch64-linux-gnu + CC=aarch64-none-linux-gnu-gcc + -DCMAKE_SYSTEM_NAME=Linux -DCMAKE_SYSTEM_PROCESSOR=aarch64 -DCMAKE_C_COMPILER=aarch64-none-linux-gnu-gcc -DCMAKE_CXX_COMPILER=aarch64-none-linux-gnu-g++ + + true + linux + aarch64-unknown-linux-gnu + --target=${quicheTarget} + ${quicheSourceDir}/target/${quicheTarget}/release + + + + leak + + -Dio.netty.leakDetectionLevel=paranoid -Dio.netty.leakDetection.targetRecords=32 + + + + + + noUnsafe + + -Dio.netty.noUnsafe=true + + + + + + + native-image-agent + + + org.bouncycastle + bcpkix-jdk15on + + + + + + maven-compiler-plugin + + 17 + 17 + + + + maven-surefire-plugin + + + io.netty.handler.codec.quic.QuicChannelEchoTest + + + + + org.graalvm.buildtools + native-maven-plugin + ${native.maven.plugin.version} + + + org.codehaus.plexus + plexus-utils + 3.4.2 + + + true + + + test-native + + test + + test + + + + true + + true + + + true + Conditional + + + ${project.basedir}/src/test/resources/netty-filter.json + ${project.basedir}/src/test/resources/test-class-filter.json + true + + + + true + true + false + false + true + + + + main + + true + + + + + + + + + + + + + kr.motd.maven + os-maven-plugin + 1.7.0 + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + generate-sources + + add-source + + + + ${nativeSourceDirectory} + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + + unpack + generate-sources + + unpack-dependencies + + + io.netty + netty-jni-util + sources + ${jniUtilIncludeDir} + **.h,**.c + false + true + + + + + + maven-antrun-plugin + + + + + build-boringssl + generate-sources + + run + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + build-quiche + generate-sources + + run + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + copy-src + generate-sources + + run + + + + + + + + + + + + + + + + + + + setup-template + generate-sources + + run + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + copy-native-lib-and-license + process-test-resources + + run + + + + + + + + + + + + + + + + + + + + + + + + copy-manifest + prepare-package + + run + + + + + + + + + + + + + org.apache.felix + maven-bundle-plugin + + + generate-manifest + process-classes + + manifest + + + + jar + bundle + + + ${project.groupId}.* + ${boringsslCommitSha} + ${boringsslBranch} + ${quicheCommitSha} + ${quicheBranch} + + + + + + + + maven-source-plugin + + + + + 2 + ${project.name} + ${project.groupId}.${project.artifactId}.source + ${project.organization.name} + ${parsedVersion.osgiVersion} + ${project.groupId}.${project.artifactId};version="${parsedVersion.osgiVersion}";roots:="." + + + + + + + attach-sources + prepare-package + + jar-no-fork + + + + attach-test-sources + prepare-package + + test-jar-no-fork + + + + + + + org.fusesource.hawtjni + hawtjni-maven-plugin + + + generate-native-lib + + ${jniLibName} + ${generatedSourcesDir} + ${templateDir} + msbuild + true + v142 + ${nativeLibOnlyDir} + true + + ${extraConfigureArg} + ${extraConfigureArg2} + --libdir=${project.build.directory}/native-build/target/lib + + + + generate + build + + + + + + + maven-jar-plugin + + + default-jar + + + + META-INF/native/** + META-INF/license/** + META-INF/NOTICE.txt + META-INF/LICENSE.txt + + + + true + true + + + ${javaModuleName} + + true + ${project.build.directory}/manifests/MANIFEST.MF + + + + + + native-jar + + jar + + + + + true + true + + + ${javaModuleNameWithClassifier} + ${fragmentHost} + ${bundleNativeCode} + + true + ${project.build.directory}/manifests/MANIFEST-native.MF + + ${jni.classifier} + + + + + + com.simpligility.maven.plugins + android-maven-plugin + true + + + ${androidMinSdkVersion} + + ${project.build.directory}/android-build/AndroidManifest.xml + ${project.build.directory}/android-build/native-libs + + META-INF + + ${jni.classifier} + + + + package + + + + + + + + + io.netty + netty-jni-util + ${netty.jni-util.version} + sources + true + + + ${project.groupId} + netty-handler-codec-classes-quic + ${project.version} + compile + + + io.netty + netty-transport-native-epoll + ${netty.version} + linux-x86_64 + test + + + io.netty + netty-transport-native-epoll + ${netty.version} + linux-aarch_64 + test + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-params + ${junit.version} + test + + + org.hamcrest + hamcrest-library + 1.3 + test + + + io.netty + netty-build-common + ${netty.build.version} + test + + + ch.qos.logback + logback-classic + 1.3.12 + test + + + + org.bouncycastle + bcpkix-jdk15on + 1.70 + test + + + org.assertj + assertj-core + 3.20.2 + test + + + diff --git a/codec-native-quic/src/main/AndroidManifest.xml b/codec-native-quic/src/main/AndroidManifest.xml new file mode 100644 index 0000000..5f6f949 --- /dev/null +++ b/codec-native-quic/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + \ No newline at end of file diff --git a/codec-native-quic/src/main/c/netty_quic.h b/codec-native-quic/src/main/c/netty_quic.h new file mode 100644 index 0000000..8501afb --- /dev/null +++ b/codec-native-quic/src/main/c/netty_quic.h @@ -0,0 +1,22 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +#ifndef NETTY_QUIC_H_ +#define NETTY_QUIC_H_ + +jint quic_get_java_env(JNIEnv **env); + + +#endif /* NETTY_QUIC_H_ */ diff --git a/codec-native-quic/src/main/c/netty_quic_boringssl.c b/codec-native-quic/src/main/c/netty_quic_boringssl.c new file mode 100644 index 0000000..3ef26ba --- /dev/null +++ b/codec-native-quic/src/main/c/netty_quic_boringssl.c @@ -0,0 +1,1630 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "netty_jni_util.h" +#include "netty_quic.h" +#include "netty_quic_boringssl.h" + +// Add define if NETTY_BUILD_STATIC is defined so it is picked up in netty_jni_util.c +#ifdef NETTY_BUILD_STATIC +#define NETTY_JNI_UTIL_BUILD_STATIC +#endif + +#define STATICALLY_CLASSNAME "io/netty/handler/codec/quic/BoringSSLNativeStaticallyReferencedJniMethods" +#define CLASSNAME "io/netty/handler/codec/quic/BoringSSL" + +#define ERR_LEN 256 + +// For encoding of keys see BoringSSLSessionTicketCallback.setSessionTicketKeys(...) +#define SSL_SESSION_TICKET_KEY_NAME_OFFSET 1 +#define SSL_SESSION_TICKET_KEY_HMAC_OFFSET 17 +#define SSL_SESSION_TICKET_KEY_EVP_OFFSET 33 +#define SSL_SESSION_TICKET_KEY_NAME_LEN 16 +#define SSL_SESSION_TICKET_AES_KEY_LEN 16 +#define SSL_SESSION_TICKET_HMAC_KEY_LEN 16 +#define SSL_SESSION_TICKET_KEY_LEN 49 + +static jweak sslTaskClassWeak = NULL; +static jmethodID sslTaskDestroyMethod = NULL; +static jfieldID sslTaskReturnValue = NULL; +static jfieldID sslTaskComplete = NULL; + +static jweak sslPrivateKeyMethodTaskClassWeak = NULL; +static jfieldID sslPrivateKeyMethodTaskResultBytesField = NULL; + +static jweak sslPrivateKeyMethodSignTaskClassWeak = NULL; +static jmethodID sslPrivateKeyMethodSignTaskInitMethod = NULL; + +static jweak sslPrivateKeyMethodDecryptTaskClassWeak = NULL; +static jmethodID sslPrivateKeyMethodDecryptTaskInitMethod = NULL; + +static jweak verifyTaskClassWeak = NULL; +static jmethodID verifyTaskClassInitMethod = NULL; + +static jweak certificateTaskClassWeak = NULL; +static jmethodID certificateTaskClassInitMethod = NULL; +static jfieldID certificateTaskClassChainField; +static jfieldID certificateTaskClassKeyField; + +static jweak handshakeCompleteCallbackClassWeak = NULL; +static jmethodID handshakeCompleteCallbackMethod = NULL; + +static jweak servernameCallbackClassWeak = NULL; +static jmethodID servernameCallbackMethod = NULL; + +static jweak keylogCallbackClassWeak = NULL; +static jmethodID keylogCallbackMethod = NULL; + +static jweak sessionCallbackClassWeak = NULL; +static jmethodID sessionCallbackMethod = NULL; + +static jweak sessionTicketCallbackClassWeak = NULL; +static jmethodID sessionTicketCallbackMethod = NULL; + +static jclass byteArrayClass = NULL; +static jclass stringClass = NULL; + +static int handshakeCompleteCallbackIdx = -1; +static int verifyCallbackIdx = -1; +static int certificateCallbackIdx = -1; +static int servernameCallbackIdx = -1; +static int keylogCallbackIdx = -1; +static int sessionCallbackIdx = -1; +static int sslPrivateKeyMethodIdx = -1; +static int sslTaskIdx = -1; +static int sessionTicketCallbackIdx = -1; +static int alpn_data_idx = -1; +static int crypto_buffer_pool_idx = -1; + +static jint netty_boringssl_ssl_verify_none(JNIEnv* env, jclass clazz) { + return SSL_VERIFY_NONE; +} + +static jint netty_boringssl_ssl_verify_fail_if_no_peer_cert(JNIEnv* env, jclass clazz) { + return SSL_VERIFY_FAIL_IF_NO_PEER_CERT; +} + +static jint netty_boringssl_ssl_verify_peer(JNIEnv* env, jclass clazz) { + return SSL_VERIFY_PEER; +} + +static jint netty_boringssl_x509_v_ok(JNIEnv* env, jclass clazz) { + return X509_V_OK; +} + +static jint netty_boringssl_x509_v_err_cert_has_expired(JNIEnv* env, jclass clazz) { + return X509_V_ERR_CERT_HAS_EXPIRED; +} + +static jint netty_boringssl_x509_v_err_cert_not_yet_valid(JNIEnv* env, jclass clazz) { + return X509_V_ERR_CERT_NOT_YET_VALID; +} + +static jint netty_boringssl_x509_v_err_cert_revoked(JNIEnv* env, jclass clazz) { + return X509_V_ERR_CERT_REVOKED; +} + +static jint netty_boringssl_x509_v_err_unspecified(JNIEnv* env, jclass clazz) { + return X509_V_ERR_UNSPECIFIED; +} + +static jint netty_boringssl_ssl_sign_rsa_pkcs_sha1(JNIEnv* env, jclass clazz) { + return SSL_SIGN_RSA_PKCS1_SHA1; +} + +static jint netty_boringssl_ssl_sign_rsa_pkcs_sha256(JNIEnv* env, jclass clazz) { + return SSL_SIGN_RSA_PKCS1_SHA256; +} + +static jint netty_boringssl_ssl_sign_rsa_pkcs_sha384(JNIEnv* env, jclass clazz) { + return SSL_SIGN_RSA_PKCS1_SHA384; +} + +static jint netty_boringssl_ssl_sign_rsa_pkcs_sha512(JNIEnv* env, jclass clazz) { + return SSL_SIGN_RSA_PKCS1_SHA512; +} + +static jint netty_boringssl_ssl_sign_ecdsa_pkcs_sha1(JNIEnv* env, jclass clazz) { + return SSL_SIGN_ECDSA_SHA1; +} + +static jint netty_boringssl_ssl_sign_ecdsa_secp256r1_sha256(JNIEnv* env, jclass clazz) { + return SSL_SIGN_ECDSA_SECP256R1_SHA256; +} + +static jint netty_boringssl_ssl_sign_ecdsa_secp384r1_sha384(JNIEnv* env, jclass clazz) { + return SSL_SIGN_ECDSA_SECP384R1_SHA384; +} + +static jint netty_boringssl_ssl_sign_ecdsa_secp521r1_sha512(JNIEnv* env, jclass clazz) { + return SSL_SIGN_ECDSA_SECP521R1_SHA512; +} + +static jint netty_boringssl_ssl_sign_rsa_pss_rsae_sha256(JNIEnv* env, jclass clazz) { + return SSL_SIGN_RSA_PSS_RSAE_SHA256; +} + +static jint netty_boringssl_ssl_sign_rsa_pss_rsae_sha384(JNIEnv* env, jclass clazz) { + return SSL_SIGN_RSA_PSS_RSAE_SHA384; +} + +static jint netty_boringssl_ssl_sign_rsa_pss_rsae_sha512(JNIEnv* env, jclass clazz) { + return SSL_SIGN_RSA_PSS_RSAE_SHA512; +} + +static jint netty_boringssl_ssl_sign_ed25519(JNIEnv* env, jclass clazz) { + return SSL_SIGN_ED25519; +} + +static jint netty_boringssl_ssl_sign_rsa_pkcs1_md5_sha1(JNIEnv* env, jclass clazz) { + return SSL_SIGN_RSA_PKCS1_MD5_SHA1; +} + +static STACK_OF(CRYPTO_BUFFER)* arrayToStack(JNIEnv* env, jobjectArray array, CRYPTO_BUFFER_POOL* pool) { + if (array == NULL) { + return NULL; + } + STACK_OF(CRYPTO_BUFFER) *stack = sk_CRYPTO_BUFFER_new_null(); + int arrayLen = (*env)->GetArrayLength(env, array); + for (int i = 0; i < arrayLen; i++) { + jbyteArray bytes = (*env)->GetObjectArrayElement(env, array, i); + int data_len = (*env)->GetArrayLength(env, bytes); + uint8_t* data = (uint8_t*) (*env)->GetByteArrayElements(env, bytes, 0); + CRYPTO_BUFFER *buffer = CRYPTO_BUFFER_new(data, data_len, pool); + (*env)->ReleaseByteArrayElements(env, bytes, (jbyte*)data, JNI_ABORT); + (*env)->DeleteLocalRef(env, bytes); + + if (buffer == NULL) { + goto cleanup; + } + + if (sk_CRYPTO_BUFFER_push(stack, buffer) <= 0) { + // If we cant push for whatever reason ensure we release the buffer. + CRYPTO_BUFFER_free(buffer); + goto cleanup; + } + } + return stack; +cleanup: + sk_CRYPTO_BUFFER_pop_free(stack, CRYPTO_BUFFER_free); + return NULL; +} + +static jobjectArray stackToArray(JNIEnv *e, const STACK_OF(CRYPTO_BUFFER)* stack, int offset) { + if (stack == NULL) { + return NULL; + } + const int len = sk_CRYPTO_BUFFER_num(stack) - offset; + if (len <= 0) { + return NULL; + } + // Create the byte[][] array that holds all the certs + jbyteArray array = (*e)->NewObjectArray(e, len, byteArrayClass, NULL); + if (array == NULL) { + return NULL; + } + + for(int i = 0; i < len; i++) { + CRYPTO_BUFFER* value = sk_CRYPTO_BUFFER_value(stack, i + offset); + int length = CRYPTO_BUFFER_len(value); + + if (length <= 0) { + return NULL; + } + + jbyteArray bArray = (*e)->NewByteArray(e, length); + if (bArray == NULL) { + return NULL; + } + (*e)->SetByteArrayRegion(e, bArray, 0, length, (jbyte*) CRYPTO_BUFFER_data(value)); + (*e)->SetObjectArrayElement(e, array, i, bArray); + // Delete the local reference as we not know how long the chain is and local references are otherwise + // only freed once jni method returns. + (*e)->DeleteLocalRef(e, bArray); + bArray = NULL; + } + return array; +} + +static jbyteArray to_byte_array(JNIEnv* env, uint8_t* bytes, size_t len) { + if (bytes == NULL || len == 0) { + return NULL; + } + jbyteArray array = (*env)->NewByteArray(env, len); + if (array == NULL) { + return NULL; + } + (*env)->SetByteArrayRegion(env,array, 0, len, (jbyte*) bytes); + return array; +} + +// Store the callback to run and also if it was consumed via SSL.getTask(...). +typedef struct netty_boringssl_ssl_task_t netty_boringssl_ssl_task_t; +struct netty_boringssl_ssl_task_t { + jboolean consumed; + jobject task; +}; + + +static netty_boringssl_ssl_task_t* netty_boringssl_ssl_task_new(JNIEnv* e, jobject task) { + if (task == NULL) { + // task was NULL which most likely means we did run out of memory when calling NewObject(...). Signal a failure back by returning NULL. + return NULL; + } + netty_boringssl_ssl_task_t* sslTask = (netty_boringssl_ssl_task_t*) OPENSSL_malloc(sizeof(netty_boringssl_ssl_task_t)); + if (sslTask == NULL) { + return NULL; + } + + if ((sslTask->task = (*e)->NewGlobalRef(e, task)) == NULL) { + // NewGlobalRef failed because we ran out of memory, free what we malloc'ed and fail the handshake. + OPENSSL_free(sslTask); + return NULL; + } + sslTask->consumed = JNI_FALSE; + return sslTask; +} + +static void netty_boringssl_ssl_task_free(JNIEnv* e, netty_boringssl_ssl_task_t* sslTask) { + if (sslTask == NULL) { + return; + } + + if (sslTask->task != NULL) { + // Execute the destroy method + (*e)->CallVoidMethod(e, sslTask->task, sslTaskDestroyMethod); + + // As we created a Global reference before we need to delete the reference as otherwise we will leak memory. + (*e)->DeleteGlobalRef(e, sslTask->task); + sslTask->task = NULL; + } + + // The task was malloc'ed before, free it and clear it from the SSL storage. + OPENSSL_free(sslTask); +} + +enum ssl_verify_result_t quic_SSL_cert_custom_verify(SSL* ssl, uint8_t *out_alert) { + enum ssl_verify_result_t ret = ssl_verify_invalid; + jint result = X509_V_ERR_UNSPECIFIED; + JNIEnv *e = NULL; + jclass verifyTaskClass = NULL; + + SSL_CTX* ctx = SSL_get_SSL_CTX(ssl); + if (ctx == NULL) { + goto complete; + } + + if (quic_get_java_env(&e) != JNI_OK) { + goto complete; + } + + jobject verifyCallback = SSL_CTX_get_ex_data(ctx, verifyCallbackIdx); + if (verifyCallback == NULL) { + goto complete; + } + + netty_boringssl_ssl_task_t* ssl_task = (netty_boringssl_ssl_task_t*) SSL_get_ex_data(ssl, sslTaskIdx); + // Let's check if we retried the operation and so have stored a sslTask that runs the certificiate callback. + if (ssl_task != NULL) { + // Check if the task complete yet. If not the complete field will be still false. + if ((*e)->GetBooleanField(e, ssl_task->task, sslTaskComplete) == JNI_FALSE) { + // Not done yet, try again later. + ret = ssl_verify_retry; + goto complete; + } + + // The task is complete, retrieve the return value that should be signaled back. + result = (*e)->GetIntField(e, ssl_task->task, sslTaskReturnValue); + + SSL_set_ex_data(ssl, sslTaskIdx, NULL); + netty_boringssl_ssl_task_free(e, ssl_task); + goto complete; + } + const STACK_OF(CRYPTO_BUFFER) *chain = SSL_get0_peer_certificates(ssl); + if (chain == NULL) { + goto complete; + } + + // Create the byte[][] array that holds all the certs + jobjectArray array = stackToArray(e, chain, 0); + if (array == NULL) { + goto complete; + } + + const char* authentication_method = NULL; + STACK_OF(SSL_CIPHER) *ciphers = SSL_get_ciphers(ssl); + if (ciphers == NULL || sk_SSL_CIPHER_num(ciphers) <= 0) { + // No cipher available so return UNKNOWN. + authentication_method = "UNKNOWN"; + } else { + authentication_method = SSL_CIPHER_get_kx_name(sk_SSL_CIPHER_value(ciphers, 0)); + if (authentication_method == NULL) { + authentication_method = "UNKNOWN"; + } + } + + jstring authMethodString = (*e)->NewStringUTF(e, authentication_method); + if (authMethodString == NULL) { + goto complete; + } + + NETTY_JNI_UTIL_NEW_LOCAL_FROM_WEAK(e, verifyTaskClass, verifyTaskClassWeak, complete); + jobject task = (*e)->NewObject(e, verifyTaskClass, verifyTaskClassInitMethod, (jlong) ssl, array, authMethodString, verifyCallback); + NETTY_JNI_UTIL_DELETE_LOCAL(e, verifyTaskClass); + + ssl_task = netty_boringssl_ssl_task_new(e, task); + if (ssl_task == NULL) { + goto complete; + } + + SSL_set_ex_data(ssl, sslTaskIdx, ssl_task); + + // Signal back that we want to suspend the handshake. + ret = ssl_verify_retry; + goto complete; +complete: + if (ret != ssl_verify_retry) { + if (result == X509_V_OK) { + ret = ssl_verify_ok; + } else { + ret = ssl_verify_invalid; + *out_alert = SSL_alert_from_verify_result(result); + } + } + return ret; +} + +static jbyteArray keyTypes(JNIEnv* e, SSL* ssl) { + jbyte* ctype_bytes = NULL; + int ctype_num = SSL_get0_certificate_types(ssl, (const uint8_t **) &ctype_bytes); + if (ctype_num <= 0) { + // No idea what we should use... Let the caller handle it. + return NULL; + } + jbyteArray types = (*e)->NewByteArray(e, ctype_num); + if (types == NULL) { + return NULL; + } + (*e)->SetByteArrayRegion(e, types, 0, ctype_num, ctype_bytes); + return types; +} + + +static enum ssl_private_key_result_t netty_boringssl_private_key_sign_java(SSL *ssl, uint8_t *out, size_t *out_len, size_t max_out, uint16_t signature_algorithm, const uint8_t *in, size_t in_len) { + enum ssl_private_key_result_t ret = ssl_private_key_failure; + jclass sslPrivateKeyMethodSignTaskClass = NULL; + jbyteArray inputArray = NULL; + JNIEnv *e = NULL; + + if (quic_get_java_env(&e) != JNI_OK) { + goto complete; + } + + jobject ssl_private_key_method = SSL_CTX_get_ex_data(SSL_get_SSL_CTX(ssl), sslPrivateKeyMethodIdx); + if (ssl_private_key_method == NULL) { + goto complete; + } + + if ((inputArray = (*e)->NewByteArray(e, in_len)) == NULL) { + goto complete; + } + (*e)->SetByteArrayRegion(e, inputArray, 0, in_len, (jbyte*) in); + + // Lets create the BoringSSLPrivateKeyMethodSignTask and store it on the SSL object. We then later retrieve it via + // BoringSSL.SSL_getTask(ssl) and run it. + + NETTY_JNI_UTIL_NEW_LOCAL_FROM_WEAK(e, sslPrivateKeyMethodSignTaskClass, sslPrivateKeyMethodSignTaskClassWeak, complete); + jobject task = (*e)->NewObject(e, sslPrivateKeyMethodSignTaskClass, sslPrivateKeyMethodSignTaskInitMethod, (jlong) ssl, + signature_algorithm, inputArray, ssl_private_key_method); + NETTY_JNI_UTIL_DELETE_LOCAL(e, sslPrivateKeyMethodSignTaskClass); + + netty_boringssl_ssl_task_t* ssl_task = netty_boringssl_ssl_task_new(e, task); + if (ssl_task == NULL) { + goto complete; + } + SSL_set_ex_data(ssl, sslTaskIdx, ssl_task); + ret = ssl_private_key_retry; +complete: + // Free up any allocated memory and return. + NETTY_JNI_UTIL_DELETE_LOCAL(e, inputArray); + return ret; +} + +static enum ssl_private_key_result_t netty_boringssl_private_key_decrypt_java(SSL *ssl, uint8_t *out, size_t *out_len, size_t max_out, const uint8_t *in, size_t in_len) { + enum ssl_private_key_result_t ret = ssl_private_key_failure; + jclass sslPrivateKeyMethodDecryptTaskClass = NULL; + jbyteArray inArray = NULL; + JNIEnv *e = NULL; + + if (quic_get_java_env(&e) != JNI_OK) { + goto complete; + } + + jobject ssl_private_key_method = SSL_CTX_get_ex_data(SSL_get_SSL_CTX(ssl), sslPrivateKeyMethodIdx); + if (ssl_private_key_method == NULL) { + goto complete; + } + + if ((inArray = (*e)->NewByteArray(e, in_len)) == NULL) { + goto complete; + } + (*e)->SetByteArrayRegion(e, inArray, 0, in_len, (jbyte*) in); + + // Lets create the SSLPrivateKeyMethodDecryptTask and store it on the SSL object. We then later retrieve it via + // BoringSSL.SSL_getTask(ssl) and run it. + NETTY_JNI_UTIL_NEW_LOCAL_FROM_WEAK(e, sslPrivateKeyMethodDecryptTaskClass, sslPrivateKeyMethodDecryptTaskClassWeak, complete); + jobject task = (*e)->NewObject(e, sslPrivateKeyMethodDecryptTaskClass, sslPrivateKeyMethodDecryptTaskInitMethod, + (jlong) ssl, inArray, ssl_private_key_method); + + netty_boringssl_ssl_task_t* ssl_task = netty_boringssl_ssl_task_new(e, task); + if (ssl_task == NULL) { + goto complete; + } + SSL_set_ex_data(ssl, sslTaskIdx, ssl_task); + ret = ssl_private_key_retry; +complete: + // Delete the local reference as this is executed by a callback. + NETTY_JNI_UTIL_DELETE_LOCAL(e, inArray); + NETTY_JNI_UTIL_DELETE_LOCAL(e, sslPrivateKeyMethodDecryptTaskClass); + return ret; +} + +static enum ssl_private_key_result_t netty_boringssl_private_key_complete_java(SSL *ssl, uint8_t *out, size_t *out_len, size_t max_out) { + jbyte* b = NULL; + int arrayLen = 0; + JNIEnv *e = NULL; + + netty_boringssl_ssl_task_t* ssl_task = SSL_get_ex_data(ssl, sslTaskIdx); + + // Let's check if we retried the operation and so have stored a sslTask that runs the sign / decrypt callback. + if (ssl_task != NULL) { + if (quic_get_java_env(&e) != JNI_OK) { + return ssl_private_key_failure; + } + + // Check if the task complete yet. If not the complete field will be still false. + if ((*e)->GetBooleanField(e, ssl_task->task, sslTaskComplete) == JNI_FALSE) { + // Not done yet, try again later. + return ssl_private_key_retry; + } + + // The task is complete, retrieve the return value that should be signaled back. + jbyteArray resultBytes = (*e)->GetObjectField(e, ssl_task->task, sslPrivateKeyMethodTaskResultBytesField); + + SSL_set_ex_data(ssl, sslTaskIdx, NULL); + netty_boringssl_ssl_task_free(e, ssl_task); + + if (resultBytes == NULL) { + return ssl_private_key_failure; + } + + arrayLen = (*e)->GetArrayLength(e, resultBytes); + if (max_out < arrayLen) { + // We need to fail as otherwise we would end up writing into memory which does not + // belong to us. + (*e)->DeleteLocalRef(e, resultBytes); + return ssl_private_key_failure; + } + b = (*e)->GetByteArrayElements(e, resultBytes, NULL); + memcpy(out, b, arrayLen); + (*e)->ReleaseByteArrayElements(e, resultBytes, b, JNI_ABORT); + (*e)->DeleteLocalRef(e, resultBytes); + *out_len = arrayLen; + return ssl_private_key_success; + } + return ssl_private_key_failure; +} + +const SSL_PRIVATE_KEY_METHOD netty_boringssl_private_key_method = { + &netty_boringssl_private_key_sign_java, + &netty_boringssl_private_key_decrypt_java, + &netty_boringssl_private_key_complete_java +}; + +// See https://www.openssl.org/docs/man1.0.2/man3/SSL_set_cert_cb.html for return values. +static int quic_certificate_cb(SSL* ssl, void* arg) { + jclass certificateTaskClass = NULL; + JNIEnv *e = NULL; + if (quic_get_java_env(&e) != JNI_OK) { + return 0; + } + + netty_boringssl_ssl_task_t* ssl_task = (netty_boringssl_ssl_task_t*) SSL_get_ex_data(ssl, sslTaskIdx); + + // Let's check if we retried the operation and so have stored a sslTask that runs the certificiate callback. + if (ssl_task == NULL) { + jobjectArray authMethods = NULL; + jobjectArray issuers = NULL; + jbyteArray types = NULL; + if (SSL_is_server(ssl) == 1) { + const STACK_OF(SSL_CIPHER) *ciphers = SSL_get_ciphers(ssl); + int len = sk_SSL_CIPHER_num(ciphers); + authMethods = (*e)->NewObjectArray(e, len, stringClass, NULL); + if (authMethods == NULL) { + return 0; + } + + for (int i = 0; i < len; i++) { + jstring methodString = (*e)->NewStringUTF(e, SSL_CIPHER_get_kx_name(sk_SSL_CIPHER_value(ciphers, i))); + if (methodString == NULL) { + // Out of memory + return 0; + } + (*e)->SetObjectArrayElement(e, authMethods, i, methodString); + } + + // TODO: Consider filling these somehow. + types = NULL; + issuers = NULL; + } else { + authMethods = NULL; + types = keyTypes(e, ssl); + issuers = stackToArray(e, SSL_get0_server_requested_CAs(ssl), 0); + } + + // Lets create the CertificateCallbackTask and store it on the SSL object. We then later retrieve it via + // SSL.getTask(ssl) and run it. + NETTY_JNI_UTIL_NEW_LOCAL_FROM_WEAK(e, certificateTaskClass, certificateTaskClassWeak, done); + jobject task = (*e)->NewObject(e, certificateTaskClass, certificateTaskClassInitMethod, (jlong) ssl, types, issuers, authMethods, arg); + NETTY_JNI_UTIL_DELETE_LOCAL(e, certificateTaskClass); + + if ((ssl_task = netty_boringssl_ssl_task_new(e, task)) == NULL) { + return 0; + } + + SSL_set_ex_data(ssl, sslTaskIdx, ssl_task); + + // Signal back that we want to suspend the handshake. + return -1; + } + + // Check if the task complete yet. If not the complete field will be still false. + if ((*e)->GetBooleanField(e, ssl_task->task, sslTaskComplete) == JNI_FALSE) { + // Not done yet, try again later. + return -1; + } + + // The task is complete, retrieve the return value that should be signaled back. + jint retValue = (*e)->GetIntField(e, ssl_task->task, sslTaskReturnValue); + if (retValue == 0) { + return 0; + } + + int ret = 0; + EVP_PKEY* pkey = (EVP_PKEY *) (*e)->GetLongField(e, ssl_task->task, certificateTaskClassKeyField); + const STACK_OF(CRYPTO_BUFFER) *cchain = (STACK_OF(CRYPTO_BUFFER) *) (*e)->GetLongField(e, ssl_task->task, certificateTaskClassChainField); + + // Set both fields to 0 so destroy() will not destroy the native allocated memory. + (*e)->SetLongField(e, ssl_task->task, certificateTaskClassKeyField, 0); + (*e)->SetLongField(e, ssl_task->task, certificateTaskClassChainField, 0); + + SSL_set_ex_data(ssl, sslTaskIdx, NULL); + netty_boringssl_ssl_task_free(e, ssl_task); + + if (pkey == NULL && cchain == NULL) { + // No key material found. + return 1; + } + + int numCerts = sk_CRYPTO_BUFFER_num(cchain); + if (numCerts == 0) { + goto done; + } + CRYPTO_BUFFER** certs = OPENSSL_malloc(sizeof(CRYPTO_BUFFER*) * numCerts); + + if (certs == NULL) { + goto done; + } + + for (int i = 0; i < numCerts; i++) { + certs[i] = sk_CRYPTO_BUFFER_value(cchain, i); + } + + if (pkey != NULL) { + if (SSL_set_chain_and_key(ssl, certs, numCerts, pkey, NULL) > 0) { + ret = 1; + } + } else { + if (SSL_set_chain_and_key(ssl, certs, numCerts, NULL, &netty_boringssl_private_key_method) > 0) { + ret = 1; + } + } +done: + OPENSSL_free(certs); + EVP_PKEY_free(pkey); + if (cchain != NULL) { + sk_CRYPTO_BUFFER_pop_free((STACK_OF(CRYPTO_BUFFER) *) cchain, CRYPTO_BUFFER_free); + } + NETTY_JNI_UTIL_DELETE_LOCAL(e, certificateTaskClass); + + return ret; +} + +struct alpn_data { + unsigned char* proto_data; + int proto_len; +} typedef alpn_data; + +int BoringSSL_callback_alpn_select_proto(SSL* ssl, const unsigned char **out, unsigned char *outlen, + const unsigned char *in, unsigned int inlen, void *arg) { + unsigned int i = 0; + unsigned char target_proto_len; + unsigned char *p = NULL; + const unsigned char *end = NULL; + unsigned char *proto = NULL; + unsigned char proto_len; + alpn_data* data = (alpn_data*) arg; + int supported_protos_len = data->proto_len; + unsigned char* supported_protos = data->proto_data; + while (i < supported_protos_len) { + target_proto_len = *supported_protos; + ++supported_protos; + + p = (unsigned char*) in; + end = p + inlen; + + while (p < end) { + proto_len = *p; + proto = ++p; + + if (proto + proto_len <= end && target_proto_len == proto_len && + memcmp(supported_protos, proto, proto_len) == 0) { + + // We found a match, so set the output and return with OK! + *out = proto; + *outlen = proto_len; + + return SSL_TLSEXT_ERR_OK; + } + // Move on to the next protocol. + p += proto_len; + } + + // increment len and pointers. + i += target_proto_len; + supported_protos += target_proto_len; + } + return SSL_TLSEXT_ERR_NOACK; +} + +static jbyteArray netty_boringssl_SSL_getSessionId(JNIEnv* env, const SSL* ssl_) { + SSL_SESSION *session = SSL_get_session(ssl_); + if (session == NULL) { + return NULL; + } + + unsigned int len = 0; + const unsigned char *session_id = SSL_SESSION_get_id(session, &len); + if (len == 0 || session_id == NULL) { + return NULL; + } + + jbyteArray bArray = (*env)->NewByteArray(env, len); + if (bArray == NULL) { + return NULL; + } + (*env)->SetByteArrayRegion(env, bArray, 0, len, (jbyte*) session_id); + return bArray; +} + + +static jstring netty_boringssl_SSL_getCipher(JNIEnv* env, const SSL* ssl) { + const char* cipher = SSL_get_cipher(ssl); + if (cipher == NULL) { + return NULL; + } + return (*env)->NewStringUTF(env, cipher); +} + +static jstring netty_boringssl_SSL_getVersion(JNIEnv* env, const SSL* ssl) { + const char* version = SSL_get_version(ssl); + if (version == NULL) { + return NULL; + } + return (*env)->NewStringUTF(env, version); +} + +static jobjectArray netty_boringssl_SSL_getPeerCertChain(JNIEnv* env, const SSL* ssl_) { + // Get a stack of all certs in the chain. + const STACK_OF(CRYPTO_BUFFER) *chain = SSL_get0_peer_certificates(ssl_); + if (chain == NULL) { + return NULL; + } + int offset = SSL_is_server(ssl_) == 1 ? 1 : 0; + return stackToArray(env, chain, offset); +} + +static jbyteArray netty_boringssl_SSL_getPeerCertificate(JNIEnv* env, const SSL* ssl_) { + // Get a stack of all certs in the chain, the first is the leaf. + const STACK_OF(CRYPTO_BUFFER) *certs = SSL_get0_peer_certificates(ssl_); + if (certs == NULL || sk_CRYPTO_BUFFER_num(certs) <= 0) { + return NULL; + } + const CRYPTO_BUFFER *leafCert = sk_CRYPTO_BUFFER_value(certs, 0); + int length = CRYPTO_BUFFER_len(leafCert); + + jbyteArray bArray = (*env)->NewByteArray(env, length); + if (bArray == NULL) { + return NULL; + } + (*env)->SetByteArrayRegion(env, bArray, 0, length, (jbyte*) CRYPTO_BUFFER_data(leafCert)); + return bArray; +} + + +static jlong netty_boringssl_SSL_getTime(JNIEnv* env, const SSL* ssl_) { + SSL_SESSION *session = SSL_get_session(ssl_); + if (session == NULL) { + // BoringSSL does not protect against a NULL session. OpenSSL + // returns 0 if the session is NULL, so do that here. + return 0; + } + + return SSL_get_time(session); +} + +static jlong netty_boringssl_SSL_getTimeout(JNIEnv* env, const SSL* ssl_) { + SSL_SESSION *session = SSL_get_session(ssl_); + if (session == NULL) { + // BoringSSL does not protect against a NULL session. OpenSSL + // returns 0 if the session is NULL, so do that here. + return 0; + } + + return SSL_get_timeout(session); +} + +static jbyteArray netty_boringssl_SSL_getAlpnSelected(JNIEnv* env, const SSL* ssl_) { + const unsigned char *proto = NULL; + unsigned int proto_len = 0; + + SSL_get0_alpn_selected(ssl_, &proto, &proto_len); + if (proto == NULL) { + return NULL; + } + jbyteArray bytes = (*env)->NewByteArray(env, proto_len); + if (bytes == NULL) { + return NULL; + } + (*env)->SetByteArrayRegion(env, bytes, 0, proto_len, (jbyte *)proto); + return bytes; +} + +void quic_SSL_info_callback(const SSL *ssl, int type, int value) { + if (type == SSL_CB_HANDSHAKE_DONE) { + SSL_CTX* ctx = SSL_get_SSL_CTX(ssl); + if (ctx == NULL) { + return; + } + + JNIEnv* e = NULL; + if (quic_get_java_env(&e) != JNI_OK) { + return; + } + + jobject handshakeCompleteCallback = SSL_CTX_get_ex_data(ctx, handshakeCompleteCallbackIdx); + if (handshakeCompleteCallback == NULL) { + return; + } + + jbyteArray session_id = netty_boringssl_SSL_getSessionId(e, ssl); + jstring cipher = netty_boringssl_SSL_getCipher(e, ssl); + jstring version = netty_boringssl_SSL_getVersion(e, ssl); + jbyteArray peerCert = netty_boringssl_SSL_getPeerCertificate(e, ssl); + jobjectArray certChain = netty_boringssl_SSL_getPeerCertChain(e, ssl); + jlong creationTime = netty_boringssl_SSL_getTime(e, ssl); + jlong timeout = netty_boringssl_SSL_getTimeout(e, ssl); + jbyteArray alpnSelected = netty_boringssl_SSL_getAlpnSelected(e, ssl); + jboolean sessionReused = SSL_session_reused((SSL *) ssl) == 1 ? JNI_TRUE : JNI_FALSE; + + // Execute the java callback + (*e)->CallVoidMethod(e, handshakeCompleteCallback, handshakeCompleteCallbackMethod, + (jlong) ssl, session_id, cipher, version, peerCert, certChain, creationTime, timeout, alpnSelected, sessionReused); + } +} + +int quic_tlsext_servername_callback(SSL *ssl, int *out_alert, void *arg) { + SSL_CTX* ctx = SSL_get_SSL_CTX((SSL*) ssl); + jobject servernameCallback = SSL_CTX_get_ex_data(ctx, servernameCallbackIdx); + if (servernameCallback == NULL) { + // No SNI should be used + return SSL_TLSEXT_ERR_NOACK; + } + + JNIEnv *e = NULL; + if (quic_get_java_env(&e) != JNI_OK) { + // There is something serious wrong just fail the SSL in a fatal way. + return SSL_TLSEXT_ERR_ALERT_FATAL; + } + + jstring servername = NULL; + int resultValue = SSL_TLSEXT_ERR_OK; + int type = SSL_get_servername_type(ssl); + if (type == TLSEXT_NAMETYPE_host_name) { + const char *name = SSL_get_servername(ssl, type); + if (name != NULL) { + servername = (*e)->NewStringUTF(e, name); + if (servername == NULL) { + // There is something serious wrong just fail the SSL in a fatal way. + return SSL_TLSEXT_ERR_ALERT_FATAL; + } + } else { + // There was no SNI infos provided so not ack at the end. + resultValue = SSL_TLSEXT_ERR_NOACK; + } + } + + jlong result = (*e)->CallLongMethod(e, servernameCallback, servernameCallbackMethod, (jlong) ssl, servername); + + if ((*e)->ExceptionCheck(e) == JNI_TRUE) { + // Some exception was thrown. Let's fail. + (*e)->ExceptionClear(e); + return SSL_TLSEXT_ERR_ALERT_FATAL; + } + if (result < 0) { + // If we returned a negative number we want to fail. + return SSL_TLSEXT_ERR_ALERT_FATAL; + } + + // Change the ctx to the one that was returned. + SSL_CTX* newCtx = SSL_set_SSL_CTX(ssl, (SSL_CTX*) result); + if (newCtx == NULL) { + // Setting the SSL_CTX failed. + return SSL_TLSEXT_ERR_ALERT_FATAL; + } + return resultValue; +} + +// see https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set_keylog_callback.html +void keylog_callback(const SSL* ssl, const char* line) { + SSL_CTX* ctx = SSL_get_SSL_CTX(ssl); + if (ctx == NULL) { + return; + } + + JNIEnv* e = NULL; + if (quic_get_java_env(&e) != JNI_OK) { + return; + } + + jobject keylogCallback = SSL_CTX_get_ex_data(ctx, keylogCallbackIdx); + if (keylogCallback == NULL) { + return; + } + + jstring keyString = NULL; + if (line != NULL) { + keyString = (*e)->NewStringUTF(e, line); + if (keyString == NULL) { + return; + } + } + + // Execute the java callback + (*e)->CallVoidMethod(e, keylogCallback, keylogCallbackMethod, (jlong) ssl, keyString); +} + +// Always return 0 as we serialize the session / params to byte[] and so no take ownership. +// See https://commondatastorage.googleapis.com/chromium-boringssl-docs/ssl.h.html#SSL_CTX_sess_set_new_cb +int new_session_callback(SSL *ssl, SSL_SESSION *session) { + SSL_CTX* ctx = SSL_get_SSL_CTX(ssl); + if (ctx == NULL) { + return 0; + } + + JNIEnv* e = NULL; + if (quic_get_java_env(&e) != JNI_OK) { + return 0; + } + + jobject sessionCallback = SSL_CTX_get_ex_data(ctx, sessionCallbackIdx); + if (sessionCallback == NULL) { + return 0; + } + + uint8_t *session_data = NULL; + size_t session_data_len = 0; + if (SSL_SESSION_to_bytes(session, &session_data, &session_data_len) == 0) { + // Get session error + return 0; + } + + jbyteArray sessionBytes = to_byte_array(e, session_data, session_data_len); + // We need to explicit free the session_data after we copied it to byte[]. + // See https://commondatastorage.googleapis.com/chromium-boringssl-docs/ssl.h.html#SSL_SESSION_to_bytes + OPENSSL_free((void *)session_data); + if (sessionBytes == NULL) { + // Get session error + return 0; + } + + jbyteArray peerParamsBytes = NULL; + // There is not need to explicit free peer_params as it will be freed as soon as SSL*. + // See https://commondatastorage.googleapis.com/chromium-boringssl-docs/ssl.h.html#SSL_get_peer_quic_transport_params + const uint8_t *peer_params = NULL; + size_t peer_params_len = 0; + SSL_get_peer_quic_transport_params((SSL*) ssl, &peer_params, &peer_params_len); + if (peer_params_len != 0) { + peerParamsBytes = to_byte_array(e, (uint8_t *) peer_params, peer_params_len); + } + + jboolean singleUse = SSL_SESSION_should_be_single_use(session) == 1 ? JNI_TRUE : JNI_FALSE; + + // Execute the java callback + (*e)->CallVoidMethod(e, sessionCallback, sessionCallbackMethod, (jlong) ssl, (jlong) SSL_SESSION_get_time(session), (jlong) SSL_SESSION_get_timeout(session), sessionBytes, singleUse, peerParamsBytes); + + return 0; +} + +static jlong netty_boringssl_SSLContext_new0(JNIEnv* env, jclass clazz, jboolean server, jbyteArray alpn_protos, jobject handshakeCompleteCallback, jobject certificateCallback, jobject verifyCallback, jobject servernameCallback, jobject keylogCallback, jobject sessionCallback, jobject privateKeyMethod, jobject sessionTicketCallback, jint verifyMode, jobjectArray subjectNames) { + jobject handshakeCompleteCallbackRef = NULL; + jobject certificateCallbackRef = NULL; + jobject verifyCallbackRef = NULL; + jobject servernameCallbackRef = NULL; + jobject keylogCallbackRef = NULL; + jobject sessionCallbackRef = NULL; + jobject privateKeyMethodRef = NULL; + jobject sessionTicketCallbackRef = NULL; + + if ((handshakeCompleteCallbackRef = (*env)->NewGlobalRef(env, handshakeCompleteCallback)) == NULL) { + goto error; + } + + if ((certificateCallbackRef = (*env)->NewGlobalRef(env, certificateCallback)) == NULL) { + goto error; + } + + if ((verifyCallbackRef = (*env)->NewGlobalRef(env, verifyCallback)) == NULL) { + goto error; + } + + if (servernameCallback != NULL) { + if ((servernameCallbackRef = (*env)->NewGlobalRef(env, servernameCallback)) == NULL) { + goto error; + } + } + + if (keylogCallback != NULL) { + if ((keylogCallbackRef = (*env)->NewGlobalRef(env, keylogCallback)) == NULL) { + goto error; + } + } + + if (sessionCallback != NULL) { + if ((sessionCallbackRef = (*env)->NewGlobalRef(env, sessionCallback)) == NULL) { + goto error; + } + } + + if (privateKeyMethod != NULL) { + if ((privateKeyMethodRef = (*env)->NewGlobalRef(env, privateKeyMethod)) == NULL) { + goto error; + } + } + if ((sessionTicketCallbackRef = (*env)->NewGlobalRef(env, sessionTicketCallback)) == NULL) { + goto error; + } + + SSL_CTX *ctx = SSL_CTX_new(TLS_with_buffers_method()); + // When using BoringSSL we want to use CRYPTO_BUFFER to reduce memory usage and minimize overhead as we do not need + // X509* at all and just need the raw bytes of the certificates to construct our Java X509Certificate. + // + // See https://github.com/google/boringssl/blob/chromium-stable/PORTING.md#crypto_buffer + SSL_CTX_set_min_proto_version(ctx, TLS1_3_VERSION); + SSL_CTX_set_max_proto_version(ctx, TLS1_3_VERSION); + + // Automatically release buffers + SSL_CTX_set_mode(ctx, SSL_MODE_RELEASE_BUFFERS); + + // Set callback which will inform when handshake is done + SSL_CTX_set_ex_data(ctx, handshakeCompleteCallbackIdx, handshakeCompleteCallbackRef); + SSL_CTX_set_info_callback(ctx, quic_SSL_info_callback); + + // So we can access this in quic_SSL_cert_custom_verify + SSL_CTX_set_ex_data(ctx, verifyCallbackIdx, verifyCallbackRef); + SSL_CTX_set_custom_verify(ctx, verifyMode, quic_SSL_cert_custom_verify); + + SSL_CTX_set_ex_data(ctx, certificateCallbackIdx, certificateCallbackRef); + SSL_CTX_set_cert_cb(ctx, quic_certificate_cb, certificateCallbackRef); + + if (servernameCallbackRef != NULL) { + SSL_CTX_set_ex_data(ctx, servernameCallbackIdx, servernameCallbackRef); + SSL_CTX_set_tlsext_servername_callback(ctx, quic_tlsext_servername_callback); + } + + if (keylogCallbackRef != NULL) { + SSL_CTX_set_ex_data(ctx, keylogCallbackIdx, keylogCallbackRef); + SSL_CTX_set_keylog_callback(ctx, keylog_callback); + } + + if (sessionCallbackRef != NULL) { + SSL_CTX_set_ex_data(ctx, sessionCallbackIdx, sessionCallbackRef); + // The internal cache is never used on a client, this only enables the callbacks. + SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_CLIENT); + SSL_CTX_sess_set_new_cb(ctx, new_session_callback); + } + + if (privateKeyMethodRef != NULL) { + SSL_CTX_set_ex_data(ctx, sslPrivateKeyMethodIdx, privateKeyMethodRef); + } + + SSL_CTX_set_ex_data(ctx, sessionTicketCallbackIdx, sessionTicketCallbackRef); + + // Use a pool for our certificates so we can share these across connections. + SSL_CTX_set_ex_data(ctx, crypto_buffer_pool_idx, CRYPTO_BUFFER_POOL_new()); + + STACK_OF(CRYPTO_BUFFER) *names = arrayToStack(env, subjectNames, NULL); + if (names != NULL) { + SSL_CTX_set0_client_CAs(ctx, names); + } + + if (alpn_protos != NULL) { + int alpn_length = (*env)->GetArrayLength(env, alpn_protos); + alpn_data* alpn = (alpn_data*) OPENSSL_malloc(sizeof(alpn_data)); + if (alpn != NULL) { + // Fill the alpn_data struct + alpn->proto_data = OPENSSL_malloc(alpn_length); + alpn->proto_len = alpn_length; + (*env)->GetByteArrayRegion(env, alpn_protos, 0, alpn_length, (jbyte*) alpn->proto_data); + + SSL_CTX_set_ex_data(ctx, alpn_data_idx, alpn); + if (server == JNI_TRUE) { + SSL_CTX_set_alpn_select_cb(ctx, BoringSSL_callback_alpn_select_proto, (void*) alpn); + } else { + SSL_CTX_set_alpn_protos(ctx, alpn->proto_data, alpn->proto_len); + } + } + } + return (jlong) ctx; +error: + if (handshakeCompleteCallbackRef != NULL) { + (*env)->DeleteGlobalRef(env, handshakeCompleteCallbackRef); + } + if (certificateCallbackRef != NULL) { + (*env)->DeleteGlobalRef(env, certificateCallbackRef); + } + if (verifyCallbackRef != NULL) { + (*env)->DeleteGlobalRef(env, verifyCallbackRef); + } + if (servernameCallbackRef != NULL) { + (*env)->DeleteGlobalRef(env, servernameCallbackRef); + } + if (keylogCallbackRef != NULL) { + (*env)->DeleteGlobalRef(env, keylogCallbackRef); + } + if (sessionCallbackRef != NULL) { + (*env)->DeleteGlobalRef(env, sessionCallbackRef); + } + if (privateKeyMethodRef != NULL) { + (*env)->DeleteGlobalRef(env, privateKeyMethodRef); + } + if (sessionTicketCallbackRef != NULL) { + (*env)->DeleteGlobalRef(env, sessionTicketCallbackRef); + } + return -1; +} + +static void netty_boringssl_SSLContext_free(JNIEnv* env, jclass clazz, jlong ctx) { + SSL_CTX* ssl_ctx = (SSL_CTX*) ctx; + + jobject handshakeCompleteCallbackRef = SSL_CTX_get_ex_data(ssl_ctx, handshakeCompleteCallbackIdx); + if (handshakeCompleteCallbackRef != NULL) { + (*env)->DeleteGlobalRef(env, handshakeCompleteCallbackRef); + } + jobject verifyCallbackRef = SSL_CTX_get_ex_data(ssl_ctx, verifyCallbackIdx); + if (verifyCallbackRef != NULL) { + (*env)->DeleteGlobalRef(env, verifyCallbackRef); + } + jobject certificateCallbackRef = SSL_CTX_get_ex_data(ssl_ctx, certificateCallbackIdx); + if (certificateCallbackRef != NULL) { + (*env)->DeleteGlobalRef(env, certificateCallbackRef); + } + + jobject servernameCallbackRef = SSL_CTX_get_ex_data(ssl_ctx, servernameCallbackIdx); + if (servernameCallbackRef != NULL) { + (*env)->DeleteGlobalRef(env, servernameCallbackRef); + } + + jobject keylogCallbackRef = SSL_CTX_get_ex_data(ssl_ctx, keylogCallbackIdx); + if (keylogCallbackRef != NULL) { + (*env)->DeleteGlobalRef(env, keylogCallbackRef); + } + + jobject sessionCallbackRef = SSL_CTX_get_ex_data(ssl_ctx, sessionCallbackIdx); + if (sessionCallbackRef != NULL) { + (*env)->DeleteGlobalRef(env, sessionCallbackRef); + } + + jobject privateKeyMethodRef = SSL_CTX_get_ex_data(ssl_ctx, sslPrivateKeyMethodIdx); + if (privateKeyMethodRef != NULL) { + (*env)->DeleteGlobalRef(env, privateKeyMethodRef); + } + + alpn_data* data = SSL_CTX_get_ex_data(ssl_ctx, alpn_data_idx); + OPENSSL_free(data); + + jobject sessionTicketCallbackRef = SSL_CTX_get_ex_data(ssl_ctx, sessionTicketCallbackIdx); + if (sessionCallbackRef != NULL) { + (*env)->DeleteGlobalRef(env, sessionTicketCallbackRef); + } + + CRYPTO_BUFFER_POOL* pool = SSL_CTX_get_ex_data(ssl_ctx, crypto_buffer_pool_idx); + SSL_CTX_free(ssl_ctx); + + // The pool should be freed last in case that the SSL_CTX has a reference to things that are stored in the + // pool itself. Otherwise we may see an assert error when trying to call CRYPTO_BUFFER_POOL_free. + if (pool != NULL) { + CRYPTO_BUFFER_POOL_free(pool); + } +} + +static jlong netty_boringssl_SSLContext_setSessionCacheTimeout(JNIEnv* env, jclass clazz, jlong ctx, jlong timeout){ + return SSL_CTX_set_timeout((SSL_CTX*) ctx, timeout); +} + +static jlong netty_boringssl_SSLContext_setSessionCacheSize(JNIEnv* env, jclass clazz, jlong ctx, jlong size) { + if (size >= 0) { + SSL_CTX* ssl_ctx = (SSL_CTX*) ctx; + int mode = SSL_CTX_get_session_cache_mode(ssl_ctx); + // Internal Cache only works on the server side for now. + SSL_CTX_set_session_cache_mode(ssl_ctx, SSL_SESS_CACHE_SERVER | mode); + return SSL_CTX_sess_set_cache_size(ssl_ctx, size); + } + + return 0; +} + +static void netty_boringssl_SSLContext_set_early_data_enabled(JNIEnv* env, jclass clazz, jlong ctx, jboolean enabled){ + SSL_CTX_set_early_data_enabled((SSL_CTX*) ctx, enabled == JNI_TRUE ? 1 : 0); +} + +jlong netty_boringssl_SSL_new0(JNIEnv* env, jclass clazz, jlong ctx, jboolean server, jstring hostname) { + SSL* ssl = SSL_new((SSL_CTX*) ctx); + + if (ssl == NULL) { + return -1; + } + + if (server == JNI_TRUE) { + SSL_set_accept_state(ssl); + } else { + SSL_set_connect_state(ssl); + if (hostname != NULL) { + const char *charHostname = (*env)->GetStringUTFChars(env, hostname, 0); + SSL_set_tlsext_host_name(ssl, charHostname); + (*env)->ReleaseStringUTFChars(env, hostname, charHostname); + } + } + + return (jlong) ssl; +} + +void netty_boringssl_SSL_free(JNIEnv* env, jclass clazz, jlong ssl) { + SSL_free((SSL *) ssl); +} + +jobject netty_boringssl_SSL_getTask(JNIEnv* env, jclass clazz, jlong ssl) { + netty_boringssl_ssl_task_t* ssl_task = SSL_get_ex_data((SSL*) ssl, sslTaskIdx); + + if (ssl_task == NULL || ssl_task->consumed == JNI_TRUE) { + // Either no task was produced or it was already consumed by SSL.getTask(...). + return NULL; + } + ssl_task->consumed = JNI_TRUE; + return ssl_task->task; +} + +void netty_boringssl_SSL_cleanup(JNIEnv* env, jclass clazz, jlong ssl) { + netty_boringssl_ssl_task_t* sslTask = SSL_get_ex_data((SSL *) ssl, sslTaskIdx); + if (sslTask != NULL) { + SSL_set_ex_data((SSL *) ssl, sslTaskIdx, NULL); + netty_boringssl_ssl_task_free(env, sslTask); + } +} + +int netty_boringssl_password_callback(char *buf, int bufsiz, int verify, void *cb) { + char *password = (char *) cb; + if (password == NULL) { + return 0; + } + strncpy(buf, password, bufsiz); + return (int) strlen(buf); +} + +jlong netty_boringssl_EVP_PKEY_parse(JNIEnv* env, jclass clazz, jbyteArray array, jstring password) { + int dataLen = (*env)->GetArrayLength(env, array); + char* data = (char*) (*env)->GetByteArrayElements(env, array, 0); + BIO* bio = BIO_new_mem_buf(data, dataLen); + + const char *charPass; + if (password == NULL) { + charPass = NULL; + } else { + charPass = (*env)->GetStringUTFChars(env, password, 0); + } + + EVP_PKEY *key = PEM_read_bio_PrivateKey(bio, NULL, + (pem_password_cb *)netty_boringssl_password_callback, + (void *)charPass); + BIO_free(bio); + if (charPass != NULL) { + (*env)->ReleaseStringUTFChars(env, password, charPass); + } + (*env)->ReleaseByteArrayElements(env, array, (jbyte*)data, JNI_ABORT); + if (key == NULL) { + return -1; + } + return (jlong) key; +} + +void netty_boringssl_EVP_PKEY_free(JNIEnv* env, jclass clazz, jlong privateKey) { + EVP_PKEY_free((EVP_PKEY*) privateKey); // Safe to call with NULL as well. +} + +jlong netty_boringssl_CRYPTO_BUFFER_stack_new(JNIEnv* env, jclass clazz, jlong ssl, jobjectArray x509Chain){ + CRYPTO_BUFFER_POOL* pool = NULL; + SSL_CTX* ctx = SSL_get_SSL_CTX((SSL*) ssl); + if (ctx != NULL) { + pool = SSL_CTX_get_ex_data(ctx, crypto_buffer_pool_idx); + } + STACK_OF(CRYPTO_BUFFER) *chain = arrayToStack(env, x509Chain, pool); + if (chain == NULL) { + return 0; + } + return (jlong) chain; + +} + +void netty_boringssl_CRYPTO_BUFFER_stack_free(JNIEnv* env, jclass clazz, jlong chain) { + sk_CRYPTO_BUFFER_pop_free((STACK_OF(CRYPTO_BUFFER) *) chain, CRYPTO_BUFFER_free); +} + +jstring netty_boringssl_ERR_last_error(JNIEnv* env, jclass clazz) { + char buf[ERR_LEN]; + unsigned long err = ERR_get_error(); + if (err == 0) { + return NULL; + } + ERR_error_string_n(err, buf, ERR_LEN); + return (*env)->NewStringUTF(env, buf); +} + +static int netty_boringssl_tlsext_ticket_key_cb(SSL *s, unsigned char key_name[16], unsigned char *iv, EVP_CIPHER_CTX *ctx, HMAC_CTX *hctx, int enc) { + SSL_CTX *c = SSL_get_SSL_CTX(s); + if (c == NULL) { + return 0; + } + + jobject sessionTicketCallback = SSL_CTX_get_ex_data(c, sessionTicketCallbackIdx); + if (sessionTicketCallback == NULL) { + return 0; + } + JNIEnv* env = NULL; + if (quic_get_java_env(&env) != JNI_OK) { + return 0; + } + + if (enc) { /* create new session */ + jbyteArray key = (jbyteArray) (*env)->CallObjectMethod(env, sessionTicketCallback, sessionTicketCallbackMethod, NULL); + if (key != NULL) { + int keyLen = (*env)->GetArrayLength(env, key); + if (keyLen != SSL_SESSION_TICKET_KEY_LEN) { + return -1; + } + if (RAND_bytes(iv, EVP_MAX_IV_LENGTH) <= 0) { + return -1; /* insufficient random */ + } + + uint8_t* data = (uint8_t*) (*env)->GetByteArrayElements(env, key, 0); + + memcpy(key_name, data + SSL_SESSION_TICKET_KEY_NAME_OFFSET, SSL_SESSION_TICKET_KEY_NAME_LEN); + + HMAC_Init_ex(hctx, (void*) (data + SSL_SESSION_TICKET_KEY_HMAC_OFFSET), SSL_SESSION_TICKET_HMAC_KEY_LEN, EVP_sha256(), NULL); + + EVP_EncryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, (void*) (data + SSL_SESSION_TICKET_KEY_EVP_OFFSET), iv); + + (*env)->ReleaseByteArrayElements(env, key, (jbyte*) data, JNI_ABORT); + + return 1; + } + // No ticket configured + return 0; + } else { /* retrieve session */ + jbyteArray name = to_byte_array(env, (uint8_t*) key_name, 16); + jbyteArray key = (jbyteArray) (*env)->CallObjectMethod(env, sessionTicketCallback, sessionTicketCallbackMethod, name); + + if (key != NULL) { + int keyLen = (*env)->GetArrayLength(env, key); + if (keyLen != SSL_SESSION_TICKET_KEY_LEN) { + return -1; + } + + uint8_t* data = (uint8_t*) (*env)->GetByteArrayElements(env, key, 0); + // The first byte is used to encode if the key needs to be upgraded. + int is_current_key = *data != 0; + + HMAC_Init_ex(hctx, (void*) (data + SSL_SESSION_TICKET_KEY_HMAC_OFFSET), SSL_SESSION_TICKET_HMAC_KEY_LEN, EVP_sha256(), NULL); + + EVP_DecryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, (void*) (data + SSL_SESSION_TICKET_KEY_EVP_OFFSET), iv); + + (*env)->ReleaseByteArrayElements(env, key, (jbyte*) data, JNI_ABORT); + + if (!is_current_key) { + // The ticket matched a key in the list, and we want to upgrade it to the current + // key. + return 2; + } + // The ticket matched the current key. + return 1; + } + // No matching ticket. + return 0; + } +} + +void netty_boringssl_SSLContext_setSessionTicketKeys(JNIEnv* env, jclass clazz, jlong ctx, jboolean enableCallback) { + if (enableCallback == JNI_TRUE) { + SSL_CTX_set_tlsext_ticket_key_cb((SSL_CTX *) ctx, netty_boringssl_tlsext_ticket_key_cb); + } else { + SSL_CTX_set_tlsext_ticket_key_cb((SSL_CTX *) ctx, NULL); + } +} + +// JNI Registered Methods End + +// JNI Method Registration Table Begin +static const JNINativeMethod statically_referenced_fixed_method_table[] = { + { "ssl_verify_none", "()I", (void *) netty_boringssl_ssl_verify_none }, + { "ssl_verify_fail_if_no_peer_cert", "()I", (void *) netty_boringssl_ssl_verify_fail_if_no_peer_cert }, + { "ssl_verify_peer", "()I", (void *) netty_boringssl_ssl_verify_peer }, + { "x509_v_ok", "()I", (void *) netty_boringssl_x509_v_ok }, + { "x509_v_err_cert_has_expired", "()I", (void *) netty_boringssl_x509_v_err_cert_has_expired }, + { "x509_v_err_cert_not_yet_valid", "()I", (void *) netty_boringssl_x509_v_err_cert_not_yet_valid }, + { "x509_v_err_cert_revoked", "()I", (void *) netty_boringssl_x509_v_err_cert_revoked }, + { "x509_v_err_unspecified", "()I", (void *) netty_boringssl_x509_v_err_unspecified }, + { "ssl_sign_rsa_pkcs_sha1", "()I", (void *) netty_boringssl_ssl_sign_rsa_pkcs_sha1 }, + { "ssl_sign_rsa_pkcs_sha256", "()I", (void *) netty_boringssl_ssl_sign_rsa_pkcs_sha256 }, + { "ssl_sign_rsa_pkcs_sha384", "()I", (void *) netty_boringssl_ssl_sign_rsa_pkcs_sha384 }, + { "ssl_sign_rsa_pkcs_sha512", "()I", (void *) netty_boringssl_ssl_sign_rsa_pkcs_sha512 }, + { "ssl_sign_ecdsa_pkcs_sha1", "()I", (void *) netty_boringssl_ssl_sign_ecdsa_pkcs_sha1 }, + { "ssl_sign_ecdsa_secp256r1_sha256", "()I", (void *) netty_boringssl_ssl_sign_ecdsa_secp256r1_sha256 }, + { "ssl_sign_ecdsa_secp384r1_sha384", "()I", (void *) netty_boringssl_ssl_sign_ecdsa_secp384r1_sha384 }, + { "ssl_sign_ecdsa_secp521r1_sha512", "()I", (void *) netty_boringssl_ssl_sign_ecdsa_secp521r1_sha512 }, + { "ssl_sign_rsa_pss_rsae_sha256", "()I", (void *) netty_boringssl_ssl_sign_rsa_pss_rsae_sha256 }, + { "ssl_sign_rsa_pss_rsae_sha384", "()I", (void *) netty_boringssl_ssl_sign_rsa_pss_rsae_sha384 }, + { "ssl_sign_rsa_pss_rsae_sha512", "()I", (void *) netty_boringssl_ssl_sign_rsa_pss_rsae_sha512 }, + { "ssl_sign_ed25519", "()I", (void *) netty_boringssl_ssl_sign_ed25519 }, + { "ssl_sign_rsa_pkcs1_md5_sha1", "()I", (void *) netty_boringssl_ssl_sign_rsa_pkcs1_md5_sha1 } +}; + +static const jint statically_referenced_fixed_method_table_size = sizeof(statically_referenced_fixed_method_table) / sizeof(statically_referenced_fixed_method_table[0]); +static const JNINativeMethod fixed_method_table[] = { + { "SSLContext_new0", "(Z[BLjava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;I[[B)J", (void *) netty_boringssl_SSLContext_new0 }, + { "SSLContext_free", "(J)V", (void *) netty_boringssl_SSLContext_free }, + { "SSLContext_setSessionCacheTimeout", "(JJ)J", (void *) netty_boringssl_SSLContext_setSessionCacheTimeout }, + { "SSLContext_setSessionCacheSize", "(JJ)J", (void *) netty_boringssl_SSLContext_setSessionCacheSize }, + { "SSLContext_set_early_data_enabled", "(JZ)V", (void *) netty_boringssl_SSLContext_set_early_data_enabled }, + { "SSLContext_setSessionTicketKeys", "(JZ)V", (void *) netty_boringssl_SSLContext_setSessionTicketKeys }, + { "SSL_new0", "(JZLjava/lang/String;)J", (void *) netty_boringssl_SSL_new0 }, + { "SSL_free", "(J)V", (void *) netty_boringssl_SSL_free }, + { "SSL_getTask", "(J)Ljava/lang/Runnable;", (void *) netty_boringssl_SSL_getTask }, + { "SSL_cleanup", "(J)V", (void *) netty_boringssl_SSL_cleanup }, + { "EVP_PKEY_parse", "([BLjava/lang/String;)J", (void *) netty_boringssl_EVP_PKEY_parse }, + { "EVP_PKEY_free", "(J)V", (void *) netty_boringssl_EVP_PKEY_free }, + { "CRYPTO_BUFFER_stack_new", "(J[[B)J", (void *) netty_boringssl_CRYPTO_BUFFER_stack_new }, + { "CRYPTO_BUFFER_stack_free", "(J)V", (void *) netty_boringssl_CRYPTO_BUFFER_stack_free }, + { "ERR_last_error", "()Ljava/lang/String;", (void *) netty_boringssl_ERR_last_error } +}; + +static const jint fixed_method_table_size = sizeof(fixed_method_table) / sizeof(fixed_method_table[0]); + +// JNI Method Registration Table End + +static void unload_all_classes(JNIEnv* env) { + NETTY_JNI_UTIL_UNLOAD_CLASS(env, byteArrayClass); + NETTY_JNI_UTIL_UNLOAD_CLASS(env, stringClass); + NETTY_JNI_UTIL_UNLOAD_CLASS_WEAK(env, sslTaskClassWeak); + NETTY_JNI_UTIL_UNLOAD_CLASS_WEAK(env, sslPrivateKeyMethodTaskClassWeak); + NETTY_JNI_UTIL_UNLOAD_CLASS_WEAK(env, sslPrivateKeyMethodSignTaskClassWeak); + NETTY_JNI_UTIL_UNLOAD_CLASS_WEAK(env, sslPrivateKeyMethodDecryptTaskClassWeak); + NETTY_JNI_UTIL_UNLOAD_CLASS_WEAK(env, certificateTaskClassWeak); + NETTY_JNI_UTIL_UNLOAD_CLASS_WEAK(env, verifyTaskClassWeak); + NETTY_JNI_UTIL_UNLOAD_CLASS_WEAK(env, handshakeCompleteCallbackClassWeak); + NETTY_JNI_UTIL_UNLOAD_CLASS_WEAK(env, servernameCallbackClassWeak); + NETTY_JNI_UTIL_UNLOAD_CLASS_WEAK(env, keylogCallbackClassWeak); + NETTY_JNI_UTIL_UNLOAD_CLASS_WEAK(env, sessionCallbackClassWeak); + NETTY_JNI_UTIL_UNLOAD_CLASS_WEAK(env, sessionTicketCallbackClassWeak); +} + +// IMPORTANT: If you add any NETTY_JNI_UTIL_LOAD_CLASS or NETTY_JNI_UTIL_FIND_CLASS calls you also need to update +// Quiche to reflect that. +jint netty_boringssl_JNI_OnLoad(JNIEnv* env, const char* packagePrefix) { + int ret = JNI_ERR; + int staticallyRegistered = 0; + int nativeRegistered = 0; + char* name = NULL; + char* combinedName = NULL; + + jclass sslTaskClass = NULL; + jclass sslPrivateKeyMethodTaskClass = NULL; + jclass sslPrivateKeyMethodSignTaskClass = NULL; + jclass sslPrivateKeyMethodDecryptTaskClass = NULL; + jclass certificateTaskClass = NULL; + jclass verifyTaskClass = NULL; + jclass handshakeCompleteCallbackClass = NULL; + jclass servernameCallbackClass = NULL; + jclass keylogCallbackClass = NULL; + jclass sessionCallbackClass = NULL; + jclass sessionTicketCallbackClass = NULL; + + // We must register the statically referenced methods first! + if (netty_jni_util_register_natives(env, + packagePrefix, + STATICALLY_CLASSNAME, + statically_referenced_fixed_method_table, + statically_referenced_fixed_method_table_size) != 0) { + goto done; + } + staticallyRegistered = 1; + + if (netty_jni_util_register_natives(env, + packagePrefix, + CLASSNAME, + fixed_method_table, + fixed_method_table_size) != 0) { + goto done; + } + nativeRegistered = 1; + // Initialize this module + + NETTY_JNI_UTIL_LOAD_CLASS(env, byteArrayClass, "[B", done); + NETTY_JNI_UTIL_LOAD_CLASS(env, stringClass, "java/lang/String", done); + + NETTY_JNI_UTIL_PREPEND(packagePrefix, "io/netty/handler/codec/quic/BoringSSLTask", name, done); + + NETTY_JNI_UTIL_LOAD_CLASS_WEAK(env, sslTaskClassWeak, name, done); + NETTY_JNI_UTIL_NEW_LOCAL_FROM_WEAK(env, sslTaskClass, sslTaskClassWeak, done); + + NETTY_JNI_UTIL_GET_FIELD(env, sslTaskClass, sslTaskReturnValue, "returnValue", "I", done); + NETTY_JNI_UTIL_GET_FIELD(env, sslTaskClass, sslTaskComplete, "complete", "Z", done); + NETTY_JNI_UTIL_GET_METHOD(env, sslTaskClass, sslTaskDestroyMethod, "destroy", "()V", done); + + NETTY_JNI_UTIL_PREPEND(packagePrefix, "io/netty/handler/codec/quic/BoringSSLPrivateKeyMethodTask", name, done); + NETTY_JNI_UTIL_LOAD_CLASS_WEAK(env, sslPrivateKeyMethodTaskClassWeak, name, done); + NETTY_JNI_UTIL_NEW_LOCAL_FROM_WEAK(env, sslPrivateKeyMethodTaskClass, sslPrivateKeyMethodTaskClassWeak, done); + NETTY_JNI_UTIL_GET_FIELD(env, sslPrivateKeyMethodTaskClass, sslPrivateKeyMethodTaskResultBytesField, "resultBytes", "[B", done); + + NETTY_JNI_UTIL_PREPEND(packagePrefix, "io/netty/handler/codec/quic/BoringSSLPrivateKeyMethodSignTask", name, done); + NETTY_JNI_UTIL_LOAD_CLASS_WEAK(env, sslPrivateKeyMethodSignTaskClassWeak, name, done); + NETTY_JNI_UTIL_NEW_LOCAL_FROM_WEAK(env, sslPrivateKeyMethodSignTaskClass, sslPrivateKeyMethodSignTaskClassWeak, done); + NETTY_JNI_UTIL_PREPEND(packagePrefix, "io/netty/handler/codec/quic/BoringSSLPrivateKeyMethod;)V", name, done); + NETTY_JNI_UTIL_PREPEND("(JI[BL", name, combinedName, done); + free(name); + name = combinedName; + combinedName = NULL; + NETTY_JNI_UTIL_GET_METHOD(env, sslPrivateKeyMethodSignTaskClass, sslPrivateKeyMethodSignTaskInitMethod, "", name, done); + + NETTY_JNI_UTIL_PREPEND(packagePrefix, "io/netty/handler/codec/quic/BoringSSLPrivateKeyMethodDecryptTask", name, done); + NETTY_JNI_UTIL_LOAD_CLASS_WEAK(env, sslPrivateKeyMethodDecryptTaskClassWeak, name, done); + NETTY_JNI_UTIL_NEW_LOCAL_FROM_WEAK(env, sslPrivateKeyMethodDecryptTaskClass, sslPrivateKeyMethodDecryptTaskClassWeak, done); + NETTY_JNI_UTIL_PREPEND(packagePrefix, "io/netty/handler/codec/quic/BoringSSLPrivateKeyMethod;)V", name, done); + NETTY_JNI_UTIL_PREPEND("(J[BL", name, combinedName, done); + free(name); + name = combinedName; + combinedName = NULL; + NETTY_JNI_UTIL_GET_METHOD(env, sslPrivateKeyMethodDecryptTaskClass, sslPrivateKeyMethodDecryptTaskInitMethod, "", name, done); + + NETTY_JNI_UTIL_PREPEND(packagePrefix, "io/netty/handler/codec/quic/BoringSSLCertificateCallbackTask", name, done); + NETTY_JNI_UTIL_LOAD_CLASS_WEAK(env, certificateTaskClassWeak, name, done); + NETTY_JNI_UTIL_NEW_LOCAL_FROM_WEAK(env, certificateTaskClass, certificateTaskClassWeak, done); + NETTY_JNI_UTIL_PREPEND(packagePrefix, "io/netty/handler/codec/quic/BoringSSLCertificateCallback;)V", name, done); + NETTY_JNI_UTIL_PREPEND("(J[B[[B[Ljava/lang/String;L", name, combinedName, done); + free(name); + name = combinedName; + combinedName = NULL; + NETTY_JNI_UTIL_GET_METHOD(env, certificateTaskClass, certificateTaskClassInitMethod, "", name, done); + NETTY_JNI_UTIL_GET_FIELD(env, certificateTaskClass, certificateTaskClassChainField, "chain", "J", done); + NETTY_JNI_UTIL_GET_FIELD(env, certificateTaskClass, certificateTaskClassKeyField, "key", "J", done); + + NETTY_JNI_UTIL_PREPEND(packagePrefix, "io/netty/handler/codec/quic/BoringSSLCertificateVerifyCallbackTask", name, done); + NETTY_JNI_UTIL_LOAD_CLASS_WEAK(env, verifyTaskClassWeak, name, done); + NETTY_JNI_UTIL_NEW_LOCAL_FROM_WEAK(env, verifyTaskClass, verifyTaskClassWeak, done); + NETTY_JNI_UTIL_PREPEND(packagePrefix, "io/netty/handler/codec/quic/BoringSSLCertificateVerifyCallback;)V", name, done); + NETTY_JNI_UTIL_PREPEND("(J[[BLjava/lang/String;L", name, combinedName, done); + free(name); + name = combinedName; + combinedName = NULL; + NETTY_JNI_UTIL_GET_METHOD(env, verifyTaskClass, verifyTaskClassInitMethod, "", name, done); + + NETTY_JNI_UTIL_PREPEND(packagePrefix, "io/netty/handler/codec/quic/BoringSSLHandshakeCompleteCallback", name, done); + NETTY_JNI_UTIL_LOAD_CLASS_WEAK(env, handshakeCompleteCallbackClassWeak, name, done); + NETTY_JNI_UTIL_NEW_LOCAL_FROM_WEAK(env, handshakeCompleteCallbackClass, handshakeCompleteCallbackClassWeak, done); + NETTY_JNI_UTIL_GET_METHOD(env, handshakeCompleteCallbackClass, handshakeCompleteCallbackMethod, "handshakeComplete", "(J[BLjava/lang/String;Ljava/lang/String;[B[[BJJ[BZ)V", done); + + NETTY_JNI_UTIL_PREPEND(packagePrefix, "io/netty/handler/codec/quic/BoringSSLTlsextServernameCallback", name, done); + NETTY_JNI_UTIL_LOAD_CLASS_WEAK(env, servernameCallbackClassWeak, name, done); + NETTY_JNI_UTIL_NEW_LOCAL_FROM_WEAK(env, servernameCallbackClass, servernameCallbackClassWeak, done); + NETTY_JNI_UTIL_GET_METHOD(env, servernameCallbackClass, servernameCallbackMethod, "selectCtx", "(JLjava/lang/String;)J", done); + + NETTY_JNI_UTIL_PREPEND(packagePrefix, "io/netty/handler/codec/quic/BoringSSLKeylogCallback", name, done); + NETTY_JNI_UTIL_LOAD_CLASS_WEAK(env, keylogCallbackClassWeak, name, done); + NETTY_JNI_UTIL_NEW_LOCAL_FROM_WEAK(env, keylogCallbackClass, keylogCallbackClassWeak, done); + NETTY_JNI_UTIL_GET_METHOD(env, keylogCallbackClass, keylogCallbackMethod, "logKey", "(JLjava/lang/String;)V", done); + + NETTY_JNI_UTIL_PREPEND(packagePrefix, "io/netty/handler/codec/quic/BoringSSLSessionCallback", name, done); + NETTY_JNI_UTIL_LOAD_CLASS_WEAK(env, sessionCallbackClassWeak, name, done); + NETTY_JNI_UTIL_NEW_LOCAL_FROM_WEAK(env, sessionCallbackClass, sessionCallbackClassWeak, done); + NETTY_JNI_UTIL_GET_METHOD(env, sessionCallbackClass, sessionCallbackMethod, "newSession", "(JJJ[BZ[B)V", done); + + NETTY_JNI_UTIL_PREPEND(packagePrefix, "io/netty/handler/codec/quic/BoringSSLSessionTicketCallback", name, done); + NETTY_JNI_UTIL_LOAD_CLASS_WEAK(env, sessionTicketCallbackClassWeak, name, done); + NETTY_JNI_UTIL_NEW_LOCAL_FROM_WEAK(env, sessionTicketCallbackClass, sessionTicketCallbackClassWeak, done); + NETTY_JNI_UTIL_GET_METHOD(env, sessionTicketCallbackClass, sessionTicketCallbackMethod, "findSessionTicket", "([B)[B", done); + + verifyCallbackIdx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL); + certificateCallbackIdx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL); + handshakeCompleteCallbackIdx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL); + servernameCallbackIdx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL); + keylogCallbackIdx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL); + sessionCallbackIdx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL); + sslPrivateKeyMethodIdx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL); + sslTaskIdx = SSL_get_ex_new_index(0, NULL, NULL, NULL, NULL); + alpn_data_idx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL); + crypto_buffer_pool_idx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL); + sessionTicketCallbackIdx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL); + + ret = NETTY_JNI_UTIL_JNI_VERSION; +done: + if (ret == JNI_ERR) { + if (staticallyRegistered == 1) { + netty_jni_util_unregister_natives(env, packagePrefix, STATICALLY_CLASSNAME); + } + if (nativeRegistered == 1) { + netty_jni_util_unregister_natives(env, packagePrefix, CLASSNAME); + } + + unload_all_classes(env); + } + + NETTY_JNI_UTIL_DELETE_LOCAL(env, sslTaskClass); + NETTY_JNI_UTIL_DELETE_LOCAL(env, sslPrivateKeyMethodTaskClass); + NETTY_JNI_UTIL_DELETE_LOCAL(env, sslPrivateKeyMethodSignTaskClass); + NETTY_JNI_UTIL_DELETE_LOCAL(env, sslPrivateKeyMethodDecryptTaskClass); + NETTY_JNI_UTIL_DELETE_LOCAL(env, certificateTaskClass); + NETTY_JNI_UTIL_DELETE_LOCAL(env, verifyTaskClass); + NETTY_JNI_UTIL_DELETE_LOCAL(env, handshakeCompleteCallbackClass); + NETTY_JNI_UTIL_DELETE_LOCAL(env, servernameCallbackClass); + NETTY_JNI_UTIL_DELETE_LOCAL(env, keylogCallbackClass); + NETTY_JNI_UTIL_DELETE_LOCAL(env, sessionCallbackClass); + NETTY_JNI_UTIL_DELETE_LOCAL(env, sessionTicketCallbackClass); + + return ret; +} + +void netty_boringssl_JNI_OnUnload(JNIEnv* env, const char* packagePrefix) { + unload_all_classes(env); + + netty_jni_util_unregister_natives(env, packagePrefix, STATICALLY_CLASSNAME); + netty_jni_util_unregister_natives(env, packagePrefix, CLASSNAME); +} diff --git a/codec-native-quic/src/main/c/netty_quic_boringssl.h b/codec-native-quic/src/main/c/netty_quic_boringssl.h new file mode 100644 index 0000000..ea2e2d5 --- /dev/null +++ b/codec-native-quic/src/main/c/netty_quic_boringssl.h @@ -0,0 +1,23 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +#ifndef NETTY_BORINGSSL_H_ +#define NETTY_BORINGSSL_H_ + +// JNI initialization hooks. Users of this file are responsible for calling these in the JNI_OnLoad and JNI_OnUnload methods. +jint netty_boringssl_JNI_OnLoad(JNIEnv* env, const char* packagePrefix); +void netty_boringssl_JNI_OnUnload(JNIEnv* env, const char* packagePrefix); + +#endif /* NETTY_BORINGSSL_H_ */ diff --git a/codec-native-quic/src/main/c/netty_quic_quiche.c b/codec-native-quic/src/main/c/netty_quic_quiche.c new file mode 100644 index 0000000..d618cd9 --- /dev/null +++ b/codec-native-quic/src/main/c/netty_quic_quiche.c @@ -0,0 +1,1366 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +// This needs to be included for quiche_recv_info and quiche_send_info structs. +#include +#else +#include +#include +#include +// This needs to be included for quiche_recv_info and quiche_send_info structs. +#include +#endif // _WIN32 + + +#include +#include "netty_jni_util.h" +#include "netty_quic_boringssl.h" +#include "netty_quic.h" + +// Add define if NETTY_QUIC_BUILD_STATIC is defined so it is picked up in netty_jni_util.c +#ifdef NETTY_QUIC_BUILD_STATIC +#define NETTY_JNI_UTIL_BUILD_STATIC +#endif + +#define STATICALLY_CLASSNAME "io/netty/handler/codec/quic/QuicheNativeStaticallyReferencedJniMethods" +#define QUICHE_CLASSNAME "io/netty/handler/codec/quic/Quiche" +#define LIBRARYNAME "netty_quiche" + +static jweak quiche_logger_class_weak = NULL; +static jmethodID quiche_logger_class_log = NULL; +static jobject quiche_logger = NULL; +static JavaVM *global_vm = NULL; + +static jclass integer_class = NULL; +static jmethodID integer_class_valueof = NULL; + +static jclass boolean_class = NULL; +static jmethodID boolean_class_valueof = NULL; + +static jclass long_class = NULL; +static jmethodID long_class_valueof = NULL; + +static jclass inet4address_class = NULL; +static jmethodID inet4address_class_get_by_address = NULL; + +static jclass inet6address_class = NULL; +static jmethodID inet6address_class_get_by_address = NULL; + +static jclass inetsocketaddress_class = NULL; +static jmethodID inetsocketaddress_class_constructor = NULL; + +static jclass object_class = NULL; + + +static char const* staticPackagePrefix = NULL; + + +jint quic_get_java_env(JNIEnv **env) +{ + return (*global_vm)->GetEnv(global_vm, (void **)env, NETTY_JNI_UTIL_JNI_VERSION); +} + +static jint netty_quiche_afInet(JNIEnv* env, jclass clazz) { + return AF_INET; +} + +static jint netty_quiche_afInet6(JNIEnv* env, jclass clazz) { + return AF_INET6; +} + +static jint netty_quiche_sizeofSockaddrIn(JNIEnv* env, jclass clazz) { + return sizeof(struct sockaddr_in); +} + +static jint netty_quiche_sizeofSockaddrIn6(JNIEnv* env, jclass clazz) { + return sizeof(struct sockaddr_in6); +} + +static jint netty_quiche_sockaddrInOffsetofSinFamily(JNIEnv* env, jclass clazz) { + return offsetof(struct sockaddr_in, sin_family); +} + +static jint netty_quiche_sockaddrInOffsetofSinPort(JNIEnv* env, jclass clazz) { + return offsetof(struct sockaddr_in, sin_port); +} + +static jint netty_quiche_sockaddrInOffsetofSinAddr(JNIEnv* env, jclass clazz) { + return offsetof(struct sockaddr_in, sin_addr); +} + +static jint netty_quiche_inAddressOffsetofSAddr(JNIEnv* env, jclass clazz) { + return offsetof(struct in_addr, s_addr); +} + +static jint netty_quiche_sockaddrIn6OffsetofSin6Family(JNIEnv* env, jclass clazz) { + return offsetof(struct sockaddr_in6, sin6_family); +} + +static jint netty_quiche_sockaddrIn6OffsetofSin6Port(JNIEnv* env, jclass clazz) { + return offsetof(struct sockaddr_in6, sin6_port); +} + +static jint netty_quiche_sockaddrIn6OffsetofSin6Flowinfo(JNIEnv* env, jclass clazz) { + return offsetof(struct sockaddr_in6, sin6_flowinfo); +} + +static jint netty_quiche_sockaddrIn6OffsetofSin6Addr(JNIEnv* env, jclass clazz) { + return offsetof(struct sockaddr_in6, sin6_addr); +} + +static jint netty_quiche_sockaddrIn6OffsetofSin6ScopeId(JNIEnv* env, jclass clazz) { + return offsetof(struct sockaddr_in6, sin6_scope_id); +} + +static jint netty_quiche_in6AddressOffsetofS6Addr(JNIEnv* env, jclass clazz) { + return offsetof(struct in6_addr, s6_addr); +} + +static jint netty_quiche_sizeofSockaddrStorage(JNIEnv* env, jclass clazz) { + return sizeof(struct sockaddr_storage); +} +static jint netty_quiche_sizeofSizeT(JNIEnv* env, jclass clazz) { + return sizeof(size_t); +} + +static jint netty_quiche_sizeofSocklenT(JNIEnv* env, jclass clazz) { + return sizeof(socklen_t); +} + +static jint netty_quiche_sizeofTimespec(JNIEnv* env, jclass clazz) { + return sizeof(struct timespec); +} + +static jint netty_quiche_sizeofTimeT(JNIEnv* env, jclass clazz) { + return sizeof(time_t); +} + +static jint netty_quiche_sizeofLong(JNIEnv* env, jclass clazz) { + return sizeof(long); +} + +static jint netty_quiche_timespecOffsetofTvSec(JNIEnv* env, jclass clazz) { + return offsetof(struct timespec, tv_sec); +} + +static jint timespecOffsetofTvNsec(JNIEnv* env, jclass clazz) { + return offsetof(struct timespec, tv_nsec); +} + +static jint netty_quicheRecvInfoOffsetofFrom(JNIEnv* env, jclass clazz) { + return offsetof(quiche_recv_info, from); +} + +static jint netty_quicheRecvInfoOffsetofFromLen(JNIEnv* env, jclass clazz) { + return offsetof(quiche_recv_info, from_len); +} + +static jint netty_quicheRecvInfoOffsetofTo(JNIEnv* env, jclass clazz) { + return offsetof(quiche_recv_info, to); +} + +static jint netty_quicheRecvInfoOffsetofToLen(JNIEnv* env, jclass clazz) { + return offsetof(quiche_recv_info, to_len); +} + +static jint netty_sizeofQuicheRecvInfo(JNIEnv* env, jclass clazz) { + return sizeof(quiche_recv_info); +} + +static jint netty_quicheSendInfoOffsetofTo(JNIEnv* env, jclass clazz) { + return offsetof(quiche_send_info, to); +} + +static jint netty_quicheSendInfoOffsetofToLen(JNIEnv* env, jclass clazz) { + return offsetof(quiche_send_info, to_len); +} + +static jint netty_quicheSendInfoOffsetofFrom(JNIEnv* env, jclass clazz) { + return offsetof(quiche_send_info, from); +} + +static jint netty_quicheSendInfoOffsetofFromLen(JNIEnv* env, jclass clazz) { + return offsetof(quiche_send_info, from_len); +} + +static jint netty_quicheSendInfoOffsetofAt(JNIEnv* env, jclass clazz) { + return offsetof(quiche_send_info, at); +} + +static jint netty_sizeofQuicheSendInfo(JNIEnv* env, jclass clazz) { + return sizeof(quiche_send_info); +} + +static jint netty_quiche_max_conn_id_len(JNIEnv* env, jclass clazz) { + return QUICHE_MAX_CONN_ID_LEN; +} + +static jint netty_quiche_protocol_version(JNIEnv* env, jclass clazz) { + return QUICHE_PROTOCOL_VERSION; +} + +static jint netty_quiche_shutdown_read(JNIEnv* env, jclass clazz) { + return QUICHE_SHUTDOWN_READ; +} + +static jint netty_quiche_shutdown_write(JNIEnv* env, jclass clazz) { + return QUICHE_SHUTDOWN_WRITE; +} + +static jint netty_quiche_err_done(JNIEnv* env, jclass clazz) { + return QUICHE_ERR_DONE; +} + +static jint netty_quiche_err_buffer_too_short(JNIEnv* env, jclass clazz) { + return QUICHE_ERR_BUFFER_TOO_SHORT; +} + +static jint netty_quiche_err_unknown_version(JNIEnv* env, jclass clazz) { + return QUICHE_ERR_UNKNOWN_VERSION; +} + +static jint netty_quiche_err_invalid_frame(JNIEnv* env, jclass clazz) { + return QUICHE_ERR_INVALID_FRAME; +} + +static jint netty_quiche_err_invalid_packet(JNIEnv* env, jclass clazz) { + return QUICHE_ERR_INVALID_PACKET; +} + +static jint netty_quiche_err_invalid_state(JNIEnv* env, jclass clazz) { + return QUICHE_ERR_INVALID_STATE; +} + +static jint netty_quiche_err_invalid_stream_state(JNIEnv* env, jclass clazz) { + return QUICHE_ERR_INVALID_STREAM_STATE; +} + +static jint netty_quiche_err_invalid_transport_param(JNIEnv* env, jclass clazz) { + return QUICHE_ERR_INVALID_TRANSPORT_PARAM; +} + +static jint netty_quiche_err_crypto_fail(JNIEnv* env, jclass clazz) { + return QUICHE_ERR_CRYPTO_FAIL; +} + +static jint netty_quiche_err_tls_fail(JNIEnv* env, jclass clazz) { + return QUICHE_ERR_TLS_FAIL; +} + +static jint netty_quiche_err_flow_control(JNIEnv* env, jclass clazz) { + return QUICHE_ERR_FLOW_CONTROL; +} + +static jint netty_quiche_err_stream_limit(JNIEnv* env, jclass clazz) { + return QUICHE_ERR_STREAM_LIMIT; +} + +static jint netty_quiche_err_final_size(JNIEnv* env, jclass clazz) { + return QUICHE_ERR_FINAL_SIZE; +} + +static jint netty_quiche_err_stream_reset(JNIEnv* env, jclass clazz) { + return QUICHE_ERR_STREAM_RESET; +} + +static jint netty_quiche_err_stream_stopped(JNIEnv* env, jclass clazz) { + return QUICHE_ERR_STREAM_STOPPED; +} + +static jint netty_quiche_err_congestion_control(JNIEnv* env, jclass clazz) { + return QUICHE_ERR_CONGESTION_CONTROL; +} + +static jint netty_quiche_err_id_limit(JNIEnv* env, jclass clazz) { + return QUICHE_ERR_ID_LIMIT; +} + +static jint netty_quiche_err_out_of_identifiers(JNIEnv* env, jclass clazz) { + return QUICHE_ERR_OUT_OF_IDENTIFIERS; +} + +static jint netty_quiche_err_key_update(JNIEnv* env, jclass clazz) { + return QUICHE_ERR_KEY_UPDATE; +} + +static jint netty_quiche_cc_reno(JNIEnv* env, jclass clazz) { + return QUICHE_CC_RENO; +} + +static jint netty_quiche_cc_cubic(JNIEnv* env, jclass clazz) { + return QUICHE_CC_CUBIC; +} + +static jint netty_quiche_cc_bbr(JNIEnv* env, jclass clazz) { + return QUICHE_CC_BBR; +} + +static jint netty_quiche_path_event_type_new(JNIEnv* env, jclass clazz) { + return QUICHE_PATH_EVENT_NEW; +} + +static jint netty_quiche_path_event_type_validated(JNIEnv* env, jclass clazz) { + return QUICHE_PATH_EVENT_VALIDATED; +} + +static jint netty_quiche_path_event_type_failed_validation(JNIEnv* env, jclass clazz) { + return QUICHE_PATH_EVENT_FAILED_VALIDATION; +} + +static jint netty_quiche_path_event_type_closed(JNIEnv* env, jclass clazz) { + return QUICHE_PATH_EVENT_CLOSED; +} + +static jint netty_quiche_path_event_type_reused_source_connection_id(JNIEnv* env, jclass clazz) { + return QUICHE_PATH_EVENT_REUSED_SOURCE_CONNECTION_ID; +} + +static jint netty_quiche_path_event_type_peer_migrated(JNIEnv* env, jclass clazz) { + return QUICHE_PATH_EVENT_PEER_MIGRATED; +} + +static jstring netty_quiche_version(JNIEnv* env, jclass clazz) { + return (*env)->NewStringUTF(env, quiche_version()); +} + +static jboolean netty_quiche_version_is_supported(JNIEnv* env, jclass clazz, jint version) { + return quiche_version_is_supported(version) == true ? JNI_TRUE : JNI_FALSE; +} + +static jboolean netty_quiche_conn_set_qlog_path(JNIEnv* env, jclass clazz, jlong conn, jstring path, + jstring log_title, jstring log_desc) { + const char *nativePath = (*env)->GetStringUTFChars(env, path, 0); + const char *nativeLogTitle = (*env)->GetStringUTFChars(env, log_title, 0); + const char *nativeLogDesc = (*env)->GetStringUTFChars(env, log_desc, 0); + bool ret = quiche_conn_set_qlog_path((quiche_conn *) conn, nativePath, + nativeLogTitle, nativeLogDesc); + (*env)->ReleaseStringUTFChars(env, path, nativePath); + (*env)->ReleaseStringUTFChars(env, log_title, nativeLogTitle); + (*env)->ReleaseStringUTFChars(env, log_desc, nativeLogDesc); + + return ret == true ? JNI_TRUE : JNI_FALSE; +} + +static jint netty_quiche_header_info(JNIEnv* env, jclass clazz, jlong buf, jint buf_len, jint dcil, jlong version, + jlong type, jlong scid, jlong scid_len, jlong dcid, jlong dcid_len, jlong token, jlong token_len) { + return (jint) quiche_header_info((const uint8_t *) buf, (size_t) buf_len, (size_t) dcil, + (uint32_t *) version, (uint8_t *) type, + (uint8_t *) scid, (size_t *) scid_len, + (uint8_t *) dcid, (size_t *) dcid_len, + (uint8_t *) token, (size_t *) token_len); +} + +static jint netty_quiche_negotiate_version(JNIEnv* env, jclass clazz, jlong scid, jint scid_len, jlong dcid, jint dcid_len, jlong out, jint out_len) { + return (jint) quiche_negotiate_version((const uint8_t *) scid, (size_t) scid_len, + (const uint8_t *) dcid, (size_t) dcid_len, + (uint8_t *)out, (size_t) out_len); +} + +static jint netty_quiche_retry(JNIEnv* env, jclass clazz, jlong scid, jint scid_len, jlong dcid, jint dcid_len, + jlong new_scid, jint new_scid_len, jlong token, jint token_len, jint version, jlong out, jint out_len) { + return (jint) quiche_retry((const uint8_t *) scid, (size_t) scid_len, + (const uint8_t *) dcid, (size_t) dcid_len, + (const uint8_t *) new_scid, (size_t) new_scid_len, + (const uint8_t *) token, (size_t) token_len, + (uint32_t) version, (uint8_t *) out, (size_t) out_len); +} + +static jlong netty_quiche_conn_new_with_tls(JNIEnv* env, jclass clazz, jlong scid, jint scid_len, jlong odcid, jint odcid_len, jlong local, jint local_len, jlong peer, jint peer_len, jlong config, jlong ssl, jboolean isServer) { + const uint8_t * odcid_pointer = NULL; + if (odcid_len != -1) { + odcid_pointer = (const uint8_t *) odcid; + } + const struct sockaddr *local_pointer = (const struct sockaddr*) local; + const struct sockaddr *peer_pointer = (const struct sockaddr*) peer; + quiche_conn *conn = quiche_conn_new_with_tls((const uint8_t *) scid, (size_t) scid_len, + odcid_pointer, (size_t) odcid_len, + local_pointer, (socklen_t) local_len, + peer_pointer, (socklen_t) peer_len, + (quiche_config *) config, (void*) ssl, isServer == JNI_TRUE ? true : false); + if (conn == NULL) { + return -1; + } + return (jlong) conn; +} + +static jbyteArray to_byte_array(JNIEnv* env, const uint8_t* bytes, size_t len) { + if (bytes == NULL || len == 0) { + return NULL; + } + jbyteArray array = (*env)->NewByteArray(env, len); + if (array == NULL) { + return NULL; + } + (*env)->SetByteArrayRegion(env,array, 0, len, (jbyte*) bytes); + return array; +} + +static jint netty_quiche_conn_send_quantum(JNIEnv* env, jclass clazz, jlong conn) { + return (jint) quiche_conn_send_quantum((quiche_conn *) conn); +} + +static jbyteArray netty_quiche_conn_trace_id(JNIEnv* env, jclass clazz, jlong conn) { + const uint8_t *trace_id = NULL; + size_t trace_id_len = 0; + + quiche_conn_trace_id((quiche_conn *) conn, &trace_id, &trace_id_len); + return to_byte_array(env, trace_id, trace_id_len); +} + +static jbyteArray netty_quiche_conn_source_id(JNIEnv* env, jclass clazz, jlong conn) { + const uint8_t *id = NULL; + size_t len = 0; + + quiche_conn_source_id((quiche_conn *) conn, &id, &len); + return to_byte_array(env, id, len); +} + +static jbyteArray netty_quiche_conn_destination_id(JNIEnv* env, jclass clazz, jlong conn) { + const uint8_t *id = NULL; + size_t len = 0; + + quiche_conn_destination_id((quiche_conn *) conn, &id, &len); + return to_byte_array(env, id, len); +} + +static jint netty_quiche_conn_recv(JNIEnv* env, jclass clazz, jlong conn, jlong buf, jint buf_len, jlong info) { + return (jint) quiche_conn_recv((quiche_conn *) conn, (uint8_t *) buf, (size_t) buf_len, (quiche_recv_info*) info); +} + +static jint netty_quiche_conn_send(JNIEnv* env, jclass clazz, jlong conn, jlong out, jint out_len, jlong info) { + return (jint) quiche_conn_send((quiche_conn *) conn, (uint8_t *) out, (size_t) out_len, (quiche_send_info*) info); +} + +static void netty_quiche_conn_free(JNIEnv* env, jclass clazz, jlong conn) { + quiche_conn_free((quiche_conn *) conn); +} + +static jobjectArray netty_quiche_conn_peer_error0(JNIEnv* env, jclass clazz, jlong conn) { + bool is_app = false; + uint64_t error_code = 0; + const uint8_t *reason = NULL; + size_t reason_len = 0; + + bool peer_error = quiche_conn_peer_error((quiche_conn *) conn, + &is_app, + &error_code, + &reason, + &reason_len); + if (peer_error) { + jobjectArray array = (*env)->NewObjectArray(env, 3, object_class, NULL); + (*env)->SetObjectArrayElement(env, array, 0, (*env)->CallStaticObjectMethod(env, boolean_class, boolean_class_valueof, is_app ? JNI_TRUE : JNI_FALSE)); + (*env)->SetObjectArrayElement(env, array, 1, (*env)->CallStaticObjectMethod(env, integer_class, integer_class_valueof, (jint) error_code)); + (*env)->SetObjectArrayElement(env, array, 2, to_byte_array(env, reason, reason_len)); + return array; + } + return NULL; +} + +static jlong netty_quiche_conn_peer_streams_left_bidi(JNIEnv* env, jclass clazz, jlong conn) { + return (jlong) quiche_conn_peer_streams_left_bidi((quiche_conn *) conn); +} + +static jlong netty_quiche_conn_peer_streams_left_uni(JNIEnv* env, jclass clazz, jlong conn) { + return (jlong) quiche_conn_peer_streams_left_uni((quiche_conn *) conn); +} + +static jint netty_quiche_conn_stream_priority(JNIEnv* env, jclass clazz, jlong conn, jlong stream_id, jbyte urgency, jboolean incremental) { + return (jint) quiche_conn_stream_priority((quiche_conn *) conn, (uint64_t) stream_id, (uint8_t) urgency, incremental == JNI_TRUE ? true : false); +} + +static jint netty_quiche_conn_stream_recv(JNIEnv* env, jclass clazz, jlong conn, jlong stream_id, jlong out, int buf_len, jlong finAddr) { + return (jint) quiche_conn_stream_recv((quiche_conn *) conn, (uint64_t) stream_id, (uint8_t *) out, (size_t) buf_len, (bool *) finAddr); +} + +static jint netty_quiche_conn_stream_send(JNIEnv* env, jclass clazz, jlong conn, jlong stream_id, jlong buf, int buf_len, jboolean fin) { + return (jint) quiche_conn_stream_send((quiche_conn *) conn, (uint64_t) stream_id, (uint8_t *) buf, (size_t) buf_len, fin == JNI_TRUE ? true : false); +} + +static jint netty_quiche_conn_stream_shutdown(JNIEnv* env, jclass clazz, jlong conn, jlong stream_id, jint direction, jlong err) { + return (jint) quiche_conn_stream_shutdown((quiche_conn *) conn, (uint64_t) stream_id, (enum quiche_shutdown) direction, (uint64_t) err); +} + +static jint netty_quiche_conn_stream_capacity(JNIEnv* env, jclass clazz, jlong conn, jlong stream_id) { + return (jint) quiche_conn_stream_capacity((quiche_conn *) conn, (uint64_t) stream_id); +} + +static jboolean netty_quiche_conn_stream_finished(JNIEnv* env, jclass clazz, jlong conn, jlong stream_id) { + return quiche_conn_stream_finished((quiche_conn *) conn, (uint64_t) stream_id) == true ? JNI_TRUE : JNI_FALSE; +} + +static jint netty_quiche_conn_close(JNIEnv* env, jclass clazz, jlong conn, jboolean app, jlong err, jlong reason, jint reason_len) { + return quiche_conn_close((quiche_conn *) conn, app == JNI_TRUE ? true : false, err, (const uint8_t *) reason, (size_t) reason_len); +} + +static jboolean netty_quiche_conn_is_established(JNIEnv* env, jclass clazz, jlong conn) { + return quiche_conn_is_established((quiche_conn *) conn) == true ? JNI_TRUE : JNI_FALSE; +} + +static jboolean netty_quiche_conn_is_in_early_data(JNIEnv* env, jclass clazz, jlong conn) { + return quiche_conn_is_in_early_data((quiche_conn *) conn) == true ? JNI_TRUE : JNI_FALSE; +} + +static jboolean netty_quiche_conn_is_closed(JNIEnv* env, jclass clazz, jlong conn) { + return quiche_conn_is_closed((quiche_conn *) conn) == true ? JNI_TRUE : JNI_FALSE; +} + +static jboolean netty_quiche_conn_is_timed_out(JNIEnv* env, jclass clazz, jlong conn) { + return quiche_conn_is_timed_out((quiche_conn *) conn) == true ? JNI_TRUE : JNI_FALSE; +} + +static jlongArray netty_quiche_conn_stats(JNIEnv* env, jclass clazz, jlong conn) { + // See https://github.com/cloudflare/quiche/blob/master/quiche/include/quiche.h#L467 + quiche_stats stats = {0,0,0,0,0,0,0,0,0}; + quiche_conn_stats((quiche_conn *) conn, &stats); + + jlongArray statsArray = (*env)->NewLongArray(env, 9); + if (statsArray == NULL) { + // This will put an OOME on the stack + return NULL; + } + jlong statsArrayElements[] = { + (jlong)stats.recv, + (jlong)stats.sent, + (jlong)stats.lost, + (jlong)stats.retrans, + (jlong)stats.sent_bytes, + (jlong)stats.recv_bytes, + (jlong)stats.lost_bytes, + (jlong)stats.stream_retrans_bytes, + (jlong)stats.paths_count + }; + (*env)->SetLongArrayRegion(env, statsArray, 0, 9, statsArrayElements); + return statsArray; +} + +static jlongArray netty_quiche_conn_peer_transport_params(JNIEnv* env, jclass clazz, jlong conn) { + // See https://github.com/cloudflare/quiche/blob/master/quiche/include/quiche.h#L563 + quiche_transport_params params = {0,0,0,0,0,0,0,0,0,0,false,0,0}; + if (!quiche_conn_peer_transport_params((quiche_conn *) conn, ¶ms)) { + return NULL; + } + + jlongArray paramsArray = (*env)->NewLongArray(env, 13); + if (paramsArray == NULL) { + // This will put an OOME on the stack + return NULL; + } + jlong paramsArrayElements[] = { + (jlong)params.peer_max_idle_timeout, + (jlong)params.peer_max_udp_payload_size, + (jlong)params.peer_initial_max_data, + (jlong)params.peer_initial_max_stream_data_bidi_local, + (jlong)params.peer_initial_max_stream_data_bidi_remote, + (jlong)params.peer_initial_max_stream_data_uni, + (jlong)params.peer_initial_max_streams_bidi, + (jlong)params.peer_initial_max_streams_uni, + (jlong)params.peer_ack_delay_exponent, + (jlong)params.peer_disable_active_migration ? 1: 0, + (jlong)params.peer_active_conn_id_limit, + (jlong)params.peer_max_datagram_frame_size + }; + (*env)->SetLongArrayRegion(env, paramsArray, 0, 13, paramsArrayElements); + return paramsArray; +} + + + +static jlong netty_quiche_conn_timeout_as_nanos(JNIEnv* env, jclass clazz, jlong conn) { + return quiche_conn_timeout_as_nanos((quiche_conn *) conn); +} + +static void netty_quiche_conn_on_timeout(JNIEnv* env, jclass clazz, jlong conn) { + quiche_conn_on_timeout((quiche_conn *) conn); +} + +static jlong netty_quiche_conn_readable(JNIEnv* env, jclass clazz, jlong conn) { + quiche_stream_iter* iter = quiche_conn_readable((quiche_conn *) conn); + if (iter == NULL) { + return -1; + } + return (jlong) iter; +} + +static jlong netty_quiche_conn_writable(JNIEnv* env, jclass clazz, jlong conn) { + quiche_stream_iter* iter = quiche_conn_writable((quiche_conn *) conn); + if (iter == NULL) { + return -1; + } + return (jlong) iter; +} + +static void netty_quiche_stream_iter_free(JNIEnv* env, jclass clazz, jlong iter) { + quiche_stream_iter_free((quiche_stream_iter*) iter); +} + +static jint netty_quiche_stream_iter_next(JNIEnv* env, jclass clazz, jlong iter, jlongArray streams) { + quiche_stream_iter* it = (quiche_stream_iter*) iter; + if (it == NULL) { + return 0; + } + int len = (*env)->GetArrayLength(env, streams); + if (len == 0) { + return 0; + } + jlong* elements = (*env)->GetLongArrayElements(env, streams, NULL); + int i = 0; + while (i < len && quiche_stream_iter_next(it, (uint64_t*) elements + i)) { + i++; + } + (*env)->ReleaseLongArrayElements(env, streams, elements, 0); + return i; +} + +static jint netty_quiche_conn_dgram_max_writable_len(JNIEnv* env, jclass clazz, jlong conn) { + return (jint) quiche_conn_dgram_max_writable_len((quiche_conn *) conn); +} + +static jint netty_quiche_conn_dgram_recv_front_len(JNIEnv* env, jclass clazz, jlong conn) { + return (jint) quiche_conn_dgram_recv_front_len((quiche_conn*) conn); +} + +static jint netty_quiche_conn_dgram_recv(JNIEnv* env, jclass clazz, jlong conn, jlong buf, jint buf_len) { + return (jint) quiche_conn_dgram_recv((quiche_conn *) conn, (uint8_t *) buf, (size_t) buf_len); +} + +static jint netty_quiche_conn_dgram_send(JNIEnv* env, jclass clazz, jlong conn, jlong buf, jint buf_len) { + return (jint) quiche_conn_dgram_send((quiche_conn *) conn, (uint8_t *) buf, (size_t) buf_len); +} + +static jint netty_quiche_conn_set_session(JNIEnv* env, jclass clazz, jlong conn, jbyteArray sessionBytes) { + int buf_len = (*env)->GetArrayLength(env, sessionBytes); + uint8_t* buf = (uint8_t*) (*env)->GetByteArrayElements(env, sessionBytes, 0); + int result = (jint) quiche_conn_set_session((quiche_conn *) conn, (uint8_t *) buf, (size_t) buf_len); + (*env)->ReleaseByteArrayElements(env, sessionBytes, (jbyte*) buf, JNI_ABORT); + return result; +} + +static jint netty_quiche_conn_max_send_udp_payload_size(JNIEnv* env, jclass clazz, jlong conn) { + return (jint) quiche_conn_max_send_udp_payload_size((quiche_conn *) conn); +} + +static jint netty_quiche_conn_scids_left(JNIEnv* env, jclass clazz, jlong conn) { + return (jint) quiche_conn_scids_left((quiche_conn *) conn); +} + +static jlong netty_quiche_conn_new_scid(JNIEnv* env, jclass clazz, jlong conn, jlong scid, jint scid_len, jbyteArray reset_token, jboolean retire_if_needed, jlong sequenceAddr) { + uint8_t* buf = (uint8_t*) (*env)->GetByteArrayElements(env, reset_token, 0); + + uint64_t* seq; + if (sequenceAddr < 0) { + uint64_t tmp; + seq = &tmp; + } else { + seq = (uint64_t *) sequenceAddr; + } + jlong ret = quiche_conn_new_scid((quiche_conn *) conn, (const uint8_t *) scid, scid_len, buf, retire_if_needed == JNI_TRUE ? true : false, seq); + (*env)->ReleaseByteArrayElements(env, reset_token, (jbyte*)buf, JNI_ABORT); + return ret; +} + +static jbyteArray netty_quiche_conn_retired_scid_next(JNIEnv* env, jclass clazz, jlong conn) { + const uint8_t *id = NULL; + size_t len = 0; + + if (quiche_conn_retired_scid_next((quiche_conn *) conn, &id, &len)) { + return to_byte_array(env, id, len); + } + return NULL; +} + +static jlong netty_quiche_conn_path_event_next(JNIEnv* env, jclass clazz, jlong conn) { + const struct quiche_path_event *ev = quiche_conn_path_event_next((quiche_conn *) conn); + if (ev == NULL) { + return -1; + } + return (jlong) ev; +} + +static jint netty_quiche_path_event_type(JNIEnv* env, jclass clazz, jlong ev) { + return (jint) quiche_path_event_type((quiche_path_event *) ev); +} + +static jobject netty_new_socket_address(JNIEnv* env, const struct sockaddr_storage* addr) { + jobject address = NULL; + jint port; + if (addr->ss_family == AF_INET) { + struct sockaddr_in* s = (struct sockaddr_in*) addr; + port = ntohs(s->sin_port); + jbyteArray array = to_byte_array(env, (uint8_t*) &s->sin_addr.s_addr, (size_t) 4); + address = (*env)->CallStaticObjectMethod(env, inet4address_class, inet4address_class_get_by_address, array); + } else { + struct sockaddr_in6* s = (struct sockaddr_in6*) addr; + port = ntohs(s->sin6_port); + jbyteArray array = to_byte_array(env, (uint8_t*) &s->sin6_addr.s6_addr, (size_t) 16); + address = (*env)->CallStaticObjectMethod(env, inet6address_class, inet6address_class_get_by_address, NULL, array, (jint) s->sin6_scope_id); + } + + return (*env)->NewObject(env, inetsocketaddress_class, inetsocketaddress_class_constructor, address, port); +} + +static jobjectArray netty_quiche_path_event_new(JNIEnv* env, jclass clazz, jlong ev) { + struct sockaddr_storage local; + socklen_t local_len; + struct sockaddr_storage peer; + socklen_t peer_len; + + quiche_path_event_new((quiche_path_event *) ev, &local, &local_len, &peer, &peer_len); + + jobject localAddr = netty_new_socket_address(env, &local); + if (localAddr == NULL) { + return NULL; + } + jobject peerAddr = netty_new_socket_address(env, &peer); + if (peerAddr == NULL) { + return NULL; + } + + jobjectArray array = (*env)->NewObjectArray(env, 2, object_class, NULL); + (*env)->SetObjectArrayElement(env, array, 0, localAddr); + (*env)->SetObjectArrayElement(env, array, 1, peerAddr); + return array; +} + +static jobjectArray netty_quiche_path_event_validated(JNIEnv* env, jclass clazz, jlong ev) { + struct sockaddr_storage local; + socklen_t local_len; + struct sockaddr_storage peer; + socklen_t peer_len; + + quiche_path_event_validated((quiche_path_event *) ev, &local, &local_len, &peer, &peer_len); + + jobject localAddr = netty_new_socket_address(env, &local); + if (localAddr == NULL) { + return NULL; + } + jobject peerAddr = netty_new_socket_address(env, &peer); + if (peerAddr == NULL) { + return NULL; + } + + jobjectArray array = (*env)->NewObjectArray(env, 2, object_class, NULL); + (*env)->SetObjectArrayElement(env, array, 0, localAddr); + (*env)->SetObjectArrayElement(env, array, 1, peerAddr); + return array; +} + +static jobjectArray netty_quiche_path_event_failed_validation(JNIEnv* env, jclass clazz, jlong ev) { + struct sockaddr_storage local; + socklen_t local_len; + struct sockaddr_storage peer; + socklen_t peer_len; + + quiche_path_event_failed_validation((quiche_path_event *) ev, &local, &local_len, &peer, &peer_len); + + jobject localAddr = netty_new_socket_address(env, &local); + if (localAddr == NULL) { + return NULL; + } + jobject peerAddr = netty_new_socket_address(env, &peer); + if (peerAddr == NULL) { + return NULL; + } + + jobjectArray array = (*env)->NewObjectArray(env, 2, object_class, NULL); + (*env)->SetObjectArrayElement(env, array, 0, localAddr); + (*env)->SetObjectArrayElement(env, array, 1, peerAddr); + return array; +} + +static jobjectArray netty_quiche_path_event_closed(JNIEnv* env, jclass clazz, jlong ev) { + struct sockaddr_storage local; + socklen_t local_len; + struct sockaddr_storage peer; + socklen_t peer_len; + + quiche_path_event_closed((quiche_path_event *) ev, &local, &local_len, &peer, &peer_len); + + jobject localAddr = netty_new_socket_address(env, &local); + if (localAddr == NULL) { + return NULL; + } + jobject peerAddr = netty_new_socket_address(env, &peer); + if (peerAddr == NULL) { + return NULL; + } + + jobjectArray array = (*env)->NewObjectArray(env, 2, object_class, NULL); + (*env)->SetObjectArrayElement(env, array, 0, localAddr); + (*env)->SetObjectArrayElement(env, array, 1, peerAddr); + return array; +} + +static jobjectArray netty_quiche_path_event_reused_source_connection_id(JNIEnv* env, jclass clazz, jlong ev) { + uint64_t id; + struct sockaddr_storage local_old; + socklen_t local_old_len; + struct sockaddr_storage peer_old; + socklen_t peer_old_len; + struct sockaddr_storage local; + socklen_t local_len; + struct sockaddr_storage peer; + socklen_t peer_len; + + quiche_path_event_reused_source_connection_id((quiche_path_event *) ev, &id, &local_old, &local_old_len, &peer_old, &peer_old_len, &local, &local_len, &peer, &peer_len); + + jobject localOldAddr = netty_new_socket_address(env, &local_old); + if (localOldAddr == NULL) { + return NULL; + } + jobject peerOldAddr = netty_new_socket_address(env, &peer_old); + if (peerOldAddr == NULL) { + return NULL; + } + jobject localAddr = netty_new_socket_address(env, &local); + if (localAddr == NULL) { + return NULL; + } + jobject peerAddr = netty_new_socket_address(env, &peer); + if (peerAddr == NULL) { + return NULL; + } + jobjectArray array = (*env)->NewObjectArray(env, 5, object_class, NULL); + (*env)->SetObjectArrayElement(env, array, 0, (*env)->CallStaticObjectMethod(env, long_class, long_class_valueof, (jlong) id)); + (*env)->SetObjectArrayElement(env, array, 1, localOldAddr); + (*env)->SetObjectArrayElement(env, array, 2, peerOldAddr); + (*env)->SetObjectArrayElement(env, array, 3, localAddr); + (*env)->SetObjectArrayElement(env, array, 4, peerAddr); + return array; +} + +static jobjectArray netty_quiche_path_event_peer_migrated(JNIEnv* env, jclass clazz, jlong ev) { + struct sockaddr_storage local; + socklen_t local_len; + struct sockaddr_storage peer; + socklen_t peer_len; + quiche_path_event_peer_migrated((quiche_path_event *) ev, &local, &local_len, &peer, &peer_len); + + jobject localAddr = netty_new_socket_address(env, &local); + if (localAddr == NULL) { + return NULL; + } + jobject peerAddr = netty_new_socket_address(env, &peer); + if (peerAddr == NULL) { + return NULL; + } + + jobjectArray array = (*env)->NewObjectArray(env, 2, object_class, NULL); + (*env)->SetObjectArrayElement(env, array, 0, localAddr); + (*env)->SetObjectArrayElement(env, array, 1, peerAddr); + return array; +} + +static void netty_quiche_path_event_free(JNIEnv* env, jclass clazz, jlong ev) { + quiche_path_event_free((quiche_path_event *) ev); +} + +static jlong netty_quiche_config_new(JNIEnv* env, jclass clazz, jint version) { + quiche_config* config = quiche_config_new((uint32_t) version); + return config == NULL ? -1 : (jlong) config; +} + +static void netty_quiche_config_enable_dgram(JNIEnv* env, jclass clazz, jlong config, jboolean enabled, jint recv_queue_len, jint send_queue_len) { + quiche_config_enable_dgram((quiche_config*) config, enabled == JNI_TRUE ? true : false, (size_t) recv_queue_len, (size_t) send_queue_len); +} + +static void netty_quiche_config_grease(JNIEnv* env, jclass clazz, jlong config, jboolean value) { + quiche_config_grease((quiche_config*) config, value == JNI_TRUE ? true : false); +} + +static void netty_quiche_config_set_max_idle_timeout(JNIEnv* env, jclass clazz, jlong config, jlong value) { + quiche_config_set_max_idle_timeout((quiche_config*) config, (uint64_t) value); +} + +static void netty_quiche_config_set_max_recv_udp_payload_size(JNIEnv* env, jclass clazz, jlong config, jlong value) { + quiche_config_set_max_recv_udp_payload_size((quiche_config*) config, (uint64_t) value); +} + +static void netty_quiche_config_set_max_send_udp_payload_size(JNIEnv* env, jclass clazz, jlong config, jlong value) { + quiche_config_set_max_send_udp_payload_size((quiche_config*) config, (uint64_t) value); +} + +static void netty_quiche_config_set_initial_max_data(JNIEnv* env, jclass clazz, jlong config, jlong value) { + quiche_config_set_initial_max_data((quiche_config*) config, (uint64_t) value); +} + +static void netty_quiche_config_set_initial_max_stream_data_bidi_local(JNIEnv* env, jclass clazz, jlong config, jlong value) { + quiche_config_set_initial_max_stream_data_bidi_local((quiche_config*) config, (uint64_t) value); +} + +static void netty_quiche_config_set_initial_max_stream_data_bidi_remote(JNIEnv* env, jclass clazz, jlong config, jlong value) { + quiche_config_set_initial_max_stream_data_bidi_remote((quiche_config*) config, (uint64_t) value); +} + +static void netty_quiche_config_set_initial_max_stream_data_uni(JNIEnv* env, jclass clazz, jlong config, jlong value) { + quiche_config_set_initial_max_stream_data_uni((quiche_config*) config, (uint64_t) value); +} + +static void netty_quiche_config_set_initial_max_streams_bidi(JNIEnv* env, jclass clazz, jlong config, jlong value) { + quiche_config_set_initial_max_streams_bidi((quiche_config*) config, (uint64_t) value); +} + +static void netty_quiche_config_set_initial_max_streams_uni(JNIEnv* env, jclass clazz, jlong config, jlong value) { + quiche_config_set_initial_max_streams_uni((quiche_config*) config, (uint64_t) value); +} + +static void netty_quiche_config_set_ack_delay_exponent(JNIEnv* env, jclass clazz, jlong config, jlong value) { + quiche_config_set_ack_delay_exponent((quiche_config*) config, (uint64_t) value); +} + +static void netty_quiche_config_set_max_ack_delay(JNIEnv* env, jclass clazz, jlong config, jlong value) { + quiche_config_set_max_ack_delay((quiche_config*) config, (uint64_t) value); +} + +static void netty_quiche_config_set_disable_active_migration(JNIEnv* env, jclass clazz, jlong config, jboolean value) { + quiche_config_set_disable_active_migration((quiche_config*) config, value == JNI_TRUE ? true : false); +} + +static void netty_quiche_config_set_cc_algorithm(JNIEnv* env, jclass clazz, jlong config, jint algo) { + quiche_config_set_cc_algorithm((quiche_config*) config, (enum quiche_cc_algorithm) algo); +} + +static void netty_quiche_config_enable_hystart(JNIEnv* env, jclass clazz, jlong config, jboolean value) { + quiche_config_enable_hystart((quiche_config*) config, value == JNI_TRUE ? true : false); +} + +static void netty_quiche_config_set_active_connection_id_limit(JNIEnv* env, jclass clazz, jlong config, jlong value) { + quiche_config_set_active_connection_id_limit((quiche_config*) config, (uint64_t) value); +} + +static void netty_quiche_config_set_stateless_reset_token(JNIEnv* env, jclass clazz, jlong config, jbyteArray token) { + uint8_t* buf = (uint8_t*) (*env)->GetByteArrayElements(env, token, 0); + quiche_config_set_stateless_reset_token((quiche_config*) config, buf); + (*env)->ReleaseByteArrayElements(env, token, (jbyte*)buf, JNI_ABORT); +} + +static void netty_quiche_config_free(JNIEnv* env, jclass clazz, jlong config) { + quiche_config_free((quiche_config*) config); +} + +static void log_to_java(const char *line, void *argp) { + JNIEnv* env = NULL; + quic_get_java_env(&env); + if (env == NULL) { + return; + } + + jstring message = (*env)->NewStringUTF(env, line); + if (message == NULL) { + // out of memory. + return; + } + (*env)->CallVoidMethod(env, quiche_logger, quiche_logger_class_log, message); +} + +static void netty_quiche_enable_debug_logging(JNIEnv* env, jclass clazz, jobject logger) { + if (quiche_logger != NULL) { + return; + } + if ((quiche_logger = (*env)->NewGlobalRef(env, logger)) == NULL) { + return; + } + + quiche_enable_debug_logging(log_to_java, NULL); +} + +static jlong netty_buffer_memory_address(JNIEnv* env, jclass clazz, jobject buffer) { + return (jlong) (*env)->GetDirectBufferAddress(env, buffer); +} + +// Based on https://gist.github.com/kazuho/45eae4f92257daceb73e. +static jint netty_sockaddr_cmp(JNIEnv* env, jclass clazz, jlong addr1, jlong addr2) { + struct sockaddr* x = (struct sockaddr*) addr1; + struct sockaddr* y = (struct sockaddr*) addr2; + + if (x == NULL && y == NULL) { + return 0; + } + if (x != NULL && y == NULL) { + return 1; + } + if (x == NULL && y != NULL) { + return -1; + } + +#define CMP(a, b) if (a != b) return a < b ? -1 : 1 + + CMP(x->sa_family, y->sa_family); + + if (x->sa_family == AF_INET) { + struct sockaddr_in *xin = (void*)x, *yin = (void*)y; + CMP(ntohl(xin->sin_addr.s_addr), ntohl(yin->sin_addr.s_addr)); + CMP(ntohs(xin->sin_port), ntohs(yin->sin_port)); + } else if (x->sa_family == AF_INET6) { + struct sockaddr_in6 *xin6 = (void*)x, *yin6 = (void*)y; + int r = memcmp(xin6->sin6_addr.s6_addr, yin6->sin6_addr.s6_addr, sizeof(xin6->sin6_addr.s6_addr)); + if (r != 0) + return r; + CMP(ntohs(xin6->sin6_port), ntohs(yin6->sin6_port)); + CMP(xin6->sin6_flowinfo, yin6->sin6_flowinfo); + CMP(xin6->sin6_scope_id, yin6->sin6_scope_id); + } + +#undef CMP + return 0; +} + +// JNI Registered Methods End + +// JNI Method Registration Table Begin +static const JNINativeMethod statically_referenced_fixed_method_table[] = { + { "afInet", "()I", (void *) netty_quiche_afInet }, + { "afInet6", "()I", (void *) netty_quiche_afInet6 }, + { "sizeofSockaddrIn", "()I", (void *) netty_quiche_sizeofSockaddrIn }, + { "sizeofSockaddrIn6", "()I", (void *) netty_quiche_sizeofSockaddrIn6 }, + { "sockaddrInOffsetofSinFamily", "()I", (void *) netty_quiche_sockaddrInOffsetofSinFamily }, + { "sockaddrInOffsetofSinPort", "()I", (void *) netty_quiche_sockaddrInOffsetofSinPort }, + { "sockaddrInOffsetofSinAddr", "()I", (void *) netty_quiche_sockaddrInOffsetofSinAddr }, + { "inAddressOffsetofSAddr", "()I", (void *) netty_quiche_inAddressOffsetofSAddr }, + { "sockaddrIn6OffsetofSin6Family", "()I", (void *) netty_quiche_sockaddrIn6OffsetofSin6Family }, + { "sockaddrIn6OffsetofSin6Port", "()I", (void *) netty_quiche_sockaddrIn6OffsetofSin6Port }, + { "sockaddrIn6OffsetofSin6Flowinfo", "()I", (void *) netty_quiche_sockaddrIn6OffsetofSin6Flowinfo }, + { "sockaddrIn6OffsetofSin6Addr", "()I", (void *) netty_quiche_sockaddrIn6OffsetofSin6Addr }, + { "sockaddrIn6OffsetofSin6ScopeId", "()I", (void *) netty_quiche_sockaddrIn6OffsetofSin6ScopeId }, + { "in6AddressOffsetofS6Addr", "()I", (void *) netty_quiche_in6AddressOffsetofS6Addr }, + { "sizeofSockaddrStorage", "()I", (void *) netty_quiche_sizeofSockaddrStorage }, + { "sizeofSizeT", "()I", (void *) netty_quiche_sizeofSizeT }, + { "sizeofSocklenT", "()I", (void *) netty_quiche_sizeofSocklenT }, + { "quicheRecvInfoOffsetofFrom", "()I", (void *) netty_quicheRecvInfoOffsetofFrom }, + { "quicheRecvInfoOffsetofFromLen", "()I", (void *) netty_quicheRecvInfoOffsetofFromLen }, + { "quicheRecvInfoOffsetofTo", "()I", (void *) netty_quicheRecvInfoOffsetofTo }, + { "quicheRecvInfoOffsetofToLen", "()I", (void *) netty_quicheRecvInfoOffsetofToLen }, + { "sizeofQuicheRecvInfo", "()I", (void *) netty_sizeofQuicheRecvInfo }, + { "quicheSendInfoOffsetofTo", "()I", (void *) netty_quicheSendInfoOffsetofTo }, + { "quicheSendInfoOffsetofToLen", "()I", (void *) netty_quicheSendInfoOffsetofToLen }, + { "quicheSendInfoOffsetofFrom", "()I", (void *) netty_quicheSendInfoOffsetofFrom }, + { "quicheSendInfoOffsetofFromLen", "()I", (void *) netty_quicheSendInfoOffsetofFromLen }, + { "quicheSendInfoOffsetofAt", "()I", (void *) netty_quicheSendInfoOffsetofAt }, + + { "sizeofQuicheSendInfo", "()I", (void *) netty_sizeofQuicheSendInfo }, + { "sizeofTimespec", "()I", (void *) netty_quiche_sizeofTimespec }, + { "sizeofTimeT", "()I", (void *) netty_quiche_sizeofTimeT }, + { "sizeofLong", "()I", (void *) netty_quiche_sizeofLong }, + { "timespecOffsetofTvSec", "()I", (void *) netty_quiche_timespecOffsetofTvSec }, + { "timespecOffsetofTvNsec", "()I", (void *) timespecOffsetofTvNsec }, + { "quiche_protocol_version", "()I", (void *) netty_quiche_protocol_version }, + { "quiche_max_conn_id_len", "()I", (void *) netty_quiche_max_conn_id_len }, + { "quiche_shutdown_read", "()I", (void *) netty_quiche_shutdown_read }, + { "quiche_shutdown_write", "()I", (void *) netty_quiche_shutdown_write }, + { "quiche_err_done", "()I", (void *) netty_quiche_err_done }, + { "quiche_err_buffer_too_short", "()I", (void *) netty_quiche_err_buffer_too_short }, + { "quiche_err_unknown_version", "()I", (void *) netty_quiche_err_unknown_version }, + { "quiche_err_invalid_frame", "()I", (void *) netty_quiche_err_invalid_frame }, + { "quiche_err_invalid_packet", "()I", (void *) netty_quiche_err_invalid_packet }, + { "quiche_err_invalid_state", "()I", (void *) netty_quiche_err_invalid_state }, + { "quiche_err_invalid_stream_state", "()I", (void *) netty_quiche_err_invalid_stream_state }, + { "quiche_err_invalid_transport_param", "()I", (void *) netty_quiche_err_invalid_transport_param }, + { "quiche_err_crypto_fail", "()I", (void *) netty_quiche_err_crypto_fail }, + { "quiche_err_tls_fail", "()I", (void *) netty_quiche_err_tls_fail }, + { "quiche_err_flow_control", "()I", (void *) netty_quiche_err_flow_control }, + { "quiche_err_stream_limit", "()I", (void *) netty_quiche_err_stream_limit }, + { "quiche_err_final_size", "()I", (void *) netty_quiche_err_final_size }, + { "quiche_err_stream_stopped", "()I", (void *) netty_quiche_err_stream_stopped }, + { "quiche_err_stream_reset", "()I", (void *) netty_quiche_err_stream_reset }, + { "quiche_err_congestion_control", "()I", (void *) netty_quiche_err_congestion_control }, + { "quiche_err_id_limit", "()I", (void *) netty_quiche_err_id_limit }, + { "quiche_err_out_of_identifiers", "()I", (void *) netty_quiche_err_out_of_identifiers }, + { "quiche_err_key_update", "()I", (void *) netty_quiche_err_key_update }, + { "quiche_cc_reno", "()I", (void *) netty_quiche_cc_reno }, + { "quiche_cc_cubic", "()I", (void *) netty_quiche_cc_cubic }, + { "quiche_cc_bbr", "()I", (void *) netty_quiche_cc_bbr }, + { "quiche_path_event_new", "()I", (void *) netty_quiche_path_event_type_new }, + { "quiche_path_event_validated", "()I", (void *) netty_quiche_path_event_type_validated }, + { "quiche_path_event_failed_validation", "()I", (void *) netty_quiche_path_event_type_failed_validation }, + { "quiche_path_event_closed", "()I", (void *) netty_quiche_path_event_type_closed }, + { "quiche_path_event_reused_source_connection_id", "()I", (void *) netty_quiche_path_event_type_reused_source_connection_id }, + { "quiche_path_event_peer_migrated", "()I", (void *) netty_quiche_path_event_type_peer_migrated } +}; + +static const jint statically_referenced_fixed_method_table_size = sizeof(statically_referenced_fixed_method_table) / sizeof(statically_referenced_fixed_method_table[0]); +static const JNINativeMethod fixed_method_table[] = { + { "quiche_version", "()Ljava/lang/String;", (void *) netty_quiche_version }, + { "quiche_version_is_supported", "(I)Z", (void *) netty_quiche_version_is_supported }, + { "quiche_header_info", "(JIIJJJJJJJJ)I", (void *) netty_quiche_header_info }, + { "quiche_negotiate_version", "(JIJIJI)I", (void *) netty_quiche_negotiate_version }, + { "quiche_retry", "(JIJIJIJIIJI)I", (void *) netty_quiche_retry }, + { "quiche_conn_set_qlog_path", "(JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;)Z", (void *) netty_quiche_conn_set_qlog_path }, + { "quiche_conn_send_quantum", "(J)I", (void *) netty_quiche_conn_send_quantum }, + { "quiche_conn_trace_id", "(J)[B", (void *) netty_quiche_conn_trace_id }, + { "quiche_conn_source_id", "(J)[B", (void *) netty_quiche_conn_source_id }, + { "quiche_conn_destination_id", "(J)[B", (void *) netty_quiche_conn_destination_id }, + { "quiche_conn_new_with_tls", "(JIJIJIJIJJZ)J", (void *) netty_quiche_conn_new_with_tls }, + { "quiche_conn_recv", "(JJIJ)I", (void *) netty_quiche_conn_recv }, + { "quiche_conn_send", "(JJIJ)I", (void *) netty_quiche_conn_send }, + { "quiche_conn_free", "(J)V", (void *) netty_quiche_conn_free }, + { "quiche_conn_peer_error0", "(J)[Ljava/lang/Object;", (void *) netty_quiche_conn_peer_error0 }, + { "quiche_conn_peer_streams_left_bidi", "(J)J", (void *) netty_quiche_conn_peer_streams_left_bidi }, + { "quiche_conn_peer_streams_left_uni", "(J)J", (void *) netty_quiche_conn_peer_streams_left_uni }, + { "quiche_conn_stream_priority", "(JJBZ)I", (void *) netty_quiche_conn_stream_priority }, + { "quiche_conn_stream_recv", "(JJJIJ)I", (void *) netty_quiche_conn_stream_recv }, + { "quiche_conn_stream_send", "(JJJIZ)I", (void *) netty_quiche_conn_stream_send }, + { "quiche_conn_stream_shutdown", "(JJIJ)I", (void *) netty_quiche_conn_stream_shutdown }, + { "quiche_conn_stream_capacity", "(JJ)I", (void *) netty_quiche_conn_stream_capacity }, + { "quiche_conn_stream_finished", "(JJ)Z", (void *) netty_quiche_conn_stream_finished }, + { "quiche_conn_close", "(JZJJI)I", (void *) netty_quiche_conn_close }, + { "quiche_conn_is_established", "(J)Z", (void *) netty_quiche_conn_is_established }, + { "quiche_conn_is_in_early_data", "(J)Z", (void *) netty_quiche_conn_is_in_early_data }, + { "quiche_conn_is_closed", "(J)Z", (void *) netty_quiche_conn_is_closed }, + { "quiche_conn_is_timed_out", "(J)Z", (void *) netty_quiche_conn_is_timed_out }, + { "quiche_conn_stats", "(J)[J", (void *) netty_quiche_conn_stats }, + { "quiche_conn_peer_transport_params", "(J)[J", (void *) netty_quiche_conn_peer_transport_params }, + { "quiche_conn_timeout_as_nanos", "(J)J", (void *) netty_quiche_conn_timeout_as_nanos }, + { "quiche_conn_on_timeout", "(J)V", (void *) netty_quiche_conn_on_timeout }, + { "quiche_conn_readable", "(J)J", (void *) netty_quiche_conn_readable }, + { "quiche_conn_writable", "(J)J", (void *) netty_quiche_conn_writable }, + { "quiche_stream_iter_free", "(J)V", (void *) netty_quiche_stream_iter_free }, + { "quiche_stream_iter_next", "(J[J)I", (void *) netty_quiche_stream_iter_next }, + { "quiche_conn_dgram_max_writable_len", "(J)I", (void* ) netty_quiche_conn_dgram_max_writable_len }, + { "quiche_conn_dgram_recv_front_len", "(J)I", (void* ) netty_quiche_conn_dgram_recv_front_len }, + { "quiche_conn_dgram_recv", "(JJI)I", (void* ) netty_quiche_conn_dgram_recv }, + { "quiche_conn_dgram_send", "(JJI)I", (void* ) netty_quiche_conn_dgram_send }, + { "quiche_conn_set_session", "(J[B)I", (void* ) netty_quiche_conn_set_session }, + { "quiche_conn_max_send_udp_payload_size", "(J)I", (void* ) netty_quiche_conn_max_send_udp_payload_size }, + { "quiche_conn_scids_left", "(J)I", (void* ) netty_quiche_conn_scids_left }, + { "quiche_conn_new_scid", "(JJI[BZJ)J", (void* ) netty_quiche_conn_new_scid }, + { "quiche_conn_retired_scid_next", "(J)[B", (void* ) netty_quiche_conn_retired_scid_next }, + { "quiche_config_new", "(I)J", (void *) netty_quiche_config_new }, + { "quiche_config_enable_dgram", "(JZII)V", (void *) netty_quiche_config_enable_dgram }, + { "quiche_config_grease", "(JZ)V", (void *) netty_quiche_config_grease }, + { "quiche_config_set_max_idle_timeout", "(JJ)V", (void *) netty_quiche_config_set_max_idle_timeout }, + { "quiche_config_set_max_recv_udp_payload_size", "(JJ)V", (void *) netty_quiche_config_set_max_recv_udp_payload_size }, + { "quiche_config_set_max_send_udp_payload_size", "(JJ)V", (void *) netty_quiche_config_set_max_send_udp_payload_size }, + { "quiche_config_set_initial_max_data", "(JJ)V", (void *) netty_quiche_config_set_initial_max_data }, + { "quiche_config_set_initial_max_stream_data_bidi_local", "(JJ)V", (void *) netty_quiche_config_set_initial_max_stream_data_bidi_local }, + { "quiche_config_set_initial_max_stream_data_bidi_remote", "(JJ)V", (void *) netty_quiche_config_set_initial_max_stream_data_bidi_remote }, + { "quiche_config_set_initial_max_stream_data_uni", "(JJ)V", (void *) netty_quiche_config_set_initial_max_stream_data_uni }, + { "quiche_config_set_initial_max_streams_bidi", "(JJ)V", (void *) netty_quiche_config_set_initial_max_streams_bidi }, + { "quiche_config_set_initial_max_streams_uni", "(JJ)V", (void *) netty_quiche_config_set_initial_max_streams_uni }, + { "quiche_config_set_ack_delay_exponent", "(JJ)V", (void *) netty_quiche_config_set_ack_delay_exponent }, + { "quiche_config_set_max_ack_delay", "(JJ)V", (void *) netty_quiche_config_set_max_ack_delay }, + { "quiche_config_set_disable_active_migration", "(JZ)V", (void *) netty_quiche_config_set_disable_active_migration }, + { "quiche_config_set_cc_algorithm", "(JI)V", (void *) netty_quiche_config_set_cc_algorithm }, + { "quiche_config_enable_hystart", "(JZ)V", (void *) netty_quiche_config_enable_hystart }, + { "quiche_config_set_active_connection_id_limit", "(JJ)V", (void *) netty_quiche_config_set_active_connection_id_limit }, + { "quiche_config_set_stateless_reset_token", "(J[B)V", (void *) netty_quiche_config_set_stateless_reset_token }, + { "quiche_config_free", "(J)V", (void *) netty_quiche_config_free }, + { "buffer_memory_address", "(Ljava/nio/ByteBuffer;)J", (void *) netty_buffer_memory_address}, + { "sockaddr_cmp", "(JJ)I", (void *) netty_sockaddr_cmp}, + { "quiche_conn_path_event_next", "(J)J", (void *) netty_quiche_conn_path_event_next }, + { "quiche_path_event_type", "(J)I", (void *) netty_quiche_path_event_type }, + { "quiche_path_event_new", "(J)[Ljava/lang/Object;", (void *) netty_quiche_path_event_new }, + { "quiche_path_event_validated", "(J)[Ljava/lang/Object;", (void *) netty_quiche_path_event_validated }, + { "quiche_path_event_failed_validation", "(J)[Ljava/lang/Object;", (void *) netty_quiche_path_event_failed_validation }, + { "quiche_path_event_closed", "(J)[Ljava/lang/Object;", (void *) netty_quiche_path_event_closed }, + { "quiche_path_event_reused_source_connection_id", "(J)[Ljava/lang/Object;", (void *) netty_quiche_path_event_reused_source_connection_id }, + { "quiche_path_event_peer_migrated", "(J)[Ljava/lang/Object;", (void *) netty_quiche_path_event_peer_migrated }, + { "quiche_path_event_free", "(J)V", (void *) netty_quiche_path_event_free } +}; + +static const jint fixed_method_table_size = sizeof(fixed_method_table) / sizeof(fixed_method_table[0]); + +static jint dynamicMethodsTableSize() { + return fixed_method_table_size + 1; +} + +static JNINativeMethod* createDynamicMethodsTable(const char* packagePrefix) { + char* dynamicTypeName = NULL; + int len = sizeof(JNINativeMethod) * dynamicMethodsTableSize(); + JNINativeMethod* dynamicMethods = malloc(len); + if (dynamicMethods == NULL) { + goto error; + } + memset(dynamicMethods, 0, len); + memcpy(dynamicMethods, fixed_method_table, sizeof(fixed_method_table)); + + JNINativeMethod* dynamicMethod = &dynamicMethods[fixed_method_table_size]; + NETTY_JNI_UTIL_PREPEND(packagePrefix, "io/netty/handler/codec/quic/QuicheLogger;)V", dynamicTypeName, error); + NETTY_JNI_UTIL_PREPEND("(L", dynamicTypeName, dynamicMethod->signature, error); + netty_jni_util_free_dynamic_name(&dynamicTypeName); + dynamicMethod->name = "quiche_enable_debug_logging"; + dynamicMethod->fnPtr = (void *) netty_quiche_enable_debug_logging; + return dynamicMethods; +error: + netty_jni_util_free_dynamic_methods_table(dynamicMethods, fixed_method_table_size, dynamicMethodsTableSize()); + free(dynamicTypeName); + return NULL; +} + +// JNI Method Registration Table End + +// IMPORTANT: If you add any NETTY_JNI_UTIL_LOAD_CLASS or NETTY_JNI_UTIL_FIND_CLASS calls you also need to update +// Quiche to reflect that. +static jint netty_quiche_JNI_OnLoad(JNIEnv* env, char const* packagePrefix) { + int ret = JNI_ERR; + int staticallyRegistered = 0; + int nativeRegistered = 0; + int boringsslLoaded = 0; + + jclass quiche_logger_class = NULL; + char* name = NULL; + + // We must register the statically referenced methods first! + if (netty_jni_util_register_natives(env, + packagePrefix, + STATICALLY_CLASSNAME, + statically_referenced_fixed_method_table, + statically_referenced_fixed_method_table_size) != 0) { + goto done; + } + staticallyRegistered = 1; + + JNINativeMethod* dynamicMethods = createDynamicMethodsTable(packagePrefix); + if (dynamicMethods == NULL) { + goto done; + } + if (netty_jni_util_register_natives(env, + packagePrefix, + QUICHE_CLASSNAME, + dynamicMethods, + dynamicMethodsTableSize()) != 0) { + goto done; + } + nativeRegistered = 1; + + NETTY_JNI_UTIL_LOAD_CLASS(env, integer_class, "java/lang/Integer", done); + if ((integer_class_valueof = (*env)->GetStaticMethodID(env, integer_class, "valueOf", "(I)Ljava/lang/Integer;")) == NULL) { + goto done; + } + NETTY_JNI_UTIL_LOAD_CLASS(env, boolean_class, "java/lang/Boolean", done); + if ((boolean_class_valueof = (*env)->GetStaticMethodID(env, boolean_class, "valueOf", "(Z)Ljava/lang/Boolean;")) == NULL) { + goto done; + } + + NETTY_JNI_UTIL_LOAD_CLASS(env, long_class, "java/lang/Long", done); + if ((long_class_valueof = (*env)->GetStaticMethodID(env, long_class, "valueOf", "(J)Ljava/lang/Long;")) == NULL) { + goto done; + } + + NETTY_JNI_UTIL_LOAD_CLASS(env, inet4address_class, "java/net/Inet4Address", done); + if ((inet4address_class_get_by_address = (*env)->GetStaticMethodID(env, inet4address_class, "getByAddress", "([B)Ljava/net/InetAddress;")) == NULL) { + goto done; + } + + NETTY_JNI_UTIL_LOAD_CLASS(env, inet6address_class, "java/net/Inet6Address", done); + if ((inet6address_class_get_by_address = (*env)->GetStaticMethodID(env, inet6address_class, "getByAddress", "(Ljava/lang/String;[BI)Ljava/net/Inet6Address;")) == NULL) { + goto done; + } + + NETTY_JNI_UTIL_LOAD_CLASS(env, inetsocketaddress_class, "java/net/InetSocketAddress", done); + NETTY_JNI_UTIL_GET_METHOD(env, inetsocketaddress_class, inetsocketaddress_class_constructor, "", "(Ljava/net/InetAddress;I)V", done); + + NETTY_JNI_UTIL_LOAD_CLASS(env, object_class, "java/lang/Object", done); + + NETTY_JNI_UTIL_PREPEND(packagePrefix, "io/netty/handler/codec/quic/QuicheLogger", name, done); + NETTY_JNI_UTIL_LOAD_CLASS_WEAK(env, quiche_logger_class_weak, name, done); + NETTY_JNI_UTIL_NEW_LOCAL_FROM_WEAK(env, quiche_logger_class, quiche_logger_class_weak, done); + NETTY_JNI_UTIL_GET_METHOD(env, quiche_logger_class, quiche_logger_class_log, "log", "(Ljava/lang/String;)V", done); + // Initialize this module + + // Load all c modules that we depend upon + if (netty_boringssl_JNI_OnLoad(env, packagePrefix) == JNI_ERR) { + goto done; + } + boringsslLoaded = 1; + + staticPackagePrefix = packagePrefix; + + ret = NETTY_JNI_UTIL_JNI_VERSION; +done: + if (ret == JNI_ERR) { + if (staticallyRegistered == 1) { + netty_jni_util_unregister_natives(env, packagePrefix, STATICALLY_CLASSNAME); + } + if (nativeRegistered == 1) { + netty_jni_util_unregister_natives(env, packagePrefix, QUICHE_CLASSNAME); + } + if (boringsslLoaded == 1) { + netty_boringssl_JNI_OnUnload(env, packagePrefix); + } + + NETTY_JNI_UTIL_UNLOAD_CLASS_WEAK(env, quiche_logger_class_weak); + NETTY_JNI_UTIL_UNLOAD_CLASS(env, integer_class); + NETTY_JNI_UTIL_UNLOAD_CLASS(env, boolean_class); + NETTY_JNI_UTIL_UNLOAD_CLASS(env, long_class); + NETTY_JNI_UTIL_UNLOAD_CLASS(env, inet4address_class); + NETTY_JNI_UTIL_UNLOAD_CLASS(env, inet6address_class); + NETTY_JNI_UTIL_UNLOAD_CLASS(env, inetsocketaddress_class); + NETTY_JNI_UTIL_UNLOAD_CLASS(env, object_class); + + netty_jni_util_free_dynamic_methods_table(dynamicMethods, fixed_method_table_size, dynamicMethodsTableSize()); + } + NETTY_JNI_UTIL_DELETE_LOCAL(env, quiche_logger_class); + return ret; +} + +static void netty_quiche_JNI_OnUnload(JNIEnv* env) { + netty_boringssl_JNI_OnUnload(env, staticPackagePrefix); + + NETTY_JNI_UTIL_UNLOAD_CLASS_WEAK(env, quiche_logger_class_weak); + NETTY_JNI_UTIL_UNLOAD_CLASS(env, integer_class); + NETTY_JNI_UTIL_UNLOAD_CLASS(env, boolean_class); + NETTY_JNI_UTIL_UNLOAD_CLASS(env, long_class); + NETTY_JNI_UTIL_UNLOAD_CLASS(env, inet4address_class); + NETTY_JNI_UTIL_UNLOAD_CLASS(env, inet6address_class); + NETTY_JNI_UTIL_UNLOAD_CLASS(env, inetsocketaddress_class); + NETTY_JNI_UTIL_UNLOAD_CLASS(env, object_class); + + netty_jni_util_unregister_natives(env, staticPackagePrefix, STATICALLY_CLASSNAME); + netty_jni_util_unregister_natives(env, staticPackagePrefix, QUICHE_CLASSNAME); + + if (quiche_logger != NULL) { + (*env)->DeleteGlobalRef(env, quiche_logger); + quiche_logger = NULL; + } + free((void*) staticPackagePrefix); + staticPackagePrefix = NULL; +} + +// Invoked by the JVM when statically linked + +// We build with -fvisibility=hidden so ensure we mark everything that needs to be visible with JNIEXPORT +// https://mail.openjdk.java.net/pipermail/core-libs-dev/2013-February/014549.html + +// Invoked by the JVM when statically linked +JNIEXPORT jint JNI_OnLoad_netty_quiche(JavaVM* vm, void* reserved) { + global_vm = vm; + return netty_jni_util_JNI_OnLoad(vm, reserved, LIBRARYNAME, netty_quiche_JNI_OnLoad); +} + +// Invoked by the JVM when statically linked +JNIEXPORT void JNI_OnUnload_netty_quiche(JavaVM* vm, void* reserved) { + netty_jni_util_JNI_OnUnload(vm, reserved, netty_quiche_JNI_OnUnload); + global_vm = NULL; +} + +#ifndef NETTY_QUIC_BUILD_STATIC +JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { + global_vm = vm; + return netty_jni_util_JNI_OnLoad(vm, reserved, LIBRARYNAME, netty_quiche_JNI_OnLoad); +} + +JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved) { + netty_jni_util_JNI_OnUnload(vm, reserved, netty_quiche_JNI_OnUnload); + global_vm = NULL; +} +#endif /* NETTY_QUIC_BUILD_STATIC */ diff --git a/codec-native-quic/src/main/native-package/m4/custom.m4.android.template b/codec-native-quic/src/main/native-package/m4/custom.m4.android.template new file mode 100644 index 0000000..2a67e30 --- /dev/null +++ b/codec-native-quic/src/main/native-package/m4/custom.m4.android.template @@ -0,0 +1,24 @@ +dnl +dnl Copyright 2021 The Netty Project +dnl +dnl The Netty Project licenses this file to you under the Apache License, +dnl version 2.0 (the "License"); you may not use this file except in compliance +dnl with the License. You may obtain a copy of the License at: +dnl +dnl https://www.apache.org/licenses/LICENSE-2.0 +dnl +dnl Unless required by applicable law or agreed to in writing, software +dnl distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +dnl WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +dnl License for the specific language governing permissions and limitations +dnl under the License. + +AC_DEFUN([CUSTOM_M4_SETUP], +[ + + # When using the android toolchain, by default it only builds a library with a version-suffix. + # This way we create a symlink without the version-suffix, so hawtjni can find the library and copy it to the output destination + # This sets the "library_names_spec" variable in libtool to the same value as when building for linux + _LT_TAGVAR(library_names_spec, )="\${libname}\${release}\${shared_ext}\$versuffix \${libname}\${release}\${shared_ext}\$major \$libname\${shared_ext}" + +]) diff --git a/codec-native-quic/src/main/native-package/m4/custom.m4.template b/codec-native-quic/src/main/native-package/m4/custom.m4.template new file mode 100644 index 0000000..ff0636c --- /dev/null +++ b/codec-native-quic/src/main/native-package/m4/custom.m4.template @@ -0,0 +1,39 @@ +dnl --------------------------------------------------------------------------- +dnl Copyright 2022 The Netty Project +dnl +dnl Licensed under the Apache License, Version 2.0 (the "License"); +dnl you may not use this file except in compliance with the License. +dnl You may obtain a copy of the License at +dnl +dnl http://www.apache.org/licenses/LICENSE-2.0 +dnl +dnl Unless required by applicable law or agreed to in writing, software +dnl distributed under the License is distributed on an "AS IS" BASIS, +dnl WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +dnl See the License for the specific language governing permissions and +dnl limitations under the License. +dnl --------------------------------------------------------------------------- + +AC_DEFUN([CUSTOM_M4_SETUP], +[ + dnl Ensure we only expose what we really need + case $host in + *-darwin*) + LDFLAGS="$LDFLAGS -Wl,-exported_symbol,_JNI_*" + ;; + *linux*) + LDFLAGS="$LDFLAGS -Wl,--exclude-libs,ALL" + ;; + *) + ;; + esac + + dnl Update the compiler/linker flags + CFLAGS="$CFLAGS -std=gnu99 -fvisibility=hidden -Werror -fno-omit-frame-pointer -Wunused -Wno-unused-value -O3 -I@BORINGSSL_INCLUDE_DIR@ -I@QUICHE_INCLUDE_DIR@ @EXTRA_CFLAGS@" + CXXFLAGS="$CXXFLAGS" + LDFLAGS="$LDFLAGS -L@BORINGSSL_LIB_DIR@ -lssl -lcrypto -L@QUICHE_LIB_DIR@ -lquiche @EXTRA_LDFLAGS@" + AC_SUBST(CFLAGS) + AC_SUBST(CXXFLAGS) + AC_SUBST(LDFLAGS) +]) + diff --git a/codec-native-quic/src/main/native-package/vs2010.custom.props.template b/codec-native-quic/src/main/native-package/vs2010.custom.props.template new file mode 100644 index 0000000..e4f0e60 --- /dev/null +++ b/codec-native-quic/src/main/native-package/vs2010.custom.props.template @@ -0,0 +1,26 @@ + + + + + + $(JAVA_HOME)\include;$(JAVA_HOME)\include\win32;@BORINGSSL_INCLUDE_DIR@;@QUICHE_INCLUDE_DIR@;%(AdditionalIncludeDirectories) + + + ntdll.lib;ws2_32.lib;crypt32.lib;bcrypt.lib;userenv.lib;@BORINGSSL_LIB_DIR@\@SSL_LIB@;@BORINGSSL_LIB_DIR@\@CRYPTO_LIB@;@QUICHE_LIB_DIR@\@QUICHE_LIB@;%(AdditionalDependencies) + + + \ No newline at end of file diff --git a/codec-native-quic/src/main/resources/META-INF/native-image/io.netty.incubator/netty-incubator-codec-native-quic/jni-config.json b/codec-native-quic/src/main/resources/META-INF/native-image/io.netty.incubator/netty-incubator-codec-native-quic/jni-config.json new file mode 100644 index 0000000..9142542 --- /dev/null +++ b/codec-native-quic/src/main/resources/META-INF/native-image/io.netty.incubator/netty-incubator-codec-native-quic/jni-config.json @@ -0,0 +1,118 @@ +[ +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"[B" +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"[Ljava.lang.Object;" +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"[Ljava.lang.String;" +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"[[B" +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"io.netty.handler.codec.quic.BoringSSL" +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"io.netty.handler.codec.quic.BoringSSLCertificateCallbackTask", + "fields":[ + {"name":"chain"}, + {"name":"key"} + ], + "methods":[{"name":"","parameterTypes":["long","byte[]","byte[][]","java.lang.String[]","io.netty.handler.codec.quic.BoringSSLCertificateCallback"] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"io.netty.handler.codec.quic.BoringSSLCertificateVerifyCallbackTask", + "methods":[{"name":"","parameterTypes":["long","byte[][]","java.lang.String","io.netty.handler.codec.quic.BoringSSLCertificateVerifyCallback"] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"io.netty.handler.codec.quic.BoringSSLHandshakeCompleteCallback", + "methods":[{"name":"handshakeComplete","parameterTypes":["long","byte[]","java.lang.String","java.lang.String","byte[]","byte[][]","long","long","byte[]","boolean"] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"io.netty.handler.codec.quic.BoringSSLKeylogCallback", + "methods":[{"name":"logKey","parameterTypes":["long","java.lang.String"] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"io.netty.handler.codec.quic.BoringSSLNativeStaticallyReferencedJniMethods" +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"io.netty.handler.codec.quic.BoringSSLPrivateKeyMethodDecryptTask", + "methods":[{"name":"","parameterTypes":["long","byte[]","io.netty.handler.codec.quic.BoringSSLPrivateKeyMethod"] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"io.netty.handler.codec.quic.BoringSSLPrivateKeyMethodSignTask", + "methods":[{"name":"","parameterTypes":["long","int","byte[]","io.netty.handler.codec.quic.BoringSSLPrivateKeyMethod"] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"io.netty.handler.codec.quic.BoringSSLPrivateKeyMethodTask", + "fields":[{"name":"resultBytes"}] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"io.netty.handler.codec.quic.BoringSSLSessionCallback", + "methods":[{"name":"newSession","parameterTypes":["long","long","long","byte[]","boolean","byte[]"] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"io.netty.handler.codec.quic.BoringSSLTask", + "fields":[ + {"name":"complete"}, + {"name":"returnValue"} + ], + "methods":[{"name":"destroy","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"io.netty.handler.codec.quic.BoringSSLTlsextServernameCallback", + "methods":[{"name":"selectCtx","parameterTypes":["long","java.lang.String"] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"io.netty.handler.codec.quic.Quiche" +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"io.netty.handler.codec.quic.QuicheLogger", + "methods":[{"name":"log","parameterTypes":["java.lang.String"] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"io.netty.handler.codec.quic.QuicheNativeStaticallyReferencedJniMethods" +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"java.lang.Boolean", + "methods":[ + {"name":"getBoolean","parameterTypes":["java.lang.String"] }, + {"name":"valueOf","parameterTypes":["boolean"] } + ] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"java.lang.Integer", + "methods":[{"name":"","parameterTypes":["int"] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"java.lang.Object" +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"java.lang.String" +} +] diff --git a/codec-native-quic/src/main/resources/META-INF/native-image/io.netty.incubator/netty-incubator-codec-native-quic/reflect-config.json b/codec-native-quic/src/main/resources/META-INF/native-image/io.netty.incubator/netty-incubator-codec-native-quic/reflect-config.json new file mode 100644 index 0000000..aef70dc --- /dev/null +++ b/codec-native-quic/src/main/resources/META-INF/native-image/io.netty.incubator/netty-incubator-codec-native-quic/reflect-config.json @@ -0,0 +1,490 @@ +[ +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"[B" +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.HmacSignQuicConnectionIdGenerator"}, + "name":"com.sun.crypto.provider.HmacCore$HmacSHA256", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicSslContextBuilder"}, + "name":"com.sun.crypto.provider.HmacCore$HmacSHA256", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicSslContextBuilder"}, + "name":"com.sun.crypto.provider.PBEKeyFactory$PBEWithMD5AndDES", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicSslContextBuilder"}, + "name":"com.sun.crypto.provider.PBES2Core$HmacSHA256AndAES_256", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicSslContextBuilder"}, + "name":"com.sun.crypto.provider.PBES2Parameters$General", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicSslContextBuilder"}, + "name":"com.sun.crypto.provider.PBES2Parameters$HmacSHA256AndAES_256", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicConnectionAddress"}, + "name":"io.netty.buffer.AbstractByteBufAllocator", + "queryAllDeclaredMethods":true +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicConnectionAddress"}, + "name":"io.netty.buffer.AbstractReferenceCountedByteBuf", + "fields":[{"name":"refCnt"}] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quic"}, + "name":"io.netty.channel.ChannelInboundHandlerAdapter", + "methods":[ + {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelWritabilityChanged","parameterTypes":["io.netty.channel.ChannelHandlerContext"] } + ] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicStreamChannelBootstrap$QuicStreamChannelBootstrapHandler"}, + "name":"io.netty.channel.ChannelInboundHandlerAdapter", + "methods":[ + {"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, + {"name":"channelUnregistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, + {"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] } + ] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicheQuicChannel"}, + "name":"io.netty.channel.ChannelInboundHandlerAdapter", + "methods":[{"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicheQuicChannel$2"}, + "name":"io.netty.channel.ChannelInboundHandlerAdapter", + "methods":[ + {"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, + {"name":"channelRegistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelUnregistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] } + ] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicheQuicChannel$QuicChannelUnsafe"}, + "name":"io.netty.channel.ChannelInboundHandlerAdapter", + "methods":[ + {"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, + {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelRegistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelUnregistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelWritabilityChanged","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, + {"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] } + ] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicheQuicServerCodec"}, + "name":"io.netty.channel.ChannelInboundHandlerAdapter", + "methods":[ + {"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, + {"name":"channelRegistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelUnregistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, + {"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] } + ] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicheQuicChannel"}, + "name":"io.netty.channel.ChannelInitializer", + "methods":[ + {"name":"channelRegistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] } + ] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicheQuicChannel$2"}, + "name":"io.netty.channel.DefaultChannelPipeline$HeadContext", + "methods":[ + {"name":"bind","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, + {"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, + {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelRegistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelUnregistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelWritabilityChanged","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"close","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, + {"name":"connect","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, + {"name":"deregister","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, + {"name":"disconnect","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, + {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, + {"name":"flush","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"read","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, + {"name":"write","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object","io.netty.channel.ChannelPromise"] } + ] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicheQuicStreamChannel$1"}, + "name":"io.netty.channel.DefaultChannelPipeline$HeadContext", + "methods":[ + {"name":"bind","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, + {"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, + {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelRegistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelUnregistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelWritabilityChanged","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"close","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, + {"name":"connect","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.net.SocketAddress","java.net.SocketAddress","io.netty.channel.ChannelPromise"] }, + {"name":"deregister","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, + {"name":"disconnect","parameterTypes":["io.netty.channel.ChannelHandlerContext","io.netty.channel.ChannelPromise"] }, + {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, + {"name":"flush","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"read","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, + {"name":"write","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object","io.netty.channel.ChannelPromise"] } + ] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicheQuicChannel$2"}, + "name":"io.netty.channel.DefaultChannelPipeline$TailContext", + "methods":[ + {"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, + {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelRegistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelUnregistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelWritabilityChanged","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, + {"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] } + ] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicheQuicStreamChannel$1"}, + "name":"io.netty.channel.DefaultChannelPipeline$TailContext", + "methods":[ + {"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelRead","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] }, + {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelRegistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelUnregistered","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"channelWritabilityChanged","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, + {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, + {"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] } + ] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"io.netty.handler.codec.quic.BoringSSLCertificateCallback" +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"io.netty.handler.codec.quic.BoringSSLCertificateVerifyCallback" +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"io.netty.handler.codec.quic.BoringSSLHandshakeCompleteCallback" +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicheQuicChannel"}, + "name":"io.netty.handler.codec.quic.QuicStreamChannelBootstrap$QuicStreamChannelBootstrapHandler" +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"io.netty.handler.codec.quic.QuicheLogger" +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicheQuicChannel"}, + "name":"io.netty.util.ReferenceCountUtil", + "queryAllDeclaredMethods":true +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"io.netty.util.internal.NativeLibraryUtil", + "methods":[{"name":"loadLibrary","parameterTypes":["java.lang.String","boolean"] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicheQuicChannel"}, + "name":"io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueueConsumerIndexField", + "fields":[{"name":"consumerIndex"}] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicheQuicChannel"}, + "name":"io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueueProducerIndexField", + "fields":[{"name":"producerIndex"}] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicheQuicChannel"}, + "name":"io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueueProducerLimitField", + "fields":[{"name":"producerLimit"}] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"java.io.FilePermission" +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"java.lang.RuntimePermission" +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"java.lang.String" +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"java.lang.management.ManagementFactory", + "methods":[{"name":"getRuntimeMXBean","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"java.lang.management.RuntimeMXBean", + "methods":[{"name":"getInputArguments","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"java.net.NetPermission" +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"java.net.SocketPermission" +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"java.net.URLPermission", + "methods":[{"name":"","parameterTypes":["java.lang.String","java.lang.String"] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"java.nio.Bits", + "methods":[{"name":"unaligned","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"java.nio.Buffer", + "fields":[{"name":"address"}] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"java.nio.ByteBuffer", + "methods":[{"name":"alignedSlice","parameterTypes":["int"] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"java.nio.DirectByteBuffer", + "methods":[{"name":"","parameterTypes":["long","int"] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"java.security.AllPermission" +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.HmacSignQuicConnectionIdGenerator"}, + "name":"java.security.SecureRandomParameters" +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicSslContextBuilder"}, + "name":"java.security.SecureRandomParameters" +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"java.security.SecureRandomParameters" +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.SecureRandomQuicConnectionIdGenerator"}, + "name":"java.security.SecureRandomParameters" +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"java.security.SecurityPermission" +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"java.util.PropertyPermission" +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"javax.management.ObjectName" +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.BoringSSLCertificateVerifyCallback"}, + "name":"javax.net.ssl.X509ExtendedTrustManager" +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.BoringSSL"}, + "name":"javax.security.auth.x500.X500Principal", + "methods":[{"name":"","parameterTypes":["sun.security.x509.X500Name"] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"jdk.internal.misc.Unsafe", + "methods":[{"name":"getUnsafe","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicheQuicSslContext"}, + "name":"org.bouncycastle.jcajce.provider.asymmetric.rsa.KeyFactorySpi", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicHeaderParser"}, + "name":"sun.misc.Unsafe", + "methods":[{"name":"invokeCleaner","parameterTypes":["java.nio.ByteBuffer"] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicSslContext"}, + "name":"sun.misc.Unsafe", + "methods":[{"name":"invokeCleaner","parameterTypes":["java.nio.ByteBuffer"] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"sun.misc.Unsafe", + "fields":[{"name":"theUnsafe"}], + "methods":[ + {"name":"copyMemory","parameterTypes":["java.lang.Object","long","java.lang.Object","long","long"] }, + {"name":"invokeCleaner","parameterTypes":["java.nio.ByteBuffer"] }, + {"name":"storeFence","parameterTypes":[] } + ] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicheQuicCodec"}, + "name":"sun.misc.Unsafe", + "methods":[{"name":"invokeCleaner","parameterTypes":["java.nio.ByteBuffer"] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicheQuicConnection"}, + "name":"sun.misc.Unsafe", + "methods":[{"name":"invokeCleaner","parameterTypes":["java.nio.ByteBuffer"] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicheQuicServerCodec"}, + "name":"sun.misc.Unsafe", + "methods":[{"name":"invokeCleaner","parameterTypes":["java.nio.ByteBuffer"] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicheQuicSslContext"}, + "name":"sun.misc.Unsafe", + "methods":[{"name":"invokeCleaner","parameterTypes":["java.nio.ByteBuffer"] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicheQuicStreamChannel$QuicStreamChannelUnsafe"}, + "name":"sun.misc.Unsafe", + "methods":[{"name":"invokeCleaner","parameterTypes":["java.nio.ByteBuffer"] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicSslContextBuilder"}, + "name":"sun.security.pkcs12.PKCS12KeyStore", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicheQuicSslContext"}, + "name":"sun.security.pkcs12.PKCS12KeyStore", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicSslContextBuilder"}, + "name":"sun.security.pkcs12.PKCS12KeyStore$DualFormatPKCS12", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicheQuicSslContext"}, + "name":"sun.security.pkcs12.PKCS12KeyStore$DualFormatPKCS12", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.HmacSignQuicConnectionIdGenerator"}, + "name":"sun.security.provider.NativePRNG", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicSslContextBuilder"}, + "name":"sun.security.provider.NativePRNG", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"sun.security.provider.NativePRNG", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.SecureRandomQuicConnectionIdGenerator"}, + "name":"sun.security.provider.NativePRNG", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "name":"sun.security.provider.SHA", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.HmacSignQuicConnectionIdGenerator"}, + "name":"sun.security.provider.SHA2$SHA256", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicSslContextBuilder"}, + "name":"sun.security.provider.SHA2$SHA256", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.BoringSSL"}, + "name":"sun.security.provider.X509Factory", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.BoringSSLCertificateVerifyCallback"}, + "name":"sun.security.provider.X509Factory", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicSslContext"}, + "name":"sun.security.provider.X509Factory", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicheQuicSslContext"}, + "name":"sun.security.provider.X509Factory", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicSslContextBuilder"}, + "name":"sun.security.rsa.RSAKeyFactory$Legacy", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.BoringSSLKeylessManagerFactory"}, + "name":"sun.security.ssl.KeyManagerFactoryImpl$SunX509", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicSslContextBuilder"}, + "name":"sun.security.ssl.KeyManagerFactoryImpl$SunX509", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "condition":{"typeReachable":"io.netty.handler.codec.quic.QuicheQuicSslContext"}, + "name":"sun.security.ssl.TrustManagerFactoryImpl$PKIXFactory", + "methods":[{"name":"","parameterTypes":[] }] +} +] diff --git a/codec-native-quic/src/main/resources/META-INF/native-image/io.netty.incubator/netty-incubator-codec-native-quic/resource-config.json b/codec-native-quic/src/main/resources/META-INF/native-image/io.netty.incubator/netty-incubator-codec-native-quic/resource-config.json new file mode 100644 index 0000000..b64c98f --- /dev/null +++ b/codec-native-quic/src/main/resources/META-INF/native-image/io.netty.incubator/netty-incubator-codec-native-quic/resource-config.json @@ -0,0 +1,26 @@ +{ + "resources":{ + "includes":[ + { + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "pattern":"\\QMETA-INF/native/libnetty_quiche_osx_x86_64.jnilib\\E" + }, + { + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "pattern":"\\QMETA-INF/native/libnetty_quiche_osx_aarch_64.jnilib\\E" + }, + { + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "pattern":"\\QMETA-INF/native/libnetty_quiche_linux_x86_64.so\\E" + }, + { + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "pattern":"\\QMETA-INF/native/libnetty_quiche_linux_aarch_64.so\\E" + }, + { + "condition":{"typeReachable":"io.netty.handler.codec.quic.Quiche"}, + "pattern":"\\QMETA-INF/native/netty_quiche_windows_x86_64.dll\\E" + } + ]}, + "bundles":[] +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/AbstractQuicTest.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/AbstractQuicTest.java new file mode 100644 index 0000000..62c6140 --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/AbstractQuicTest.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.util.concurrent.ImmediateExecutor; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Timeout; + +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@Timeout(10) +public abstract class AbstractQuicTest { + + @BeforeAll + public static void ensureAvailability() { + Quic.ensureAvailability(); + } + + static Executor[] newSslTaskExecutors() { + return new Executor[] { + ImmediateExecutor.INSTANCE, + Executors.newSingleThreadExecutor() + }; + } + + static void shutdown(Executor executor) { + if (executor instanceof ExecutorService) { + ((ExecutorService) executor).shutdown(); + } + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/FlushStrategyTest.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/FlushStrategyTest.java new file mode 100644 index 0000000..39b5f2e --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/FlushStrategyTest.java @@ -0,0 +1,38 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class FlushStrategyTest { + + @Test + public void testAfterNumBytes() { + FlushStrategy strategy = FlushStrategy.afterNumBytes(10); + assertFalse(strategy.shouldFlushNow(1, 10)); + assertTrue(strategy.shouldFlushNow(1, 11)); + } + + @Test + public void testAfterNumPackets() { + FlushStrategy strategy = FlushStrategy.afterNumPackets(10); + assertFalse(strategy.shouldFlushNow(10, 10)); + assertTrue(strategy.shouldFlushNow(11, 11)); + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/InsecureQuicTokenHandlerTest.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/InsecureQuicTokenHandlerTest.java new file mode 100644 index 0000000..4719f3e --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/InsecureQuicTokenHandlerTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +import org.junit.jupiter.api.Test; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.concurrent.ThreadLocalRandom; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.lessThanOrEqualTo; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +public class InsecureQuicTokenHandlerTest extends AbstractQuicTest { + + @Test + public void testMaxTokenLength() { + assertEquals(InsecureQuicTokenHandler.MAX_TOKEN_LEN, InsecureQuicTokenHandler.INSTANCE.maxTokenLength()); + } + + @Test + public void testTokenProcessingIpv4() throws UnknownHostException { + testTokenProcessing(true); + } + + @Test + public void testTokenProcessingIpv6() throws UnknownHostException { + testTokenProcessing(false); + } + + private static void testTokenProcessing(boolean ipv4) throws UnknownHostException { + byte[] bytes = new byte[Quiche.QUICHE_MAX_CONN_ID_LEN]; + ThreadLocalRandom.current().nextBytes(bytes); + ByteBuf dcid = Unpooled.wrappedBuffer(bytes); + ByteBuf out = Unpooled.buffer(); + try { + final InetSocketAddress validAddress; + final InetSocketAddress invalidAddress; + if (ipv4) { + validAddress = new InetSocketAddress( + InetAddress.getByAddress(new byte[] { 10, 10, 10, 1}), 9999); + invalidAddress = new InetSocketAddress( + InetAddress.getByAddress(new byte[] { 10, 10, 10, 10}), 9999); + } else { + validAddress = new InetSocketAddress(InetAddress.getByAddress( + new byte[] { 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 1}), 9999); + invalidAddress = new InetSocketAddress(InetAddress.getByAddress( + new byte[] { 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10}), 9999); + } + + InsecureQuicTokenHandler.INSTANCE.writeToken(out, dcid, validAddress); + assertThat(out.readableBytes(), lessThanOrEqualTo(InsecureQuicTokenHandler.INSTANCE.maxTokenLength())); + assertNotEquals(-1, InsecureQuicTokenHandler.INSTANCE.validateToken(out, validAddress)); + + // Use another address and check that the validate fails. + assertEquals(-1, InsecureQuicTokenHandler.INSTANCE.validateToken(out, invalidAddress)); + } finally { + dcid.release(); + out.release(); + } + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicChannelConnectTest.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicChannelConnectTest.java new file mode 100644 index 0000000..e2ca453 --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicChannelConnectTest.java @@ -0,0 +1,1547 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelOption; +import io.netty.channel.ConnectTimeoutException; +import io.netty.channel.socket.ChannelInputShutdownEvent; +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.SniCompletionEvent; +import io.netty.handler.ssl.SslHandshakeCompletionEvent; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.ssl.util.TrustManagerFactoryWrapper; +import io.netty.util.DomainWildcardMappingBuilder; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.ImmediateEventExecutor; +import org.hamcrest.CoreMatchers; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.opentest4j.AssertionFailedError; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509ExtendedTrustManager; +import javax.net.ssl.X509TrustManager; +import java.io.File; +import java.net.DatagramSocket; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.channels.AlreadyConnectedException; +import java.nio.channels.ClosedChannelException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.spec.MGF1ParameterSpec; +import java.security.spec.PSSParameterSpec; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +public class QuicChannelConnectTest extends AbstractQuicTest { + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + @Timeout(value = 5000, unit = TimeUnit.MILLISECONDS) + public void testConnectAndQLog(Executor executor) throws Throwable { + Path path = Files.createTempFile("qlog", ".quic"); + assertTrue(path.toFile().delete()); + testQLog(executor, path, p -> { + try { + // Some log should have been written at some point. + while (Files.readAllLines(p).isEmpty()) { + Thread.sleep(100); + } + } catch (Exception e) { + throw new AssertionError(e); + } + }); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + @Timeout(value = 5000, unit = TimeUnit.MILLISECONDS) + public void testConnectAndQLogDir(Executor executor) throws Throwable { + Path path = Files.createTempDirectory("qlogdir-"); + testQLog(executor, path, p -> { + try { + for (;;) { + File[] files = path.toFile().listFiles(); + if (files != null && files.length == 1) { + if (!Files.readAllLines(files[0].toPath()).isEmpty()) { + return; + } + } + Thread.sleep(100); + } + } catch (Exception e) { + throw new AssertionError(e); + } + }); + } + + private void testQLog(Executor executor, Path path, Consumer consumer) throws Throwable { + QuicChannelValidationHandler serverValidationHandler = new QuicChannelValidationHandler(); + QuicChannelValidationHandler clientValidationHandler = new QuicChannelValidationHandler(); + Channel server = QuicTestUtils.newServer(executor, serverValidationHandler, + new ChannelInboundHandlerAdapter()); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + Channel channel = QuicTestUtils.newClient(executor); + try { + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientValidationHandler) + .option(QuicChannelOption.QLOG, + new QLogConfiguration(path.toString(), "testTitle", "test")) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(address) + .connect() + .get(); + QuicStreamChannel stream = quicChannel.createStream(QuicStreamType.BIDIRECTIONAL, + new ChannelInboundHandlerAdapter()).get(); + + stream.writeAndFlush(Unpooled.directBuffer().writeZero(10)).sync(); + stream.close().sync(); + quicChannel.close().sync(); + quicChannel.closeFuture().sync(); + consumer.accept(path); + + serverValidationHandler.assertState(); + clientValidationHandler.assertState(); + } finally { + server.close().sync(); + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(executor); + } + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testKeylogEnabled(Executor executor) throws Throwable { + testKeylog(executor, true); + assertNotEquals(0, TestLogBackAppender.getLogs().size()); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testKeylogDisabled(Executor executor) throws Throwable { + testKeylog(executor, false); + assertEquals(0, TestLogBackAppender.getLogs().size()); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testCustomKeylog(Executor executor) throws Throwable { + AtomicBoolean called = new AtomicBoolean(); + testKeylog(executor, (BoringSSLKeylog) (engine, log) -> { + called.set(true); + }); + assertTrue(called.get()); + } + + private static void testKeylog(Executor sslTaskExecutor, Object keylog) throws Throwable { + TestLogBackAppender.clearLogs(); + QuicChannelValidationHandler serverValidationHandler = new QuicChannelValidationHandler(); + QuicChannelValidationHandler clientValidationHandler = new QuicChannelValidationHandler(); + Channel server = QuicTestUtils.newServer(sslTaskExecutor, serverValidationHandler, + new ChannelInboundHandlerAdapter()); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + QuicSslContextBuilder ctxClientBuilder = QuicSslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .applicationProtocols(QuicTestUtils.PROTOS); + if (keylog instanceof Boolean) { + ctxClientBuilder.keylog((Boolean) keylog); + } else { + ctxClientBuilder.keylog((BoringSSLKeylog) keylog); + } + + QuicSslContext context = ctxClientBuilder.build(); + Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(sslTaskExecutor, context)); + + try { + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientValidationHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(address) + .connect() + .get(); + + quicChannel.close().sync(); + quicChannel.closeFuture().sync(); + serverValidationHandler.assertState(); + clientValidationHandler.assertState(); + } finally { + server.close().sync(); + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(sslTaskExecutor); + } + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testAddressValidation(Executor executor) throws Throwable { + // Bind to something so we can use the port to connect too and so can ensure we really timeout. + DatagramSocket socket = new DatagramSocket(); + Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor) + .localConnectionIdLength(10)); + try { + ChannelStateVerifyHandler verifyHandler = new ChannelStateVerifyHandler(); + Future future = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(verifyHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(socket.getLocalSocketAddress()) + .connectionAddress(QuicConnectionAddress.random(20)) + .connect(); + Throwable cause = future.await().cause(); + assertThat(cause, CoreMatchers.instanceOf(IllegalArgumentException.class)); + verifyHandler.assertState(); + } finally { + socket.close(); + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(executor); + } + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testConnectWithCustomIdLength(Executor executor) throws Throwable { + testConnectWithCustomIdLength(executor, 10); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testConnectWithCustomIdLengthOfZero(Executor executor) throws Throwable { + testConnectWithCustomIdLength(executor, 0); + } + + private static void testConnectWithCustomIdLength(Executor executor, int idLength) throws Throwable { + ChannelActiveVerifyHandler serverQuicChannelHandler = new ChannelActiveVerifyHandler(); + ChannelStateVerifyHandler serverQuicStreamHandler = new ChannelStateVerifyHandler(); + Channel server = QuicTestUtils.newServer(QuicTestUtils.newQuicServerBuilder(executor) + .localConnectionIdLength(idLength), + InsecureQuicTokenHandler.INSTANCE, serverQuicChannelHandler, serverQuicStreamHandler); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor) + .localConnectionIdLength(idLength)); + try { + ChannelActiveVerifyHandler clientQuicChannelHandler = new ChannelActiveVerifyHandler(); + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientQuicChannelHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(address) + .connect() + .get(); + assertTrue(quicChannel.close().await().isSuccess()); + ChannelFuture closeFuture = quicChannel.closeFuture().await(); + assertTrue(closeFuture.isSuccess()); + clientQuicChannelHandler.assertState(); + } finally { + serverQuicChannelHandler.assertState(); + serverQuicStreamHandler.assertState(); + + server.close().sync(); + // Close the parent Datagram channel as well. + channel.close().sync(); + shutdown(executor); + } + } + + private void testConnectWithDroppedPackets(Executor executor, int numDroppedPackets, + QuicConnectionIdGenerator connectionIdGenerator) throws Throwable { + Channel server = QuicTestUtils.newServer(QuicTestUtils.newQuicServerBuilder(executor) + .connectionIdAddressGenerator(connectionIdGenerator), + NoQuicTokenHandler.INSTANCE, + new ChannelInboundHandlerAdapter() { + @Override + public boolean isSharable() { + return true; + } + }, + new ChannelInboundHandlerAdapter() { + @Override + public boolean isSharable() { + return true; + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + // Server closes the stream whenever the client sends a FIN. + if (evt instanceof ChannelInputShutdownEvent) { + ctx.close(); + } + ctx.fireUserEventTriggered(evt); + } + }); + + // Have the server drop the few first numDroppedPackets incoming packets. + server.pipeline().addFirst( + new ChannelInboundHandlerAdapter() { + private int counter = 0; + + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (counter++ < numDroppedPackets) { + System.out.println("Server dropping incoming packet #" + counter); + ReferenceCountUtil.release(msg); + } else { + ctx.fireChannelRead(msg); + } + } + }); + + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor)); + ChannelActiveVerifyHandler clientQuicChannelHandler = new ChannelActiveVerifyHandler(); + try { + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientQuicChannelHandler) + .remoteAddress(address) + .connect() + .get(); + + QuicStreamChannel quicStream = quicChannel.createStream(QuicStreamType.BIDIRECTIONAL, + new ChannelInboundHandlerAdapter()).get(); + + ByteBuf payload = Unpooled.wrappedBuffer("HELLO!".getBytes(StandardCharsets.US_ASCII)); + quicStream.writeAndFlush(payload).sync(); + quicStream.shutdownOutput().sync(); + assertTrue(quicStream.closeFuture().await().isSuccess()); + + ChannelFuture closeFuture = channel.close().await(); + assertTrue(closeFuture.isSuccess()); + } finally { + clientQuicChannelHandler.assertState(); + channel.close().sync(); + server.close().sync(); + shutdown(executor); + } + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + @Timeout(3) + public void testConnectWithNoDroppedPacketsAndRandomConnectionIdGenerator(Executor executor) throws Throwable { + testConnectWithDroppedPackets(executor, 0, QuicConnectionIdGenerator.randomGenerator()); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + @Timeout(5) + public void testConnectWithDroppedPacketsAndRandomConnectionIdGenerator(Executor executor) throws Throwable { + testConnectWithDroppedPackets(executor, 2, QuicConnectionIdGenerator.randomGenerator()); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + @Timeout(3) + public void testConnectWithNoDroppedPacketsAndSignConnectionIdGenerator(Executor executor) throws Throwable { + testConnectWithDroppedPackets(executor, 0, QuicConnectionIdGenerator.signGenerator()); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + @Timeout(5) + public void testConnectWithDroppedPacketsAndSignConnectionIdGenerator(Executor executor) throws Throwable { + testConnectWithDroppedPackets(executor, 2, QuicConnectionIdGenerator.signGenerator()); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testConnectTimeout(Executor executor) throws Throwable { + // Bind to something so we can use the port to connect too and so can ensure we really timeout. + DatagramSocket socket = new DatagramSocket(); + Channel channel = QuicTestUtils.newClient(executor); + try { + ChannelStateVerifyHandler verifyHandler = new ChannelStateVerifyHandler(); + Future future = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(verifyHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10) + .remoteAddress(socket.getLocalSocketAddress()) + .connect(); + Throwable cause = future.await().cause(); + assertThat(cause, CoreMatchers.instanceOf(ConnectTimeoutException.class)); + verifyHandler.assertState(); + } finally { + socket.close(); + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(executor); + } + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testConnectAlreadyConnected(Executor executor) throws Throwable { + ChannelActiveVerifyHandler serverQuicChannelHandler = new ChannelActiveVerifyHandler(); + ChannelStateVerifyHandler serverQuicStreamHandler = new ChannelStateVerifyHandler(); + + Channel server = QuicTestUtils.newServer(executor, serverQuicChannelHandler, serverQuicStreamHandler); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + Channel channel = QuicTestUtils.newClient(executor); + try { + ChannelActiveVerifyHandler clientQuicChannelHandler = new ChannelActiveVerifyHandler(); + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientQuicChannelHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(address) + .connect() + .get(); + + // Try to connect again + ChannelFuture connectFuture = quicChannel.connect(QuicConnectionAddress.random()); + Throwable cause = connectFuture.await().cause(); + assertThat(cause, CoreMatchers.instanceOf(AlreadyConnectedException.class)); + assertTrue(quicChannel.close().await().isSuccess()); + ChannelFuture closeFuture = quicChannel.closeFuture().await(); + assertTrue(closeFuture.isSuccess()); + clientQuicChannelHandler.assertState(); + serverQuicChannelHandler.assertState(); + serverQuicStreamHandler.assertState(); + } finally { + server.close().sync(); + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(executor); + } + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testConnectWithoutTokenValidation(Executor executor) throws Throwable { + int numBytes = 8; + ChannelActiveVerifyHandler serverQuicChannelHandler = new ChannelActiveVerifyHandler(); + CountDownLatch serverLatch = new CountDownLatch(1); + CountDownLatch clientLatch = new CountDownLatch(1); + + // Disable token validation + Channel server = QuicTestUtils.newServer(executor, NoQuicTokenHandler.INSTANCE, + serverQuicChannelHandler, new BytesCountingHandler(serverLatch, numBytes)); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + Channel channel = QuicTestUtils.newClient(executor); + try { + ChannelActiveVerifyHandler clientQuicChannelHandler = new ChannelActiveVerifyHandler(); + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientQuicChannelHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(address) + .connect() + .get(); + QuicConnectionAddress localAddress = (QuicConnectionAddress) quicChannel.localAddress(); + QuicConnectionAddress remoteAddress = (QuicConnectionAddress) quicChannel.remoteAddress(); + assertNotNull(localAddress); + assertNotNull(remoteAddress); + + QuicStreamChannel stream = quicChannel.createStream(QuicStreamType.BIDIRECTIONAL, + new BytesCountingHandler(clientLatch, numBytes)).get(); + stream.writeAndFlush(Unpooled.directBuffer().writeZero(numBytes)).sync(); + clientLatch.await(); + + assertEquals(QuicTestUtils.PROTOS[0], + // Just do the cast as getApplicationProtocol() only exists in SSLEngine itself since Java9+ and + // we may run on an earlier version + ((QuicheQuicSslEngine) quicChannel.sslEngine()).getApplicationProtocol()); + stream.close().sync(); + quicChannel.close().sync(); + ChannelFuture closeFuture = quicChannel.closeFuture().await(); + assertTrue(closeFuture.isSuccess()); + + clientQuicChannelHandler.assertState(); + serverQuicChannelHandler.assertState(); + + assertEquals(serverQuicChannelHandler.localAddress(), remoteAddress); + assertEquals(serverQuicChannelHandler.remoteAddress(), localAddress); + + // Check if we also can access these after the channel was closed. + assertNotNull(quicChannel.localAddress()); + assertNotNull(quicChannel.remoteAddress()); + } finally { + serverLatch.await(); + + server.close().sync(); + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(executor); + } + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testConnectWith0RTT(Executor executor) throws Throwable { + final CountDownLatch readLatch = new CountDownLatch(1); + Channel server = QuicTestUtils.newServer(QuicTestUtils.newQuicServerBuilder(executor, + QuicSslContextBuilder.forServer( + QuicTestUtils.SELF_SIGNED_CERTIFICATE.privateKey(), null, + QuicTestUtils.SELF_SIGNED_CERTIFICATE.certificate()) + .applicationProtocols(QuicTestUtils.PROTOS) + .earlyData(true) + .build()), + InsecureQuicTokenHandler.INSTANCE, new ChannelInboundHandlerAdapter() { + @Override + public boolean isSharable() { + return true; + } + }, new ChannelInboundHandlerAdapter() { + @Override + public boolean isSharable() { + return true; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ByteBuf buffer = (ByteBuf) msg; + try { + assertEquals(4, buffer.readableBytes()); + assertEquals(1, buffer.readInt()); + readLatch.countDown(); + ctx.close(); + ctx.channel().parent().close(); + } finally { + buffer.release(); + } + } + }); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + + QuicSslContext sslContext = QuicSslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .applicationProtocols(QuicTestUtils.PROTOS) + .earlyData(true) + .build(); + Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor, sslContext) + .sslEngineProvider(q -> sslContext.newEngine(q.alloc(), "localhost", 9999))); + final CountDownLatch activeLatch = new CountDownLatch(1); + final CountDownLatch eventLatch = new CountDownLatch(1); + final CountDownLatch streamLatch = new CountDownLatch(1); + final AtomicReference errorRef = new AtomicReference<>(); + + try { + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(new ChannelInboundHandlerAdapter() { + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt instanceof SslEarlyDataReadyEvent) { + errorRef.set(new AssertionFailedError("Shouldn't be called on the first connection")); + } + ctx.fireUserEventTriggered(evt); + } + }) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(address) + .connect() + .get(); + + QuicClientSessionCache cache = ((QuicheQuicSslContext) sslContext).getSessionCache(); + + // Let's spin until the session shows up in the cache. This is needed as this might happen a bit after + // the connection is already established. + // See https://commondatastorage.googleapis.com/chromium-boringssl-docs/ssl.h.html#SSL_CTX_sess_set_new_cb + while (!cache.hasSession("localhost", 9999)) { + // Check again in 100ms. + Thread.sleep(100); + } + + quicChannel.close().sync(); + + if (errorRef.get() != null) { + throw errorRef.get(); + } + + quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + activeLatch.countDown(); + ctx.fireChannelActive(); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt instanceof SslEarlyDataReadyEvent) { + eventLatch.countDown(); + ((QuicChannel) ctx.channel()).createStream(QuicStreamType.BIDIRECTIONAL, + new ChannelInboundHandlerAdapter()).addListener(f -> { + try { + // This should succeed as we have the transport params cached as part of + // the session. + assertTrue(f.isSuccess()); + Channel stream = (Channel) f.getNow(); + + // Let's write some data as part of the client hello. + stream.writeAndFlush(stream.alloc().buffer().writeInt(1)); + } catch (Throwable error) { + errorRef.set(error); + } finally { + streamLatch.countDown(); + } + }); + } + ctx.fireUserEventTriggered(evt); + } + }) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(address) + .connect() + .get(); + + awaitAndCheckError(activeLatch, errorRef); + awaitAndCheckError(eventLatch, errorRef); + awaitAndCheckError(streamLatch, errorRef); + + quicChannel.closeFuture().sync(); + readLatch.await(); + } finally { + server.close().sync(); + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(executor); + } + } + + private static void awaitAndCheckError(CountDownLatch latch, AtomicReference errorRef) throws Throwable { + while (!latch.await(500, TimeUnit.MILLISECONDS)) { + if (errorRef.get() != null) { + throw errorRef.get(); + } + } + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testConnectAndStreamPriority(Executor executor) throws Throwable { + int numBytes = 8; + ChannelActiveVerifyHandler serverQuicChannelHandler = new ChannelActiveVerifyHandler(); + CountDownLatch serverLatch = new CountDownLatch(1); + CountDownLatch clientLatch = new CountDownLatch(1); + + Channel server = QuicTestUtils.newServer(executor, serverQuicChannelHandler, + new BytesCountingHandler(serverLatch, numBytes)); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + Channel channel = QuicTestUtils.newClient(executor); + try { + ChannelActiveVerifyHandler clientQuicChannelHandler = new ChannelActiveVerifyHandler(); + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientQuicChannelHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(address) + .connect() + .get(); + QuicStreamChannel stream = quicChannel.createStream(QuicStreamType.BIDIRECTIONAL, + new BytesCountingHandler(clientLatch, numBytes)).get(); + assertNull(stream.priority()); + QuicStreamPriority priority = new QuicStreamPriority(0, false); + stream.updatePriority(priority).sync(); + assertEquals(priority, stream.priority()); + + stream.writeAndFlush(Unpooled.directBuffer().writeZero(numBytes)).sync(); + clientLatch.await(); + + stream.close().sync(); + quicChannel.close().sync(); + ChannelFuture closeFuture = quicChannel.closeFuture().await(); + assertTrue(closeFuture.isSuccess()); + clientQuicChannelHandler.assertState(); + } finally { + serverLatch.await(); + serverQuicChannelHandler.assertState(); + + server.close().sync(); + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(executor); + } + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testExtendedTrustManagerFailureOnTheClient(Executor executor) throws Throwable { + testTrustManagerFailureOnTheClient(executor, true); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testTrustManagerFailureOnTheClient(Executor executor) throws Throwable { + testTrustManagerFailureOnTheClient(executor, false); + } + + private void testTrustManagerFailureOnTheClient(Executor executor, boolean extended) throws Throwable { + final X509TrustManager trustManager; + if (extended) { + trustManager = new TestX509ExtendedTrustManager() { + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) + throws CertificateException { + throw new CertificateException(); + } + }; + } else { + trustManager = new TestX509TrustManager() { + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + throw new CertificateException(); + } + }; + } + Channel server = QuicTestUtils.newServer(executor, new ChannelInboundHandlerAdapter(), + new ChannelInboundHandlerAdapter()); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor, + QuicSslContextBuilder.forClient() + .trustManager(new TrustManagerFactoryWrapper(trustManager)) + .applicationProtocols(QuicTestUtils.PROTOS).build())); + try { + Throwable cause = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(new ChannelInboundHandlerAdapter()) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(address) + .connect() + .await().cause(); + assertThat(cause, Matchers.instanceOf(SSLException.class)); + } finally { + server.close().sync(); + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(executor); + } + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testALPNProtocolMissmatch(Executor executor) throws Throwable { + CountDownLatch latch = new CountDownLatch(1); + CountDownLatch eventLatch = new CountDownLatch(1); + + Channel server = QuicTestUtils.newServer(QuicTestUtils.newQuicServerBuilder(executor, + QuicSslContextBuilder.forServer( + QuicTestUtils.SELF_SIGNED_CERTIFICATE.privateKey(), null, + QuicTestUtils.SELF_SIGNED_CERTIFICATE.certificate()) + .applicationProtocols("my-protocol").build()), + InsecureQuicTokenHandler.INSTANCE, new ChannelInboundHandlerAdapter() { + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt instanceof SslHandshakeCompletionEvent) { + if (((SslHandshakeCompletionEvent) evt).cause() instanceof SSLHandshakeException) { + eventLatch.countDown(); + return; + } + } + ctx.fireUserEventTriggered(evt); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + if (cause instanceof SSLHandshakeException) { + latch.countDown(); + } else { + ctx.fireExceptionCaught(cause); + } + } + }, + new ChannelInboundHandlerAdapter()); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor, + QuicSslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE).applicationProtocols("protocol").build())); + AtomicReference closeEventRef = new AtomicReference<>(); + try { + Throwable cause = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(new ChannelInboundHandlerAdapter() { + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof QuicConnectionCloseEvent) { + closeEventRef.set((QuicConnectionCloseEvent) evt); + } + super.userEventTriggered(ctx, evt); + } + }) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(address) + .connect() + .await().cause(); + assertThat(cause, Matchers.instanceOf(ClosedChannelException.class)); + latch.await(); + eventLatch.await(); + QuicConnectionCloseEvent closeEvent = closeEventRef.get(); + assertNotNull(closeEvent); + assertTrue(closeEvent.isTlsError()); + // 120 is the ALPN error. + // See https://datatracker.ietf.org/doc/html/rfc8446#section-6 + assertEquals(120, QuicConnectionCloseEvent.extractTlsError(closeEvent.error())); + assertEquals(closeEvent, ((QuicClosedChannelException) cause).event()); + } finally { + server.close().sync(); + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(executor); + } + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testConnectSuccessWhenTrustManagerBuildFromSameCert(Executor executor) throws Throwable { + Channel server = QuicTestUtils.newServer(QuicTestUtils.newQuicServerBuilder(executor, + QuicSslContextBuilder.forServer( + QuicTestUtils.SELF_SIGNED_CERTIFICATE.privateKey(), null, + QuicTestUtils.SELF_SIGNED_CERTIFICATE.certificate()) + .applicationProtocols(QuicTestUtils.PROTOS).clientAuth(ClientAuth.NONE).build()), + InsecureQuicTokenHandler.INSTANCE, new ChannelInboundHandlerAdapter(), + new ChannelInboundHandlerAdapter()); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + + Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor, + QuicSslContextBuilder.forClient() + .trustManager(QuicTestUtils.SELF_SIGNED_CERTIFICATE.certificate()) + .applicationProtocols(QuicTestUtils.PROTOS).build())); + try { + ChannelActiveVerifyHandler clientQuicChannelHandler = new ChannelActiveVerifyHandler(); + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientQuicChannelHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(address) + .connect() + .get(); + assertTrue(quicChannel.close().await().isSuccess()); + ChannelFuture closeFuture = quicChannel.closeFuture().await(); + assertTrue(closeFuture.isSuccess()); + clientQuicChannelHandler.assertState(); + } finally { + server.close().sync(); + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(executor); + } + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testConnectMutualAuthRequiredSuccess(Executor executor) throws Throwable { + testConnectMutualAuthSuccess(executor, MutalAuthTestMode.REQUIRED); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testConnectMutualAuthOptionalWithCertSuccess(Executor executor) throws Throwable { + testConnectMutualAuthSuccess(executor, MutalAuthTestMode.OPTIONAL_CERT); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testConnectMutualAuthOptionalWithoutKeyManagerSuccess(Executor executor) throws Throwable { + testConnectMutualAuthSuccess(executor, MutalAuthTestMode.OPTIONAL_NO_KEYMANAGER); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testConnectMutualAuthOptionalWithoutKeyInKeyManagerSuccess(Executor executor) throws Throwable { + testConnectMutualAuthSuccess(executor, MutalAuthTestMode.OPTIONAL_NO_KEY_IN_KEYMANAGER); + } + + private void testConnectMutualAuthSuccess(Executor executor, MutalAuthTestMode mode) throws Throwable { + Channel server = QuicTestUtils.newServer(QuicTestUtils.newQuicServerBuilder(executor, + QuicSslContextBuilder.forServer( + QuicTestUtils.SELF_SIGNED_CERTIFICATE.privateKey(), null, + QuicTestUtils.SELF_SIGNED_CERTIFICATE.certificate()).trustManager( + InsecureTrustManagerFactory.INSTANCE) + .applicationProtocols(QuicTestUtils.PROTOS) + .clientAuth(mode == MutalAuthTestMode.REQUIRED ? + ClientAuth.REQUIRE : ClientAuth.OPTIONAL).build()), + InsecureQuicTokenHandler.INSTANCE, new ChannelInboundHandlerAdapter(), + new ChannelInboundHandlerAdapter()); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + + QuicSslContextBuilder clientSslCtxBuilder = QuicSslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .applicationProtocols(QuicTestUtils.PROTOS); + switch (mode) { + case OPTIONAL_CERT: + case REQUIRED: + clientSslCtxBuilder.keyManager( + QuicTestUtils.SELF_SIGNED_CERTIFICATE.privateKey(), null, + QuicTestUtils.SELF_SIGNED_CERTIFICATE.certificate()); + break; + case OPTIONAL_NO_KEY_IN_KEYMANAGER: + clientSslCtxBuilder.keyManager(new X509ExtendedKeyManager() { + @Override + public String[] getClientAliases(String keyType, Principal[] issuers) { + throw new UnsupportedOperationException(); + } + + @Override + public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { + return null; + } + + @Override + public String[] getServerAliases(String keyType, Principal[] issuers) { + throw new UnsupportedOperationException(); + } + + @Override + public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { + throw new UnsupportedOperationException(); + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + throw new UnsupportedOperationException(); + } + + @Override + public PrivateKey getPrivateKey(String alias) { + throw new UnsupportedOperationException(); + } + }, null); + break; + case OPTIONAL_NO_KEYMANAGER: + break; + default: + throw new IllegalStateException(); + } + + Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor, + clientSslCtxBuilder.build())); + try { + ChannelActiveVerifyHandler clientQuicChannelHandler = new ChannelActiveVerifyHandler(); + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientQuicChannelHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(address) + .connect() + .get(); + assertTrue(quicChannel.close().await().isSuccess()); + ChannelFuture closeFuture = quicChannel.closeFuture().await(); + assertTrue(closeFuture.isSuccess()); + clientQuicChannelHandler.assertState(); + } finally { + server.close().sync(); + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(executor); + } + } + + private enum MutalAuthTestMode { + REQUIRED, + OPTIONAL_CERT, + OPTIONAL_NO_KEYMANAGER, + OPTIONAL_NO_KEY_IN_KEYMANAGER + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testConnectMutualAuthFailsIfClientNotSendCertificate(Executor executor) throws Throwable { + CountDownLatch latch = new CountDownLatch(1); + AtomicReference causeRef = new AtomicReference<>(); + + Channel server = QuicTestUtils.newServer(QuicTestUtils.newQuicServerBuilder(executor, + QuicSslContextBuilder.forServer( + QuicTestUtils.SELF_SIGNED_CERTIFICATE.privateKey(), null, + QuicTestUtils.SELF_SIGNED_CERTIFICATE.certificate()) + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .applicationProtocols(QuicTestUtils.PROTOS).clientAuth(ClientAuth.REQUIRE).build()), + InsecureQuicTokenHandler.INSTANCE, new ChannelInboundHandlerAdapter() { + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + causeRef.compareAndSet(null, cause); + latch.countDown(); + ctx.close(); + } + }, + new ChannelInboundHandlerAdapter()); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor, + QuicSslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .applicationProtocols(QuicTestUtils.PROTOS).build())); + QuicChannel client = null; + try { + client = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(new ChannelInboundHandlerAdapter() { + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + cause.printStackTrace(); + } + }) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(address) + .connect() + .get(); + latch.await(); + + assertThat(causeRef.get(), Matchers.instanceOf(SSLHandshakeException.class)); + } finally { + server.close().sync(); + + if (client != null) { + client.close().sync(); + } + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(executor); + } + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testSniMatch(Executor executor) throws Throwable { + QuicSslContext defaultServerSslContext = QuicSslContextBuilder.forServer( + QuicTestUtils.SELF_SIGNED_CERTIFICATE.privateKey(), null, + QuicTestUtils.SELF_SIGNED_CERTIFICATE.certificate()) + .applicationProtocols("default-protocol").build(); + + QuicSslContext sniServerSslContext = QuicSslContextBuilder.forServer( + QuicTestUtils.SELF_SIGNED_CERTIFICATE.privateKey(), null, + QuicTestUtils.SELF_SIGNED_CERTIFICATE.certificate()) + .applicationProtocols("sni-protocol").build(); + + CountDownLatch sniEventLatch = new CountDownLatch(1); + CountDownLatch sslEventLatch = new CountDownLatch(1); + String hostname = "quic.netty.io"; + QuicSslContext serverSslContext = QuicSslContextBuilder.buildForServerWithSni( + new DomainWildcardMappingBuilder<>(defaultServerSslContext) + .add(hostname, sniServerSslContext).build()); + + Channel server = QuicTestUtils.newServer(QuicTestUtils.newQuicServerBuilder(executor, serverSslContext), + InsecureQuicTokenHandler.INSTANCE, new ChannelInboundHandlerAdapter() { + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof SniCompletionEvent) { + if (hostname.equals(((SniCompletionEvent) evt).hostname())) { + sniEventLatch.countDown(); + } + } else if (evt instanceof SslHandshakeCompletionEvent) { + if (((SslHandshakeCompletionEvent) evt).isSuccess()) { + sslEventLatch.countDown(); + } + } + super.userEventTriggered(ctx, evt); + } + }, + new ChannelInboundHandlerAdapter()); + + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + + QuicSslContext clientSslContext = QuicSslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE).applicationProtocols("sni-protocol").build(); + + Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor) + .sslEngineProvider(c -> clientSslContext.newEngine(c.alloc(), hostname, 8080))); + try { + ChannelActiveVerifyHandler clientQuicChannelHandler = new ChannelActiveVerifyHandler(); + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientQuicChannelHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(address) + .connect() + .get(); + + quicChannel.close().sync(); + ChannelFuture closeFuture = quicChannel.closeFuture().await(); + assertTrue(closeFuture.isSuccess()); + clientQuicChannelHandler.assertState(); + sniEventLatch.await(); + sslEventLatch.await(); + } finally { + server.close().sync(); + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(executor); + } + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testSniFallbackToDefault(Executor executor) throws Throwable { + testSniFallbackToDefault(executor, true); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testNoSniFallbackToDefault(Executor executor) throws Throwable { + testSniFallbackToDefault(executor, false); + } + + private void testSniFallbackToDefault(Executor executor, boolean sendSni) throws Throwable { + QuicSslContext defaultServerSslContext = QuicSslContextBuilder.forServer( + QuicTestUtils.SELF_SIGNED_CERTIFICATE.privateKey(), null, + QuicTestUtils.SELF_SIGNED_CERTIFICATE.certificate()) + .applicationProtocols("default-protocol").build(); + + QuicSslContext sniServerSslContext = QuicSslContextBuilder.forServer( + QuicTestUtils.SELF_SIGNED_CERTIFICATE.privateKey(), null, + QuicTestUtils.SELF_SIGNED_CERTIFICATE.certificate()) + .applicationProtocols("sni-protocol").build(); + + QuicSslContext serverSslContext = QuicSslContextBuilder.buildForServerWithSni( + new DomainWildcardMappingBuilder<>(defaultServerSslContext) + .add("quic.netty.io", sniServerSslContext).build()); + + Channel server = QuicTestUtils.newServer(QuicTestUtils.newQuicServerBuilder(executor, serverSslContext), + InsecureQuicTokenHandler.INSTANCE, new ChannelInboundHandlerAdapter(), + new ChannelInboundHandlerAdapter()); + + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + + QuicSslContext clientSslContext = QuicSslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE).applicationProtocols("default-protocol").build(); + + Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor) + .sslEngineProvider(c -> { + if (sendSni) { + return clientSslContext.newEngine(c.alloc(), "netty.io", 8080); + } else { + return clientSslContext.newEngine(c.alloc()); + } + })); + try { + ChannelActiveVerifyHandler clientQuicChannelHandler = new ChannelActiveVerifyHandler(); + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientQuicChannelHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(address) + .connect() + .get(); + + quicChannel.close().sync(); + ChannelFuture closeFuture = quicChannel.closeFuture().await(); + assertTrue(closeFuture.isSuccess()); + clientQuicChannelHandler.assertState(); + } finally { + server.close().sync(); + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(executor); + } + } + + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testConnectKeyless(Executor executor) throws Throwable { + testConnectKeyless0(executor, false); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testConnectKeylessSignFailure(Executor executor) throws Throwable { + testConnectKeyless0(executor, true); + } + + public void testConnectKeyless0(Executor executor, boolean fail) throws Throwable { + AtomicReference causeRef = new AtomicReference<>(); + AtomicBoolean signCalled = new AtomicBoolean(); + BoringSSLAsyncPrivateKeyMethod keyMethod = new BoringSSLAsyncPrivateKeyMethod() { + @Override + public Future sign(SSLEngine engine, int signatureAlgorithm, byte[] input) { + signCalled.set(true); + + assertEquals(QuicTestUtils.SELF_SIGNED_CERTIFICATE.cert().getPublicKey(), + engine.getSession().getLocalCertificates()[0].getPublicKey()); + + try { + if (fail) { + return ImmediateEventExecutor.INSTANCE.newFailedFuture(new SignatureException()); + } + // Delegate signing to Java implementation. + final Signature signature; + // Depending on the Java version it will pick one or the other. + if (signatureAlgorithm == SSL_SIGN_RSA_PKCS1_SHA256) { + signature = Signature.getInstance("SHA256withRSA"); + } else if (signatureAlgorithm == SSL_SIGN_RSA_PSS_RSAE_SHA256) { + signature = Signature.getInstance("RSASSA-PSS"); + signature.setParameter(new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, + 32, 1)); + } else { + throw new AssertionError("Unexpected signature algorithm " + signatureAlgorithm); + } + signature.initSign(QuicTestUtils.SELF_SIGNED_CERTIFICATE.key()); + signature.update(input); + return ImmediateEventExecutor.INSTANCE.newSucceededFuture(signature.sign()); + } catch (Throwable cause) { + return ImmediateEventExecutor.INSTANCE.newFailedFuture(cause); + } + } + + @Override + public Future decrypt(SSLEngine engine, byte[] input) { + throw new UnsupportedOperationException(); + } + }; + + BoringSSLKeylessManagerFactory factory = BoringSSLKeylessManagerFactory.newKeyless( + keyMethod, QuicTestUtils.SELF_SIGNED_CERTIFICATE.certificate()); + Channel server = QuicTestUtils.newServer(QuicTestUtils.newQuicServerBuilder(executor, + QuicSslContextBuilder.forServer(factory, null) + .applicationProtocols(QuicTestUtils.PROTOS).clientAuth(ClientAuth.NONE).build()), + InsecureQuicTokenHandler.INSTANCE, new ChannelInboundHandlerAdapter() { + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + causeRef.set(cause); + } + } , + new ChannelInboundHandlerAdapter()); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + + Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor, + QuicSslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .applicationProtocols(QuicTestUtils.PROTOS).build())); + try { + ChannelActiveVerifyHandler clientQuicChannelHandler = new ChannelActiveVerifyHandler(); + Future connectFuture = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientQuicChannelHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(address) + .connect().await(); + if (fail) { + assertThat(connectFuture.cause(), Matchers.instanceOf(ClosedChannelException.class)); + assertThat(causeRef.get(), Matchers.instanceOf(SSLHandshakeException.class)); + } else { + QuicChannel quicChannel = connectFuture.get(); + assertTrue(quicChannel.close().await().isSuccess()); + ChannelFuture closeFuture = quicChannel.closeFuture().await(); + assertTrue(closeFuture.isSuccess()); + clientQuicChannelHandler.assertState(); + assertNull(causeRef.get()); + } + assertTrue(signCalled.get()); + } finally { + server.close().sync(); + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(executor); + } + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testSessionTickets(Executor executor) throws Throwable { + testSessionReuse(executor, true); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + @Timeout(5) + public void testSessionReusedOnClientSide(Executor executor) throws Exception { + testSessionReuse(executor, false); + } + + private static void testSessionReuse(Executor executor, boolean ticketKey) throws Exception { + QuicSslContext sslServerCtx = QuicSslContextBuilder.forServer( + QuicTestUtils.SELF_SIGNED_CERTIFICATE.key(), null, + QuicTestUtils.SELF_SIGNED_CERTIFICATE.cert()) + .applicationProtocols(QuicTestUtils.PROTOS) + .build(); + QuicSslContext sslClientCtx = QuicSslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE).applicationProtocols(QuicTestUtils.PROTOS).build(); + + if (ticketKey) { + + SslSessionTicketKey key = new SslSessionTicketKey(new byte[SslSessionTicketKey.NAME_SIZE], + new byte[SslSessionTicketKey.HMAC_KEY_SIZE], new byte[SslSessionTicketKey.AES_KEY_SIZE]); + sslClientCtx.sessionContext().setTicketKeys(key); + sslServerCtx.sessionContext().setTicketKeys(key); + } + CountDownLatch serverSslCompletionEventLatch = new CountDownLatch(2); + Channel server = QuicTestUtils.newServer(QuicTestUtils.newQuicServerBuilder(executor, sslServerCtx), + InsecureQuicTokenHandler.INSTANCE, + new ChannelInboundHandlerAdapter() { + @Override + public boolean isSharable() { + return true; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + ((QuicChannel) ctx.channel()).createStream(QuicStreamType.BIDIRECTIONAL, + new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + ctx.writeAndFlush(ctx.alloc().directBuffer(10).writeZero(10)) + .addListener(f -> ctx.close()); + } + }); + ctx.fireChannelActive(); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof SslHandshakeCompletionEvent) { + serverSslCompletionEventLatch.countDown(); + } + } + }, + new ChannelInboundHandlerAdapter()); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + + Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor).sslEngineProvider(c -> + sslClientCtx.newEngine(c.alloc(), "localhost", 9999))); + try { + CountDownLatch clientSslCompletionEventLatch = new CountDownLatch(2); + + QuicChannelBootstrap bootstrap = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(new ChannelInboundHandlerAdapter() { + @Override + public boolean isSharable() { + return true; + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof SslHandshakeCompletionEvent) { + clientSslCompletionEventLatch.countDown(); + } + } + }) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(address); + + CountDownLatch latch1 = new CountDownLatch(1); + QuicChannel quicChannel1 = bootstrap + .streamHandler(new BytesCountingHandler(latch1, 10)) + .connect() + .get(); + latch1.await(); + assertSessionReused(quicChannel1, false); + + CountDownLatch latch2 = new CountDownLatch(1); + QuicChannel quicChannel2 = bootstrap + .streamHandler(new BytesCountingHandler(latch2, 10)) + .connect() + .get(); + + latch2.await(); + + // Ensure the session is reused. + assertSessionReused(quicChannel2, true); + + quicChannel1.close().sync(); + quicChannel2.close().sync(); + + serverSslCompletionEventLatch.await(); + clientSslCompletionEventLatch.await(); + } finally { + server.close().sync(); + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(executor); + } + } + + private static void assertSessionReused(QuicChannel channel, boolean reused) throws Exception { + QuicheQuicSslEngine engine = (QuicheQuicSslEngine) channel.sslEngine(); + while (engine.getHandshakeStatus() != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) { + // Let's wait a bit and re-check if the handshake is done. + Thread.sleep(50); + } + assertEquals(reused, engine.isSessionReused()); + } + + private static final class BytesCountingHandler extends ChannelInboundHandlerAdapter { + private final CountDownLatch latch; + private final int numBytes; + private int bytes; + + BytesCountingHandler(CountDownLatch latch, int numBytes) { + this.latch = latch; + this.numBytes = numBytes; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ByteBuf buffer = (ByteBuf) msg; + bytes += buffer.readableBytes(); + ctx.writeAndFlush(buffer); + if (bytes == numBytes) { + latch.countDown(); + } + } + } + + private static final class ChannelStateVerifyHandler extends QuicChannelValidationHandler { + @Override + public void channelActive(ChannelHandlerContext ctx) { + ctx.fireChannelActive(); + fail(); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + ctx.fireChannelInactive(); + fail(); + } + } + + private static final class ChannelActiveVerifyHandler extends QuicChannelValidationHandler { + private final BlockingQueue states = new LinkedBlockingQueue<>(); + private volatile QuicConnectionAddress localAddress; + private volatile QuicConnectionAddress remoteAddress; + + @Override + public void channelRegistered(ChannelHandlerContext ctx) { + ctx.fireChannelRegistered(); + states.add(0); + } + + @Override + public void channelUnregistered(ChannelHandlerContext ctx) { + ctx.fireChannelUnregistered(); + states.add(3); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + localAddress = (QuicConnectionAddress) ctx.channel().localAddress(); + remoteAddress = (QuicConnectionAddress) ctx.channel().remoteAddress(); + ctx.fireChannelActive(); + states.add(1); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + ctx.fireChannelInactive(); + states.add(2); + } + + void assertState() throws Throwable { + // Check that we receive the different events in the correct order. + for (long i = 0; i < 4; i++) { + assertEquals(i, (int) states.take()); + } + assertNull(states.poll()); + super.assertState(); + } + + QuicConnectionAddress localAddress() { + return localAddress; + } + + QuicConnectionAddress remoteAddress() { + return remoteAddress; + } + } + + private abstract static class TestX509ExtendedTrustManager extends X509ExtendedTrustManager { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) + throws CertificateException { + // NOOP + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) + throws CertificateException { + // NOOP + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) + throws CertificateException { + // NOOP + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) + throws CertificateException { + // NOOP + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + // NOOP + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + // NOOP + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + } + + private abstract static class TestX509TrustManager implements X509TrustManager { + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + // NOOP + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + // NOOP + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicChannelDatagramTest.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicChannelDatagramTest.java new file mode 100644 index 0000000..0b6042d --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicChannelDatagramTest.java @@ -0,0 +1,305 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelOption; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.concurrent.ImmediateEventExecutor; +import io.netty.util.concurrent.Promise; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.net.InetSocketAddress; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class QuicChannelDatagramTest extends AbstractQuicTest { + + private static final Random random = new Random(); + static final byte[] data = new byte[512]; + + static { + random.nextBytes(data); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testDatagramFlushInChannelRead(Executor executor) throws Throwable { + testDatagram(executor, false); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testDatagramFlushInChannelReadComplete(Executor executor) throws Throwable { + testDatagram(executor, true); + } + + private void testDatagram(Executor executor, boolean flushInReadComplete) throws Throwable { + AtomicReference serverEventRef = new AtomicReference<>(); + + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler() { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof ByteBuf) { + final ChannelFuture future; + if (!flushInReadComplete) { + future = ctx.writeAndFlush(msg); + } else { + future = ctx.write(msg); + } + future.addListener(ChannelFutureListener.CLOSE); + } else { + ctx.fireChannelRead(msg); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + if (flushInReadComplete) { + ctx.flush(); + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof QuicDatagramExtensionEvent) { + serverEventRef.set((QuicDatagramExtensionEvent) evt); + } + super.userEventTriggered(ctx, evt); + } + }; + Channel server = QuicTestUtils.newServer(QuicTestUtils.newQuicServerBuilder(executor) + .datagram(10, 10), + InsecureQuicTokenHandler.INSTANCE, serverHandler , new ChannelInboundHandlerAdapter()); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + + Promise receivedBuffer = ImmediateEventExecutor.INSTANCE.newPromise(); + AtomicReference clientEventRef = new AtomicReference<>(); + Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor) + .datagram(10, 10)); + + QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (!receivedBuffer.trySuccess((ByteBuf) msg)) { + ReferenceCountUtil.release(msg); + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof QuicDatagramExtensionEvent) { + clientEventRef.set((QuicDatagramExtensionEvent) evt); + } + super.userEventTriggered(ctx, evt); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + receivedBuffer.tryFailure(cause); + super.exceptionCaught(ctx, cause); + } + }; + + try { + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .remoteAddress(address) + .connect() + .get(); + quicChannel.writeAndFlush(Unpooled.copiedBuffer(data)).sync(); + + ByteBuf buffer = receivedBuffer.get(); + ByteBuf expected = Unpooled.wrappedBuffer(data); + assertEquals(expected, buffer); + buffer.release(); + expected.release(); + + assertNotEquals(0, serverEventRef.get().maxLength()); + assertNotEquals(0, clientEventRef.get().maxLength()); + + quicChannel.close().sync(); + + serverHandler.assertState(); + clientHandler.assertState(); + } finally { + server.close().sync(); + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(executor); + } + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testDatagramNoAutoReadMaxMessagesPerRead1(Executor executor) throws Throwable { + testDatagramNoAutoRead(executor, 1, false); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testDatagramNoAutoReadMaxMessagesPerRead3(Executor executor) throws Throwable { + testDatagramNoAutoRead(executor, 3, false); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testDatagramNoAutoReadMaxMessagesPerRead1OutSideEventLoop(Executor executor) throws Throwable { + testDatagramNoAutoRead(executor, 1, true); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testDatagramNoAutoReadMaxMessagesPerRead3OutSideEventLoop(Executor executor) throws Throwable { + testDatagramNoAutoRead(executor, 3, true); + } + + private void testDatagramNoAutoRead(Executor executor, int maxMessagesPerRead, boolean readLater) throws Throwable { + Promise serverPromise = ImmediateEventExecutor.INSTANCE.newPromise(); + Promise clientPromise = ImmediateEventExecutor.INSTANCE.newPromise(); + + int numDatagrams = 5; + AtomicInteger serverReadCount = new AtomicInteger(); + CountDownLatch latch = new CountDownLatch(numDatagrams); + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler() { + private int readPerLoop; + + @Override + public void channelActive(ChannelHandlerContext ctx) { + ctx.read(); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof ByteBuf) { + readPerLoop++; + + ctx.writeAndFlush(msg).addListener(future -> { + if (future.isSuccess()) { + latch.countDown(); + } + }); + if (serverReadCount.incrementAndGet() == numDatagrams) { + serverPromise.trySuccess(null); + } + } else { + ctx.fireChannelRead(msg); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + if (readPerLoop > maxMessagesPerRead) { + ctx.close(); + serverPromise.tryFailure(new AssertionError( + "Read more then " + maxMessagesPerRead + " time per read loop")); + return; + } + readPerLoop = 0; + if (serverReadCount.get() < numDatagrams) { + if (readLater) { + ctx.executor().execute(ctx::read); + } else { + ctx.read(); + } + } + } + }; + Channel server = QuicTestUtils.newServer(QuicTestUtils.newQuicServerBuilder(executor) + .option(ChannelOption.AUTO_READ, false) + .option(ChannelOption.MAX_MESSAGES_PER_READ, maxMessagesPerRead) + .datagram(10, 10), + InsecureQuicTokenHandler.INSTANCE, serverHandler, new ChannelInboundHandlerAdapter()); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + + Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor) + .datagram(10, 10)); + AtomicInteger clientReadCount = new AtomicInteger(); + QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler() { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof ByteBuf) { + + if (clientReadCount.incrementAndGet() == numDatagrams) { + if (!clientPromise.trySuccess((ByteBuf) msg)) { + ReferenceCountUtil.release(msg); + } + } else { + ReferenceCountUtil.release(msg); + } + } else { + ctx.fireChannelRead(msg); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + clientPromise.tryFailure(cause); + } + }; + try { + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .remoteAddress(address) + .connect() + .get(); + for (int i = 0; i < numDatagrams; i++) { + quicChannel.writeAndFlush(Unpooled.copiedBuffer(data)).sync(); + // Let's add some sleep in between as this is UDP so we may loose some data otherwise. + Thread.sleep(50); + } + assertTrue(serverPromise.await(3000), "Server received: " + serverReadCount.get() + + ", Client received: " + clientReadCount.get()); + serverPromise.sync(); + + assertTrue(clientPromise.await(3000), "Server received: " + serverReadCount.get() + + ", Client received: " + clientReadCount.get()); + ByteBuf buffer = clientPromise.get(); + ByteBuf expected = Unpooled.wrappedBuffer(data); + assertEquals(expected, buffer); + buffer.release(); + expected.release(); + + quicChannel.close().sync(); + + serverHandler.assertState(); + clientHandler.assertState(); + } finally { + server.close().sync(); + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(executor); + } + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicChannelEchoTest.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicChannelEchoTest.java new file mode 100644 index 0000000..f7b36d1 --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicChannelEchoTest.java @@ -0,0 +1,439 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.AbstractByteBufAllocator; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.buffer.UnpooledByteBufAllocator; +import io.netty.buffer.UnpooledDirectByteBuf; +import io.netty.buffer.UnpooledHeapByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelOption; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.ImmediateExecutor; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class QuicChannelEchoTest extends AbstractQuicTest { + + private static final Random random = new Random(); + static final byte[] data = new byte[1048576]; + + static { + random.nextBytes(data); + } + + public static Collection data() { + List config = new ArrayList<>(); + for (int a = 0; a < 2; a++) { + for (int b = 0; b < 2; b++) { + for (int c = 0; c < 2; c++) { + config.add(new Object[] { a == 0, b == 0, c == 0 }); + } + } + } + return config; + } + + private void setAllocator(Channel channel, ByteBufAllocator allocator) { + channel.config().setAllocator(allocator); + } + + private ByteBufAllocator getAllocator(boolean directBuffer) { + if (directBuffer) { + return new UnpooledByteBufAllocator(true); + } else { + // Force usage of heap buffers and also ensure memoryAddress() is not not supported. + return new AbstractByteBufAllocator(false) { + + @Override + public ByteBuf ioBuffer() { + return heapBuffer(); + } + + @Override + public ByteBuf ioBuffer(int initialCapacity) { + return heapBuffer(initialCapacity); + } + + @Override + public ByteBuf ioBuffer(int initialCapacity, int maxCapacity) { + return heapBuffer(initialCapacity, maxCapacity); + } + + @Override + protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) { + return new UnpooledHeapByteBuf(this, initialCapacity, maxCapacity); + } + + @Override + protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) { + return new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity); + } + + @Override + public boolean isDirectBufferPooled() { + return false; + } + }; + } + } + + @ParameterizedTest(name = + "{index}: autoRead = {0}, directBuffer = {1}, composite = {2}") + @MethodSource("data") + public void testEchoStartedFromServer(boolean autoRead, boolean directBuffer, boolean composite) throws Throwable { + ByteBufAllocator allocator = getAllocator(directBuffer); + final EchoHandler sh = new EchoHandler(true, autoRead, allocator); + final EchoHandler ch = new EchoHandler(false, autoRead, allocator); + AtomicReference> writeFutures = new AtomicReference<>(); + Channel server = QuicTestUtils.newServer(ImmediateExecutor.INSTANCE, new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + setAllocator(ctx.channel(), allocator); + ((QuicChannel) ctx.channel()).createStream(QuicStreamType.BIDIRECTIONAL, sh) + .addListener((Future future) -> { + QuicStreamChannel stream = future.getNow(); + setAllocator(stream, allocator); + List futures = writeAllData(stream, composite, allocator); + writeFutures.set(futures); + }); + + ctx.channel().config().setAutoRead(autoRead); + if (!autoRead) { + ctx.read(); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + if (!autoRead) { + ctx.read(); + } + } + }, sh); + setAllocator(server, allocator); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + Channel channel = QuicTestUtils.newClient(ImmediateExecutor.INSTANCE); + QuicChannel quicChannel = null; + try { + quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + if (!autoRead) { + ctx.read(); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + if (!autoRead) { + ctx.read(); + } + } + }) + .streamHandler(ch) + // Use the same allocator for the streams. + .streamOption(ChannelOption.ALLOCATOR, allocator) + .remoteAddress(address) + .option(ChannelOption.AUTO_READ, autoRead) + .option(ChannelOption.ALLOCATOR, allocator) + .connect() + .get(); + + waitForData(ch, sh); + + for (;;) { + List futures = writeFutures.get(); + if (futures != null) { + for (ChannelFuture f: futures) { + f.sync(); + } + break; + } + + try { + Thread.sleep(50); + } catch (InterruptedException e) { + // Ignore. + } + } + waitForData(sh, ch); + + // Close underlying streams. + sh.channel.close().sync(); + ch.channel.close().sync(); + + // Close underlying quic channels + sh.channel.parent().close().sync(); + ch.channel.parent().close().sync(); + + checkForException(ch, sh); + } finally { + server.close().sync(); + QuicTestUtils.closeIfNotNull(quicChannel); + // Close the parent Datagram channel as well. + channel.close().sync(); + } + } + + @ParameterizedTest(name = + "{index}: autoRead = {0}, directBuffer = {1}, composite = {2}") + @MethodSource("data") + public void testEchoStartedFromClient(boolean autoRead, boolean directBuffer, boolean composite) throws Throwable { + ByteBufAllocator allocator = getAllocator(directBuffer); + + final EchoHandler sh = new EchoHandler(true, autoRead, allocator); + final EchoHandler ch = new EchoHandler(false, autoRead, allocator); + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + setAllocator(ctx.channel(), allocator); + ctx.channel().config().setAutoRead(autoRead); + if (!autoRead) { + ctx.read(); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + if (!autoRead) { + ctx.read(); + } + } + }; + + Channel server = QuicTestUtils.newServer(ImmediateExecutor.INSTANCE, serverHandler, sh); + setAllocator(server, allocator); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + Channel channel = QuicTestUtils.newClient(ImmediateExecutor.INSTANCE); + QuicChannel quicChannel = null; + try { + QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + if (!autoRead) { + ctx.read(); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + if (!autoRead) { + ctx.read(); + } + } + }; + quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(ch) + // Use the same allocator for the streams. + .streamOption(ChannelOption.ALLOCATOR, allocator) + .remoteAddress(address) + .option(ChannelOption.AUTO_READ, autoRead) + .option(ChannelOption.ALLOCATOR, allocator) + .connect() + .get(); + + QuicStreamChannel stream = quicChannel.createStream(QuicStreamType.BIDIRECTIONAL, ch).sync().getNow(); + setAllocator(stream, allocator); + + assertEquals(QuicStreamType.BIDIRECTIONAL, stream.type()); + assertEquals(0, stream.streamId()); + assertTrue(stream.isLocalCreated()); + + for (int i = 0; i < 5; i++) { + ch.counter = 0; + sh.counter = 0; + List futures = writeAllData(stream, composite, allocator); + + for (ChannelFuture f : futures) { + f.sync(); + } + waitForData(ch, sh); + waitForData(sh, ch); + Thread.sleep(100); + } + + // Close underlying streams. + sh.channel.close().sync(); + ch.channel.close().sync(); + + // Close underlying quic channels + sh.channel.parent().close().sync(); + ch.channel.parent().close().sync(); + checkForException(ch, sh); + + serverHandler.assertState(); + clientHandler.assertState(); + } finally { + server.close().syncUninterruptibly(); + QuicTestUtils.closeIfNotNull(quicChannel); + // Close the parent Datagram channel as well. + channel.close().sync(); + } + } + + private List writeAllData(Channel channel, boolean composite, ByteBufAllocator allocator) { + if (composite) { + CompositeByteBuf compositeByteBuf = allocator.compositeBuffer(); + for (int i = 0; i < data.length;) { + int length = Math.min(random.nextInt(1024 * 64), data.length - i); + ByteBuf buf = allocator.buffer().writeBytes(data, i, length); + compositeByteBuf.addComponent(true, buf); + i += length; + } + return Collections.singletonList(channel.writeAndFlush(compositeByteBuf)); + } else { + List futures = new ArrayList<>(); + for (int i = 0; i < data.length;) { + int length = Math.min(random.nextInt(1024 * 64), data.length - i); + ByteBuf buf = allocator.buffer().writeBytes(data, i, length); + futures.add(channel.writeAndFlush(buf)); + i += length; + } + return futures; + } + } + + private static void waitForData(EchoHandler h1, EchoHandler h2) { + while (h1.counter < data.length) { + if (h2.exception.get() != null) { + break; + } + if (h1.exception.get() != null) { + break; + } + + try { + Thread.sleep(50); + } catch (InterruptedException e) { + // Ignore. + } + } + } + + private static void checkForException(EchoHandler h1, EchoHandler h2) throws Throwable { + if (h1.exception.get() != null && !(h1.exception.get() instanceof IOException)) { + throw h1.exception.get(); + } + if (h2.exception.get() != null && !(h2.exception.get() instanceof IOException)) { + throw h2.exception.get(); + } + if (h1.exception.get() != null) { + throw h1.exception.get(); + } + if (h2.exception.get() != null) { + throw h2.exception.get(); + } + } + + private class EchoHandler extends SimpleChannelInboundHandler { + private final boolean server; + private final boolean autoRead; + private final ByteBufAllocator allocator; + volatile Channel channel; + final AtomicReference exception = new AtomicReference<>(); + volatile int counter; + + EchoHandler(boolean server, boolean autoRead, ByteBufAllocator allocator) { + this.server = server; + this.autoRead = autoRead; + this.allocator = allocator; + } + + @Override + public void channelRegistered(ChannelHandlerContext ctx) { + ctx.channel().config().setAutoRead(autoRead); + setAllocator(ctx.channel(), allocator); + ctx.fireChannelRegistered(); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + channel = ctx.channel(); + QuicStreamChannel channel = (QuicStreamChannel) ctx.channel(); + assertEquals(QuicStreamType.BIDIRECTIONAL, channel.type()); + if (channel.isLocalCreated()) { + // Server starts with 1, client with 0 + assertEquals(server ? 1 : 0, channel.streamId()); + } else { + // Server starts with 1, client with 0 + assertEquals(server ? 0 : 1, channel.streamId()); + } + if (!autoRead) { + ctx.read(); + } + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, ByteBuf in) { + byte[] actual = new byte[in.readableBytes()]; + in.readBytes(actual); + + int lastIdx = counter; + for (int i = 0; i < actual.length; i ++) { + assertEquals(data[i + lastIdx], actual[i]); + } + + if (!((QuicStreamChannel) ctx.channel()).isLocalCreated()) { + channel.write(Unpooled.wrappedBuffer(actual)); + } + + counter += actual.length; + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + try { + ctx.flush(); + } finally { + if (!autoRead) { + ctx.read(); + } + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, + Throwable cause) { + if (exception.compareAndSet(null, cause)) { + cause.printStackTrace(); + ctx.close(); + } + } + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicChannelValidationHandler.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicChannelValidationHandler.java new file mode 100644 index 0000000..a60859a --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicChannelValidationHandler.java @@ -0,0 +1,36 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; + +class QuicChannelValidationHandler extends ChannelInboundHandlerAdapter { + + private volatile Throwable cause; + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + this.cause = cause; + } + + void assertState() throws Throwable { + if (cause != null) { + throw cause; + } + } + +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicCodecBuilderTest.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicCodecBuilderTest.java new file mode 100644 index 0000000..35863f7 --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicCodecBuilderTest.java @@ -0,0 +1,100 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.channel.ChannelHandler; +import io.netty.util.concurrent.ImmediateExecutor; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.util.concurrent.Executor; +import java.util.function.Function; + +import static org.assertj.core.api.Assertions.assertThat; + +class QuicCodecBuilderTest { + + @Test + void testCopyConstructor() throws IllegalAccessException { + TestQuicCodecBuilder original = new TestQuicCodecBuilder(); + init(original); + TestQuicCodecBuilder copy = new TestQuicCodecBuilder(original); + assertThat(copy).usingRecursiveComparison().isEqualTo(original); + } + + private static void init(TestQuicCodecBuilder builder) throws IllegalAccessException { + Field[] fields = builder.getClass().getSuperclass().getDeclaredFields(); + for (Field field : fields) { + modifyField(builder, field); + } + } + + private static void modifyField(TestQuicCodecBuilder builder, Field field) throws IllegalAccessException { + field.setAccessible(true); + Class clazz = field.getType(); + if (Boolean.class == clazz) { + field.set(builder, Boolean.TRUE); + } else if (Integer.class == clazz) { + field.set(builder, Integer.MIN_VALUE); + } else if (Long.class == clazz) { + field.set(builder, Long.MIN_VALUE); + } else if (QuicCongestionControlAlgorithm.class == clazz) { + field.set(builder, QuicCongestionControlAlgorithm.CUBIC); + } else if (FlushStrategy.class == clazz) { + field.set(builder, FlushStrategy.afterNumBytes(10)); + } else if (Function.class == clazz) { + field.set(builder, Function.identity()); + } else if (boolean.class == clazz) { + field.setBoolean(builder, true); + } else if (int.class == clazz) { + field.setInt(builder, -1); + } else if (byte[].class == clazz) { + field.set(builder, new byte[16]); + } else if (Executor.class == clazz) { + field.set(builder, ImmediateExecutor.INSTANCE); + } else { + throw new IllegalArgumentException("Unknown field type " + clazz); + } + } + + private static final class TestQuicCodecBuilder extends QuicCodecBuilder { + + TestQuicCodecBuilder() { + super(true); + } + + TestQuicCodecBuilder(TestQuicCodecBuilder builder) { + super(builder); + } + + @Override + public TestQuicCodecBuilder clone() { + // no-op + return null; + } + + @Override + protected ChannelHandler build( + QuicheConfig config, + Function sslContextProvider, + Executor sslTaskExecutor, + int localConnIdLength, + FlushStrategy flushStrategy) { + // no-op + return null; + } + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicConnectionAddressTest.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicConnectionAddressTest.java new file mode 100644 index 0000000..b10ad60 --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicConnectionAddressTest.java @@ -0,0 +1,59 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.concurrent.ThreadLocalRandom; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class QuicConnectionAddressTest extends AbstractQuicTest { + + @Test + public void testNullByteArray() { + assertThrows(NullPointerException.class, () -> new QuicConnectionAddress((byte[]) null)); + } + + @Test + public void testNullByteBuffer() { + assertThrows(NullPointerException.class, () -> new QuicConnectionAddress((ByteBuffer) null)); + } + + @Test + public void testByteArrayIsCloned() { + byte[] bytes = new byte[8]; + ThreadLocalRandom.current().nextBytes(bytes); + QuicConnectionAddress address = new QuicConnectionAddress(bytes); + assertEquals(ByteBuffer.wrap(bytes), address.connId); + ThreadLocalRandom.current().nextBytes(bytes); + assertNotEquals(ByteBuffer.wrap(bytes), address.connId); + } + + @Test + public void tesByteBufferIsDuplicated() { + byte[] bytes = new byte[8]; + ThreadLocalRandom.current().nextBytes(bytes); + ByteBuffer buffer = ByteBuffer.wrap(bytes); + QuicConnectionAddress address = new QuicConnectionAddress(bytes); + assertEquals(buffer, address.connId); + buffer.position(1); + assertNotEquals(buffer, address.connId); + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicConnectionIdGeneratorTest.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicConnectionIdGeneratorTest.java new file mode 100644 index 0000000..7669f29 --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicConnectionIdGeneratorTest.java @@ -0,0 +1,91 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.concurrent.ThreadLocalRandom; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class QuicConnectionIdGeneratorTest extends AbstractQuicTest { + + @Test + public void testRandomness() { + QuicConnectionIdGenerator idGenerator = QuicConnectionIdGenerator.randomGenerator(); + ByteBuffer id = idGenerator.newId(Quiche.QUICHE_MAX_CONN_ID_LEN); + ByteBuffer id2 = idGenerator.newId(Quiche.QUICHE_MAX_CONN_ID_LEN); + assertThat(id.remaining(), greaterThan(0)); + assertThat(id2.remaining(), greaterThan(0)); + assertNotEquals(id, id2); + + id = idGenerator.newId(10); + id2 = idGenerator.newId(10); + assertEquals(10, id.remaining()); + assertEquals(10, id2.remaining()); + assertNotEquals(id, id2); + + byte[] input = new byte[1024]; + ThreadLocalRandom.current().nextBytes(input); + id = idGenerator.newId(ByteBuffer.wrap(input), 10); + id2 = idGenerator.newId(ByteBuffer.wrap(input), 10); + assertEquals(10, id.remaining()); + assertEquals(10, id2.remaining()); + assertNotEquals(id, id2); + } + + @Test + public void testThrowsIfInputTooBig() { + QuicConnectionIdGenerator idGenerator = QuicConnectionIdGenerator.randomGenerator(); + assertThrows(IllegalArgumentException.class, () -> idGenerator.newId(Integer.MAX_VALUE)); + } + + @Test + public void testThrowsIfInputTooBig2() { + QuicConnectionIdGenerator idGenerator = QuicConnectionIdGenerator.randomGenerator(); + assertThrows(IllegalArgumentException.class, () -> + idGenerator.newId(ByteBuffer.wrap(new byte[8]), Integer.MAX_VALUE)); + } + + @Test + public void testSignIdGenerator() { + QuicConnectionIdGenerator idGenerator = QuicConnectionIdGenerator.signGenerator(); + + byte[] input = new byte[1024]; + byte[] input2 = new byte[1024]; + ThreadLocalRandom.current().nextBytes(input); + ThreadLocalRandom.current().nextBytes(input2); + ByteBuffer id = idGenerator.newId(ByteBuffer.wrap(input), 10); + ByteBuffer id2 = idGenerator.newId(ByteBuffer.wrap(input), 10); + ByteBuffer id3 = idGenerator.newId(ByteBuffer.wrap(input2), 10); + assertEquals(10, id.remaining()); + assertEquals(10, id2.remaining()); + assertEquals(10, id3.remaining()); + assertEquals(id, id2); + assertNotEquals(id, id3); + + assertThrows(UnsupportedOperationException.class, () -> idGenerator.newId(10)); + assertThrows(NullPointerException.class, () -> idGenerator.newId(null, 10)); + assertThrows(IllegalArgumentException.class, () -> idGenerator.newId(ByteBuffer.wrap(new byte[0]), 10)); + assertThrows(IllegalArgumentException.class, () -> + idGenerator.newId(ByteBuffer.wrap(input), Integer.MAX_VALUE)); + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicConnectionStatsTest.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicConnectionStatsTest.java new file mode 100644 index 0000000..337eb64 --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicConnectionStatsTest.java @@ -0,0 +1,143 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.concurrent.ImmediateEventExecutor; +import io.netty.util.concurrent.Promise; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; + +public class QuicConnectionStatsTest extends AbstractQuicTest { + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testStatsAreCollected(Executor executor) throws Throwable { + Channel server = null; + Channel channel = null; + AtomicInteger counter = new AtomicInteger(); + + Promise serverActiveStats = ImmediateEventExecutor.INSTANCE.newPromise(); + Promise serverInactiveStats = ImmediateEventExecutor.INSTANCE.newPromise(); + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + collectStats(ctx, serverActiveStats); + ctx.fireChannelActive(); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + collectStats(ctx, serverInactiveStats); + ctx.fireChannelInactive(); + } + + private void collectStats(ChannelHandlerContext ctx, Promise promise) { + QuicheQuicChannel channel = (QuicheQuicChannel) ctx.channel(); + channel.collectStats(promise); + } + }; + QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler(); + try { + server = QuicTestUtils.newServer(executor, serverHandler, new ChannelInboundHandlerAdapter() { + + @Override + public void channelActive(ChannelHandlerContext ctx) { + counter.incrementAndGet(); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + // Let's just echo back the message. + ctx.writeAndFlush(msg); + } + + @Override + public boolean isSharable() { + return true; + } + }); + channel = QuicTestUtils.newClient(executor); + + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(server.localAddress()) + .connect().get(); + assertNotNull(quicChannel.collectStats().sync().getNow()); + quicChannel.createStream(QuicStreamType.BIDIRECTIONAL, new ChannelInboundHandlerAdapter() { + private final int bufferSize = 8; + private int received; + + @Override + public void channelActive(ChannelHandlerContext ctx) { + ctx.writeAndFlush(Unpooled.buffer().writeZero(bufferSize)); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ByteBuf buffer = (ByteBuf) msg; + received += buffer.readableBytes(); + buffer.release(); + if (received == bufferSize) { + ctx.close().addListener((ChannelFuture future) -> { + // Close the underlying QuicChannel as well. + future.channel().parent().close(); + }); + } + } + }).sync(); + + // Wait until closure + quicChannel.closeFuture().sync(); + assertStats(quicChannel.collectStats().sync().getNow()); + assertNotNull(serverActiveStats.sync().getNow()); + assertStats(serverInactiveStats.sync().getNow()); + assertEquals(1, counter.get()); + + serverHandler.assertState(); + clientHandler.assertState(); + } finally { + QuicTestUtils.closeIfNotNull(channel); + QuicTestUtils.closeIfNotNull(server); + + shutdown(executor); + } + } + + private static void assertStats(QuicConnectionStats stats) { + assertNotNull(stats); + assertThat(stats.lost(), greaterThanOrEqualTo(0L)); + assertThat(stats.recv(), greaterThan(0L)); + assertThat(stats.sent(), greaterThan(0L)); + assertThat(stats.sentBytes(), greaterThan(0L)); + assertThat(stats.recvBytes(), greaterThan(0L)); + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicPacketTypeTest.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicPacketTypeTest.java new file mode 100644 index 0000000..9229e0b --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicPacketTypeTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class QuicPacketTypeTest extends AbstractQuicTest { + + @Test + public void testOfValidType() { + for (QuicPacketType type: QuicPacketType.values()) { + assertEquals(type, QuicPacketType.of(type.type)); + } + } + + @Test + public void testOfInvalidType() { + assertThrows(IllegalArgumentException.class, () -> QuicPacketType.of((byte) -1)); + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicReadableTest.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicReadableTest.java new file mode 100644 index 0000000..d2ca570 --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicReadableTest.java @@ -0,0 +1,138 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +public class QuicReadableTest extends AbstractQuicTest { + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testCorrectlyHandleReadableStreams(Executor executor) throws Throwable { + int numOfStreams = 256; + int readStreams = numOfStreams / 2; + // We do write longs. + int expectedDataRead = readStreams * Long.BYTES; + final CountDownLatch latch = new CountDownLatch(numOfStreams); + final AtomicInteger bytesRead = new AtomicInteger(); + final AtomicReference serverErrorRef = new AtomicReference<>(); + final AtomicReference clientErrorRef = new AtomicReference<>(); + + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler(); + Channel server = QuicTestUtils.newServer( + QuicTestUtils.newQuicServerBuilder(executor).initialMaxStreamsBidirectional(5000), + InsecureQuicTokenHandler.INSTANCE, + serverHandler, new ChannelInboundHandlerAdapter() { + private int counter; + @Override + public void channelRegistered(ChannelHandlerContext ctx) { + // Ensure we dont read from the streams so all of these will be reported as readable + ctx.channel().config().setAutoRead(false); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + counter++; + latch.countDown(); + if (counter > readStreams) { + // Now set it to readable again for some channels + ctx.channel().config().setAutoRead(true); + } + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ByteBuf buffer = (ByteBuf) msg; + bytesRead.addAndGet(buffer.readableBytes()); + buffer.release(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + serverErrorRef.set(cause); + } + + @Override + public boolean isSharable() { + return true; + } + }); + Channel channel = QuicTestUtils.newClient(executor); + QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler(); + ByteBuf data = Unpooled.directBuffer().writeLong(8); + try { + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(server.localAddress()) + .connect() + .get(); + + List streams = new ArrayList<>(); + for (int i = 0; i < numOfStreams; i++) { + QuicStreamChannel stream = quicChannel.createStream( + QuicStreamType.BIDIRECTIONAL, new ChannelInboundHandlerAdapter() { + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + clientErrorRef.set(cause); + } + }).get(); + streams.add(stream.writeAndFlush(data.retainedSlice()).sync().channel()); + } + latch.await(); + while (bytesRead.get() < expectedDataRead) { + Thread.sleep(50); + } + for (Channel stream: streams) { + stream.close().sync(); + } + quicChannel.close().sync(); + + throwIfNotNull(serverErrorRef); + throwIfNotNull(clientErrorRef); + + serverHandler.assertState(); + clientHandler.assertState(); + } finally { + data.release(); + server.close().sync(); + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(executor); + } + } + + private static void throwIfNotNull(AtomicReference errorRef) throws Throwable { + Throwable cause = errorRef.get(); + if (cause != null) { + throw cause; + } + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicSslContextTest.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicSslContextTest.java new file mode 100644 index 0000000..113a628 --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicSslContextTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.handler.ssl.SslContext; +import io.netty.util.internal.EmptyArrays; +import org.junit.jupiter.api.Test; + +import javax.net.ssl.SSLSessionContext; +import javax.net.ssl.X509ExtendedKeyManager; + +import java.net.Socket; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class QuicSslContextTest { + + @Test + public void testSessionContextSettingsForClient() { + testSessionContextSettings(QuicSslContextBuilder.forClient(), 20, 50); + } + + @Test + public void testSessionContextSettingsForServer() { + testSessionContextSettings(QuicSslContextBuilder.forServer(new X509ExtendedKeyManager() { + @Override + public String[] getClientAliases(String keyType, Principal[] issuers) { + return EmptyArrays.EMPTY_STRINGS; + } + + @Override + public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { + return null; + } + + @Override + public String[] getServerAliases(String keyType, Principal[] issuers) { + return EmptyArrays.EMPTY_STRINGS; + } + + @Override + public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { + return null; + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + return new X509Certificate[0]; + } + + @Override + public PrivateKey getPrivateKey(String alias) { + return null; + } + }, null), 20, 50); + } + + private void testSessionContextSettings(QuicSslContextBuilder builder, int size, int timeout) { + SslContext context = builder.sessionCacheSize(size).sessionTimeout(timeout).build(); + assertEquals(size, context.sessionCacheSize()); + assertEquals(timeout, context.sessionTimeout()); + SSLSessionContext sessionContext = context.sessionContext(); + assertEquals(size, sessionContext.getSessionCacheSize()); + assertEquals(timeout, sessionContext.getSessionTimeout()); + + int newSize = size / 2; + sessionContext.setSessionCacheSize(newSize); + assertEquals(newSize, context.sessionCacheSize()); + + int newTimeout = timeout / 2; + sessionContext.setSessionTimeout(newTimeout); + assertEquals(newTimeout, sessionContext.getSessionTimeout()); + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamChannelCloseTest.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamChannelCloseTest.java new file mode 100644 index 0000000..85b0553 --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamChannelCloseTest.java @@ -0,0 +1,304 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.socket.ChannelInputShutdownReadComplete; +import io.netty.channel.socket.ChannelOutputShutdownException; +import io.netty.util.ReferenceCountUtil; + +import io.netty.util.concurrent.ImmediateEventExecutor; +import io.netty.util.concurrent.Promise; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.nio.channels.ClosedChannelException; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +public class QuicStreamChannelCloseTest extends AbstractQuicTest { + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testCloseFromServerWhileInActiveUnidirectional(Executor executor) throws Throwable { + testCloseFromServerWhileInActive(executor, QuicStreamType.UNIDIRECTIONAL, false); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testCloseFromServerWhileInActiveBidirectional(Executor executor) throws Throwable { + testCloseFromServerWhileInActive(executor, QuicStreamType.BIDIRECTIONAL, false); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testHalfCloseFromServerWhileInActiveUnidirectional(Executor executor) throws Throwable { + testCloseFromServerWhileInActive(executor, QuicStreamType.UNIDIRECTIONAL, true); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testHalfCloseFromServerWhileInActiveBidirectional(Executor executor) throws Throwable { + testCloseFromServerWhileInActive(executor, QuicStreamType.BIDIRECTIONAL, true); + } + + private static void testCloseFromServerWhileInActive(Executor executor, QuicStreamType type, + boolean halfClose) throws Throwable { + Channel server = null; + Channel channel = null; + try { + final Promise streamPromise = ImmediateEventExecutor.INSTANCE.newPromise(); + QuicChannelValidationHandler serverHandler = new StreamCreationHandler(type, halfClose, streamPromise); + server = QuicTestUtils.newServer(executor, serverHandler, + new ChannelInboundHandlerAdapter()); + channel = QuicTestUtils.newClient(executor); + + QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler(); + + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(new StreamHandler()) + .remoteAddress(server.localAddress()) + .connect() + .get(); + + Channel streamChannel = streamPromise.get(); + + // Wait for the steam to close. It needs to happen before the 5-second connection idle timeout. + streamChannel.closeFuture().get(3000, TimeUnit.MILLISECONDS); + + streamChannel.parent().close(); + + // Wait till the client was closed + quicChannel.closeFuture().sync(); + + serverHandler.assertState(); + clientHandler.assertState(); + } finally { + QuicTestUtils.closeIfNotNull(channel); + QuicTestUtils.closeIfNotNull(server); + + shutdown(executor); + } + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testCloseFromClientWhileInActiveUnidirectional(Executor executor) throws Throwable { + testCloseFromClientWhileInActive(executor, QuicStreamType.UNIDIRECTIONAL, false); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testCloseFromClientWhileInActiveBidirectional(Executor executor) throws Throwable { + testCloseFromClientWhileInActive(executor, QuicStreamType.BIDIRECTIONAL, false); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testHalfCloseFromClientWhileInActiveUnidirectional(Executor executor) throws Throwable { + testCloseFromClientWhileInActive(executor, QuicStreamType.UNIDIRECTIONAL, true); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testHalfCloseFromClientWhileInActiveBidirectional(Executor executor) throws Throwable { + testCloseFromClientWhileInActive(executor, QuicStreamType.BIDIRECTIONAL, true); + } + + private static void testCloseFromClientWhileInActive(Executor executor, QuicStreamType type, + boolean halfClose) throws Throwable { + Channel server = null; + Channel channel = null; + try { + final Promise streamPromise = ImmediateEventExecutor.INSTANCE.newPromise(); + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler(); + server = QuicTestUtils.newServer(executor, serverHandler, new StreamHandler()); + channel = QuicTestUtils.newClient(executor); + + StreamCreationHandler creationHandler = new StreamCreationHandler(type, halfClose, streamPromise); + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(creationHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(server.localAddress()) + .connect() + .get(); + + Channel streamChannel = streamPromise.get(); + + // Wait for the steam to close. It needs to happen before the 5-second connection idle timeout. + streamChannel.closeFuture().get(3000, TimeUnit.MILLISECONDS); + + streamChannel.parent().close(); + + // Wait till the client was closed + quicChannel.closeFuture().sync(); + + serverHandler.assertState(); + creationHandler.assertState(); + } finally { + QuicTestUtils.closeIfNotNull(channel); + QuicTestUtils.closeIfNotNull(server); + + shutdown(executor); + } + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testWriteToUnidirectionalAfterShutdownOutput(Executor executor) throws Throwable { + testWriteAfterClosedOrShutdown(executor, QuicStreamType.UNIDIRECTIONAL, true); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testWriteToBidirectionalAfterShutdownOutput(Executor executor) throws Throwable { + testWriteAfterClosedOrShutdown(executor, QuicStreamType.BIDIRECTIONAL, true); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testWriteToUnidirectionalAfterClose(Executor executor) throws Throwable { + testWriteAfterClosedOrShutdown(executor, QuicStreamType.UNIDIRECTIONAL, false); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testWriteToBidirectionalAfterClose(Executor executor) throws Throwable { + testWriteAfterClosedOrShutdown(executor, QuicStreamType.BIDIRECTIONAL, false); + } + + private static void testWriteAfterClosedOrShutdown(Executor executor, QuicStreamType type, + boolean halfClose) throws Throwable { + Channel server = null; + Channel channel = null; + try { + final Promise streamPromise = ImmediateEventExecutor.INSTANCE.newPromise(); + server = QuicTestUtils.newServer(executor, new ChannelInboundHandlerAdapter(), new StreamHandler()); + channel = QuicTestUtils.newClient(executor); + + StreamCreationAndTearDownHandler creationHandler = + new StreamCreationAndTearDownHandler(type, halfClose, streamPromise); + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(creationHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(server.localAddress()) + .connect() + .get(); + + // ChannelOutputShutdownException should only be used when its a BIDIRECTIONAL channel and half-closure + // is used. + Class causeClass = + halfClose && type != QuicStreamType.UNIDIRECTIONAL ? + ChannelOutputShutdownException.class : ClosedChannelException.class; + assertInstanceOf(causeClass, streamPromise.await().cause()); + quicChannel.close().sync(); + + // Wait till the client was closed + quicChannel.closeFuture().sync(); + creationHandler.assertState(); + } finally { + QuicTestUtils.closeIfNotNull(channel); + QuicTestUtils.closeIfNotNull(server); + + shutdown(executor); + } + } + + private static final class StreamCreationAndTearDownHandler extends QuicChannelValidationHandler { + private final QuicStreamType type; + private final boolean halfClose; + private final Promise streamPromise; + + StreamCreationAndTearDownHandler(QuicStreamType type, boolean halfClose, Promise streamPromise) { + this.type = type; + this.halfClose = halfClose; + this.streamPromise = streamPromise; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + QuicChannel channel = (QuicChannel) ctx.channel(); + channel.createStream(type, new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + final ChannelFuture future; + if (halfClose) { + future = ((QuicStreamChannel) ctx.channel()).shutdownOutput(); + } else { + future = ctx.channel().close(); + } + future.addListener(f -> { + ctx.channel().writeAndFlush("Unsupported message").addListener(wf -> { + streamPromise.setFailure(wf.cause()); + }); + }); + } + }); + } + } + + private static final class StreamCreationHandler extends QuicChannelValidationHandler { + private final QuicStreamType type; + private final boolean halfClose; + private final Promise streamPromise; + + StreamCreationHandler(QuicStreamType type, boolean halfClose, Promise streamPromise) { + this.type = type; + this.halfClose = halfClose; + this.streamPromise = streamPromise; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + QuicChannel channel = (QuicChannel) ctx.channel(); + channel.createStream(type, new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + streamPromise.trySuccess(ctx.channel()); + // Do the write and close the channel + ctx.writeAndFlush(Unpooled.buffer().writeZero(8)) + .addListener(halfClose + ? QuicStreamChannel.SHUTDOWN_OUTPUT + : ChannelFutureListener.CLOSE); + } + }); + } + } + + private static final class StreamHandler extends ChannelInboundHandlerAdapter { + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt == ChannelInputShutdownReadComplete.INSTANCE) { + // Received a FIN + ctx.close(); + } + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ReferenceCountUtil.release(msg); + } + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamChannelCreationTest.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamChannelCreationTest.java new file mode 100644 index 0000000..6f119d2 --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamChannelCreationTest.java @@ -0,0 +1,132 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelOption; +import io.netty.util.AttributeKey; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.net.InetSocketAddress; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class QuicStreamChannelCreationTest extends AbstractQuicTest { + + private static final AttributeKey ATTRIBUTE_KEY = AttributeKey.newInstance("testKey"); + private static final String ATTRIBUTE_VALUE = "Test"; + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testCreateStream(Executor executor) throws Throwable { + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler(); + Channel server = QuicTestUtils.newServer(executor, serverHandler, + new ChannelInboundHandlerAdapter()); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + Channel channel = QuicTestUtils.newClient(executor); + QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler(); + + try { + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(address) + .connect() + .get(); + CountDownLatch latch = new CountDownLatch(1); + QuicStreamChannel stream = quicChannel.createStream(QuicStreamType.UNIDIRECTIONAL, + new ChannelInboundHandlerAdapter() { + @Override + public void channelRegistered(ChannelHandlerContext ctx) { + assertQuicStreamChannel((QuicStreamChannel) ctx.channel(), + QuicStreamType.UNIDIRECTIONAL, Boolean.TRUE, null); + latch.countDown(); + } + }).sync().get(); + assertQuicStreamChannel(stream, QuicStreamType.UNIDIRECTIONAL, Boolean.TRUE, null); + latch.await(); + stream.close().sync(); + quicChannel.close().sync(); + + serverHandler.assertState(); + clientHandler.assertState(); + } finally { + server.close().sync(); + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(executor); + } + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testCreateStreamViaBootstrap(Executor executor) throws Throwable { + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler(); + Channel server = QuicTestUtils.newServer(executor, serverHandler, + new ChannelInboundHandlerAdapter()); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + Channel channel = QuicTestUtils.newClient(executor); + QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler(); + + try { + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(address) + .connect() + .get(); + CountDownLatch latch = new CountDownLatch(1); + QuicStreamChannel stream = quicChannel.newStreamBootstrap() + .type(QuicStreamType.UNIDIRECTIONAL) + .attr(ATTRIBUTE_KEY, ATTRIBUTE_VALUE) + .option(ChannelOption.AUTO_READ, Boolean.FALSE) + .handler(new ChannelInboundHandlerAdapter() { + @Override + public void channelRegistered(ChannelHandlerContext ctx) { + assertQuicStreamChannel((QuicStreamChannel) ctx.channel(), + QuicStreamType.UNIDIRECTIONAL, Boolean.FALSE, ATTRIBUTE_VALUE); + latch.countDown(); + } + }).create().sync().get(); + assertQuicStreamChannel(stream, QuicStreamType.UNIDIRECTIONAL, Boolean.FALSE, ATTRIBUTE_VALUE); + latch.await(); + stream.close().sync(); + quicChannel.close().sync(); + + serverHandler.assertState(); + clientHandler.assertState(); + } finally { + server.close().syncUninterruptibly(); + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(executor); + } + } + + private static void assertQuicStreamChannel(QuicStreamChannel channel, QuicStreamType expectedType, + Boolean expectedAutoRead, String expectedAttribute) { + assertEquals(expectedType, channel.type()); + assertEquals(expectedAutoRead, channel.config().getOption(ChannelOption.AUTO_READ)); + assertEquals(expectedAttribute, channel.attr(ATTRIBUTE_KEY).get()); + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamFrameTest.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamFrameTest.java new file mode 100644 index 0000000..e22afc2 --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamFrameTest.java @@ -0,0 +1,143 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.socket.ChannelInputShutdownReadComplete; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +public class QuicStreamFrameTest extends AbstractQuicTest { + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testCloseHalfClosureUnidirectional(Executor executor) throws Throwable { + testCloseHalfClosure(executor, QuicStreamType.UNIDIRECTIONAL); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testCloseHalfClosureBidirectional(Executor executor) throws Throwable { + testCloseHalfClosure(executor, QuicStreamType.BIDIRECTIONAL); + } + + private static void testCloseHalfClosure(Executor executor, QuicStreamType type) throws Throwable { + Channel server = null; + Channel channel = null; + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler(); + QuicChannelValidationHandler clientHandler = new StreamCreationHandler(type); + try { + StreamHandler handler = new StreamHandler(); + server = QuicTestUtils.newServer(executor, serverHandler, handler); + channel = QuicTestUtils.newClient(executor); + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(server.localAddress()) + .connect() + .get(); + + handler.assertSequence(); + quicChannel.closeFuture().sync(); + + serverHandler.assertState(); + clientHandler.assertState(); + } finally { + QuicTestUtils.closeIfNotNull(channel); + QuicTestUtils.closeIfNotNull(server); + + shutdown(executor); + } + } + + private static final class StreamCreationHandler extends QuicChannelValidationHandler { + private final QuicStreamType type; + + StreamCreationHandler(QuicStreamType type) { + this.type = type; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + QuicChannel channel = (QuicChannel) ctx.channel(); + channel.createStream(type, new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + // Do the write and close the channel + ctx.writeAndFlush(Unpooled.buffer().writeZero(8)) + .addListener(QuicStreamChannel.SHUTDOWN_OUTPUT); + } + }); + } + } + + private static final class StreamHandler extends ChannelInboundHandlerAdapter { + private final BlockingQueue queue = new LinkedBlockingQueue<>(); + + @Override + public void channelRegistered(ChannelHandlerContext ctx) { + ctx.channel().config().setOption(QuicChannelOption.READ_FRAMES, true); + queue.add(0); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + queue.add(3); + // Close the QUIC channel as well. + ctx.channel().parent().close(); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt == ChannelInputShutdownReadComplete.INSTANCE) { + queue.add(2); + if (((QuicStreamChannel) ctx.channel()).type() == QuicStreamType.BIDIRECTIONAL) { + // Let's write back a fin which will also close the channel and so call channelInactive(...) + ctx.writeAndFlush(new DefaultQuicStreamFrame(Unpooled.EMPTY_BUFFER, true)); + } + ctx.channel().parent().close(); + } + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + QuicStreamFrame frame = (QuicStreamFrame) msg; + if (frame.hasFin()) { + queue.add(1); + } + frame.release(); + } + + void assertSequence() throws Exception { + assertEquals(0, (int) queue.take()); + assertEquals(1, (int) queue.take()); + assertEquals(2, (int) queue.take()); + assertEquals(3, (int) queue.take()); + assertTrue(queue.isEmpty()); + } + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamHalfClosureTest.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamHalfClosureTest.java new file mode 100644 index 0000000..62ad30a --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamHalfClosureTest.java @@ -0,0 +1,155 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.socket.ChannelInputShutdownEvent; +import io.netty.channel.socket.ChannelInputShutdownReadComplete; +import io.netty.util.ReferenceCountUtil; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class QuicStreamHalfClosureTest extends AbstractQuicTest { + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testCloseHalfClosureUnidirectional(Executor executor) throws Throwable { + testCloseHalfClosure(executor, QuicStreamType.UNIDIRECTIONAL); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testCloseHalfClosureBidirectional(Executor executor) throws Throwable { + testCloseHalfClosure(executor, QuicStreamType.BIDIRECTIONAL); + } + + private static void testCloseHalfClosure(Executor executor, QuicStreamType type) throws Throwable { + Channel server = null; + Channel channel = null; + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler(); + QuicChannelValidationHandler clientHandler = new StreamCreationHandler(type); + try { + StreamHandler handler = new StreamHandler(); + server = QuicTestUtils.newServer(executor, serverHandler, handler); + channel = QuicTestUtils.newClient(executor); + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(server.localAddress()) + .connect() + .get(); + + handler.assertSequence(); + quicChannel.closeFuture().sync(); + + serverHandler.assertState(); + clientHandler.assertState(); + } finally { + QuicTestUtils.closeIfNotNull(channel); + QuicTestUtils.closeIfNotNull(server); + + shutdown(executor); + } + } + + private static final class StreamCreationHandler extends QuicChannelValidationHandler { + private final QuicStreamType type; + + StreamCreationHandler(QuicStreamType type) { + this.type = type; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + QuicChannel channel = (QuicChannel) ctx.channel(); + channel.createStream(type, new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + // Do the write and close the channel + ctx.writeAndFlush(Unpooled.buffer().writeZero(8)) + .addListener(ChannelFutureListener.CLOSE); + } + }); + } + } + + private static final class StreamHandler extends ChannelInboundHandlerAdapter { + private final BlockingQueue queue = new LinkedBlockingQueue<>(); + + @Override + public void channelRegistered(ChannelHandlerContext ctx) { + queue.add(0); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + queue.add(5); + // Close the QUIC channel as well. + ctx.channel().parent().close(); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ReferenceCountUtil.release(msg); + if (((QuicStreamChannel) ctx.channel()).isInputShutdown()) { + queue.add(1); + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt == ChannelInputShutdownEvent.INSTANCE) { + addIsShutdown(ctx); + queue.add(3); + } else if (evt == ChannelInputShutdownReadComplete.INSTANCE) { + queue.add(4); + ctx.close(); + } + } + + private void addIsShutdown(ChannelHandlerContext ctx) { + if (((QuicStreamChannel) ctx.channel()).isInputShutdown()) { + queue.add(2); + } + } + + void assertSequence() throws Exception { + assertEquals(0, (int) queue.take()); + int value = queue.take(); + if (value == 1) { + // If we did see the value of 1 it should be followed by 2 directly. + assertEquals(2, (int) queue.take()); + } else { + assertEquals(2, value); + } + assertEquals(3, (int) queue.take()); + assertEquals(4, (int) queue.take()); + assertEquals(5, (int) queue.take()); + assertTrue(queue.isEmpty()); + } + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamIdGeneratorTest.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamIdGeneratorTest.java new file mode 100644 index 0000000..1c4e32e --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamIdGeneratorTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class QuicStreamIdGeneratorTest extends AbstractQuicTest { + + @Test + public void testServerStreamIds() { + QuicStreamIdGenerator generator = new QuicStreamIdGenerator(true); + assertEquals(1, generator.nextStreamId(true)); + assertEquals(5, generator.nextStreamId(true)); + assertEquals(3, generator.nextStreamId(false)); + assertEquals(9, generator.nextStreamId(true)); + assertEquals(7, generator.nextStreamId(false)); + } + + @Test + public void testClientStreamIds() { + QuicStreamIdGenerator generator = new QuicStreamIdGenerator(false); + assertEquals(0, generator.nextStreamId(true)); + assertEquals(4, generator.nextStreamId(true)); + assertEquals(2, generator.nextStreamId(false)); + assertEquals(8, generator.nextStreamId(true)); + assertEquals(6, generator.nextStreamId(false)); + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamLimitTest.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamLimitTest.java new file mode 100644 index 0000000..43eb8c7 --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamLimitTest.java @@ -0,0 +1,193 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.socket.ChannelInputShutdownReadComplete; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.ImmediateEventExecutor; +import io.netty.util.concurrent.Promise; +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class QuicStreamLimitTest extends AbstractQuicTest { + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testStreamLimitEnforcedWhenCreatingViaClientBidirectional(Executor executor) throws Throwable { + testStreamLimitEnforcedWhenCreatingViaClient(executor, QuicStreamType.BIDIRECTIONAL); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testStreamLimitEnforcedWhenCreatingViaClientUnidirectional(Executor executor) throws Throwable { + testStreamLimitEnforcedWhenCreatingViaClient(executor, QuicStreamType.UNIDIRECTIONAL); + } + + private static void testStreamLimitEnforcedWhenCreatingViaClient(Executor executor, QuicStreamType type) throws Throwable { + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler(); + Channel server = QuicTestUtils.newServer( + QuicTestUtils.newQuicServerBuilder(executor).initialMaxStreamsBidirectional(1) + .initialMaxStreamsUnidirectional(1), + InsecureQuicTokenHandler.INSTANCE, + serverHandler, new ChannelInboundHandlerAdapter() { + @Override + public boolean isSharable() { + return true; + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt == ChannelInputShutdownReadComplete.INSTANCE) { + ctx.close(); + } + } + }); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + Channel channel = QuicTestUtils.newClient(executor); + + CountDownLatch latch = new CountDownLatch(1); + CountDownLatch latch2 = new CountDownLatch(1); + QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler() { + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof QuicStreamLimitChangedEvent) { + if (latch.getCount() == 0) { + latch2.countDown(); + } else { + latch.countDown(); + } + } + super.userEventTriggered(ctx, evt); + } + }; + try { + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(address) + .connect().get(); + latch.await(); + assertEquals(1, quicChannel.peerAllowedStreams(QuicStreamType.UNIDIRECTIONAL)); + assertEquals(1, quicChannel.peerAllowedStreams(QuicStreamType.BIDIRECTIONAL)); + QuicStreamChannel stream = quicChannel.createStream( + type, new ChannelInboundHandlerAdapter()).get(); + + assertEquals(0, quicChannel.peerAllowedStreams(type)); + + // Second stream creation should fail. + Throwable cause = quicChannel.createStream( + type, new ChannelInboundHandlerAdapter()).await().cause(); + assertThat(cause, CoreMatchers.instanceOf(IOException.class)); + stream.close().sync(); + latch2.await(); + + assertEquals(1, quicChannel.peerAllowedStreams(type)); + quicChannel.close().sync(); + + serverHandler.assertState(); + clientHandler.assertState(); + } finally { + server.close().sync(); + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(executor); + } + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testStreamLimitEnforcedWhenCreatingViaServerBidirectional(Executor executor) throws Throwable { + testStreamLimitEnforcedWhenCreatingViaServer(executor, QuicStreamType.BIDIRECTIONAL); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testStreamLimitEnforcedWhenCreatingViaServerUnidirectional(Executor executor) throws Throwable { + testStreamLimitEnforcedWhenCreatingViaServer(executor, QuicStreamType.UNIDIRECTIONAL); + } + + private static void testStreamLimitEnforcedWhenCreatingViaServer(Executor executor, QuicStreamType type) throws Throwable { + Promise streamPromise = ImmediateEventExecutor.INSTANCE.newPromise(); + Promise stream2Promise = ImmediateEventExecutor.INSTANCE.newPromise(); + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + QuicChannel channel = (QuicChannel) ctx.channel(); + channel.createStream(type, new ChannelInboundHandlerAdapter()) + .addListener((Future future) -> { + if (future.isSuccess()) { + QuicStreamChannel stream = future.getNow(); + streamPromise.setSuccess(null); + channel.createStream(type, new ChannelInboundHandlerAdapter()) + .addListener((Future f) -> { + stream.close(); + stream2Promise.setSuccess(f.cause()); + }); + } else { + streamPromise.setFailure(future.cause()); + } + }); + } + }; + Channel server = QuicTestUtils.newServer( + QuicTestUtils.newQuicServerBuilder(executor), + InsecureQuicTokenHandler.INSTANCE, + serverHandler, new ChannelInboundHandlerAdapter() { + @Override + public boolean isSharable() { + return true; + } + }); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor) + .initialMaxStreamsBidirectional(1).initialMaxStreamsUnidirectional(1)); + + QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler(); + try { + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(address) + .connect().get(); + streamPromise.sync(); + // Second stream creation should fail. + assertThat(stream2Promise.get(), CoreMatchers.instanceOf(IOException.class)); + quicChannel.close().sync(); + + serverHandler.assertState(); + clientHandler.assertState(); + } finally { + server.close().sync(); + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(executor); + } + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamShutdownTest.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamShutdownTest.java new file mode 100644 index 0000000..0817b2c --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamShutdownTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2023 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class QuicStreamShutdownTest extends AbstractQuicTest { + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testShutdownInputClosureCausesStreamStopped(Executor executor) throws Throwable { + Channel server = null; + Channel channel = null; + CountDownLatch latch = new CountDownLatch(2); + try { + server = QuicTestUtils.newServer(executor, new ChannelInboundHandlerAdapter(), new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + ChannelFutureListener futureListener = new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture channelFuture) { + Throwable cause = channelFuture.cause(); + if (cause instanceof QuicException && + ((QuicException) cause).error() == QuicError.STREAM_STOPPED) { + latch.countDown(); + } + } + }; + ByteBuf buffer = (ByteBuf) msg; + ctx.write(buffer.retainedDuplicate()).addListener(futureListener); + ctx.writeAndFlush(buffer).addListener(futureListener); + } + }); + channel = QuicTestUtils.newClient(executor); + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(new ChannelInboundHandlerAdapter()) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(server.localAddress()) + .connect() + .get(); + + QuicStreamChannel streamChannel = quicChannel.createStream(QuicStreamType.BIDIRECTIONAL, + new ChannelInboundHandlerAdapter()).sync().getNow(); + streamChannel.shutdownInput().sync(); + assertTrue(streamChannel.isInputShutdown()); + streamChannel.writeAndFlush(Unpooled.buffer().writeLong(8)).sync(); + + latch.await(); + } finally { + QuicTestUtils.closeIfNotNull(channel); + QuicTestUtils.closeIfNotNull(server); + + shutdown(executor); + } + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamTypeTest.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamTypeTest.java new file mode 100644 index 0000000..be0255f --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicStreamTypeTest.java @@ -0,0 +1,157 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.concurrent.ImmediateEventExecutor; +import io.netty.util.concurrent.Promise; +import io.netty.util.concurrent.PromiseNotifier; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.concurrent.Executor; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +public class QuicStreamTypeTest extends AbstractQuicTest { + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testUnidirectionalCreatedByClient(Executor executor) throws Throwable { + Channel server = null; + Channel channel = null; + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler(); + QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler(); + + try { + Promise serverWritePromise = ImmediateEventExecutor.INSTANCE.newPromise(); + server = QuicTestUtils.newServer(executor, serverHandler, new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + QuicStreamChannel channel = (QuicStreamChannel) ctx.channel(); + assertEquals(QuicStreamType.UNIDIRECTIONAL, channel.type()); + assertFalse(channel.isLocalCreated()); + ctx.writeAndFlush(Unpooled.buffer().writeZero(8)) + .addListener(future -> serverWritePromise.setSuccess(future.cause())); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ReferenceCountUtil.release(msg); + } + }); + + channel = QuicTestUtils.newClient(executor); + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(server.localAddress()) + .connect() + .sync() + .get(); + QuicStreamChannel streamChannel = quicChannel.createStream( + QuicStreamType.UNIDIRECTIONAL, new ChannelInboundHandlerAdapter()).get(); + // Do the write which should succeed + streamChannel.writeAndFlush(Unpooled.buffer().writeZero(8)).sync(); + + // Close stream and quic channel + streamChannel.close().sync(); + quicChannel.close().sync(); + assertThat(serverWritePromise.get(), instanceOf(UnsupportedOperationException.class)); + + serverHandler.assertState(); + clientHandler.assertState(); + } finally { + QuicTestUtils.closeIfNotNull(channel); + QuicTestUtils.closeIfNotNull(server); + } + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testUnidirectionalCreatedByServer(Executor executor) throws Throwable { + Channel server = null; + Channel channel = null; + Promise serverWritePromise = ImmediateEventExecutor.INSTANCE.newPromise(); + Promise clientWritePromise = ImmediateEventExecutor.INSTANCE.newPromise(); + + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + QuicChannel channel = (QuicChannel) ctx.channel(); + channel.createStream(QuicStreamType.UNIDIRECTIONAL, new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + // Do the write which should succeed + ctx.writeAndFlush(Unpooled.buffer().writeZero(8)) + .addListener(new PromiseNotifier<>(serverWritePromise)); + } + }); + } + }; + QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler(); + try { + server = QuicTestUtils.newServer(executor, serverHandler, new ChannelInboundHandlerAdapter()); + + channel = QuicTestUtils.newClient(executor); + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + // Do the write should fail + ctx.writeAndFlush(Unpooled.buffer().writeZero(8)) + .addListener(future -> clientWritePromise.setSuccess(future.cause())); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + // Close the QUIC channel as well. + ctx.channel().parent().close(); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ReferenceCountUtil.release(msg); + // Let's close the stream + ctx.close(); + } + }) + .remoteAddress(server.localAddress()) + .connect() + .get(); + + quicChannel.closeFuture().sync(); + assertTrue(serverWritePromise.await().isSuccess()); + assertThat(clientWritePromise.get(), instanceOf(UnsupportedOperationException.class)); + + serverHandler.assertState(); + clientHandler.assertState(); + } finally { + QuicTestUtils.closeIfNotNull(channel); + QuicTestUtils.closeIfNotNull(server); + } + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicTest.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicTest.java new file mode 100644 index 0000000..680d72c --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.channel.ChannelOption; +import io.netty.util.AttributeKey; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class QuicTest extends AbstractQuicTest { + + @Test + public void test() { + Quic.ensureAvailability(); + assertNotNull(Quiche.quiche_version()); + } + + @Test + public void testVersionSupported() { + // Only v1 should be supported. + assertFalse(Quic.isVersionSupported(0xff00_001c)); + assertFalse(Quic.isVersionSupported(0xff00_001d)); + assertFalse(Quic.isVersionSupported(0xff00_001c)); + assertTrue(Quic.isVersionSupported(0x0000_0001)); + } + + @Test + public void testToAttributesArrayDoesCopy() { + AttributeKey key = AttributeKey.valueOf(UUID.randomUUID().toString()); + String value = "testValue"; + Map, Object> attributes = new HashMap<>(); + attributes.put(key, value); + Map.Entry, Object>[] array = Quic.toAttributesArray(attributes); + assertEquals(1, array.length); + attributes.put(key, "newTestValue"); + Map.Entry, Object> entry = array[0]; + assertEquals(key, entry.getKey()); + assertEquals(value, entry.getValue()); + } + + @Test + public void testToOptionsArrayDoesCopy() { + Map, Object> options = new HashMap<>(); + options.put(ChannelOption.AUTO_READ, true); + Map.Entry, Object>[] array = Quic.toOptionsArray(options); + assertEquals(1, array.length); + options.put(ChannelOption.AUTO_READ, false); + Map.Entry, Object> entry = array[0]; + assertEquals(ChannelOption.AUTO_READ, entry.getKey()); + assertEquals(true, entry.getValue()); + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicTestUtils.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicTestUtils.java new file mode 100644 index 0000000..22d9ca0 --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicTestUtils.java @@ -0,0 +1,189 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.FixedRecvByteBufAllocator; +import io.netty.channel.epoll.Epoll; +import io.netty.channel.epoll.EpollChannelOption; +import io.netty.channel.epoll.EpollDatagramChannel; +import io.netty.channel.epoll.EpollEventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.netty.util.NetUtil; + +import java.net.InetSocketAddress; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +final class QuicTestUtils { + static final String[] PROTOS = new String[]{"hq-29"}; + static final SelfSignedCertificate SELF_SIGNED_CERTIFICATE; + + private static final int DATAGRAM_SIZE = 2048; + + static { + SelfSignedCertificate cert; + try { + cert = new SelfSignedCertificate(); + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + } + SELF_SIGNED_CERTIFICATE = cert; + } + + private QuicTestUtils() { + } + + private static final EventLoopGroup GROUP = Epoll.isAvailable() ? new EpollEventLoopGroup() : + new NioEventLoopGroup(); + + static { + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + try { + GROUP.shutdownGracefully().sync(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }); + } + + static Channel newClient(Executor sslTaskExecutor) throws Exception { + return newClient(newQuicClientBuilder(sslTaskExecutor)); + } + + private static Bootstrap newBootstrap() { + Bootstrap bs = new Bootstrap(); + if (GROUP instanceof EpollEventLoopGroup) { + bs.channel(EpollDatagramChannel.class) + // Use recvmmsg when possible. + .option(EpollChannelOption.MAX_DATAGRAM_PAYLOAD_SIZE, DATAGRAM_SIZE) + .option(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(DATAGRAM_SIZE * 8)); + } else { + bs.channel(NioDatagramChannel.class) + .option(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(DATAGRAM_SIZE)); + } + return bs.group(GROUP); + } + + static Channel newClient(QuicClientCodecBuilder builder) throws Exception { + return newBootstrap() + // We don't want any special handling of the channel so just use a dummy handler. + .handler(builder.build()) + .bind(new InetSocketAddress(NetUtil.LOCALHOST4, 0)).sync().channel(); + } + + static QuicChannelBootstrap newQuicChannelBootstrap(Channel channel) { + QuicChannelBootstrap bs = QuicChannel.newBootstrap(channel); + if (GROUP instanceof EpollEventLoopGroup) { + bs.option(QuicChannelOption.SEGMENTED_DATAGRAM_PACKET_ALLOCATOR, + EpollQuicUtils.newSegmentedAllocator(10)); + } + return bs; + } + + static QuicClientCodecBuilder newQuicClientBuilder(Executor sslTaskExecutor) { + return newQuicClientBuilder(sslTaskExecutor, QuicSslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE).applicationProtocols(PROTOS).build()); + } + + static QuicClientCodecBuilder newQuicClientBuilder(Executor sslTaskExecutor, QuicSslContext sslContext) { + return new QuicClientCodecBuilder() + .sslEngineProvider(q -> sslContext.newEngine(q.alloc())) + .maxIdleTimeout(5000, TimeUnit.MILLISECONDS) + .initialMaxData(10000000) + .initialMaxStreamDataBidirectionalLocal(1000000) + .initialMaxStreamDataBidirectionalRemote(1000000) + .initialMaxStreamsBidirectional(100) + .initialMaxStreamsUnidirectional(100) + .initialMaxStreamDataUnidirectional(1000000) + .activeMigration(false).sslTaskExecutor(sslTaskExecutor); + } + + static QuicServerCodecBuilder newQuicServerBuilder(Executor sslTaskExecutor) { + return newQuicServerBuilder(sslTaskExecutor, QuicSslContextBuilder.forServer( + SELF_SIGNED_CERTIFICATE.privateKey(), null, SELF_SIGNED_CERTIFICATE.certificate()) + .applicationProtocols(PROTOS).build()); + } + + static QuicServerCodecBuilder newQuicServerBuilder(Executor sslTaskExecutor, QuicSslContext context) { + QuicServerCodecBuilder builder = new QuicServerCodecBuilder() + .sslEngineProvider(q -> context.newEngine(q.alloc())) + .maxIdleTimeout(5000, TimeUnit.MILLISECONDS) + .initialMaxData(10000000) + .initialMaxStreamDataBidirectionalLocal(1000000) + .initialMaxStreamDataBidirectionalRemote(1000000) + .initialMaxStreamDataUnidirectional(1000000) + .initialMaxStreamsBidirectional(100) + .initialMaxStreamsUnidirectional(100) + .activeMigration(false) + .sslTaskExecutor(sslTaskExecutor); + if (GROUP instanceof EpollEventLoopGroup) { + builder.option(QuicChannelOption.SEGMENTED_DATAGRAM_PACKET_ALLOCATOR, + EpollQuicUtils.newSegmentedAllocator(10)); + } + return builder; + } + + private static Bootstrap newServerBootstrap(QuicServerCodecBuilder serverBuilder, + QuicTokenHandler tokenHandler, ChannelHandler handler, + ChannelHandler streamHandler) { + serverBuilder.tokenHandler(tokenHandler) + .streamHandler(streamHandler); + if (handler != null) { + serverBuilder.handler(handler); + } + ChannelHandler codec = serverBuilder.build(); + return newBootstrap() + // We don't want any special handling of the channel so just use a dummy handler. + .handler(codec) + .localAddress(new InetSocketAddress(NetUtil.LOCALHOST4, 0)); + } + + static Channel newServer(QuicServerCodecBuilder serverBuilder, QuicTokenHandler tokenHandler, + ChannelHandler handler, ChannelHandler streamHandler) + throws Exception { + return newServerBootstrap(serverBuilder, tokenHandler, handler, streamHandler) + .bind().sync().channel(); + } + + static Channel newServer(Executor sslTaskExecutor, QuicTokenHandler tokenHandler, + ChannelHandler handler, ChannelHandler streamHandler) + throws Exception { + return newServer(newQuicServerBuilder(sslTaskExecutor), tokenHandler, handler, streamHandler); + } + + static Channel newServer(Executor sslTaskExecutor, ChannelHandler handler, + ChannelHandler streamHandler) throws Exception { + return newServer(sslTaskExecutor, InsecureQuicTokenHandler.INSTANCE, handler, streamHandler); + } + + static void closeIfNotNull(Channel channel) throws Exception { + if (channel != null) { + channel.close().sync(); + } + } + +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicTransportParametersTest.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicTransportParametersTest.java new file mode 100644 index 0000000..b429236 --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicTransportParametersTest.java @@ -0,0 +1,94 @@ +/* + * Copyright 2023 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.concurrent.ImmediateEventExecutor; +import io.netty.util.concurrent.Promise; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.concurrent.Executor; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class QuicTransportParametersTest extends AbstractQuicTest { + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testParameters(Executor executor) throws Throwable { + Channel server = null; + Channel channel = null; + Promise serverParams = ImmediateEventExecutor.INSTANCE.newPromise(); + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + QuicheQuicChannel channel = (QuicheQuicChannel) ctx.channel(); + serverParams.setSuccess(channel.peerTransportParameters()); + ctx.fireChannelActive(); + } + }; + QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler(); + try { + server = QuicTestUtils.newServer(executor, serverHandler, new ChannelInboundHandlerAdapter() { + @Override + public boolean isSharable() { + return true; + } + }); + channel = QuicTestUtils.newClient(executor); + + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(server.localAddress()) + .connect().get(); + assertTransportParameters(quicChannel.peerTransportParameters()); + assertTransportParameters(serverParams.sync().getNow()); + + quicChannel.close().sync(); + serverHandler.assertState(); + clientHandler.assertState(); + } finally { + QuicTestUtils.closeIfNotNull(channel); + QuicTestUtils.closeIfNotNull(server); + + shutdown(executor); + } + } + + private static void assertTransportParameters(QuicTransportParameters parameters) { + assertNotNull(parameters); + assertThat(parameters.maxIdleTimeout(), greaterThanOrEqualTo(1L)); + assertThat(parameters.maxUdpPayloadSize(), greaterThanOrEqualTo(1L)); + assertThat(parameters.initialMaxData(), greaterThanOrEqualTo(1L)); + assertThat(parameters.initialMaxStreamDataBidiLocal(), greaterThanOrEqualTo(1L)); + assertThat(parameters.initialMaxStreamDataBidiRemote(), greaterThanOrEqualTo(1L)); + assertThat(parameters.initialMaxStreamDataUni(), greaterThanOrEqualTo(1L)); + assertThat(parameters.initialMaxStreamsBidi(), greaterThanOrEqualTo(1L)); + assertThat(parameters.initialMaxStreamsUni(), greaterThanOrEqualTo(1L)); + assertThat(parameters.ackDelayExponent(), greaterThanOrEqualTo(1L)); + assertThat(parameters.maxAckDelay(), greaterThanOrEqualTo(1L)); + assertFalse(parameters.disableActiveMigration()); + assertThat(parameters.activeConnIdLimit(), greaterThanOrEqualTo(1L)); + assertThat(parameters.maxDatagramFrameSize(), greaterThanOrEqualTo(0L)); + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicWritableTest.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicWritableTest.java new file mode 100644 index 0000000..3c6e62c --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicWritableTest.java @@ -0,0 +1,301 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.concurrent.ImmediateEventExecutor; +import io.netty.util.concurrent.Promise; +import io.netty.util.concurrent.PromiseNotifier; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.net.InetSocketAddress; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class QuicWritableTest extends AbstractQuicTest { + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testCorrectlyHandleWritabilityReadRequestedInReadComplete(Executor executor) throws Throwable { + testCorrectlyHandleWritability(executor, true); + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testCorrectlyHandleWritabilityReadRequestedInRead(Executor executor) throws Throwable { + testCorrectlyHandleWritability(executor, false); + } + + private static void testCorrectlyHandleWritability(Executor executor, boolean readInComplete) throws Throwable { + int bufferSize = 64 * 1024; + Promise writePromise = ImmediateEventExecutor.INSTANCE.newPromise(); + final AtomicReference serverErrorRef = new AtomicReference<>(); + final AtomicReference clientErrorRef = new AtomicReference<>(); + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler(); + Channel server = QuicTestUtils.newServer( + QuicTestUtils.newQuicServerBuilder(executor).initialMaxStreamsBidirectional(5000), + InsecureQuicTokenHandler.INSTANCE, + serverHandler, new ChannelInboundHandlerAdapter() { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ByteBuf buffer = (ByteBuf) msg; + buffer.release(); + ctx.writeAndFlush(ctx.alloc().buffer(bufferSize).writeZero(bufferSize)) + .addListener(new PromiseNotifier<>(writePromise)); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + serverErrorRef.set(cause); + } + + @Override + public boolean isSharable() { + return true; + } + }); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor) + .initialMaxStreamDataBidirectionalLocal(bufferSize / 4)); + + QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler(); + try { + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(address) + .connect() + .get(); + QuicStreamChannel stream = quicChannel.createStream( + QuicStreamType.BIDIRECTIONAL, new ChannelInboundHandlerAdapter() { + int bytes; + + @Override + public void channelRegistered(ChannelHandlerContext ctx) { + ctx.channel().config().setAutoRead(false); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + ctx.writeAndFlush(ctx.alloc().buffer(8).writeLong(8)); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (bytes == 0) { + // First read + assertFalse(writePromise.isDone()); + } + ByteBuf buffer = (ByteBuf) msg; + bytes += buffer.readableBytes(); + buffer.release(); + if (bytes == bufferSize) { + ctx.close(); + assertTrue(writePromise.isDone()); + } + + if (!readInComplete) { + ctx.read(); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + if (readInComplete) { + ctx.read(); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + clientErrorRef.set(cause); + } + }).get(); + assertFalse(writePromise.isDone()); + + // Let's trigger the reads. This will ensure we will consume the data and the remote peer + // should be notified that it can write more data. + stream.read(); + + writePromise.sync(); + stream.closeFuture().sync(); + quicChannel.close().sync(); + + throwIfNotNull(serverErrorRef); + throwIfNotNull(clientErrorRef); + + serverHandler.assertState(); + clientHandler.assertState(); + } finally { + server.close().sync(); + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(executor); + } + } + + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + @Timeout(value = 5000, unit = TimeUnit.MILLISECONDS) + public void testBytesUntilUnwritable(Executor executor) throws Throwable { + Promise writePromise = ImmediateEventExecutor.INSTANCE.newPromise(); + final AtomicReference serverErrorRef = new AtomicReference<>(); + final AtomicReference clientErrorRef = new AtomicReference<>(); + final CountDownLatch writableAgainLatch = new CountDownLatch(1); + int firstWriteNumBytes = 8; + int maxData = 32 * 1024; + final AtomicLong beforeWritableRef = new AtomicLong(); + QuicChannelValidationHandler serverHandler = new QuicChannelValidationHandler(); + Channel server = QuicTestUtils.newServer( + QuicTestUtils.newQuicServerBuilder(executor).initialMaxStreamsBidirectional(5000), + InsecureQuicTokenHandler.INSTANCE, + serverHandler, new ChannelInboundHandlerAdapter() { + + private int numBytesRead; + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ByteBuf buffer = (ByteBuf) msg; + numBytesRead += buffer.readableBytes(); + buffer.release(); + if (numBytesRead == firstWriteNumBytes) { + long before = ctx.channel().bytesBeforeUnwritable(); + beforeWritableRef.set(before); + assertTrue(before > 0); + + while (before != 0) { + int size = (int) Math.min(before, 1024); + ctx.write(ctx.alloc().buffer(size).writeZero(size)); + long newBefore = ctx.channel().bytesBeforeUnwritable(); + + assertEquals(before, newBefore + size); + before = newBefore; + } + ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(new PromiseNotifier<>(writePromise)); + } + } + + @Override + public void channelWritabilityChanged(ChannelHandlerContext ctx) { + if (ctx.channel().isWritable()) { + if (ctx.channel().bytesBeforeUnwritable() > 0) { + writableAgainLatch.countDown(); + } + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + serverErrorRef.set(cause); + } + + @Override + public boolean isSharable() { + return true; + } + }); + InetSocketAddress address = (InetSocketAddress) server.localAddress(); + Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor) + .initialMaxStreamDataBidirectionalLocal(maxData)); + + QuicChannelValidationHandler clientHandler = new QuicChannelValidationHandler(); + try { + QuicChannel quicChannel = QuicTestUtils.newQuicChannelBootstrap(channel) + .handler(clientHandler) + .streamHandler(new ChannelInboundHandlerAdapter()) + .remoteAddress(address) + .connect() + .get(); + QuicStreamChannel stream = quicChannel.createStream( + QuicStreamType.BIDIRECTIONAL, new ChannelInboundHandlerAdapter() { + int bytes; + + @Override + public void channelRegistered(ChannelHandlerContext ctx) { + ctx.channel().config().setAutoRead(false); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + ctx.writeAndFlush(ctx.alloc().buffer(firstWriteNumBytes).writeZero(firstWriteNumBytes)); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ByteBuf buffer = (ByteBuf) msg; + bytes += buffer.readableBytes(); + buffer.release(); + if (bytes == beforeWritableRef.get()) { + assertTrue(writePromise.isDone()); + } + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.read(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + clientErrorRef.set(cause); + } + }).get(); + + // Let's trigger the reads. This will ensure we will consume the data and the remote peer + // should be notified that it can write more data. + stream.read(); + + writePromise.sync(); + writableAgainLatch.await(); + stream.close().sync(); + stream.closeFuture().sync(); + quicChannel.close().sync(); + + throwIfNotNull(serverErrorRef); + throwIfNotNull(clientErrorRef); + + serverHandler.assertState(); + clientHandler.assertState(); + } finally { + server.close().sync(); + // Close the parent Datagram channel as well. + channel.close().sync(); + + shutdown(executor); + } + } + + private static void throwIfNotNull(AtomicReference errorRef) throws Throwable { + Throwable cause = errorRef.get(); + if (cause != null) { + throw cause; + } + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicheQuicClientCodecTest.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicheQuicClientCodecTest.java new file mode 100644 index 0000000..a78e1b5 --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicheQuicClientCodecTest.java @@ -0,0 +1,25 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.util.concurrent.ImmediateExecutor; + +public class QuicheQuicClientCodecTest extends QuicheQuicCodecTest { + @Override + protected QuicClientCodecBuilder newCodecBuilder() { + return QuicTestUtils.newQuicClientBuilder(ImmediateExecutor.INSTANCE); + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicheQuicCodecTest.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicheQuicCodecTest.java new file mode 100644 index 0000000..631d77b --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicheQuicCodecTest.java @@ -0,0 +1,111 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.channel.socket.DatagramPacket; +import org.junit.jupiter.api.Test; + +import java.net.InetSocketAddress; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public abstract class QuicheQuicCodecTest> extends AbstractQuicTest { + + protected abstract B newCodecBuilder(); + + @Test + public void testDefaultVersionIsV1() { + B builder = newCodecBuilder(); + assertEquals(0x0000_0001, builder.version); + } + + @Test + public void testFlushStrategyUsedWithBytes() { + testFlushStrategy(true); + } + + @Test + public void testFlushStrategyUsedWithPackets() { + testFlushStrategy(false); + } + + private void testFlushStrategy(boolean useBytes) { + final int bytes = 8; + final AtomicInteger numBytesTracker = new AtomicInteger(); + final AtomicInteger numPacketsTracker = new AtomicInteger(); + final AtomicInteger flushCount = new AtomicInteger(); + B builder = newCodecBuilder(); + builder.flushStrategy((numPackets, numBytes) -> { + numPacketsTracker.set(numPackets); + numBytesTracker.set(numBytes); + if (useBytes) { + return numBytes > 8; + } + if (numPackets == 2) { + return true; + } + return false; + }); + + EmbeddedChannel channel = new EmbeddedChannel(new ChannelOutboundHandlerAdapter() { + @Override + public void flush(ChannelHandlerContext ctx) throws Exception { + flushCount.incrementAndGet(); + super.flush(ctx); + } + }, builder.build()); + assertEquals(0, numPacketsTracker.get()); + assertEquals(0, numBytesTracker.get()); + assertEquals(0, flushCount.get()); + + channel.write(new DatagramPacket(Unpooled.buffer().writeZero(bytes), new InetSocketAddress(0))); + assertEquals(1, numPacketsTracker.get()); + assertEquals(8, numBytesTracker.get()); + assertEquals(0, flushCount.get()); + + channel.write(new DatagramPacket(Unpooled.buffer().writeZero(bytes), new InetSocketAddress(0))); + assertEquals(2, numPacketsTracker.get()); + assertEquals(16, numBytesTracker.get()); + assertEquals(1, flushCount.get()); + + // As a flush did happen we should see two packets in the outbound queue. + for (int i = 0; i < 2; i++) { + DatagramPacket packet = channel.readOutbound(); + assertNotNull(packet); + packet.release(); + } + + ChannelFuture future = channel.write(new DatagramPacket(Unpooled.buffer().writeZero(bytes), + new InetSocketAddress(0))); + assertEquals(1, numPacketsTracker.get()); + assertEquals(8, numBytesTracker.get()); + assertEquals(1, flushCount.get()); + + // We never flushed the last datagram packet so it should be failed. + assertFalse(channel.finish()); + assertTrue(future.isDone()); + assertFalse(future.isSuccess()); + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicheQuicServerCodecTest.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicheQuicServerCodecTest.java new file mode 100644 index 0000000..59e38c3 --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/QuicheQuicServerCodecTest.java @@ -0,0 +1,28 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.concurrent.ImmediateExecutor; + +public class QuicheQuicServerCodecTest extends QuicheQuicCodecTest { + @Override + protected QuicServerCodecBuilder newCodecBuilder() { + return QuicTestUtils.newQuicServerBuilder(ImmediateExecutor.INSTANCE) + .streamHandler(new ChannelInboundHandlerAdapter()) + .tokenHandler(InsecureQuicTokenHandler.INSTANCE); + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/TestLogBackAppender.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/TestLogBackAppender.java new file mode 100644 index 0000000..41264b0 --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/TestLogBackAppender.java @@ -0,0 +1,39 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +public final class TestLogBackAppender extends AppenderBase { + private static final List logs = new CopyOnWriteArrayList<>(); + + @Override + protected void append(ILoggingEvent iLoggingEvent) { + logs.add(iLoggingEvent.getFormattedMessage()); + } + + public static List getLogs() { + return logs; + } + + public static void clearLogs() { + logs.clear(); + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/example/QuicClientExample.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/example/QuicClientExample.java new file mode 100644 index 0000000..8bd906e --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/example/QuicClientExample.java @@ -0,0 +1,111 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic.example; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.ChannelInputShutdownReadComplete; +import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.handler.codec.quic.QuicChannel; +import io.netty.handler.codec.quic.QuicClientCodecBuilder; +import io.netty.handler.codec.quic.QuicSslContext; +import io.netty.handler.codec.quic.QuicSslContextBuilder; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.handler.codec.quic.QuicStreamType; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.util.CharsetUtil; +import io.netty.util.NetUtil; + +import java.net.InetSocketAddress; +import java.util.concurrent.TimeUnit; + +public final class QuicClientExample { + + private QuicClientExample() { } + + public static void main(String[] args) throws Exception { + QuicSslContext context = QuicSslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE). + applicationProtocols("http/0.9").build(); + NioEventLoopGroup group = new NioEventLoopGroup(1); + try { + ChannelHandler codec = new QuicClientCodecBuilder() + .sslContext(context) + .maxIdleTimeout(5000, TimeUnit.MILLISECONDS) + .initialMaxData(10000000) + // As we don't want to support remote initiated streams just setup the limit for local initiated + // streams in this example. + .initialMaxStreamDataBidirectionalLocal(1000000) + .build(); + + Bootstrap bs = new Bootstrap(); + Channel channel = bs.group(group) + .channel(NioDatagramChannel.class) + .handler(codec) + .bind(0).sync().channel(); + + QuicChannel quicChannel = QuicChannel.newBootstrap(channel) + .streamHandler(new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + // As we did not allow any remote initiated streams we will never see this method called. + // That said just let us keep it here to demonstrate that this handle would be called + // for each remote initiated stream. + ctx.close(); + } + }) + .remoteAddress(new InetSocketAddress(NetUtil.LOCALHOST4, 9999)) + .connect() + .get(); + + QuicStreamChannel streamChannel = quicChannel.createStream(QuicStreamType.BIDIRECTIONAL, + new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ByteBuf byteBuf = (ByteBuf) msg; + System.err.println(byteBuf.toString(CharsetUtil.US_ASCII)); + byteBuf.release(); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt == ChannelInputShutdownReadComplete.INSTANCE) { + // Close the connection once the remote peer did send the FIN for this stream. + ((QuicChannel) ctx.channel().parent()).close(true, 0, + ctx.alloc().directBuffer(16) + .writeBytes(new byte[]{'k', 't', 'h', 'x', 'b', 'y', 'e'})); + } + } + }).sync().getNow(); + // Write the data and send the FIN. After this its not possible anymore to write any more data. + streamChannel.writeAndFlush(Unpooled.copiedBuffer("GET /\r\n", CharsetUtil.US_ASCII)) + .addListener(QuicStreamChannel.SHUTDOWN_OUTPUT); + + // Wait for the stream channel and quic channel to be closed (this will happen after we received the FIN). + // After this is done we will close the underlying datagram channel. + streamChannel.closeFuture().sync(); + quicChannel.closeFuture().sync(); + channel.close().sync(); + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/example/QuicClientZeroRTTExample.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/example/QuicClientZeroRTTExample.java new file mode 100644 index 0000000..22555db --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/example/QuicClientZeroRTTExample.java @@ -0,0 +1,142 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic.example; + +import io.netty.handler.codec.quic.QuicChannel; +import io.netty.handler.codec.quic.QuicClientCodecBuilder; +import io.netty.handler.codec.quic.QuicSslContext; +import io.netty.handler.codec.quic.QuicSslContextBuilder; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.handler.codec.quic.QuicStreamType; +import io.netty.handler.codec.quic.SslEarlyDataReadyEvent; +import java.net.InetSocketAddress; +import java.util.concurrent.TimeUnit; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.ChannelInputShutdownReadComplete; +import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.codec.quic.QuicChannelBootstrap; +import io.netty.util.CharsetUtil; +import io.netty.util.NetUtil; +import io.netty.util.concurrent.Future; + +public final class QuicClientZeroRTTExample { + + private QuicClientZeroRTTExample() { } + + public static void main(String[] args) throws Exception { + QuicSslContext context = QuicSslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE). + applicationProtocols("http/0.9").earlyData(true).build(); + + newChannelAndSendData(context, null); + newChannelAndSendData(context, new ChannelInboundHandlerAdapter() { + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof SslEarlyDataReadyEvent) { + createStream(((QuicChannel) ctx.channel())).addListener(f -> { + if (f.isSuccess()) { + QuicStreamChannel streamChannel = (QuicStreamChannel) f.getNow(); + streamChannel.writeAndFlush( + Unpooled.copiedBuffer("0rtt stream data\r\n", CharsetUtil.US_ASCII)); + } + }); + } + super.userEventTriggered(ctx, evt); + } + }); + } + + static void newChannelAndSendData(QuicSslContext context, ChannelHandler earlyDataSendHandler) throws Exception { + NioEventLoopGroup group = new NioEventLoopGroup(1); + try { + ChannelHandler codec = new QuicClientCodecBuilder() + .sslEngineProvider(q -> context.newEngine(q.alloc(), "localhost", 9999)) + .maxIdleTimeout(5000, TimeUnit.MILLISECONDS) + .initialMaxData(10000000) + // As we don't want to support remote initiated streams just setup the limit for local initiated + // streams in this example. + .initialMaxStreamDataBidirectionalLocal(1000000) + .build(); + + Bootstrap bs = new Bootstrap(); + Channel channel = bs.group(group) + .channel(NioDatagramChannel.class) + .handler(codec) + .bind(0).sync().channel(); + + + QuicChannelBootstrap quicChannelBootstrap = QuicChannel.newBootstrap(channel) + .streamHandler(new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + // As we did not allow any remote initiated streams we will never see this method called. + // That said just let us keep it here to demonstrate that this handle would be called + // for each remote initiated stream. + ctx.close(); + } + }) + .remoteAddress(new InetSocketAddress(NetUtil.LOCALHOST4, 9999)); + + if (earlyDataSendHandler != null) { + quicChannelBootstrap.handler(earlyDataSendHandler); + } + + QuicChannel quicChannel = quicChannelBootstrap + .connect() + .get(); + + QuicStreamChannel streamChannel = createStream(quicChannel).sync().getNow(); + // Write the data and send the FIN. After this its not possible anymore to write any more data. + streamChannel.writeAndFlush(Unpooled.copiedBuffer("Bye\r\n", CharsetUtil.US_ASCII)) + .addListener(QuicStreamChannel.SHUTDOWN_OUTPUT); + streamChannel.closeFuture().sync(); + quicChannel.closeFuture().sync(); + channel.close().sync(); + } finally { + group.shutdownGracefully(); + } + } + + static Future createStream(QuicChannel quicChannel) { + return quicChannel.createStream(QuicStreamType.BIDIRECTIONAL, + new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ByteBuf byteBuf = (ByteBuf) msg; + System.err.println(byteBuf.toString(CharsetUtil.US_ASCII)); + byteBuf.release(); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt == ChannelInputShutdownReadComplete.INSTANCE) { + // Close the connection once the remote peer did send the FIN for this stream. + ((QuicChannel) ctx.channel().parent()).close(true, 0, + ctx.alloc().directBuffer(16) + .writeBytes(new byte[]{'k', 't', 'h', 'x', 'b', 'y', 'e'})); + } + } + }); + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/example/QuicServerExample.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/example/QuicServerExample.java new file mode 100644 index 0000000..e98201f --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/example/QuicServerExample.java @@ -0,0 +1,122 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic.example; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.handler.codec.LineBasedFrameDecoder; +import io.netty.handler.codec.quic.InsecureQuicTokenHandler; +import io.netty.handler.codec.quic.QuicChannel; +import io.netty.handler.codec.quic.QuicServerCodecBuilder; +import io.netty.handler.codec.quic.QuicSslContext; +import io.netty.handler.codec.quic.QuicSslContextBuilder; +import io.netty.handler.codec.quic.QuicStreamChannel; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.netty.util.CharsetUtil; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.net.InetSocketAddress; +import java.util.concurrent.TimeUnit; + +public final class QuicServerExample { + + private static final InternalLogger LOGGER = InternalLoggerFactory.getInstance(QuicServerExample.class); + + private QuicServerExample() { } + + public static void main(String[] args) throws Exception { + SelfSignedCertificate selfSignedCertificate = new SelfSignedCertificate(); + QuicSslContext context = QuicSslContextBuilder.forServer( + selfSignedCertificate.privateKey(), null, selfSignedCertificate.certificate()) + .applicationProtocols("http/0.9").build(); + NioEventLoopGroup group = new NioEventLoopGroup(1); + ChannelHandler codec = new QuicServerCodecBuilder().sslContext(context) + .maxIdleTimeout(5000, TimeUnit.MILLISECONDS) + // Configure some limits for the maximal number of streams (and the data) that we want to handle. + .initialMaxData(10000000) + .initialMaxStreamDataBidirectionalLocal(1000000) + .initialMaxStreamDataBidirectionalRemote(1000000) + .initialMaxStreamsBidirectional(100) + .initialMaxStreamsUnidirectional(100) + .activeMigration(true) + + // Setup a token handler. In a production system you would want to implement and provide your custom + // one. + .tokenHandler(InsecureQuicTokenHandler.INSTANCE) + // ChannelHandler that is added into QuicChannel pipeline. + .handler(new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + QuicChannel channel = (QuicChannel) ctx.channel(); + // Create streams etc.. + } + + public void channelInactive(ChannelHandlerContext ctx) { + ((QuicChannel) ctx.channel()).collectStats().addListener(f -> { + if (f.isSuccess()) { + LOGGER.info("Connection closed: {}", f.getNow()); + } + }); + } + + @Override + public boolean isSharable() { + return true; + } + }) + .streamHandler(new ChannelInitializer() { + @Override + protected void initChannel(QuicStreamChannel ch) { + // Add a LineBasedFrameDecoder here as we just want to do some simple HTTP 0.9 handling. + ch.pipeline().addLast(new LineBasedFrameDecoder(1024)) + .addLast(new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ByteBuf byteBuf = (ByteBuf) msg; + try { + if (byteBuf.toString(CharsetUtil.US_ASCII).trim().equals("GET /")) { + ByteBuf buffer = ctx.alloc().directBuffer(); + buffer.writeCharSequence("Hello World!\r\n", CharsetUtil.US_ASCII); + // Write the buffer and shutdown the output by writing a FIN. + ctx.writeAndFlush(buffer).addListener(QuicStreamChannel.SHUTDOWN_OUTPUT); + } + } finally { + byteBuf.release(); + } + } + }); + } + }).build(); + try { + Bootstrap bs = new Bootstrap(); + Channel channel = bs.group(group) + .channel(NioDatagramChannel.class) + .handler(codec) + .bind(new InetSocketAddress(9999)).sync().channel(); + channel.closeFuture().sync(); + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/codec-native-quic/src/test/java/io/netty/handler/codec/quic/example/QuicServerZeroRTTExample.java b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/example/QuicServerZeroRTTExample.java new file mode 100644 index 0000000..73b8cb2 --- /dev/null +++ b/codec-native-quic/src/test/java/io/netty/handler/codec/quic/example/QuicServerZeroRTTExample.java @@ -0,0 +1,120 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.quic.example; + +import io.netty.handler.codec.quic.QuicChannel; +import io.netty.handler.codec.quic.QuicServerCodecBuilder; +import io.netty.handler.codec.quic.QuicSslContext; +import io.netty.handler.codec.quic.QuicSslContextBuilder; +import io.netty.handler.codec.quic.QuicStreamChannel; +import java.net.InetSocketAddress; +import java.util.concurrent.TimeUnit; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.handler.codec.LineBasedFrameDecoder; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.netty.util.CharsetUtil; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +public final class QuicServerZeroRTTExample { + + private static final InternalLogger LOGGER = InternalLoggerFactory.getInstance(QuicServerZeroRTTExample.class); + + private QuicServerZeroRTTExample() { } + + public static void main(String[] args) throws Exception { + SelfSignedCertificate selfSignedCertificate = new SelfSignedCertificate(); + QuicSslContext context = QuicSslContextBuilder.forServer( + selfSignedCertificate.privateKey(), null, selfSignedCertificate.certificate()) + .applicationProtocols("http/0.9").earlyData(true).build(); + NioEventLoopGroup group = new NioEventLoopGroup(1); + ChannelHandler codec = new QuicServerCodecBuilder().sslContext(context) + .maxIdleTimeout(5000, TimeUnit.MILLISECONDS) + // Configure some limits for the maximal number of streams (and the data) that we want to handle. + .initialMaxData(10000000) + .initialMaxStreamDataBidirectionalLocal(1000000) + .initialMaxStreamDataBidirectionalRemote(1000000) + .initialMaxStreamsBidirectional(100) + .initialMaxStreamsUnidirectional(100) + + // Disable token usage. In a production system you would want to implement and provide your custom + // one. + .tokenHandler(null) + // ChannelHandler that is added into QuicChannel pipeline. + .handler(new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) { + QuicChannel channel = (QuicChannel) ctx.channel(); + // Create streams etc.. + } + + public void channelInactive(ChannelHandlerContext ctx) { + ((QuicChannel) ctx.channel()).collectStats().addListener(f -> { + if (f.isSuccess()) { + LOGGER.info("Connection closed: {}", f.getNow()); + } + }); + } + + @Override + public boolean isSharable() { + return true; + } + }) + .streamHandler(new ChannelInitializer() { + @Override + protected void initChannel(QuicStreamChannel ch) { + // Add a LineBasedFrameDecoder here as we just want to do some simple HTTP 0.9 handling. + ch.pipeline().addLast(new LineBasedFrameDecoder(1024)) + .addLast(new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ByteBuf byteBuf = (ByteBuf) msg; + try { + if (byteBuf.toString(CharsetUtil.US_ASCII).trim().equals("Bye")) { + ByteBuf buffer = ctx.alloc().directBuffer(); + buffer.writeCharSequence("Bye\r\n", CharsetUtil.US_ASCII); + // Write the buffer and shutdown the output by writing a FIN. + ctx.writeAndFlush(buffer).addListener(QuicStreamChannel.SHUTDOWN_OUTPUT); + } + } finally { + byteBuf.release(); + } + } + }); + } + }).build(); + try { + Bootstrap bs = new Bootstrap(); + Channel channel = bs.group(group) + .channel(NioDatagramChannel.class) + .handler(codec) + .bind(new InetSocketAddress(9999)).sync().channel(); + channel.closeFuture().sync(); + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/codec-native-quic/src/test/resources/logback-test.xml b/codec-native-quic/src/test/resources/logback-test.xml new file mode 100644 index 0000000..aae8a0a --- /dev/null +++ b/codec-native-quic/src/test/resources/logback-test.xml @@ -0,0 +1,33 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + \ No newline at end of file diff --git a/codec-native-quic/src/test/resources/netty-filter.json b/codec-native-quic/src/test/resources/netty-filter.json new file mode 100644 index 0000000..3440819 --- /dev/null +++ b/codec-native-quic/src/test/resources/netty-filter.json @@ -0,0 +1,6 @@ +{ + "rules": [ + {"excludeClasses": "**"}, + {"includeClasses": "io.netty.handler.codec.quic.**"} + ] +} diff --git a/codec-native-quic/src/test/resources/test-class-filter.json b/codec-native-quic/src/test/resources/test-class-filter.json new file mode 100644 index 0000000..215d67c --- /dev/null +++ b/codec-native-quic/src/test/resources/test-class-filter.json @@ -0,0 +1,12 @@ +{ + "rules": [ + {"includeClasses": "**"}, + {"excludeClasses": "ch.qos.logback.**"}, + {"excludeClasses": "io.netty.handler.codec.quic.NoValidationQuicTokenHandler"}, + {"excludeClasses": "io.netty.handler.codec.quic.QuicChannelValidationHandler"}, + {"excludeClasses": "org.apache.maven.surefire.**"} + ], + "regexRules": [ + {"excludeClasses": ".*Test.*"} + ] +} diff --git a/docker/Dockerfile.centos7 b/docker/Dockerfile.centos7 new file mode 100644 index 0000000..0387aa9 --- /dev/null +++ b/docker/Dockerfile.centos7 @@ -0,0 +1,73 @@ +FROM centos:7 + +ENV SOURCE_DIR /root/source +ENV LIBS_DIR /root/libs +ENV CMAKE_VERSION_BASE 3.26 +ENV CMAKE_VERSION $CMAKE_VERSION_BASE.4 +ENV NINJA_VERSION 1.7.2 +ENV GO_VERSION 1.9.3 +ENV MAVEN_VERSION 3.9.1 + + +# install dependencies +RUN yum install -y \ + apr-devel \ + autoconf \ + automake \ + bzip2 \ + git \ + glibc-devel \ + gnupg \ + libtool \ + lsb-core \ + make \ + perl \ + tar \ + unzip \ + wget \ + zip \ + zlib-devel + +RUN mkdir $SOURCE_DIR +WORKDIR $SOURCE_DIR + +RUN yum install -y centos-release-scl + +RUN yum -y install devtoolset-11-gcc devtoolset-11-gcc-c++ +RUN echo 'source /opt/rh/devtoolset-11/enable' >> ~/.bashrc + +RUN wget -q https://github.com/ninja-build/ninja/releases/download/v$NINJA_VERSION/ninja-linux.zip && unzip ninja-linux.zip && mkdir -p /opt/ninja-$NINJA_VERSION/bin && mv ninja /opt/ninja-$NINJA_VERSION/bin && echo 'PATH=/opt/ninja-$NINJA_VERSION/bin:$PATH' >> ~/.bashrc +RUN wget -q https://storage.googleapis.com/golang/go$GO_VERSION.linux-amd64.tar.gz && tar zxf go$GO_VERSION.linux-amd64.tar.gz && mv go /opt/ && echo 'PATH=/opt/go/bin:$PATH' >> ~/.bashrc && echo 'export GOROOT=/opt/go/' >> ~/.bashrc +RUN curl -s https://cmake.org/files/v$CMAKE_VERSION_BASE/cmake-$CMAKE_VERSION-linux-x86_64.tar.gz --output cmake-$CMAKE_VERSION-linux-x86_64.tar.gz && tar zvxf cmake-$CMAKE_VERSION-linux-x86_64.tar.gz && mv cmake-$CMAKE_VERSION-linux-x86_64 /opt/ && echo 'PATH=/opt/cmake-$CMAKE_VERSION-linux-x86_64/bin:$PATH' >> ~/.bashrc + +# Downloading and installing SDKMAN! +RUN curl -s "https://get.sdkman.io" | bash + +ARG java_version="8.0.302-zulu" +ENV JAVA_VERSION $java_version + +# Installing Java and Maven, removing some unnecessary SDKMAN files +RUN bash -c "source $HOME/.sdkman/bin/sdkman-init.sh && \ + yes | sdk install java $JAVA_VERSION && \ + yes | sdk install maven $MAVEN_VERSION && \ + rm -rf $HOME/.sdkman/archives/* && \ + rm -rf $HOME/.sdkman/tmp/*" + +RUN echo 'export JAVA_HOME="/root/.sdkman/candidates/java/current"' >> ~/.bashrc +RUN echo 'PATH=$JAVA_HOME/bin:$PATH' >> ~/.bashrc + +# install rust and setup PATH +run curl https://sh.rustup.rs -sSf | sh -s -- -y +RUN echo 'PATH=$PATH:$HOME/.cargo/bin' >> ~/.bashrc + +# Prepare our own build +ENV PATH /root/.sdkman/candidates/maven/current:$PATH +ENV JAVA_HOME /root/.sdkman/candidates/java/current + +# Cleanup +RUN rm -rf $SOURCE_DIR +RUN yum clean all && \ + rm -rf /var/cache/yum + +# when the JDK is GraalVM install native-image +RUN if [ -O /root/.sdkman/candidates/java/current/bin/gu ]; then /root/.sdkman/candidates/java/current/bin/gu install native-image; else echo "Not GraalVM, skip installation of native-image" ; fi diff --git a/docker/Dockerfile.cross_compile_aarch64 b/docker/Dockerfile.cross_compile_aarch64 new file mode 100644 index 0000000..7aa17dd --- /dev/null +++ b/docker/Dockerfile.cross_compile_aarch64 @@ -0,0 +1,74 @@ +FROM centos:7.6.1810 + +ARG GCC_VERSION=10.2-2020.11 +ENV MAVEN_VERSION 3.9.1 +ENV SOURCE_DIR /root/source +ENV WORKSPACE_DIR /root/workspace +ENV PROJECT_DIR /root/workspace/project + +# We want to have git 2.x for the maven scm plugin and also for boringssl +RUN yum install -y http://opensource.wandisco.com/centos/6/git/x86_64/wandisco-git-release-6-1.noarch.rpm + +RUN yum -y install epel-release + +# Install requirements +RUN yum install -y \ + apr-devel \ + autoconf \ + automake \ + bzip2 \ + git \ + glibc-devel \ + golang \ + gnupg \ + libtool \ + lsb-core \ + ninja-build \ + make \ + perl \ + tar \ + unzip \ + wget \ + zip + + +RUN mkdir $SOURCE_DIR +WORKDIR $SOURCE_DIR + +# Install Java +RUN yum install -y java-1.8.0-openjdk-devel golang +ENV JAVA_HOME="/usr/lib/jvm/java-1.8.0-openjdk/" + +# Install aarch64 gcc 10.2 toolchain +RUN wget https://developer.arm.com/-/media/Files/downloads/gnu-a/$GCC_VERSION/binrel/gcc-arm-$GCC_VERSION-x86_64-aarch64-none-linux-gnu.tar.xz && \ + tar xvf gcc-arm-$GCC_VERSION-x86_64-aarch64-none-linux-gnu.tar.xz && mv gcc-arm-$GCC_VERSION-x86_64-aarch64-none-linux-gnu /opt/ +ENV PATH="/opt/gcc-arm-$GCC_VERSION-x86_64-aarch64-none-linux-gnu/bin:${PATH}" + +# Install CMake +RUN yum install -y cmake3 && ln -s /usr/bin/cmake3 /usr/bin/cmake + +# install rust and setup PATH and install correct toolchain +RUN curl https://sh.rustup.rs -sSf | sh -s -- -y +ENV PATH="$HOME/.cargo/bin:${PATH}" +RUN /root/.cargo/bin/rustup target add aarch64-unknown-linux-gnu + +# Setup the correct linker +RUN echo '[target.aarch64-unknown-linux-gnu]' >> /root/.cargo/config +RUN echo 'linker = "aarch64-none-linux-gnu-gcc"' >> /root/.cargo/config + +# Downloading and installing SDKMAN! +RUN curl -s "https://get.sdkman.io" | bash + +# Installing Java and Maven, removing some unnecessary SDKMAN files +RUN bash -c "source $HOME/.sdkman/bin/sdkman-init.sh && \ + yes | sdk install maven $MAVEN_VERSION && \ + rm -rf $HOME/.sdkman/archives/* && \ + rm -rf $HOME/.sdkman/tmp/*" + +# Cleanup +RUN rm -rf $SOURCE_DIR +RUN yum clean all && \ + rm -rf /var/cache/yum + +# Prepare our own build +ENV PATH /root/.sdkman/candidates/maven/current:$PATH diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..9d0ed01 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,11 @@ +# Using the docker images + +``` +cd /path/to/source/ +``` + +## centos 6 with java 8 + +``` +docker-compose -f docker/docker-compose.centos-6.yaml -f docker/docker-compose.centos-6.18.yaml run build +``` diff --git a/docker/docker-compose.centos-7-cross.yaml b/docker/docker-compose.centos-7-cross.yaml new file mode 100644 index 0000000..e52b0d1 --- /dev/null +++ b/docker/docker-compose.centos-7-cross.yaml @@ -0,0 +1,80 @@ +version: "3" + +services: + + cross-compile-aarch64-runtime-setup: + image: netty-quic-centos:cross_compile_aarch64 + build: + context: ../ + dockerfile: docker/Dockerfile.cross_compile_aarch64 + args: + gcc_version: "10.2-2020.11" + + cross-compile-aarch64-common: &cross-compile-aarch64-common + image: netty-quic-centos:cross_compile_aarch64 + depends_on: [cross-compile-aarch64-runtime-setup] + environment: + - GPG_KEYNAME + - GPG_PASSPHRASE + - GPG_PRIVATE_KEY + - MAVEN_OPTS + volumes: + - ~/.ssh:/root/.ssh:delegated + - ~/.gnupg:/root/.gnupg:delegated + - ~/.m2/repository:/root/.m2/repository + - ..:/code:delegated + working_dir: /code + + cross-compile-aarch64-shell: + <<: *cross-compile-aarch64-common + volumes: + - ~/.ssh:/root/.ssh:delegated + - ~/.gnupg:/root/.gnupg:delegated + - ~/.m2:/root/.m2:delegated + - ~/.gitconfig:/root/.gitconfig:delegated + - ~/.gitignore:/root/.gitignore:delegated + - ..:/code:delegated + entrypoint: /bin/bash + + cross-compile-aarch64-build: + <<: *cross-compile-aarch64-common + command: /bin/bash -cl "./mvnw -B -ntp clean package -Plinux-aarch64 -DskipTests" + + cross-compile-aarch64-deploy: + <<: *cross-compile-aarch64-common + volumes: + - ~/.ssh:/root/.ssh + - ~/.gnupg:/root/.gnupg + - ~/.m2/repository:/root/.m2/repository + - ~/.m2/settings.xml:/root/.m2/settings.xml + command: /bin/bash -cl "./mvnw -B -ntp clean deploy -Plinux-aarch64 -DskipTests" + + cross-compile-aarch64-stage-snapshot: + <<: *cross-compile-aarch64-common + volumes: + - ~/.ssh:/root/.ssh + - ~/.gnupg:/root/.gnupg + - ~/.m2/repository:/root/.m2/repository + - ~/.m2/settings.xml:/root/.m2/settings.xml + - ~/local-staging:/root/local-staging + - ..:/code + command: /bin/bash -cl "./mvnw -B -ntp -Plinux-aarch64 clean package org.sonatype.plugins:nexus-staging-maven-plugin:deploy -DaltStagingDirectory=/root/local-staging -DskipRemoteStaging=true -DskipTests=true" + + cross-compile-aarch64-stage-release: + <<: *cross-compile-aarch64-common + volumes: + - ~/.ssh:/root/.ssh + - ~/.m2/settings.xml:/root/.m2/settings.xml + - ~/.m2/repository:/root/.m2/repository + - ~/local-staging:/root/local-staging + - ..:/code + command: /bin/bash -cl "cat <(echo -e \"${GPG_PRIVATE_KEY}\") | gpg --batch --import && ./mvnw -B -ntp -Plinux-aarch64 clean javadoc:jar package gpg:sign org.sonatype.plugins:nexus-staging-maven-plugin:deploy -DnexusUrl=https://oss.sonatype.org -DserverId=sonatype-nexus-staging -DaltStagingDirectory=/root/local-staging -DskipRemoteStaging=true -DskipTests=true -Dgpg.passphrase=${GPG_PASSPHRASE} -Dgpg.keyname=${GPG_KEYNAME}" + + cross-compile-aarch64-install: + <<: *cross-compile-aarch64-common + volumes: + - ~/.ssh:/root/.ssh + - ~/.gnupg:/root/.gnupg + - ~/.m2:/root/.m2 + - ..:/code + command: /bin/bash -cl "./mvnw -B -ntp clean install -Plinux-aarch64 -DskipTests=true" diff --git a/docker/docker-compose.centos-7.117.yaml b/docker/docker-compose.centos-7.117.yaml new file mode 100644 index 0000000..f01c318 --- /dev/null +++ b/docker/docker-compose.centos-7.117.yaml @@ -0,0 +1,36 @@ +version: "3" + +services: + + runtime-setup: + image: netty-codec-quic-centos6:centos-6-1.17 + build: + args: + java_version : "17.0.9-zulu" + + build: + image: netty-codec-quic-centos6:centos-6-1.17 + + build-leak: + image: netty-codec-quic-centos6:centos-6-1.17 + + build-no-unsafe: + image: netty-codec-quic-centos6:centos-6-1.17 + + build-clean: + image: netty-codec-quic-centos6:centos-6-1.17 + + deploy: + image: netty-codec-quic-centos6:centos-6-1.17 + + deploy-clean: + image: netty-codec-quic-centos6:centos-6-1.17 + + stage-snapshot: + image: netty-codec-quic-centos6:centos-6-1.17 + + stage-release: + image: netty-codec-quic-centos6:centos-6-1.17 + + shell: + image: netty-codec-quic-centos6:centos-6-1.17 diff --git a/docker/docker-compose.centos-7.18.yaml b/docker/docker-compose.centos-7.18.yaml new file mode 100644 index 0000000..a6470c4 --- /dev/null +++ b/docker/docker-compose.centos-7.18.yaml @@ -0,0 +1,36 @@ +version: "3" + +services: + + runtime-setup: + image: netty-codec-quic-centos6:centos-6-1.8 + build: + args: + java_version : "8.0.392-zulu" + + build: + image: netty-codec-quic-centos6:centos-6-1.8 + + build-leak: + image: netty-codec-quic-centos6:centos-6-1.8 + + build-no-unsafe: + image: netty-codec-quic-centos6:centos-6-1.8 + + build-clean: + image: netty-codec-quic-centos6:centos-6-1.8 + + deploy: + image: netty-codec-quic-centos6:centos-6-1.8 + + deploy-clean: + image: netty-codec-quic-centos6:centos-6-1.8 + + stage-snapshot: + image: netty-codec-quic-centos6:centos-6-1.8 + + stage-release: + image: netty-codec-quic-centos6:centos-6-1.8 + + shell: + image: netty-codec-quic-centos6:centos-6-1.8 diff --git a/docker/docker-compose.centos-7.21.yaml b/docker/docker-compose.centos-7.21.yaml new file mode 100644 index 0000000..2fa8959 --- /dev/null +++ b/docker/docker-compose.centos-7.21.yaml @@ -0,0 +1,36 @@ +version: "3" + +services: + + runtime-setup: + image: netty-codec-quic-centos7:centos-7-21 + build: + args: + java_version : "21.0.1-zulu" + + build: + image: netty-codec-quic-centos7:centos-7-21 + + build-leak: + image: netty-codec-quic-centos7:centos-7-21 + + build-no-unsafe: + image: netty-codec-quic-centos7:centos-7-21 + + build-clean: + image: netty-codec-quic-centos7:centos-7-21 + + deploy: + image: netty-codec-quic-centos7:centos-7-21 + + deploy-clean: + image: netty-codec-quic-centos7:centos-7-21 + + stage-snapshot: + image: netty-codec-quic-centos7:centos-7-21 + + stage-release: + image: netty-codec-quic-centos7:centos-7-21 + + shell: + image: netty-codec-quic-centos7:centos-7-21 diff --git a/docker/docker-compose.centos-7.graalvm117.yaml b/docker/docker-compose.centos-7.graalvm117.yaml new file mode 100644 index 0000000..8b191b3 --- /dev/null +++ b/docker/docker-compose.centos-7.graalvm117.yaml @@ -0,0 +1,24 @@ +version: "3" + +services: + + runtime-setup: + image: netty-codec-quic-centos6:centos-6-1.17 + build: + args: + java_version : "22.3.r17-grl" + + build: + image: netty-codec-quic-centos6:centos-6-1.17 + + build-leak: + image: netty-codec-quic-centos6:centos-6-1.17 + + build-no-unsafe: + image: netty-codec-quic-centos6:centos-6-1.17 + + build-clean: + image: netty-codec-quic-centos6:centos-6-1.17 + + shell: + image: netty-codec-quic-centos6:centos-6-1.17 diff --git a/docker/docker-compose.centos-7.yaml b/docker/docker-compose.centos-7.yaml new file mode 100644 index 0000000..7af8536 --- /dev/null +++ b/docker/docker-compose.centos-7.yaml @@ -0,0 +1,92 @@ +version: "3" + +services: + + runtime-setup: + image: netty-codec-quic-centos7:default + build: + context: ../ + dockerfile: docker/Dockerfile.centos7 + + common: &common + image: netty-codec-quic-centos7:default + depends_on: [runtime-setup] + environment: + - GPG_KEYNAME + - GPG_PASSPHRASE + - GPG_PRIVATE_KEY + - MAVEN_OPTS + volumes: + - ~/.m2/repository:/root/.m2/repository + - ~/.ssh:/root/.ssh:delegated + - ~/.gnupg:/root/.gnupg:delegated + - ..:/code:delegated + working_dir: /code + + build: + <<: *common + command: /bin/bash -cl "./mvnw -B -ntp clean package" + + build-leak: + <<: *common + command: /bin/bash -cl "./mvnw -B -ntp -Pleak clean package" + + build-no-unsafe: + <<: *common + command: /bin/bash -cl "./mvnw -B -ntp -PnoUnsafe clean package" + + build-clean: + <<: *common + command: /bin/bash -cl "./mvnw -B -ntp clean package" + + deploy: + <<: *common + command: /bin/bash -cl "./mvnw -B -ntp clean deploy -DskipTests=true" + volumes: + - ~/.ssh:/root/.ssh + - ~/.gnupg:/root/.gnupg + - ~/.m2/repository:/root/.m2/repository + - ~/.m2/settings.xml:/root/.m2/settings.xml + - ..:/code + + deploy-clean: + <<: *common + command: /bin/bash -cl "./mvnw -B -ntp clean deploy -DskipTests=true" + volumes: + - ~/.ssh:/root/.ssh + - ~/.gnupg:/root/.gnupg + - ~/.m2/repository:/root/.m2/repository + - ~/.m2/settings.xml:/root/.m2/settings.xml + - ..:/code + + stage-snapshot: + <<: *common + volumes: + - ~/.ssh:/root/.ssh + - ~/.gnupg:/root/.gnupg + - ~/.m2/settings.xml:/root/.m2/settings.xml + - ~/.m2/repository:/root/.m2/repository + - ~/local-staging:/root/local-staging + - ..:/code + command: /bin/bash -cl "./mvnw -B -ntp clean package org.sonatype.plugins:nexus-staging-maven-plugin:deploy -DaltStagingDirectory=/root/local-staging -DskipRemoteStaging=true -DskipTests=true" + + stage-release: + <<: *common + volumes: + - ~/.ssh:/root/.ssh + - ~/.m2/settings.xml:/root/.m2/settings.xml + - ~/.m2/repository:/root/.m2/repository + - ~/local-staging:/root/local-staging + - ..:/code + command: /bin/bash -cl "cat <(echo -e \"${GPG_PRIVATE_KEY}\") | gpg --batch --import && ./mvnw -B -ntp clean javadoc:jar package gpg:sign org.sonatype.plugins:nexus-staging-maven-plugin:deploy -DnexusUrl=https://oss.sonatype.org -DserverId=sonatype-nexus-staging -DaltStagingDirectory=/root/local-staging -DskipRemoteStaging=true -DskipTests=true -Dgpg.passphrase=${GPG_PASSPHRASE} -Dgpg.keyname=${GPG_KEYNAME}" + + shell: + <<: *common + volumes: + - ~/.ssh:/root/.ssh:delegated + - ~/.gnupg:/root/.gnupg:delegated + - ~/.m2:/root/.m2:delegated + - ~/.gitconfig:/root/.gitconfig:delegated + - ~/.gitignore:/root/.gitignore:delegated + - ..:/code:delegated + entrypoint: /bin/bash diff --git a/license/LICENSE.boringssl.txt b/license/LICENSE.boringssl.txt new file mode 100644 index 0000000..e861036 --- /dev/null +++ b/license/LICENSE.boringssl.txt @@ -0,0 +1,221 @@ +BoringSSL is a fork of OpenSSL. As such, large parts of it fall under OpenSSL +licensing. Files that are completely new have a Google copyright and an ISC +license. This license is reproduced at the bottom of this file. +Contributors to BoringSSL are required to follow the CLA rules for Chromium: +https://cla.developers.google.com/clas +Files in third_party/ have their own licenses, as described therein. The MIT +license, for third_party/fiat, which, unlike other third_party directories, is +compiled into non-test libraries, is included below. +The OpenSSL toolkit stays under a dual license, i.e. both the conditions of the +OpenSSL License and the original SSLeay license apply to the toolkit. See below +for the actual license texts. Actually both licenses are BSD-style Open Source +licenses. In case of any license issues related to OpenSSL please contact +openssl-core@openssl.org. +The following are Google-internal bug numbers where explicit permission from +some authors is recorded for use of their work. (This is purely for our own +record keeping.) + 27287199 + 27287880 + 27287883 + OpenSSL License + --------------- +/* ==================================================================== + * Copyright (c) 1998-2011 The OpenSSL Project. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. All advertising materials mentioning features or use of this + * software must display the following acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit. (http://www.openssl.org/)" + * + * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please contact + * openssl-core@openssl.org. + * + * 5. Products derived from this software may not be called "OpenSSL" + * nor may "OpenSSL" appear in their names without prior written + * permission of the OpenSSL Project. + * + * 6. Redistributions of any form whatsoever must retain the following + * acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit (http://www.openssl.org/)" + * + * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY + * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * ==================================================================== + * + * This product includes cryptographic software written by Eric Young + * (eay@cryptsoft.com). This product includes software written by Tim + * Hudson (tjh@cryptsoft.com). + * + */ + Original SSLeay License + ----------------------- +/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) + * All rights reserved. + * + * This package is an SSL implementation written + * by Eric Young (eay@cryptsoft.com). + * The implementation was written so as to conform with Netscapes SSL. + * + * This library is free for commercial and non-commercial use as long as + * the following conditions are aheared to. The following conditions + * apply to all code found in this distribution, be it the RC4, RSA, + * lhash, DES, etc., code; not just the SSL code. The SSL documentation + * included with this distribution is covered by the same copyright terms + * except that the holder is Tim Hudson (tjh@cryptsoft.com). + * + * Copyright remains Eric Young's, and as such any Copyright notices in + * the code are not to be removed. + * If this package is used in a product, Eric Young should be given attribution + * as the author of the parts of the library used. + * This can be in the form of a textual message at program startup or + * in documentation (online or textual) provided with the package. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * "This product includes cryptographic software written by + * Eric Young (eay@cryptsoft.com)" + * The word 'cryptographic' can be left out if the rouines from the library + * being used are not cryptographic related :-). + * 4. If you include any Windows specific code (or a derivative thereof) from + * the apps directory (application code) you must include an acknowledgement: + * "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" + * + * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * The licence and distribution terms for any publically available version or + * derivative of this code cannot be changed. i.e. this code cannot simply be + * copied and put under another distribution licence + * [including the GNU Public Licence.] + */ +ISC license used for completely new code in BoringSSL: +/* Copyright (c) 2015, Google Inc. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ +The code in third_party/fiat carries the MIT license: +Copyright (c) 2015-2016 the fiat-crypto authors (see +https://github.com/mit-plv/fiat-crypto/blob/master/AUTHORS). +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +Licenses for support code +------------------------- +Parts of the TLS test suite are under the Go license. This code is not included +in BoringSSL (i.e. libcrypto and libssl) when compiled, however, so +distributing code linked against BoringSSL does not trigger this license: +Copyright (c) 2009 The Go Authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +BoringSSL uses the Chromium test infrastructure to run a continuous build, +trybots etc. The scripts which manage this, and the script for generating build +metadata, are under the Chromium license. Distributing code linked against +BoringSSL does not trigger this license. +Copyright 2015 The Chromium Authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/license/LICENSE.mvn-wrapper.txt b/license/LICENSE.mvn-wrapper.txt new file mode 100644 index 0000000..62589ed --- /dev/null +++ b/license/LICENSE.mvn-wrapper.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/license/LICENSE.quiche.txt b/license/LICENSE.quiche.txt new file mode 100644 index 0000000..c044775 --- /dev/null +++ b/license/LICENSE.quiche.txt @@ -0,0 +1,23 @@ +Copyright (C) 2018-2019, Cloudflare, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..8d937f4 --- /dev/null +++ b/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..c4586b5 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,205 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.2.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..59c36f8 --- /dev/null +++ b/pom.xml @@ -0,0 +1,456 @@ + + + + 4.0.0 + + org.sonatype.oss + oss-parent + 9 + + + org.xbib.netty + netty-handler-codec-parent-quic + 0.0.56.Final-SNAPSHOT + Netty/Handler/Codec/Parent/Quic + pom + https://netty.io/ + + Netty is an asynchronous event-driven network application framework for + rapid development of maintainable high performance protocol servers and + clients. + + + + The Netty Project + https://netty.io/ + + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0 + + + 2020 + + + https://github.com/xbib/netty-handler-codec-quic + scm:git:git://github.com/xbib/netty-handler-codec-quic.git + scm:git:ssh://git@github.com/xbib/netty-handler-codec-quic.git + HEAD + + + + + netty.io + The Netty Project Contributors + netty@googlegroups.com + https://netty.io/ + The Netty Project + https://netty.io/ + + + + + codec-classes-quic + codec-native-quic + testsuite-native-image + + + + false + jar + 4.1.104.Final + 31 + 0.0.9.Final + 5.9.0 + 0.9.27 + -D_ + + + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.2.0 + + + org.apache.maven.plugins + maven-dependency-plugin + 3.1.2 + + + maven-antrun-plugin + 1.8 + + + org.apache.ant + ant + 1.10.11 + + + org.apache.ant + ant-commons-net + 1.9.6 + + + ant-contrib + ant-contrib + 1.0b3 + + + + + maven-compiler-plugin + 3.8.0 + + + maven-checkstyle-plugin + 3.1.0 + + + com.puppycrawl.tools + checkstyle + 8.29 + + + io.netty + netty-build-common + ${netty.build.version} + + + + + maven-surefire-plugin + 2.22.1 + + + maven-javadoc-plugin + 2.10.4 + + + org.apache.felix + maven-bundle-plugin + 2.5.4 + + + maven-deploy-plugin + 2.8.2 + + + maven-source-plugin + 3.2.1 + + + maven-release-plugin + 2.5.3 + + + org.apache.maven.scm + maven-scm-api + 1.9.4 + + + org.apache.maven.scm + maven-scm-provider-gitexe + 1.9.4 + + + + + org.fusesource.hawtjni + hawtjni-maven-plugin + 1.18 + + + maven-jar-plugin + 3.2.0 + + + com.simpligility.maven.plugins + android-maven-plugin + 4.6.0 + + + + + + + kr.motd.maven + os-maven-plugin + 1.7.0 + + + + + maven-compiler-plugin + + 1.8 + true + 1.8 + 1.8 + true + true + true + true + -Xlint:-options + 256m + 1024m + + **/package-info.java + + + + + maven-checkstyle-plugin + + + check-style + + check + + validate + + true + true + true + true + io/netty/checkstyle.xml + + ${project.build.sourceDirectory} + ${project.build.testSourceDirectory} + + + false + + + + + maven-surefire-plugin + + + **/*Test*.java + + + **/Abstract* + + random + + src/test/resources/logback-test.xml + info + + + false + + -ea -Xcheck:jni ${test.argLine} + + + + + org.apache.felix + maven-bundle-plugin + + + generate-manifest + process-classes + + manifest + + + + jar + bundle + + + ${project.groupId}.* + + + + + + + + maven-source-plugin + + + + + 2 + ${project.name} + ${project.groupId}.${project.artifactId}.source + ${project.organization.name} + ${parsedVersion.osgiVersion} + ${project.groupId}.${project.artifactId};version="${parsedVersion.osgiVersion}";roots:="." + + + + + + + attach-sources + prepare-package + + jar-no-fork + + + + attach-test-sources + prepare-package + + test-jar-no-fork + + + + + + maven-javadoc-plugin + + false + true + false + false + true + 8 + + + + maven-deploy-plugin + + 10 + + + + maven-release-plugin + + false + -P restricted-release,sonatype-oss-release,full + true + false + ${project.artifactId}-@{project.version} + + + + + + + + + io.netty + netty-jni-util + ${netty.jni-util.version} + sources + true + + + io.netty + netty-common + ${netty.version} + + + io.netty + netty-buffer + ${netty.version} + + + io.netty + netty-codec + ${netty.version} + + + io.netty + netty-handler + ${netty.version} + + + io.netty + netty-transport + ${netty.version} + + + io.netty + netty-transport-classes-epoll + ${netty.version} + compile + + true + + + io.netty + netty-transport-native-epoll + ${netty.version} + linux-x86_64 + test + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-params + ${junit.version} + test + + + org.hamcrest + hamcrest-library + 1.3 + test + + + io.netty + netty-build-common + ${netty.build.version} + test + + + ch.qos.logback + logback-classic + 1.3.12 + test + + + + org.bouncycastle + bcpkix-jdk15on + 1.70 + test + + + org.assertj + assertj-core + 3.20.2 + test + + + + + diff --git a/scripts/finish_release.sh b/scripts/finish_release.sh new file mode 100755 index 0000000..0cb4220 --- /dev/null +++ b/scripts/finish_release.sh @@ -0,0 +1,36 @@ +#!/bin/bash +set -e + +if [ "$#" -ne 2 ]; then + echo "Expected staging profile id and tag, login into oss.sonatype.org to retrieve it" + exit 1 +fi + +OS=$(uname) +ARCH=$(uname -p) +if [ "$OS" != "Darwin" ]; then + echo "Needs to be executed on macOS" + exit 1 +fi + +BRANCH=$(git branch --show-current) + +if git tag | grep -q "$2" ; then + echo "Tag $2 already existed locally, deleting it" + git tag -d "$2" +fi + +CROSS_COMPILE_PROFILE="mac-m1-cross-compile" +if [ "$ARCH" == "arm" ]; then + CROSS_COMPILE_PROFILE="mac-intel-cross-compile" +fi + +git fetch +git checkout "$2" + +export JAVA_HOME="$JAVA8_HOME" + +./mvnw -Psonatype-oss-release,"$CROSS_COMPILE_PROFILE" clean package gpg:sign org.sonatype.plugins:nexus-staging-maven-plugin:deploy -DstagingRepositoryId="$1" -DnexusUrl=https://oss.sonatype.org -DserverId=sonatype-nexus-staging -DskipTests=true +./mvnw -Psonatype-oss-release clean package gpg:sign org.sonatype.plugins:nexus-staging-maven-plugin:deploy -DstagingRepositoryId="$1" -DnexusUrl=https://oss.sonatype.org -DserverId=sonatype-nexus-staging -DskipTests=true -DstagingProgressTimeoutMinutes=10 +./mvnw -Psonatype-oss-release org.sonatype.plugins:nexus-staging-maven-plugin:rc-close org.sonatype.plugins:nexus-staging-maven-plugin:rc-release -DstagingRepositoryId="$1" -DnexusUrl=https://oss.sonatype.org -DserverId=sonatype-nexus-staging -DskipTests=true -DstagingProgressTimeoutMinutes=10 +git checkout "$BRANCH" diff --git a/scripts/list_staged_release.sh b/scripts/list_staged_release.sh new file mode 100755 index 0000000..c71aac0 --- /dev/null +++ b/scripts/list_staged_release.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -e + +RC_LIST=$(mvn org.sonatype.plugins:nexus-staging-maven-plugin:rc-list -DserverId=sonatype-nexus-staging -DnexusUrl=https://oss.sonatype.org | grep -A 2 "\[INFO\] ID State Description") +STAGED=$(echo "$RC_LIST" | grep 'OPEN' | cut -f 2 -d ' ') +echo "$STAGED" diff --git a/testsuite-native-image/pom.xml b/testsuite-native-image/pom.xml new file mode 100644 index 0000000..602f70a --- /dev/null +++ b/testsuite-native-image/pom.xml @@ -0,0 +1,216 @@ + + + + 4.0.0 + + org.xbib.netty + netty-handler-codec-parent-quic + 0.0.56.Final-SNAPSHOT + + + netty-handler-codec-quic-testsuite-native-image + ${packaging.type} + Netty/Testsuite/NativeImage + + + + + native-image-quic-server + + + + ${java.home}/bin/gu + + + + + ${project.groupId} + netty-handler-codec-native-quic + ${project.version} + ${os.detected.classifier} + + + org.bouncycastle + bcpkix-jdk15on + runtime + + + + + + maven-compiler-plugin + + 17 + 17 + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + generate-sources + + add-source + + + + ${project.basedir}/../codec-native-quic/src/test/java/io/netty/handler/codec/quic/example/ + + + + + + + + org.graalvm.buildtools + native-maven-plugin + ${native.maven.plugin.version} + true + + + build-native + + compile-no-fork + + package + + + + example.io.netty.handler.codec.quic.QuicServerExample + native-image-quic-server + + true + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + + verify-native-image + verify + + exec + + + + + ${project.build.directory}/native-image-quic-server + + + + + + + native-image-quic-client + + + + ${java.home}/bin/gu + + + + + ${project.groupId} + netty-handler-codec-native-quic + ${project.version} + ${os.detected.classifier} + + + org.bouncycastle + bcpkix-jdk15on + runtime + + + + + + maven-compiler-plugin + + 17 + 17 + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + generate-sources + + add-source + + + + ${project.basedir}/../codec-native-quic/src/test/java/io/netty/handler/codec/quic/example/ + + + + + + + + org.graalvm.buildtools + native-maven-plugin + ${native.maven.plugin.version} + true + + + build-native + + compile-no-fork + + package + + + + example.io.netty.handler.codec.quic.QuicClientExample + native-image-quic-client + + true + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + + verify-native-image + verify + + exec + + + + + ${project.build.directory}/native-image-quic-client + + + + + + +