From 09700dfdf9c9d15e0614fa19808a2a110065f3ea Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 20 Oct 2026 04:54:03 -0400 Subject: [PATCH 1/9] joule-code: vendor wtransport 0.7.2 as published, patched in The crate exactly as published (its .crate's SHA-256 is the lock's checksum, b4273ce3...), with the upstream's MIT and Apache-2.0 licence files from the commit its .cargo_vcs_info.json names. Patched into the joule-code workspace and into joule-moq-room, a workspace of its own that builds the relay; both locks lose only the registry source and checksum. check-lockfiles.sh skips the vendored crate's own lock, as it does the other vendored tree's. No change to the crate yet: the next commit adds the session close. Co-Authored-By: Claude Opus 5.5 --- joule-code/joule-code-rs/Cargo.lock | 2 - joule-code/joule-code-rs/Cargo.toml | 6 + .../crates/joule-moq-room/Cargo.lock | 2 - .../crates/joule-moq-room/Cargo.toml | 5 + .../vendor/wtransport/.cargo_vcs_info.json | 6 + .../vendor/wtransport/Cargo.lock | 1941 +++++++++++++++++ .../vendor/wtransport/Cargo.toml | 214 ++ .../vendor/wtransport/Cargo.toml.orig | 105 + .../vendor/wtransport/LICENSE-APACHE | 176 ++ .../vendor/wtransport/LICENSE-MIT | 21 + .../joule-code-rs/vendor/wtransport/README.md | 139 ++ .../vendor/wtransport/examples/client.rs | 20 + .../vendor/wtransport/examples/full.rs | 558 +++++ .../vendor/wtransport/examples/gencert.rs | 45 + .../vendor/wtransport/examples/server.rs | 111 + .../vendor/wtransport/src/config.rs | 1287 +++++++++++ .../vendor/wtransport/src/connection.rs | 426 ++++ .../vendor/wtransport/src/datagram.rs | 76 + .../vendor/wtransport/src/driver/mod.rs | 677 ++++++ .../wtransport/src/driver/streams/connect.rs | 105 + .../wtransport/src/driver/streams/mod.rs | 588 +++++ .../wtransport/src/driver/streams/qpack.rs | 106 + .../wtransport/src/driver/streams/settings.rs | 161 ++ .../vendor/wtransport/src/driver/utils.rs | 236 ++ .../vendor/wtransport/src/endpoint.rs | 797 +++++++ .../vendor/wtransport/src/error.rs | 352 +++ .../vendor/wtransport/src/lib.rs | 148 ++ .../vendor/wtransport/src/stream.rs | 392 ++++ .../vendor/wtransport/src/tls.rs | 1373 ++++++++++++ scripts/check-lockfiles.sh | 4 + 30 files changed, 10075 insertions(+), 4 deletions(-) create mode 100644 joule-code/joule-code-rs/vendor/wtransport/.cargo_vcs_info.json create mode 100644 joule-code/joule-code-rs/vendor/wtransport/Cargo.lock create mode 100644 joule-code/joule-code-rs/vendor/wtransport/Cargo.toml create mode 100644 joule-code/joule-code-rs/vendor/wtransport/Cargo.toml.orig create mode 100644 joule-code/joule-code-rs/vendor/wtransport/LICENSE-APACHE create mode 100644 joule-code/joule-code-rs/vendor/wtransport/LICENSE-MIT create mode 100644 joule-code/joule-code-rs/vendor/wtransport/README.md create mode 100644 joule-code/joule-code-rs/vendor/wtransport/examples/client.rs create mode 100644 joule-code/joule-code-rs/vendor/wtransport/examples/full.rs create mode 100644 joule-code/joule-code-rs/vendor/wtransport/examples/gencert.rs create mode 100644 joule-code/joule-code-rs/vendor/wtransport/examples/server.rs create mode 100644 joule-code/joule-code-rs/vendor/wtransport/src/config.rs create mode 100644 joule-code/joule-code-rs/vendor/wtransport/src/connection.rs create mode 100644 joule-code/joule-code-rs/vendor/wtransport/src/datagram.rs create mode 100644 joule-code/joule-code-rs/vendor/wtransport/src/driver/mod.rs create mode 100644 joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/connect.rs create mode 100644 joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/mod.rs create mode 100644 joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/qpack.rs create mode 100644 joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/settings.rs create mode 100644 joule-code/joule-code-rs/vendor/wtransport/src/driver/utils.rs create mode 100644 joule-code/joule-code-rs/vendor/wtransport/src/endpoint.rs create mode 100644 joule-code/joule-code-rs/vendor/wtransport/src/error.rs create mode 100644 joule-code/joule-code-rs/vendor/wtransport/src/lib.rs create mode 100644 joule-code/joule-code-rs/vendor/wtransport/src/stream.rs create mode 100644 joule-code/joule-code-rs/vendor/wtransport/src/tls.rs diff --git a/joule-code/joule-code-rs/Cargo.lock b/joule-code/joule-code-rs/Cargo.lock index 81fce2b67..541e4dfff 100644 --- a/joule-code/joule-code-rs/Cargo.lock +++ b/joule-code/joule-code-rs/Cargo.lock @@ -4648,8 +4648,6 @@ checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "wtransport" version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4273ce3157a3262a68665f8d3f20a0ac0c5b8a69ffd67f05ae986832ebec036" dependencies = [ "bytes", "pem", diff --git a/joule-code/joule-code-rs/Cargo.toml b/joule-code/joule-code-rs/Cargo.toml index af6d8f1f3..3add5c1dc 100644 --- a/joule-code/joule-code-rs/Cargo.toml +++ b/joule-code/joule-code-rs/Cargo.toml @@ -50,3 +50,9 @@ serde_json = "1" base64 = "0.22" hex = "0.4" thiserror = "2" + +[patch.crates-io] +# wtransport 0.7.2 with a WebTransport session close: a CLOSE_WEBTRANSPORT_SESSION +# capsule on the CONNECT stream (draft-ietf-webtrans-http3 §6), which the release +# cannot send. What was changed, and why, is in vendor/wtransport/VENDORED.md. +wtransport = { path = "vendor/wtransport" } diff --git a/joule-code/joule-code-rs/crates/joule-moq-room/Cargo.lock b/joule-code/joule-code-rs/crates/joule-moq-room/Cargo.lock index 5a24449a6..873216be0 100644 --- a/joule-code/joule-code-rs/crates/joule-moq-room/Cargo.lock +++ b/joule-code/joule-code-rs/crates/joule-moq-room/Cargo.lock @@ -2502,8 +2502,6 @@ checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "wtransport" version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4273ce3157a3262a68665f8d3f20a0ac0c5b8a69ffd67f05ae986832ebec036" dependencies = [ "bytes", "pem", diff --git a/joule-code/joule-code-rs/crates/joule-moq-room/Cargo.toml b/joule-code/joule-code-rs/crates/joule-moq-room/Cargo.toml index fc70ad04e..705d8e909 100644 --- a/joule-code/joule-code-rs/crates/joule-moq-room/Cargo.toml +++ b/joule-code/joule-code-rs/crates/joule-moq-room/Cargo.toml @@ -12,6 +12,11 @@ repository = "https://github.com/transaction-science/open-standards" # joule-code workspace. [workspace] +# The relay's wtransport, vendored with its WebTransport session close; the joule-code +# workspace patches it the same way (vendor/wtransport/VENDORED.md). +[patch.crates-io] +wtransport = { path = "../../vendor/wtransport" } + [lib] path = "src/lib.rs" diff --git a/joule-code/joule-code-rs/vendor/wtransport/.cargo_vcs_info.json b/joule-code/joule-code-rs/vendor/wtransport/.cargo_vcs_info.json new file mode 100644 index 000000000..f4bbaaed1 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "d022a6ad5f6260cb526e06d8ca42d09516ce6440" + }, + "path_in_vcs": "wtransport" +} \ No newline at end of file diff --git a/joule-code/joule-code-rs/vendor/wtransport/Cargo.lock b/joule-code/joule-code-rs/vendor/wtransport/Cargo.lock new file mode 100644 index 000000000..c74b349ab --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/Cargo.lock @@ -0,0 +1,1941 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-lc-fips-sys" +version = "0.13.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b00953a69b2cfb471d13d72538d2e66930832340b0f31deadd404b48c573c5" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", + "regex", +] + +[[package]] +name = "aws-lc-rs" +version = "1.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" +dependencies = [ + "aws-lc-fips-sys", + "aws-lc-sys", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.3.0", + "syn 2.0.119", +] + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex 2.0.1", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "rand_core", + "wasm-bindgen", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "httlib-huffman" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a9fcbcc408c5526c3ab80d534e5c86e7967c1fb7aa0a8c76abd1edc27deb877" + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "octets" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "866cb5af6f3aa3c1b44c3c2d79d22165fbb1b102e1b3fb499864bfe34736ec4b" + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rcgen" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustls" +version = "0.23.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +dependencies = [ + "aws-lc-rs", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wtransport" +version = "0.7.2" +dependencies = [ + "anyhow", + "axum", + "bytes", + "hyper", + "pem", + "quinn", + "rcgen", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "sha2", + "socket2", + "thiserror", + "time", + "tokio", + "tracing", + "tracing-subscriber", + "url", + "wtransport-proto", + "x509-parser", +] + +[[package]] +name = "wtransport-proto" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aad9059572c7dbd6901ccef37f3b7321678cd708dcf58a64b1921dbeab7bfede" +dependencies = [ + "httlib-huffman", + "octets", + "thiserror", + "url", +] + +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "aws-lc-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "yasna" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" +dependencies = [ + "bit-vec", + "time", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/joule-code/joule-code-rs/vendor/wtransport/Cargo.toml b/joule-code/joule-code-rs/vendor/wtransport/Cargo.toml new file mode 100644 index 000000000..5b8c6f268 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/Cargo.toml @@ -0,0 +1,214 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.88" +name = "wtransport" +version = "0.7.2" +authors = ["Biagio Festa"] +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Implementation of the WebTransport (over HTTP3) protocol" +readme = "README.md" +keywords = ["webtransport"] +categories = [ + "network-programming", + "asynchronous", +] +license = "MIT OR Apache-2.0" +repository = "https://github.com/BiagioFesta/wtransport" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = [ + "--cfg", + "docsrs", +] + +[package.metadata.cargo_check_external_types] +allowed_external_types = [ + "bytes::bytes::Bytes", + "quinn", + "quinn::connection::Connecting", + "quinn::connection::Connection", + "quinn::incoming::Incoming", + "quinn::recv_stream::ReadError", + "quinn::recv_stream::RecvStream", + "quinn::send_stream::SendStream", + "quinn::send_stream::WriteError", + "quinn_proto::config::ClientConfig", + "quinn_proto::config::EndpointConfig", + "quinn_proto::config::ServerConfig", + "quinn_proto::config::transport::TransportConfig", + "quinn_proto::connection::ConnectionError", + "rustls", + "rustls::client::client_conn::ClientConfig", + "rustls::server::server_conn::ServerConfig", + "rustls::verify::ServerCertVerifier", + "rustls::webpki::anchors::RootCertStore", + "time", + "time::Duration", + "time::offset_date_time::OffsetDateTime", + "tokio::io::async_read::AsyncRead", + "tokio::io::async_write::AsyncWrite", + "wtransport_proto", + "wtransport_proto::WEBTRANSPORT_ALPN", + "wtransport_proto::ids::SessionId", + "wtransport_proto::ids::StreamId", + "wtransport_proto::varint::VarInt", +] + +[features] +aws-lc-rs = [ + "quinn/aws-lc-rs", + "quinn/rustls-aws-lc-rs", + "rustls/aws-lc-rs", + "rcgen?/aws_lc_rs", +] +aws-lc-rs-fips = [ + "quinn/aws-lc-rs-fips", + "quinn/rustls-aws-lc-rs-fips", + "rustls/fips", + "rcgen?/fips", +] +dangerous-configuration = [] +default = [ + "self-signed", + "ring", +] +fips = ["aws-lc-rs-fips"] +quinn = [] +quinn-log = ["quinn/log"] +ring = [ + "quinn/ring", + "quinn/rustls", + "rustls/ring", + "rcgen?/ring", +] +self-signed = ["dep:rcgen"] + +[lib] +name = "wtransport" +path = "src/lib.rs" + +[[example]] +name = "client" +path = "examples/client.rs" +required-features = ["dangerous-configuration"] + +[[example]] +name = "full" +path = "examples/full.rs" +required-features = ["self-signed"] + +[[example]] +name = "gencert" +path = "examples/gencert.rs" +required-features = ["self-signed"] + +[[example]] +name = "server" +path = "examples/server.rs" +required-features = ["self-signed"] + +[dependencies.bytes] +version = "1.4.0" + +[dependencies.pem] +version = "3.0.4" + +[dependencies.quinn] +version = "0.11.6" +features = ["runtime-tokio"] +default-features = false + +[dependencies.rcgen] +version = "0.14.5" +optional = true +default-features = false + +[dependencies.rustls] +version = "0.23.23" +default-features = false + +[dependencies.rustls-native-certs] +version = "0.8.0" + +[dependencies.rustls-pki-types] +version = "1.13.2" + +[dependencies.sha2] +version = "0.11.0" + +[dependencies.socket2] +version = "0.6.3" + +[dependencies.thiserror] +version = "2.0.3" + +[dependencies.time] +version = "0.3.21" + +[dependencies.tokio] +version = "1.28.1" +features = [ + "macros", + "fs", + "io-util", +] +default-features = false + +[dependencies.tracing] +version = "0.1.37" + +[dependencies.url] +version = "2.4.0" + +[dependencies.wtransport-proto] +version = "0.7.2" +features = ["async"] + +[dependencies.x509-parser] +version = "0.18.1" + +[dev-dependencies.anyhow] +version = "1.0.71" + +[dev-dependencies.axum] +version = "0.7.1" + +[dev-dependencies.hyper] +version = "1.0.1" + +[dev-dependencies.tokio] +version = "1.28.1" +features = ["rt-multi-thread"] +default-features = false + +[dev-dependencies.tracing-subscriber] +version = "0.3.17" +features = ["env-filter"] + +[lints.clippy] +manual_is_variant_and = "warn" +manual_let_else = "warn" +unnecessary_cast = "deny" + +[lints.rust] +unfulfilled_lint_expectations = "deny" +unit_bindings = "deny" +unsafe_op_in_unsafe_fn = "deny" +unused_qualifications = "warn" diff --git a/joule-code/joule-code-rs/vendor/wtransport/Cargo.toml.orig b/joule-code/joule-code-rs/vendor/wtransport/Cargo.toml.orig new file mode 100644 index 000000000..7c7db7aa4 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/Cargo.toml.orig @@ -0,0 +1,105 @@ +[package] +name = "wtransport" +version = "0.7.2" +license = "MIT OR Apache-2.0" +authors = ["Biagio Festa"] +description = "Implementation of the WebTransport (over HTTP3) protocol" +repository = "https://github.com/BiagioFesta/wtransport" +keywords = ["webtransport"] +categories = [ "network-programming", "asynchronous" ] +edition = "2021" +readme = "../README.md" +workspace = ".." +rust-version = "1.88" + +[[example]] +name = "client" +required-features = ["dangerous-configuration"] + +[[example]] +name = "full" +required-features = ["self-signed"] + +[[example]] +name = "server" +required-features = ["self-signed"] + +[[example]] +name = "gencert" +required-features = ["self-signed"] + +[dependencies] +bytes = "1.4.0" +pem = "3.0.4" +quinn = { version = "0.11.6", default-features = false, features = ["runtime-tokio"] } +rcgen = { version = "0.14.5", default-features = false, optional = true } +rustls = { version = "0.23.23", default-features = false } +rustls-native-certs = "0.8.0" +rustls-pki-types = "1.13.2" +sha2 = "0.11.0" +socket2 = "0.6.3" +thiserror = "2.0.3" +time = "0.3.21" +tokio = { version = "1.28.1", default-features = false, features = ["macros", "fs", "io-util"] } +tracing = "0.1.37" +url = "2.4.0" +wtransport-proto = { version = "0.7.2", path = "../wtransport-proto", features = ["async"] } +x509-parser = "0.18.1" + +[dev-dependencies] +anyhow = "1.0.71" +axum = "0.7.1" +hyper = "1.0.1" +tokio = { version = "1.28.1", default-features = false, features = ["rt-multi-thread"] } +tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } + +[features] +default = ["self-signed", "ring"] +aws-lc-rs = ["quinn/aws-lc-rs", "quinn/rustls-aws-lc-rs", "rustls/aws-lc-rs", "rcgen?/aws_lc_rs"] +aws-lc-rs-fips = ["quinn/aws-lc-rs-fips", "quinn/rustls-aws-lc-rs-fips", "rustls/fips", "rcgen?/fips"] +dangerous-configuration = [] +fips = ["aws-lc-rs-fips"] +quinn = [] +quinn-log = ["quinn/log"] +ring = ["quinn/ring", "quinn/rustls", "rustls/ring", "rcgen?/ring"] +self-signed = ["dep:rcgen"] + +[lints] +workspace = true + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[package.metadata.cargo_check_external_types] +allowed_external_types = [ + "bytes::bytes::Bytes", + "quinn", + "quinn::connection::Connecting", + "quinn::connection::Connection", + "quinn::incoming::Incoming", + "quinn::recv_stream::ReadError", + "quinn::recv_stream::RecvStream", + "quinn::send_stream::SendStream", + "quinn::send_stream::WriteError", + "quinn_proto::config::ClientConfig", + "quinn_proto::config::EndpointConfig", + "quinn_proto::config::ServerConfig", + "quinn_proto::config::transport::TransportConfig", + "quinn_proto::connection::ConnectionError", + "rustls", + "rustls::client::client_conn::ClientConfig", + "rustls::server::server_conn::ServerConfig", + "rustls::verify::ServerCertVerifier", + "rustls::webpki::anchors::RootCertStore", + "time", + "time::Duration", + "time::offset_date_time::OffsetDateTime", + "tokio::io::async_read::AsyncRead", + "tokio::io::async_write::AsyncWrite", + "wtransport_proto", + "wtransport_proto::WEBTRANSPORT_ALPN", + "wtransport_proto::ids::SessionId", + "wtransport_proto::ids::StreamId", + "wtransport_proto::varint::VarInt", +] diff --git a/joule-code/joule-code-rs/vendor/wtransport/LICENSE-APACHE b/joule-code/joule-code-rs/vendor/wtransport/LICENSE-APACHE new file mode 100644 index 000000000..d9a10c0d8 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://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 diff --git a/joule-code/joule-code-rs/vendor/wtransport/LICENSE-MIT b/joule-code/joule-code-rs/vendor/wtransport/LICENSE-MIT new file mode 100644 index 000000000..2070cd3d0 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Biagio Festa + +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. diff --git a/joule-code/joule-code-rs/vendor/wtransport/README.md b/joule-code/joule-code-rs/vendor/wtransport/README.md new file mode 100644 index 000000000..5db50f419 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/README.md @@ -0,0 +1,139 @@ +

+ WTransport Logo +

+ +[![Documentation](https://docs.rs/wtransport/badge.svg)](https://docs.rs/wtransport/) +[![Crates.io](https://img.shields.io/crates/v/wtransport.svg)](https://crates.io/crates/wtransport) +[![CI](https://github.com/BiagioFesta/wtransport/actions/workflows/ci.yml/badge.svg)](https://github.com/BiagioFesta/wtransport/actions/workflows/ci.yml) +[![Chat](https://img.shields.io/badge/chat-join-blue?logo=discord)](https://discord.gg/KPrrbWe5zg) + +# WTransport +[WebTransport](https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3/) protocol, pure-*rust*, *async*-friendly. + +## Introduction + +WebTransport is a new protocol being developed to enable *low-latency*, *bidirectional* communication between clients and servers over the web. +It aims to address the limitations of existing protocols like *HTTP* and *WebSocket* by offering a more *efficient* and *flexible* transport layer. + +### Benefits of WebTransport +* 🚀 **Low latency**: WebTransport is designed to minimize latency, making it suitable for real-time applications such as gaming, video streaming, and collaborative editing. +* 🔄 **Bidirectional communication**: WebTransport allows simultaneous data exchange between the client and server, enabling efficient back-and-forth communication without the need for multiple requests. +* 🔀 **Multiplexing**: With WebTransport, multiple streams can be multiplexed over a single connection, reducing overhead and improving performance. +* 🔒 **Security**: WebTransport benefits from the security features provided by the web platform, including transport encryption and same-origin policy. +* 🌐 **Native Browser Support**: WebTransport is natively supported in modern web browsers, ensuring seamless integration and enhanced performance for web applications. + +

+ Check Library Documentation +

+ +### Notes +Please be aware that WebTransport is still a *draft* and not yet standardized. +The *WTransport* library, while functional, is not considered completely production-ready. +It should be used with caution and may undergo changes as the WebTransport specification evolves. + +## Simple API + + + + + + + + + +
Server Client
+ +```rust +#[tokio::main] +async fn main() -> Result<()> { + let config = ServerConfig::builder() + .with_bind_default(4433) + .with_identity(&identity) + .build(); + + let connection = Endpoint::server(config)? + .accept() + .await // Awaits connection + .await? // Awaits session request + .accept() // Accepts request + .await?; // Awaits ready session + + let stream = connection.accept_bi().await?; + // ... +} +``` + + + +```rust +#[tokio::main] +async fn main() -> Result<()> { + let config = ClientConfig::default(); + + let connection = Endpoint::client(config)? + .connect("https://[::1]:4433") + .await?; + + let stream = connection.open_bi().await?.await?; + // ... +} +``` + +
+ +## Browser Integration +WebTransport [is supported](https://caniuse.com/mdn-api_webtransport) in modern browsers, +enhancing the capabilities of web applications. + +For instance, you can create a native *browser WebTransport* client connecting to a *Rust +server* using the following JavaScript code: + +```javascript +// Create a WebTransport instance connecting to the Rust server +let transport = new WebTransport('https://[::1]:4433'); +await transport.ready; + +// Create a bidirectional stream +let stream = await transport.createBidirectionalStream(); + +// Send data from the client to the server +await stream.writable.getWriter().write(new TextEncoder().encode("hello")); + +// Read data reply from the server +let data = await stream.readable.getReader().read(); +console.log(data); +``` + +Check out the [W3C WebTransport API documentation](https://w3c.github.io/webtransport/) for more details and to +explore the full capabilities of WebTransport in the browser. + +## Getting Started +### Clone the Repository +```bash +git clone https://github.com/BiagioFesta/wtransport.git +``` +```bash +cd wtransport/ +``` + +### Run `Full` Example + +The [`examples/full.rs`](wtransport/examples/full.rs) is a minimal but complete server example that demonstrates the usage of WebTransport. + +You can run this example using [*Cargo*](https://rustup.rs/), Rust's package manager, with the following command: +```bash +cargo run --example full +``` + +This example initiates an *echo* WebTransport server that can receive messages. It also includes an integrated HTTP server. + +Open [a supported web browser](https://caniuse.com/mdn-api_webtransport) and navigate to the page http://127.0.0.1:8080. + +## Examples +* [Local Examples](https://github.com/BiagioFesta/wtransport/tree/master/wtransport/examples) + +## Other Languages + +WTransport has bindings for the following languages: + +- Elixir: [wtransport-elixir](https://github.com/bugnano/wtransport-elixir) diff --git a/joule-code/joule-code-rs/vendor/wtransport/examples/client.rs b/joule-code/joule-code-rs/vendor/wtransport/examples/client.rs new file mode 100644 index 000000000..ed8450737 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/examples/client.rs @@ -0,0 +1,20 @@ +use wtransport::ClientConfig; +use wtransport::Endpoint; + +#[tokio::main] +async fn main() { + let config = ClientConfig::builder() + .with_bind_default() + .with_no_cert_validation() + .build(); + + let connection = Endpoint::client(config) + .unwrap() + .connect("https://[::1]:4433") + .await + .unwrap(); + + let mut stream = connection.open_bi().await.unwrap().await.unwrap(); + stream.0.write_all(b"HELLO").await.unwrap(); + stream.0.finish().await.unwrap(); +} diff --git a/joule-code/joule-code-rs/vendor/wtransport/examples/full.rs b/joule-code/joule-code-rs/vendor/wtransport/examples/full.rs new file mode 100644 index 000000000..fef22bd0e --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/examples/full.rs @@ -0,0 +1,558 @@ +use anyhow::Context; +use anyhow::Result; +use http::HttpServer; +use tracing::error; +use tracing::info; +use tracing::info_span; +use tracing::Instrument; +use webtransport::WebTransportServer; +use wtransport::tls::Sha256Digest; +use wtransport::tls::Sha256DigestFmt; +use wtransport::Identity; + +#[tokio::main] +async fn main() -> Result<()> { + utils::init_logging(); + + let identity = Identity::self_signed(["localhost", "127.0.0.1", "::1"]).unwrap(); + let cert_digest = identity.certificate_chain().as_slice()[0].hash(); + + let webtransport_server = WebTransportServer::new(identity)?; + let http_server = HttpServer::new(&cert_digest, webtransport_server.local_port()).await?; + + info!( + "Open the browser and go to: http://127.0.0.1:{}", + http_server.local_port() + ); + + tokio::select! { + result = http_server.serve() => { + error!("HTTP server: {:?}", result); + } + result = webtransport_server.serve() => { + error!("WebTransport server: {:?}", result); + } + } + + Ok(()) +} + +mod webtransport { + use super::*; + use std::time::Duration; + use wtransport::endpoint::endpoint_side::Server; + use wtransport::endpoint::IncomingSession; + use wtransport::Endpoint; + use wtransport::ServerConfig; + + pub struct WebTransportServer { + endpoint: Endpoint, + } + + impl WebTransportServer { + pub fn new(identity: Identity) -> Result { + let config = ServerConfig::builder() + .with_bind_default(0) + .with_identity(identity) + .keep_alive_interval(Some(Duration::from_secs(3))) + .build(); + + let endpoint = Endpoint::server(config)?; + + Ok(Self { endpoint }) + } + + pub fn local_port(&self) -> u16 { + self.endpoint.local_addr().unwrap().port() + } + + pub async fn serve(self) -> Result<()> { + info!("Server running on port {}", self.local_port()); + + for id in 0.. { + let incoming_session = self.endpoint.accept().await; + + tokio::spawn( + Self::handle_incoming_session(incoming_session) + .instrument(info_span!("Connection", id)), + ); + } + + Ok(()) + } + + async fn handle_incoming_session(incoming_session: IncomingSession) { + async fn handle_incoming_session_impl(incoming_session: IncomingSession) -> Result<()> { + let mut buffer = vec![0; 65536].into_boxed_slice(); + + info!("Waiting for session request..."); + + let session_request = incoming_session.await?; + + info!( + "New session: Authority: '{}', Path: '{}'", + session_request.authority(), + session_request.path() + ); + + let connection = session_request.accept().await?; + + info!("Waiting for data from client..."); + + loop { + tokio::select! { + stream = connection.accept_bi() => { + let mut stream = stream?; + info!("Accepted BI stream"); + + let Some(bytes_read) = stream.1.read(&mut buffer).await? else { + continue; + }; + + let str_data = std::str::from_utf8(&buffer[..bytes_read])?; + + info!("Received (bi) '{str_data}' from client"); + + stream.0.write_all(b"ACK").await?; + } + stream = connection.accept_uni() => { + let mut stream = stream?; + info!("Accepted UNI stream"); + + let Some(bytes_read) = stream.read(&mut buffer).await? else { + continue; + }; + + let str_data = std::str::from_utf8(&buffer[..bytes_read])?; + + info!("Received (uni) '{str_data}' from client"); + + let mut stream = connection.open_uni().await?.await?; + stream.write_all(b"ACK").await?; + } + dgram = connection.receive_datagram() => { + let dgram = dgram?; + let str_data = std::str::from_utf8(&dgram)?; + + info!("Received (dgram) '{str_data}' from client"); + + connection.send_datagram(b"ACK")?; + } + } + } + } + + let result = handle_incoming_session_impl(incoming_session).await; + info!("Result: {:?}", result); + } + } +} + +mod http { + use super::*; + use axum::http::header::CONTENT_TYPE; + use axum::response::Html; + use axum::routing::get; + use axum::serve; + use axum::serve::Serve; + use axum::Router; + use std::net::Ipv4Addr; + use std::net::SocketAddr; + use tokio::net::TcpListener; + + pub struct HttpServer { + serve: Serve, + local_port: u16, + } + + impl HttpServer { + const PORT: u16 = 8080; + + pub async fn new(cert_digest: &Sha256Digest, webtransport_port: u16) -> Result { + let router = Self::build_router(cert_digest, webtransport_port); + + let listener = + TcpListener::bind(SocketAddr::new(Ipv4Addr::LOCALHOST.into(), Self::PORT)) + .await + .context("Cannot bind TCP listener for HTTP server")?; + + let local_port = listener + .local_addr() + .context("Cannot get local port")? + .port(); + + Ok(HttpServer { + serve: serve(listener, router), + local_port, + }) + } + + pub fn local_port(&self) -> u16 { + self.local_port + } + + pub async fn serve(self) -> Result<()> { + info!("Server running on port {}", self.local_port()); + + self.serve.await.context("HTTP server error")?; + + Ok(()) + } + + fn build_router(cert_digest: &Sha256Digest, webtransport_port: u16) -> Router { + let cert_digest = cert_digest.fmt(Sha256DigestFmt::BytesArray); + + let root = move || async move { + Html( + http_data::INDEX_DATA + .replace("${WEBTRANSPORT_PORT}", &webtransport_port.to_string()), + ) + }; + + let style = + move || async move { ([(CONTENT_TYPE, "text/css")], http_data::STYLE_DATA) }; + + let client = move || async move { + ( + [(CONTENT_TYPE, "application/javascript")], + http_data::CLIENT_DATA.replace("${CERT_DIGEST}", &cert_digest), + ) + }; + + Router::new() + .route("/", get(root)) + .route("/style.css", get(style)) + .route("/client.js", get(client)) + } + } +} + +mod utils { + use tracing_subscriber::filter::LevelFilter; + use tracing_subscriber::EnvFilter; + + pub fn init_logging() { + let env_filter = EnvFilter::builder() + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(); + + tracing_subscriber::fmt() + .with_target(true) + .with_level(true) + .with_env_filter(env_filter) + .init(); + } +} + +mod http_data { + + pub const INDEX_DATA: &str = r#" + + + WTransport-Example + + + + + + +

WTransport Example

+ +
+

Establish WebTransport connection

+
+ + + +
+
+ +
+

Send data over WebTransport

+
+ +
+ + +
+
+ + +
+
+ + +
+ +
+
+ +
+

Event log

+
    +
+
+ + + +"#; + + pub const STYLE_DATA: &str = r#" +body { + font-family: sans-serif; +} + +h1 { + margin: 0 auto; + width: fit-content; +} + +h2 { + border-bottom: 1px dotted #333; + font-size: 120%; + font-weight: normal; + padding-bottom: 0.2em; + padding-top: 0.5em; +} + +code { + background-color: #eee; +} + +input[type=text], textarea { + font-family: monospace; +} + +#top { + display: flex; + flex-direction: row-reverse; + flex-wrap: wrap; + justify-content: center; +} + +#explanation { + border: 1px dotted black; + font-size: 90%; + height: fit-content; + margin-bottom: 1em; + padding: 1em; + width: 13em; +} + +#tool { + flex-grow: 1; + margin: 0 auto; + max-width: 26em; + padding: 0 1em; + width: 26em; +} + +.input-line { + display: flex; +} + +.input-line input[type=text] { + flex-grow: 1; + margin: 0 0.5em; +} + +textarea { + height: 3em; + width: 100%; +} + +#send { + margin-top: 0.5em; + width: 15em; +} + +#event-log { + border: 1px dotted black; + font-family: monospace; + height: 12em; + overflow: scroll; + padding-bottom: 1em; + padding-top: 1em; +} + +.log-error { + color: darkred; +} + +#explanation ul { + padding-left: 1em; +} +"#; + + pub const CLIENT_DATA: &str = r#" +// Adds an entry to the event log on the page, optionally applying a specified +// CSS class. + +const HASH = new Uint8Array(${CERT_DIGEST}); + +let currentTransport, streamNumber, currentTransportDatagramWriter; + +// "Connect" button handler. +async function connect() { + const url = document.getElementById('url').value; + try { + var transport = new WebTransport(url, { serverCertificateHashes: [ { algorithm: "sha-256", value: HASH.buffer } ] } ); + addToEventLog('Initiating connection...'); + } catch (e) { + addToEventLog('Failed to create connection object. ' + e, 'error'); + return; + } + + try { + await transport.ready; + addToEventLog('Connection ready.'); + } catch (e) { + addToEventLog('Connection failed. ' + e, 'error'); + return; + } + + transport.closed + .then(() => { + addToEventLog('Connection closed normally.'); + }) + .catch(() => { + addToEventLog('Connection closed abruptly.', 'error'); + }); + + currentTransport = transport; + streamNumber = 1; + try { + currentTransportDatagramWriter = transport.datagrams.writable.getWriter(); + addToEventLog('Datagram writer ready.'); + } catch (e) { + addToEventLog('Sending datagrams not supported: ' + e, 'error'); + return; + } + readDatagrams(transport); + acceptUnidirectionalStreams(transport); + document.forms.sending.elements.send.disabled = false; + document.getElementById('connect').disabled = true; +} + +// "Send data" button handler. +async function sendData() { + let form = document.forms.sending.elements; + let encoder = new TextEncoder('utf-8'); + let rawData = sending.data.value; + let data = encoder.encode(rawData); + let transport = currentTransport; + try { + switch (form.sendtype.value) { + case 'datagram': + await currentTransportDatagramWriter.write(data); + addToEventLog('Sent datagram: ' + rawData); + break; + case 'unidi': { + let stream = await transport.createUnidirectionalStream(); + let writer = stream.getWriter(); + await writer.write(data); + await writer.close(); + addToEventLog('Sent a unidirectional stream with data: ' + rawData); + break; + } + case 'bidi': { + let stream = await transport.createBidirectionalStream(); + let number = streamNumber++; + readFromIncomingStream(stream.readable, number); + + let writer = stream.writable.getWriter(); + await writer.write(data); + await writer.close(); + addToEventLog( + 'Opened bidirectional stream #' + number + + ' with data: ' + rawData); + break; + } + } + } catch (e) { + addToEventLog('Error while sending data: ' + e, 'error'); + } +} + +// Reads datagrams from |transport| into the event log until EOF is reached. +async function readDatagrams(transport) { + try { + var reader = transport.datagrams.readable.getReader(); + addToEventLog('Datagram reader ready.'); + } catch (e) { + addToEventLog('Receiving datagrams not supported: ' + e, 'error'); + return; + } + let decoder = new TextDecoder('utf-8'); + try { + while (true) { + const { value, done } = await reader.read(); + if (done) { + addToEventLog('Done reading datagrams!'); + return; + } + let data = decoder.decode(value); + addToEventLog('Datagram received: ' + data); + } + } catch (e) { + addToEventLog('Error while reading datagrams: ' + e, 'error'); + } +} + +async function acceptUnidirectionalStreams(transport) { + let reader = transport.incomingUnidirectionalStreams.getReader(); + try { + while (true) { + const { value, done } = await reader.read(); + if (done) { + addToEventLog('Done accepting unidirectional streams!'); + return; + } + let stream = value; + let number = streamNumber++; + addToEventLog('New incoming unidirectional stream #' + number); + readFromIncomingStream(stream, number); + } + } catch (e) { + addToEventLog('Error while accepting streams: ' + e, 'error'); + } +} + +async function readFromIncomingStream(stream, number) { + let decoder = new TextDecoderStream('utf-8'); + let reader = stream.pipeThrough(decoder).getReader(); + try { + while (true) { + const { value, done } = await reader.read(); + if (done) { + addToEventLog('Stream #' + number + ' closed'); + return; + } + let data = value; + addToEventLog('Received data on stream #' + number + ': ' + data); + } + } catch (e) { + addToEventLog( + 'Error while reading from stream #' + number + ': ' + e, 'error'); + addToEventLog(' ' + e.message); + } +} + +function addToEventLog(text, severity = 'info') { + let log = document.getElementById('event-log'); + let mostRecentEntry = log.lastElementChild; + let entry = document.createElement('li'); + entry.innerText = text; + entry.className = 'log-' + severity; + log.appendChild(entry); + + // If the most recent entry in the log was visible, scroll the log to the + // newly added element. + if (mostRecentEntry != null && + mostRecentEntry.getBoundingClientRect().top < + log.getBoundingClientRect().bottom) { + entry.scrollIntoView(); + } +} +"#; +} diff --git a/joule-code/joule-code-rs/vendor/wtransport/examples/gencert.rs b/joule-code/joule-code-rs/vendor/wtransport/examples/gencert.rs new file mode 100644 index 000000000..3e48e30a6 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/examples/gencert.rs @@ -0,0 +1,45 @@ +use anyhow::Context; +use anyhow::Result; +use wtransport::tls::Sha256DigestFmt; +use wtransport::Identity; + +const CERT_FILE: &str = "cert.pem"; +const KEY_FILE: &str = "key.pem"; + +#[tokio::main] +async fn main() -> Result<()> { + println!("Generating self signed certificate for WebTransport"); + + let identity = + Identity::self_signed(["localhost"]).context("cannot create self signed identity")?; + + println!("Storing certificate to file: '{CERT_FILE}'"); + + identity + .certificate_chain() + .store_pemfile(CERT_FILE) + .await + .context("cannot store certificate chain")?; + + println!("Storing private key to file: '{KEY_FILE}'"); + + identity + .private_key() + .store_secret_pemfile(KEY_FILE) + .await + .context("cannot store private key")?; + + println!( + "Certificate serial: {}", + identity.certificate_chain().as_slice()[0].serial() + ); + + println!( + "Certificate hash: {}", + identity.certificate_chain().as_slice()[0] + .hash() + .fmt(Sha256DigestFmt::BytesArray) + ); + + Ok(()) +} diff --git a/joule-code/joule-code-rs/vendor/wtransport/examples/server.rs b/joule-code/joule-code-rs/vendor/wtransport/examples/server.rs new file mode 100644 index 000000000..212bed68c --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/examples/server.rs @@ -0,0 +1,111 @@ +use anyhow::Result; +use std::time::Duration; +use tracing::error; +use tracing::info; +use tracing::info_span; +use tracing::Instrument; +use tracing_subscriber::filter::LevelFilter; +use tracing_subscriber::EnvFilter; +use wtransport::endpoint::IncomingSession; +use wtransport::Endpoint; +use wtransport::Identity; +use wtransport::ServerConfig; + +#[tokio::main] +async fn main() -> Result<()> { + init_logging(); + + let config = ServerConfig::builder() + .with_bind_default(4433) + .with_identity(Identity::self_signed(["localhost"]).unwrap()) + .keep_alive_interval(Some(Duration::from_secs(3))) + .build(); + + let server = Endpoint::server(config)?; + + info!("Server ready!"); + + for id in 0.. { + let incoming_session = server.accept().await; + tokio::spawn(handle_connection(incoming_session).instrument(info_span!("Connection", id))); + } + + Ok(()) +} + +async fn handle_connection(incoming_session: IncomingSession) { + let result = handle_connection_impl(incoming_session).await; + error!("{:?}", result); +} + +async fn handle_connection_impl(incoming_session: IncomingSession) -> Result<()> { + let mut buffer = vec![0; 65536].into_boxed_slice(); + + info!("Waiting for session request..."); + + let session_request = incoming_session.await?; + + info!( + "New session: Authority: '{}', Path: '{}'", + session_request.authority(), + session_request.path() + ); + + let connection = session_request.accept().await?; + + info!("Waiting for data from client..."); + + loop { + tokio::select! { + stream = connection.accept_bi() => { + let mut stream = stream?; + info!("Accepted BI stream"); + + let Some(bytes_read) = stream.1.read(&mut buffer).await? else { + continue; + }; + + let str_data = std::str::from_utf8(&buffer[..bytes_read])?; + + info!("Received (bi) '{str_data}' from client"); + + stream.0.write_all(b"ACK").await?; + } + stream = connection.accept_uni() => { + let mut stream = stream?; + info!("Accepted UNI stream"); + + let Some(bytes_read) = stream.read(&mut buffer).await? else { + continue; + }; + + let str_data = std::str::from_utf8(&buffer[..bytes_read])?; + + info!("Received (uni) '{str_data}' from client"); + + let mut stream = connection.open_uni().await?.await?; + stream.write_all(b"ACK").await?; + } + dgram = connection.receive_datagram() => { + let dgram = dgram?; + let str_data = std::str::from_utf8(&dgram)?; + + info!("Received (dgram) '{str_data}' from client"); + + connection.send_datagram(b"ACK")?; + } + } + } +} + +fn init_logging() { + let env_filter = EnvFilter::builder() + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(); + + tracing_subscriber::fmt() + .with_target(true) + .with_level(true) + .with_env_filter(env_filter) + .init(); +} diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/config.rs b/joule-code/joule-code-rs/vendor/wtransport/src/config.rs new file mode 100644 index 000000000..12112d195 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/src/config.rs @@ -0,0 +1,1287 @@ +//! +//! This module defines configurations for the WebTransport server and client. +//! +//! It provides builders for creating server and client configurations with various options. +//! +//! The module includes: +//! - [`ServerConfig`]: Configuration for the WebTransport server. +//! - [`ClientConfig`]: Configuration for the WebTransport client. +//! +//! Example for creating a server configuration: +//! +//! ```no_run +//! # async fn run() -> anyhow::Result<()> { +//! use wtransport::Identity; +//! use wtransport::ServerConfig; +//! +//! let server_config = ServerConfig::builder() +//! .with_bind_default(443) +//! .with_identity(Identity::load_pemfiles("cert.pem", "key.pem").await?) +//! .build(); +//! +//! # Ok(()) +//! # } +//! ``` +//! +//! Example for creating a client configuration: +//! +//! ```no_run +//! use wtransport::ClientConfig; +//! +//! let client_config = ClientConfig::builder() +//! .with_bind_default() +//! .with_native_certs() +//! .build(); +//! ``` + +use crate::tls::build_native_cert_store; +use crate::tls::Identity; +use socket2::Domain as SocketDomain; +use socket2::Protocol as SocketProtocol; +use socket2::Socket; +use socket2::Type as SocketType; +use std::fmt::Debug; +use std::fmt::Display; +use std::future::Future; +use std::net::IpAddr; +use std::net::Ipv4Addr; +use std::net::Ipv6Addr; +use std::net::SocketAddr; +use std::net::SocketAddrV4; +use std::net::SocketAddrV6; +use std::net::UdpSocket; +use std::pin::Pin; +use std::sync::Arc; +use std::time::Duration; + +/// Alias of [`crate::tls::rustls::ServerConfig`]. +pub type TlsServerConfig = rustls::ServerConfig; + +/// Alias of [`crate::tls::rustls::ClientConfig`]. +pub type TlsClientConfig = rustls::ClientConfig; + +/// Alias of [`crate::quinn::TransportConfig`]. +#[cfg(feature = "quinn")] +#[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] +pub type QuicTransportConfig = quinn::TransportConfig; + +/// Alias of [`crate::quinn::ServerConfig`]. +#[cfg(feature = "quinn")] +#[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] +pub type QuicServerConfig = quinn::ServerConfig; + +/// Alias of [`crate::quinn::ClientConfig`]. +#[cfg(feature = "quinn")] +#[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] +pub type QuicClientConfig = quinn::ClientConfig; + +/// Configuration for IP address socket bind. +#[derive(Debug, Copy, Clone)] +pub enum IpBindConfig { + /// Bind to LOCALHOST IPv4 address (no IPv6). + LocalV4, + + /// Bind to LOCALHOST IPv6 address (no IPv4). + LocalV6, + + /// Bind to LOCALHOST both IPv4 and IPv6 address (dual stack, if supported). + LocalDual, + + /// Bind to `INADDR_ANY` IPv4 address (no IPv6). + InAddrAnyV4, + + /// Bind to `INADDR_ANY` IPv6 address (no IPv4). + InAddrAnyV6, + + /// Bind to `INADDR_ANY` both IPv4 and IPv6 address (dual stack, if supported). + InAddrAnyDual, +} + +impl IpBindConfig { + fn into_ip(self) -> IpAddr { + match self { + IpBindConfig::LocalV4 => Ipv4Addr::LOCALHOST.into(), + IpBindConfig::LocalV6 => Ipv6Addr::LOCALHOST.into(), + IpBindConfig::LocalDual => Ipv6Addr::LOCALHOST.into(), + IpBindConfig::InAddrAnyV4 => Ipv4Addr::UNSPECIFIED.into(), + IpBindConfig::InAddrAnyV6 => Ipv6Addr::UNSPECIFIED.into(), + IpBindConfig::InAddrAnyDual => Ipv6Addr::UNSPECIFIED.into(), + } + } + + fn into_dual_stack_config(self) -> Ipv6DualStackConfig { + match self { + IpBindConfig::LocalV4 | IpBindConfig::InAddrAnyV4 => Ipv6DualStackConfig::OsDefault, + IpBindConfig::LocalV6 | IpBindConfig::InAddrAnyV6 => Ipv6DualStackConfig::Deny, + IpBindConfig::LocalDual | IpBindConfig::InAddrAnyDual => Ipv6DualStackConfig::Allow, + } + } +} + +/// Configuration for IPv6 dual stack. +#[derive(Debug, Copy, Clone)] +pub enum Ipv6DualStackConfig { + /// Do not configure dual stack. Use OS's default. + OsDefault, + + /// Deny dual stack. This is equivalent to `IPV6_V6ONLY`. + /// + /// Socket will only bind for IPv6 (IPv4 port will still be available). + Deny, + + /// Allow dual stack. + /// + /// Please note that not all configurations/platforms support dual stack. + Allow, +} + +/// Invalid idle timeout. +pub struct InvalidIdleTimeout; + +/// Server configuration. +/// +/// You can create an instance of `ServerConfig` using its builder pattern by calling +/// the [`builder()`](Self::builder) method. +/// Once you have an instance, you can further customize it by chaining method calls +/// to set various configuration options. +/// +/// ## Configuration Builder States +/// +/// The configuration process follows a *state-based builder pattern*, where the server +/// configuration progresses through *3* states. +/// +/// ### 1. `WantsBindAddress` +/// +/// The caller must supply a binding address for the server. This is where to specify +/// the listening port of the server. +/// The following options are mutually exclusive: +/// +/// - [`with_bind_default`](ServerConfigBuilder::with_bind_default): the simplest +/// configuration where only the port will be specified. +/// - [`with_bind_config`](ServerConfigBuilder::with_bind_config): configures +/// binding to an address determined by a configuration preset. +/// - [`with_bind_address`](ServerConfigBuilder::with_bind_address): configures +/// binding to a custom-specified socket address. +/// - [`with_bind_address_v6`](ServerConfigBuilder::with_bind_address_v6): configures +/// binding to a custom-specified socket address for *IPv6*, along with the [dual stack +/// configuration](Ipv6DualStackConfig). +/// - [`with_bind_socket`](ServerConfigBuilder::with_bind_socket): configures +/// binding directly to a custom-specified socket. +/// +/// Only one of these options can be selected during the client configuration process. +/// +/// #### Examples: +/// +/// ``` +/// use wtransport::ServerConfig; +/// +/// // Configuration for accepting incoming connection on port 443 +/// ServerConfig::builder().with_bind_default(443); +/// ``` +/// +/// ### 2. `WantsIdentity` +/// +/// The caller must supply a TLS identity for the server. +/// +/// - [`with_identity`](ServerConfigBuilder::with_identity): configures +/// a TLS [`Identity`] for the server. +/// - [`with_custom_tls`](ServerConfigBuilder::with_custom_tls): sets the TLS +/// server configuration manually. +/// - [`with_custom_transport`](ServerConfigBuilder::with_custom_transport): sets the QUIC +/// transport configuration manually (using default TLS). +/// - [`with_custom_tls_and_transport`](ServerConfigBuilder::with_custom_tls_and_transport): sets both +/// a custom TLS and QUIC transport configuration. +/// - [`build_with_quic_config`](ServerConfigBuilder::build_with_quic_config): directly builds +/// [`ServerConfig`] providing both TLS and QUIC transport configuration given by +/// [`quic_config`](QuicServerConfig). +/// +/// #### Examples: +/// ``` +/// # use anyhow::Result; +/// use wtransport::Identity; +/// use wtransport::ServerConfig; +/// +/// # async fn run() -> Result<()> { +/// ServerConfig::builder() +/// .with_bind_default(443) +/// .with_identity(Identity::load_pemfiles("cert.pem", "key.pem").await?); +/// # Ok(()) +/// # } +/// ``` +/// +/// ### 3. `WantsTransportConfigServer` +/// +/// The caller can supply *additional* transport configurations. +/// Multiple options can be given at this stage. Once the configuration is completed, it is possible +/// to finalize with the method [`build()`](ServerConfigBuilder::build). +/// +/// All these options can be omitted in the configuration; default values will be used. +/// +/// - [`max_idle_timeout`](ServerConfigBuilder::max_idle_timeout) +/// - [`keep_alive_interval`](ServerConfigBuilder::keep_alive_interval) +/// - [`allow_migration`](ServerConfigBuilder::allow_migration) +/// +/// #### Examples: +/// ``` +/// # use anyhow::Result; +/// use wtransport::ServerConfig; +/// use wtransport::Identity; +/// use std::time::Duration; +/// +/// # async fn run() -> Result<()> { +/// let server_config = ServerConfig::builder() +/// .with_bind_default(443) +/// .with_identity(Identity::load_pemfiles("cert.pem", "key.pem").await?) +/// .keep_alive_interval(Some(Duration::from_secs(3))) +/// .build(); +/// # Ok(()) +/// # } +#[derive(Debug)] +pub struct ServerConfig { + pub(crate) bind_address_config: BindAddressConfig, + pub(crate) endpoint_config: quinn::EndpointConfig, + pub(crate) quic_config: quinn::ServerConfig, +} + +impl ServerConfig { + /// Creates a builder to build up the server configuration. + /// + /// For more information, see the [`ServerConfigBuilder`] documentation. + pub fn builder() -> ServerConfigBuilder { + ServerConfigBuilder::default() + } + + /// Returns a reference to the inner QUIC endpoint configuration. + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn quic_endpoint_config(&self) -> &quinn::EndpointConfig { + &self.endpoint_config + } + + /// Returns a mutable reference to the inner QUIC endpoint configuration. + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn quic_endpoint_config_mut(&mut self) -> &mut quinn::EndpointConfig { + &mut self.endpoint_config + } + + /// Returns a reference to the inner QUIC configuration. + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn quic_config(&self) -> &quinn::ServerConfig { + &self.quic_config + } + + /// Returns a mutable reference to the inner QUIC configuration. + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn quic_config_mut(&mut self) -> &mut quinn::ServerConfig { + &mut self.quic_config + } +} + +/// Server builder configuration. +/// +/// The builder might have different state at compile time. +/// +/// # Examples: +/// ```no_run +/// # async fn run() -> anyhow::Result<()> { +/// # use std::net::Ipv4Addr; +/// # use std::net::SocketAddr; +/// # use wtransport::Identity; +/// # use wtransport::ServerConfig; +/// let config = ServerConfig::builder() +/// .with_bind_default(4433) +/// .with_identity(Identity::load_pemfiles("cert.pem", "key.pem").await?); +/// # Ok(()) +/// # } +/// ``` +#[must_use] +pub struct ServerConfigBuilder(State); + +impl ServerConfigBuilder { + /// Configures for accepting incoming connections binding ANY IP (allowing IP dual-stack). + /// + /// `listening_port` is the port where the server will accept incoming connections. + /// + /// This is equivalent to: [`Self::with_bind_config`] with [`IpBindConfig::InAddrAnyDual`]. + pub fn with_bind_default( + self, + listening_port: u16, + ) -> ServerConfigBuilder { + self.with_bind_config(IpBindConfig::InAddrAnyDual, listening_port) + } + + /// Sets the binding (local) socket address with a specific [`IpBindConfig`]. + /// + /// `listening_port` is the port where the server will accept incoming connections. + pub fn with_bind_config( + self, + ip_bind_config: IpBindConfig, + listening_port: u16, + ) -> ServerConfigBuilder { + let ip_address: IpAddr = ip_bind_config.into_ip(); + + match ip_address { + IpAddr::V4(ip) => self.with_bind_address(SocketAddr::new(ip.into(), listening_port)), + IpAddr::V6(ip) => self.with_bind_address_v6( + SocketAddrV6::new(ip, listening_port, 0, 0), + ip_bind_config.into_dual_stack_config(), + ), + } + } + + /// Sets the binding (local) socket address for the endpoint. + pub fn with_bind_address( + self, + address: SocketAddr, + ) -> ServerConfigBuilder { + ServerConfigBuilder(states::WantsIdentity { + bind_address_config: BindAddressConfig::from(address), + }) + } + + /// Sets the binding (local) socket address for the endpoint with Ipv6 address. + /// + /// `dual_stack_config` allows/denies dual stack port binding. + pub fn with_bind_address_v6( + self, + address: SocketAddrV6, + dual_stack_config: Ipv6DualStackConfig, + ) -> ServerConfigBuilder { + ServerConfigBuilder(states::WantsIdentity { + bind_address_config: BindAddressConfig::AddressV6(address, dual_stack_config), + }) + } + + /// Configures the server to bind to a pre-existing [`UdpSocket`]. + /// + /// This allows the server to use an already created socket, which may be beneficial + /// for scenarios where socket reuse or specific socket configuration is needed. + pub fn with_bind_socket(self, socket: UdpSocket) -> ServerConfigBuilder { + ServerConfigBuilder(states::WantsIdentity { + bind_address_config: BindAddressConfig::Socket(socket), + }) + } +} + +impl ServerConfigBuilder { + /// Configures TLS with safe defaults and a TLS [`Identity`]. + /// + /// # Example + /// ```no_run + /// use wtransport::Identity; + /// use wtransport::ServerConfig; + /// # use anyhow::Result; + /// + /// # async fn run() -> Result<()> { + /// let identity = Identity::load_pemfiles("cert.pem", "key.pem").await?; + /// + /// let server_config = ServerConfig::builder() + /// .with_bind_default(4433) + /// .with_identity(identity) + /// .build(); + /// # Ok(()) + /// # } + /// ``` + pub fn with_identity( + self, + identity: Identity, + ) -> ServerConfigBuilder { + use crate::tls::server::build_default_tls_config; + + let tls_config = build_default_tls_config(identity); + let quic_endpoint_config = quinn::EndpointConfig::default(); + let quic_transport_config = quinn::TransportConfig::default(); + + self.with(tls_config, quic_endpoint_config, quic_transport_config) + } + + /// Allows for manual configuration of a custom TLS setup using a provided + /// [`rustls::ServerConfig`], which must support + /// [`rustls::CipherSuite::TLS13_AES_128_GCM_SHA256`]. A suitable configuration + /// can be obtained using the `ring` crypto provider with a set of versions containing + /// [`rustls::version::TLS13`]. + /// + /// This method is provided for advanced users who need fine-grained control over the + /// TLS configuration. It allows you to pass a preconfigured [`rustls::ServerConfig`] + /// instance to customize the TLS settings according to your specific requirements. + /// + /// Generally, it is recommended to use the [`with_identity`](Self::with_identity) method + /// to configure TLS with safe defaults and an TLS [`Identity`]. + /// + /// # Example + /// + /// ```no_run + /// use wtransport::tls::rustls; + /// use wtransport::ServerConfig; + /// + /// // Create a custom rustls::ServerConfig with specific TLS settings + /// let custom_tls_config = rustls::ServerConfig::builder(); + /// // Customize TLS settings here... + /// # let custom_tls_config = custom_tls_config + /// # .with_no_client_auth() + /// # .with_single_cert(todo!(), todo!()).unwrap(); + /// + /// // Create a ServerConfigBuilder with the custom TLS configuration + /// let server_config = ServerConfig::builder() + /// .with_bind_default(4433) + /// .with_custom_tls(custom_tls_config) + /// .build(); + /// ``` + pub fn with_custom_tls( + self, + tls_config: TlsServerConfig, + ) -> ServerConfigBuilder { + let quic_endpoint_config = quinn::EndpointConfig::default(); + let quic_transport_config = quinn::TransportConfig::default(); + + self.with(tls_config, quic_endpoint_config, quic_transport_config) + } + + /// Configures the server with a custom QUIC transport configuration and a default TLS setup + /// using the provided [`Identity`]. + /// + /// This method is useful for scenarios where you need to customize the transport settings + /// while relying on a default TLS configuration built from an [`Identity`]. It gives you + /// control over the transport layer while maintaining safe and standard TLS settings. + /// + /// **See**: [`with_identity`](Self::with_identity) + /// for a simpler configuration option that does not require custom transport settings. + /// + /// # Parameters + /// + /// - `identity`: A reference to an [`Identity`] that contains the server's certificate and + /// private key. This will be used to generate the default TLS configuration. + /// - `quic_transport_config`: A custom [`QuicTransportConfig`] instance that allows you to specify + /// various QUIC transport-layer settings according to your requirements. + /// + /// # Example + /// + /// ``` + /// use wtransport::config::QuicTransportConfig; + /// use wtransport::Identity; + /// use wtransport::ServerConfig; + /// + /// // Generate a server identity (self signed certificate and private key) + /// let identity = Identity::self_signed(["localhost", "127.0.0.1", "::1"]).unwrap(); + /// + /// // Create a custom QuicTransportConfig with specific settings + /// let mut custom_transport_config = QuicTransportConfig::default(); + /// custom_transport_config.datagram_send_buffer_size(1024); + /// + /// // Create a ServerConfigBuilder with the custom transport configuration and default TLS settings + /// let server_config = ServerConfig::builder() + /// .with_bind_default(4433) + /// .with_custom_transport(identity, custom_transport_config) + /// .build(); + /// ``` + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn with_custom_transport( + self, + identity: Identity, + quic_transport_config: QuicTransportConfig, + ) -> ServerConfigBuilder { + use crate::tls::server::build_default_tls_config; + + let tls_config = build_default_tls_config(identity); + let quic_endpoint_config = quinn::EndpointConfig::default(); + + self.with(tls_config, quic_endpoint_config, quic_transport_config) + } + + /// Configures the server with both a custom TLS configuration and a custom QUIC transport + /// configuration. + /// + /// This method is designed for advanced users who require full control over both the TLS + /// and transport settings. It allows you to pass a preconfigured [`TlsServerConfig`] and + /// a custom [`QuicTransportConfig`] to fine-tune both layers of the server configuration. + /// + /// # Parameters + /// + /// - `tls_config`: A custom [`TlsServerConfig`] instance that allows you to specify + /// detailed TLS settings, such as ciphersuites, certificate verification, and more. It must + /// support TLS 1.3 (see the documentation of [`Self::with_custom_tls`]). + /// - `quic_transport_config`: A custom [`QuicTransportConfig`] instance that allows you to specify + /// various QUIC transport-layer settings according to your requirements. + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn with_custom_tls_and_transport( + self, + tls_config: TlsServerConfig, + quic_transport_config: QuicTransportConfig, + ) -> ServerConfigBuilder { + let quic_endpoint_config = quinn::EndpointConfig::default(); + self.with(tls_config, quic_endpoint_config, quic_transport_config) + } + + /// Directly builds [`ServerConfig`] skipping TLS and transport configuration. + /// + /// Both TLS and transport configuration is given by [`quic_config`](QuicServerConfig). + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn build_with_quic_config(self, quic_config: QuicServerConfig) -> ServerConfig { + ServerConfig { + bind_address_config: self.0.bind_address_config, + endpoint_config: quinn::EndpointConfig::default(), + quic_config, + } + } + + fn with( + self, + tls_config: TlsServerConfig, + endpoint_config: quinn::EndpointConfig, + transport_config: quinn::TransportConfig, + ) -> ServerConfigBuilder { + ServerConfigBuilder(states::WantsTransportConfigServer { + bind_address_config: self.0.bind_address_config, + tls_config, + endpoint_config, + transport_config, + migration: true, + }) + } +} + +impl ServerConfigBuilder { + /// Completes configuration process. + /// + /// # Panics + /// + /// See the documentation of [`Self::with_custom_tls`] for the TLS 1.3 requirement. + #[must_use] + pub fn build(self) -> ServerConfig { + let crypto: Arc = Arc::new( + quinn::crypto::rustls::QuicServerConfig::try_from(self.0.tls_config) + .expect("CipherSuite::TLS13_AES_128_GCM_SHA256 missing"), + ); + + let mut quic_config = quinn::ServerConfig::with_crypto(crypto); + + quic_config.transport_config(Arc::new(self.0.transport_config)); + quic_config.migration(self.0.migration); + + ServerConfig { + bind_address_config: self.0.bind_address_config, + endpoint_config: self.0.endpoint_config, + quic_config, + } + } + + /// Maximum duration of inactivity to accept before timing out the connection. + /// + /// The true idle timeout is the minimum of this and the peer's own max idle timeout. `None` + /// represents an infinite timeout. + /// + /// **WARNING**: If a peer or its network path malfunctions or acts maliciously, an infinite + /// idle timeout can result in permanently hung futures! + pub fn max_idle_timeout( + mut self, + idle_timeout: Option, + ) -> Result { + let idle_timeout = idle_timeout + .map(quinn::IdleTimeout::try_from) + .transpose() + .map_err(|_| InvalidIdleTimeout)?; + + self.0.transport_config.max_idle_timeout(idle_timeout); + + Ok(self) + } + + /// Period of inactivity before sending a keep-alive packet + /// + /// Keep-alive packets prevent an inactive but otherwise healthy connection from timing out. + /// + /// `None` to disable, which is the default. Only one side of any given connection needs keep-alive + /// enabled for the connection to be preserved. Must be set lower than the + /// [`max_idle_timeout`](Self::max_idle_timeout) of both peers to be effective. + pub fn keep_alive_interval(mut self, interval: Option) -> Self { + self.0.transport_config.keep_alive_interval(interval); + self + } + + /// Whether to allow clients to migrate to new addresses. + /// + /// Improves behavior for clients that move between different internet connections or suffer NAT + /// rebinding. Enabled by default. + pub fn allow_migration(mut self, value: bool) -> Self { + self.0.migration = value; + self + } +} + +/// Client configuration. +/// +/// +/// You can create an instance of `ClientConfig` using its builder pattern by calling +/// the [`builder()`](Self::builder) method. +/// Once you have an instance, you can further customize it by chaining method calls +/// to set various configuration options. +/// +/// ## Configuration Builder States +/// +/// The configuration process follows a *state-based builder pattern*, where the client +/// configuration progresses through *3* states. +/// +/// ### 1. `WantsBindAddress` +/// +/// The caller must supply a binding address for the client. +/// The following options are mutually exclusive: +/// +/// - [`with_bind_default`](ClientConfigBuilder::with_bind_default): configures to use +/// the default bind address. This is generally the *default* choice for a client. +/// - [`with_bind_config`](ClientConfigBuilder::with_bind_config): configures +/// binding to an address determined by a configuration preset. +/// - [`with_bind_address`](ClientConfigBuilder::with_bind_address): configures +/// binding to a custom-specified socket address. +/// - [`with_bind_address_v6`](ClientConfigBuilder::with_bind_address_v6): configures +/// binding to a custom-specified socket address for *IPv6*, along with the [dual stack +/// configuration](Ipv6DualStackConfig). +/// - [`with_bind_socket`](ClientConfigBuilder::with_bind_socket): configures +/// binding directly to a custom-specified socket. +/// +/// Only one of these options can be selected during the client configuration process. +/// +/// #### Examples: +/// +/// ``` +/// use wtransport::ClientConfig; +/// +/// ClientConfig::builder().with_bind_default(); +/// ``` +/// +/// ### 2. `WantsRootStore` +/// +/// The caller must supply a TLS root store configuration for server certificate validation. +/// The following options are mutually exclusive: +/// +/// - [`with_native_certs`](ClientConfigBuilder::with_native_certs): configures to use +/// root certificates found in the platform's native certificate store. This is the *default* +/// configuration as it uses root store installed on the current machine. +/// - [`with_server_certificate_hashes`][cert_hashes]: configures the client to accept +/// *some* certificates mapped to hashes. This can be used to connect to self signed +/// certificates securely, where the hash of the certificate is shared in advance +/// through some other mechanism (such as an invite link). +/// - (**insecure**) [`with_no_cert_validation`](ClientConfigBuilder::with_no_cert_validation): +/// configure to skip server certificate validation. This might be handy for testing purpose +/// to accept *self-signed* certificate, but you should almost always prefer +/// [`with_server_certificate_hashes`][cert_hashes] for that use case. +/// - [`with_custom_tls`](ClientConfigBuilder::with_custom_tls): sets the TLS client +/// configuration manually. +/// - [`with_custom_transport`](ClientConfigBuilder::with_custom_transport): sets the QUIC +/// transport configuration manually (using default TLS). +/// - [`with_custom_tls_and_transport`](ClientConfigBuilder::with_custom_tls_and_transport): sets both +/// a custom TLS and QUIC transport configuration. +/// - [`build_with_quic_config`](ClientConfigBuilder::build_with_quic_config): directly builds +/// [`ClientConfig`] providing both TLS and QUIC transport configuration given by +/// [`quic_config`](QuicClientConfig). +/// +/// Only one of these options can be selected during the client configuration process. +/// +/// [cert_hashes]: ClientConfigBuilder::with_server_certificate_hashes +/// +/// #### Examples: +/// ``` +/// use wtransport::ClientConfig; +/// +/// ClientConfig::builder() +/// .with_bind_default() +/// .with_native_certs(); +/// ``` +/// +/// ### 3. `WantsTransportConfigClient` +/// +/// The caller can supply *additional* transport configurations. +/// Multiple options can be given at this stage. Once the configuration is completed, it is possible +/// to finalize with the method [`build()`](ClientConfigBuilder::build). +/// +/// All these options can be omitted in the configuration; default values will be used. +/// +/// - [`max_idle_timeout`](ClientConfigBuilder::max_idle_timeout) +/// - [`keep_alive_interval`](ClientConfigBuilder::keep_alive_interval) +/// - [`dns_resolver`](ClientConfigBuilder::dns_resolver) +/// +/// #### Examples: +/// ``` +/// use std::time::Duration; +/// use wtransport::ClientConfig; +/// +/// let client_config = ClientConfig::builder() +/// .with_bind_default() +/// .with_native_certs() +/// .max_idle_timeout(Some(Duration::from_secs(30))) +/// .unwrap() +/// .keep_alive_interval(Some(Duration::from_secs(3))) +/// .build(); +/// ``` +#[derive(Debug)] +pub struct ClientConfig { + pub(crate) bind_address_config: BindAddressConfig, + pub(crate) endpoint_config: quinn::EndpointConfig, + pub(crate) quic_config: quinn::ClientConfig, + pub(crate) dns_resolver: Arc, +} + +impl ClientConfig { + /// Creates a builder to build up the client configuration. + /// + /// For more information, see the [`ClientConfigBuilder`] documentation. + pub fn builder() -> ClientConfigBuilder { + ClientConfigBuilder::default() + } + + /// Allows setting a custom [`DnsResolver`] for this configuration. + /// + /// Default resolver is [`TokioDnsResolver`]. + pub fn set_dns_resolver(&mut self, dns_resolver: R) + where + R: DnsResolver + Send + Sync + 'static, + { + self.dns_resolver = Arc::new(dns_resolver); + } + + /// Returns a reference to the inner QUIC endpoint configuration. + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn quic_endpoint_config(&self) -> &quinn::EndpointConfig { + &self.endpoint_config + } + + /// Returns a mutable reference to the inner QUIC endpoint configuration. + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn quic_endpoint_config_mut(&mut self) -> &mut quinn::EndpointConfig { + &mut self.endpoint_config + } + + /// Returns a reference to the inner QUIC configuration. + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn quic_config(&self) -> &quinn::ClientConfig { + &self.quic_config + } + + /// Returns a mutable reference to the inner QUIC configuration. + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn quic_config_mut(&mut self) -> &mut quinn::ClientConfig { + &mut self.quic_config + } +} + +impl Default for ClientConfig { + fn default() -> Self { + ClientConfig::builder() + .with_bind_default() + .with_native_certs() + .build() + } +} + +/// Client builder configuration. +/// +/// The builder might have different state at compile time. +/// +/// # Example +/// ```no_run +/// # use std::net::Ipv4Addr; +/// # use std::net::SocketAddr; +/// # use wtransport::ClientConfig; +/// let config = ClientConfig::builder().with_bind_default(); +/// ``` +#[must_use] +pub struct ClientConfigBuilder(State); + +impl ClientConfigBuilder { + /// Configures for connecting binding ANY IP (allowing IP dual-stack). + /// + /// Bind port will be randomly picked. + /// + /// This is equivalent to: [`Self::with_bind_config`] with [`IpBindConfig::InAddrAnyDual`]. + pub fn with_bind_default(self) -> ClientConfigBuilder { + self.with_bind_config(IpBindConfig::InAddrAnyDual) + } + + /// Sets the binding (local) socket address with a specific [`IpBindConfig`]. + /// + /// Bind port will be randomly picked. + pub fn with_bind_config( + self, + ip_bind_config: IpBindConfig, + ) -> ClientConfigBuilder { + let ip_address: IpAddr = ip_bind_config.into_ip(); + + match ip_address { + IpAddr::V4(ip) => self.with_bind_address(SocketAddr::new(ip.into(), 0)), + IpAddr::V6(ip) => self.with_bind_address_v6( + SocketAddrV6::new(ip, 0, 0, 0), + ip_bind_config.into_dual_stack_config(), + ), + } + } + + /// Sets the binding (local) socket address for the endpoint. + pub fn with_bind_address( + self, + address: SocketAddr, + ) -> ClientConfigBuilder { + ClientConfigBuilder(states::WantsRootStore { + bind_address_config: BindAddressConfig::from(address), + }) + } + + /// Sets the binding (local) socket address for the endpoint. + /// + /// `dual_stack_config` allows/denies dual stack port binding. + pub fn with_bind_address_v6( + self, + address: SocketAddrV6, + dual_stack_config: Ipv6DualStackConfig, + ) -> ClientConfigBuilder { + ClientConfigBuilder(states::WantsRootStore { + bind_address_config: BindAddressConfig::AddressV6(address, dual_stack_config), + }) + } + + /// Configures the client to bind to a pre-existing [`UdpSocket`]. + /// + /// This allows the client to use an already created socket, which can be useful in cases + /// where socket reuse or specific socket configurations are necessary. + pub fn with_bind_socket( + self, + socket: UdpSocket, + ) -> ClientConfigBuilder { + ClientConfigBuilder(states::WantsRootStore { + bind_address_config: BindAddressConfig::Socket(socket), + }) + } +} + +impl ClientConfigBuilder { + /// Configures the client to use native (local) root certificates for server validation. + /// + /// This method loads trusted root certificates from the system's certificate store, + /// ensuring that your client can trust certificates signed by well-known authorities. + /// + /// It configures safe default TLS configuration. + pub fn with_native_certs(self) -> ClientConfigBuilder { + use crate::tls::client::build_default_tls_config; + + let tls_config = build_default_tls_config(Arc::new(build_native_cert_store()), None); + let endpoint_config = quinn::EndpointConfig::default(); + let transport_config = quinn::TransportConfig::default(); + + self.with(tls_config, endpoint_config, transport_config) + } + + /// Configures the client to skip server certificate validation, potentially + /// compromising security. + /// + /// This method is intended for advanced users and should be used with caution. It + /// configures the client to bypass server certificate validation during the TLS + /// handshake, effectively trusting any server certificate presented, even if it is + /// not signed by a trusted certificate authority (CA). Using this method can expose + /// your application to security risks. + /// + /// # Safety Note + /// + /// Using [`with_no_cert_validation`] should only be considered when you have a + /// specific need to disable certificate validation. In most cases, it is strongly + /// recommended to validate server certificates using trusted root certificates + /// (e.g., [`with_native_certs`]) to ensure secure communication. + /// + /// However, this method can be useful in testing environments or situations where + /// you intentionally want to skip certificate validation for specific use cases. + /// + /// [`with_native_certs`]: #method.with_native_certs + /// [`with_no_cert_validation`]: #method.with_no_cert_validation + #[cfg(feature = "dangerous-configuration")] + #[cfg_attr(docsrs, doc(cfg(feature = "dangerous-configuration")))] + pub fn with_no_cert_validation( + self, + ) -> ClientConfigBuilder { + use crate::tls::client::build_default_tls_config; + use crate::tls::client::NoServerVerification; + use rustls::RootCertStore; + + let tls_config = build_default_tls_config( + Arc::new(RootCertStore::empty()), + Some(Arc::new(NoServerVerification::new())), + ); + + let endpoint_config = quinn::EndpointConfig::default(); + let transport_config = quinn::TransportConfig::default(); + + self.with(tls_config, endpoint_config, transport_config) + } + + /// Configures the client to skip *some* server certificates validation. + /// + /// This method configures the client to accept server certificates + /// whose digests match the specified *SHA-256* hashes and fulfill + /// some additional constraints (*see notes below*). + /// + /// This is useful for scenarios where clients need to accept known + /// self-signed certificates or certificates from non-standard authorities. + /// + /// This method configuration is similar to the + /// [browser W3C WebTransport API](https://www.w3.org/TR/webtransport/#dom-webtransportoptions-servercertificatehashes). + /// + /// # Notes + /// + /// - The current time MUST be within the validity period of the certificate. + /// - The total length of the validity period MUST NOT exceed *two* weeks. + /// - Only certificates for which the public key algorithm is *ECDSA* with the *secp256r1* are accepted. + pub fn with_server_certificate_hashes( + self, + hashes: I, + ) -> ClientConfigBuilder + where + I: IntoIterator, + { + use crate::tls::client::build_default_tls_config; + use crate::tls::client::ServerHashVerification; + use rustls::RootCertStore; + + let tls_config = build_default_tls_config( + Arc::new(RootCertStore::empty()), + Some(Arc::new(ServerHashVerification::new(hashes))), + ); + + let endpoint_config = quinn::EndpointConfig::default(); + let transport_config = quinn::TransportConfig::default(); + + self.with(tls_config, endpoint_config, transport_config) + } + + /// Allows for manual configuration of a custom TLS setup using a provided + /// [`rustls::ClientConfig`], which must support + /// [`rustls::CipherSuite::TLS13_AES_128_GCM_SHA256`]. A suitable configuration + /// can be obtained using the `ring` crypto provider with a set of versions containing + /// [`rustls::version::TLS13`]. + /// + /// This method is provided for advanced users who need fine-grained control over the + /// TLS configuration. It allows you to pass a preconfigured [`rustls::ClientConfig`] + /// instance to customize the TLS settings according to your specific requirements. + /// + /// For most use cases, it is recommended to use the [`with_native_certs`](Self::with_native_certs) + /// method to configure TLS with safe defaults. + pub fn with_custom_tls( + self, + tls_config: TlsClientConfig, + ) -> ClientConfigBuilder { + let endpoint_config = quinn::EndpointConfig::default(); + let transport_config = quinn::TransportConfig::default(); + + self.with(tls_config, endpoint_config, transport_config) + } + + /// Similar to [`with_native_certs`](Self::with_native_certs), but it allows specifying a custom + /// QUIC transport configuration. + /// + /// # Parameters + /// + /// - `quic_transport_config`: A custom [`QuicTransportConfig`] instance that allows you to specify + /// various QUIC transport-layer settings according to your requirements. + /// + /// # Example + /// + /// ``` + /// use wtransport::config::QuicTransportConfig; + /// use wtransport::ClientConfig; + /// + /// // Create a custom QuicTransportConfig with specific settings + /// let mut custom_transport_config = QuicTransportConfig::default(); + /// custom_transport_config.datagram_send_buffer_size(1024); + /// + /// // Create a ClientConfigBuilder with the custom transport configuration and default TLS settings + /// let client_config = ClientConfig::builder() + /// .with_bind_default() + /// .with_custom_transport(custom_transport_config) + /// .build(); + /// ``` + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn with_custom_transport( + self, + quic_transport_config: QuicTransportConfig, + ) -> ClientConfigBuilder { + use crate::tls::client::build_default_tls_config; + + let tls_config = build_default_tls_config(Arc::new(build_native_cert_store()), None); + let quic_endpoint_config = quinn::EndpointConfig::default(); + + self.with(tls_config, quic_endpoint_config, quic_transport_config) + } + + /// Configures the client with both a custom TLS configuration and a custom QUIC transport + /// configuration. + /// + /// This method is designed for advanced users who require full control over both the TLS + /// and transport settings. It allows you to pass a preconfigured [`TlsClientConfig`] and + /// a custom [`QuicTransportConfig`] to fine-tune both layers of the server configuration. + /// + /// # Parameters + /// + /// - `tls_config`: A custom [`TlsClientConfig`] instance that allows you to specify + /// detailed TLS settings, such as ciphersuites, certificate verification, and more. It must + /// support TLS 1.3 (see the documentation of [`Self::with_custom_tls`]). + /// - `quic_transport_config`: A custom [`QuicTransportConfig`] instance that allows you to specify + /// various QUIC transport-layer settings according to your requirements. + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn with_custom_tls_and_transport( + self, + tls_config: TlsClientConfig, + quic_transport_config: QuicTransportConfig, + ) -> ClientConfigBuilder { + let quic_endpoint_config = quinn::EndpointConfig::default(); + self.with(tls_config, quic_endpoint_config, quic_transport_config) + } + + /// Directly builds [`ClientConfig`] skipping TLS and transport configuration. + /// + /// Both TLS and transport configuration is given by [`quic_config`](QuicClientConfig). + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn build_with_quic_config(self, quic_config: QuicClientConfig) -> ClientConfig { + ClientConfig { + bind_address_config: self.0.bind_address_config, + endpoint_config: quinn::EndpointConfig::default(), + quic_config, + dns_resolver: Arc::::default(), + } + } + + fn with( + self, + tls_config: TlsClientConfig, + endpoint_config: quinn::EndpointConfig, + transport_config: quinn::TransportConfig, + ) -> ClientConfigBuilder { + ClientConfigBuilder(states::WantsTransportConfigClient { + bind_address_config: self.0.bind_address_config, + tls_config, + endpoint_config, + transport_config, + dns_resolver: Arc::::default(), + }) + } +} + +impl ClientConfigBuilder { + /// Completes configuration process. + /// + /// # Panics + /// + /// See the documentation of [`Self::with_custom_tls`] for the TLS 1.3 requirement. + #[must_use] + pub fn build(self) -> ClientConfig { + let crypto = quinn::crypto::rustls::QuicClientConfig::try_from(self.0.tls_config) + .expect("CipherSuite::TLS13_AES_128_GCM_SHA256 missing"); + + let mut quic_config = quinn::ClientConfig::new(Arc::new(crypto)); + quic_config.transport_config(Arc::new(self.0.transport_config)); + + ClientConfig { + bind_address_config: self.0.bind_address_config, + endpoint_config: self.0.endpoint_config, + quic_config, + dns_resolver: self.0.dns_resolver, + } + } + + /// Maximum duration of inactivity to accept before timing out the connection. + /// + /// The true idle timeout is the minimum of this and the peer's own max idle timeout. `None` + /// represents an infinite timeout. + /// + /// **WARNING**: If a peer or its network path malfunctions or acts maliciously, an infinite + /// idle timeout can result in permanently hung futures! + pub fn max_idle_timeout( + mut self, + idle_timeout: Option, + ) -> Result { + let idle_timeout = idle_timeout + .map(quinn::IdleTimeout::try_from) + .transpose() + .map_err(|_| InvalidIdleTimeout)?; + + self.0.transport_config.max_idle_timeout(idle_timeout); + + Ok(self) + } + + /// Period of inactivity before sending a keep-alive packet + /// + /// Keep-alive packets prevent an inactive but otherwise healthy connection from timing out. + /// + /// `None` to disable, which is the default. Only one side of any given connection needs keep-alive + /// enabled for the connection to be preserved. Must be set lower than the + /// [`max_idle_timeout`](Self::max_idle_timeout) of both peers to be effective. + pub fn keep_alive_interval(mut self, interval: Option) -> Self { + self.0.transport_config.keep_alive_interval(interval); + self + } + + /// Sets the *DNS* resolver used during [`Endpoint::connect`](crate::Endpoint::connect). + /// + /// Default configuration uses [`TokioDnsResolver`]. + pub fn dns_resolver(mut self, dns_resolver: R) -> Self + where + R: DnsResolver + Send + Sync + 'static, + { + self.0.dns_resolver = Arc::new(dns_resolver); + self + } +} + +impl Default for ServerConfigBuilder { + fn default() -> Self { + Self(states::WantsBindAddress {}) + } +} + +impl Default for ClientConfigBuilder { + fn default() -> Self { + Self(states::WantsBindAddress {}) + } +} + +#[derive(Debug)] +pub(crate) enum BindAddressConfig { + AddressV4(SocketAddrV4), + AddressV6(SocketAddrV6, Ipv6DualStackConfig), + Socket(UdpSocket), +} + +impl BindAddressConfig { + pub(crate) fn bind_socket(self) -> std::io::Result { + let (bind_address, dual_stack_config) = match self { + BindAddressConfig::AddressV4(address) => { + (SocketAddr::from(address), Ipv6DualStackConfig::OsDefault) + } + BindAddressConfig::AddressV6(address, ipv6_dual_stack_config) => { + (SocketAddr::from(address), ipv6_dual_stack_config) + } + BindAddressConfig::Socket(socket) => { + return Ok(socket); + } + }; + + let domain = match bind_address { + SocketAddr::V4(_) => SocketDomain::IPV4, + SocketAddr::V6(_) => SocketDomain::IPV6, + }; + + let socket = Socket::new(domain, SocketType::DGRAM, Some(SocketProtocol::UDP))?; + + match dual_stack_config { + Ipv6DualStackConfig::OsDefault => {} + Ipv6DualStackConfig::Deny => socket.set_only_v6(true)?, + Ipv6DualStackConfig::Allow => socket.set_only_v6(false)?, + } + + socket.bind(&bind_address.into())?; + + Ok(UdpSocket::from(socket)) + } +} + +impl From for BindAddressConfig { + fn from(value: SocketAddr) -> Self { + match value { + SocketAddr::V4(address) => BindAddressConfig::AddressV4(address), + SocketAddr::V6(address) => { + BindAddressConfig::AddressV6(address, Ipv6DualStackConfig::OsDefault) + } + } + } +} + +/// State-types for client/server builder. +pub mod states { + use super::*; + + /// Config builder state where the caller must supply binding address. + pub struct WantsBindAddress {} + + /// Config builder state where the caller must supply TLS certificate. + pub struct WantsIdentity { + pub(super) bind_address_config: BindAddressConfig, + } + + /// Config builder state where the caller must supply TLS root store. + pub struct WantsRootStore { + pub(super) bind_address_config: BindAddressConfig, + } + + /// Config builder state where transport properties can be set. + pub struct WantsTransportConfigServer { + pub(super) bind_address_config: BindAddressConfig, + pub(super) tls_config: TlsServerConfig, + pub(super) endpoint_config: quinn::EndpointConfig, + pub(super) transport_config: quinn::TransportConfig, + pub(super) migration: bool, + } + + /// Config builder state where transport properties can be set. + pub struct WantsTransportConfigClient { + pub(super) bind_address_config: BindAddressConfig, + pub(super) tls_config: TlsClientConfig, + pub(super) endpoint_config: quinn::EndpointConfig, + pub(super) transport_config: quinn::TransportConfig, + pub(super) dns_resolver: Arc, + } +} + +/// Future resolving domain name. +/// +/// See [`DnsResolver::resolve`]. +pub trait DnsLookupFuture: Future>> + Send {} + +impl DnsLookupFuture for F where F: Future>> + Send {} + +/// A trait for asynchronously resolving domain names to IP addresses using DNS. +pub trait DnsResolver: Debug { + /// Resolves a domain name to one IP address. + fn resolve(&self, host: &str) -> Pin>; +} + +/// A DNS resolver implementation using the *Tokio* asynchronous runtime. +/// +/// Internally, it uses [`tokio::net::lookup_host`]. +#[derive(Default)] +pub struct TokioDnsResolver; + +impl DnsResolver for TokioDnsResolver { + fn resolve(&self, host: &str) -> Pin> { + let host = host.to_string(); + + Box::pin(async move { Ok(tokio::net::lookup_host(host).await?.next()) }) + } +} + +impl Debug for TokioDnsResolver { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TokioDnsResolver").finish() + } +} + +impl std::error::Error for InvalidIdleTimeout {} + +impl Debug for InvalidIdleTimeout { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("idle timeout value configuration is invalid") + } +} + +impl Display for InvalidIdleTimeout { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Debug::fmt(self, f) + } +} diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/connection.rs b/joule-code/joule-code-rs/vendor/wtransport/src/connection.rs new file mode 100644 index 000000000..60f642579 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/src/connection.rs @@ -0,0 +1,426 @@ +//! # WebTransport Connection +//! +//! [`Connection`] provides an essential building block for managing WebTransport +//! connections. It allows you to initiate, accept, and control data *streams*, send and receive +//! *datagrams*, monitor connection status, and interact with various aspects of your WebTransport +//! communication. +//! +//! WebTransport exchanges data either via [*streams*](crate#streams) or [*datagrams*](crate#datagrams). +//! +//! ## Streams +//! WebTransport streams provide a lightweight, ordered byte-stream abstraction. +//! +//! There are two fundamental types of streams: +//! - *Unidirectional* streams carry data in a single direction, from the stream initiator to its peer. +//! - *Bidirectional* streams allow for data to be sent in both directions. +//! +//! Both server and client endpoints have the capability to create an arbitrary number of streams to +//! operate concurrently. +//! +//! Each stream can be independently cancelled by both side. +//! +//! ### Examples +//! #### Open a stream +//! ```no_run +//! # use anyhow::Result; +//! # async fn foo(connection: wtransport::Connection) -> Result<()> { +//! use wtransport::Connection; +//! +//! // Open a bi-directional stream +//! let (mut send_stream, mut recv_stream) = connection.open_bi().await?.await?; +//! +//! // Send data on the stream +//! send_stream.write_all(b"Hello, wtransport!").await?; +//! +//! // Receive data from the stream +//! let mut buffer = vec![0; 1024]; +//! let bytes_read = recv_stream.read(&mut buffer).await?; +//! +//! // Open an uni-directional stream (can only send data) +//! let mut send_stream = connection.open_uni().await?.await?; +//! +//! // Send data on the stream +//! send_stream.write_all(b"Hello, wtransport!").await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! #### Accept a stream +//! ```no_run +//! # use anyhow::Result; +//! # async fn foo(connection: wtransport::Connection) -> Result<()> { +//! use wtransport::Connection; +//! +//! // Await the peer opens a bi-directional stream +//! let (mut send_stream, mut recv_stream) = connection.accept_bi().await?; +//! +//! // Can send and receive data on peer's stream +//! send_stream.write_all(b"Hello, wtransport!").await?; +//! # let mut buffer = vec![0; 1024]; +//! let bytes_read = recv_stream.read(&mut buffer).await?; +//! +//! // Await the peer opens an uni-directional stream (can only receive data) +//! let mut recv_stream = connection.accept_uni().await?; +//! +//! // Receive data on the stream +//! let bytes_read = recv_stream.read(&mut buffer).await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Datagrams +//! WebTransport datagrams are similar to UDP datagrams but come with an +//! added layer of security through *encryption* and *congestion control*. +//! Datagrams can arrive out of order or might not arrive at all, offering +//! flexibility in data exchange scenarios. +//! +//! Unlike streams, which operate as byte-stream abstractions, WebTransport +//! datagrams act more like messages. +//! +//! ### Examples +//! ```no_run +//! # use anyhow::Result; +//! # async fn foo(connection: wtransport::Connection) -> Result<()> { +//! use wtransport::Connection; +//! +//! // Send datagram message +//! connection.send_datagram(b"Hello, wtransport!")?; +//! +//! // Receive a datagram message +//! let message = connection.receive_datagram().await?; +//! # Ok(()) +//! # } +//! ``` + +use crate::datagram::Datagram; +use crate::driver::utils::varint_w2q; +use crate::driver::Driver; +use crate::error::ConnectionError; +use crate::error::ExportKeyingMaterialError; +use crate::error::SendDatagramError; +use crate::stream::OpeningBiStream; +use crate::stream::OpeningUniStream; +use crate::stream::RecvStream; +use crate::stream::SendStream; +use crate::tls::Certificate; +use crate::tls::CertificateChain; +use crate::tls::HandshakeData; +use crate::SessionId; +use crate::VarInt; +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::Duration; + +/// A WebTransport session connection. +/// +/// For more details, see the [module documentation](crate::connection). +/// +/// May be cloned to obtain another handle to the same connection. +#[derive(Clone, Debug)] +pub struct Connection { + quic_connection: quinn::Connection, + driver: Arc, + session_id: SessionId, +} + +impl Connection { + pub(crate) fn new( + quic_connection: quinn::Connection, + driver: Driver, + session_id: SessionId, + ) -> Self { + Self { + quic_connection, + driver: Arc::new(driver), + session_id, + } + } + + /// Asynchronously accepts a unidirectional stream. + /// + /// This method is used to accept incoming unidirectional streams that have been initiated + /// by the remote peer. + /// It waits for the next unidirectional stream to be available, then wraps it in a + /// [`RecvStream`] that can be used to read data from the stream. + /// + /// # Cancel safety + /// + /// This method is cancel safe. + pub async fn accept_uni(&self) -> Result { + let stream = self + .driver + .accept_uni(self.session_id) + .await + .map_err(|driver_error| { + ConnectionError::with_driver_error(driver_error, &self.quic_connection) + })? + .into_stream(); + + Ok(RecvStream::new(stream)) + } + + /// Asynchronously accepts a bidirectional stream. + /// + /// This method is used to accept incoming bidirectional streams that have been initiated + /// by the remote peer. + /// It waits for the next bidirectional stream to be available, then wraps it in a + /// tuple containing a [`SendStream`] for sending data and a [`RecvStream`] for receiving + /// data on the stream. + /// + /// # Cancel safety + /// + /// This method is cancel safe. + pub async fn accept_bi(&self) -> Result<(SendStream, RecvStream), ConnectionError> { + let stream = self + .driver + .accept_bi(self.session_id) + .await + .map_err(|driver_error| { + ConnectionError::with_driver_error(driver_error, &self.quic_connection) + })? + .into_stream(); + + Ok((SendStream::new(stream.0), RecvStream::new(stream.1))) + } + + /// Asynchronously opens a new unidirectional stream. + /// + /// This method is used to initiate the opening of a new unidirectional stream. + /// + /// # Asynchronous Behavior + /// + /// This method is asynchronous and involves two `await` points: + /// + /// 1. The first `await` occurs during the initial phase of opening the stream, which may involve awaiting + /// the flow controller. This wait is necessary to ensure proper resource allocation and flow control. + /// It is safe to cancel this `await` point if needed. + /// + /// 2. The second `await` is internal to the returned [`OpeningUniStream`] object when it is used to initialize + /// the WebTransport stream. Cancelling this latter future before it completes may result in the stream + /// being closed during initialization. + /// + /// # Example + /// + /// ```no_run + /// # use wtransport::Connection; + /// # use anyhow::Result; + /// # async fn run(connection: Connection) -> Result<()> { + /// let send_stream = connection.open_uni().await?.await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn open_uni(&self) -> Result { + self.driver + .open_uni(self.session_id) + .await + .map_err(|driver_error| { + ConnectionError::with_driver_error(driver_error, &self.quic_connection) + }) + } + + /// Asynchronously opens a new bidirectional stream. + /// + /// This method is used to initiate the opening of a new bidirectional stream. + /// + /// # Asynchronous Behavior + /// + /// This method is asynchronous and involves two `await` points: + /// + /// 1. The first `await` occurs during the initial phase of opening the stream, which may involve awaiting + /// the flow controller. This wait is necessary to ensure proper resource allocation and flow control. + /// It is safe to cancel this `await` point if needed. + /// + /// 2. The second `await` is internal to the returned [`OpeningBiStream`] object when it is used to initialize + /// the WebTransport stream. Cancelling this latter future before it completes may result in the stream + /// being closed during initialization. + /// + /// # Example + /// + /// ```no_run + /// # use wtransport::Connection; + /// # use anyhow::Result; + /// # async fn run(connection: Connection) -> Result<()> { + /// let (send_stream, recv_stream) = connection.open_bi().await?.await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn open_bi(&self) -> Result { + self.driver + .open_bi(self.session_id) + .await + .map_err(|driver_error| { + ConnectionError::with_driver_error(driver_error, &self.quic_connection) + }) + } + + /// Asynchronously receives an application datagram from the remote peer. + /// + /// This method is used to receive an application datagram sent by the remote + /// peer over the connection. + /// It waits for a datagram to become available and returns the received [`Datagram`]. + /// + /// # Example + /// + /// ```no_run + /// # use wtransport::Connection; + /// # use anyhow::Result; + /// # async fn run(connection: Connection) -> Result<()> { + /// let datagram = connection.receive_datagram().await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn receive_datagram(&self) -> Result { + self.driver + .receive_datagram(self.session_id) + .await + .map_err(|driver_error| { + ConnectionError::with_driver_error(driver_error, &self.quic_connection) + }) + } + + /// Sends an application datagram to the remote peer. + /// + /// This method is used to send an application datagram to the remote peer + /// over the connection. + /// The datagram payload is provided as a reference to a slice of bytes. + /// + /// # Example + /// + /// ```no_run + /// # use wtransport::Connection; + /// # use anyhow::Result; + /// # async fn run(connection: Connection) -> Result<()> { + /// connection.send_datagram(b"Hello, wtransport!")?; + /// # Ok(()) + /// # } + /// ``` + pub fn send_datagram(&self, payload: D) -> Result<(), SendDatagramError> + where + D: AsRef<[u8]>, + { + self.driver.send_datagram(self.session_id, payload.as_ref()) + } + + /// Closes the connection immediately. + pub fn close(&self, error_code: VarInt, reason: &[u8]) { + self.quic_connection.close(varint_w2q(error_code), reason); + } + + /// Waits for the connection to be closed for any reason. + pub async fn closed(&self) -> ConnectionError { + self.quic_connection.closed().await.into() + } + + /// Returns the WebTransport session identifier. + #[inline(always)] + pub fn session_id(&self) -> SessionId { + self.session_id + } + + /// Returns the peer's UDP address. + /// + /// **Note**: as QUIC supports migration, remote address may change + /// during connection. Furthermore, when IPv6 support is enabled, IPv4 + /// addresses may be mapped to IPv6. + #[inline(always)] + pub fn remote_address(&self) -> SocketAddr { + self.quic_connection.remote_address() + } + + /// A stable identifier for this connection. + /// + /// Peer addresses and connection IDs can change, but this value will remain + /// fixed for the lifetime of the connection. + #[inline(always)] + pub fn stable_id(&self) -> usize { + self.quic_connection.stable_id() + } + + /// Computes the maximum size of datagrams that may be passed to + /// [`send_datagram`](Self::send_datagram). + /// + /// Returns `None` if datagrams are unsupported by the peer or disabled locally. + /// + /// This may change over the lifetime of a connection according to variation in the path MTU + /// estimate. The peer can also enforce an arbitrarily small fixed limit, but if the peer's + /// limit is large this is guaranteed to be a little over a kilobyte at minimum. + /// + /// Not necessarily the maximum size of received datagrams. + #[inline(always)] + pub fn max_datagram_size(&self) -> Option { + self.quic_connection + .max_datagram_size() + .map(|quic_max_size| quic_max_size - Datagram::header_size(self.session_id)) + } + + /// Current best estimate of this connection's latency (round-trip-time). + #[inline(always)] + pub fn rtt(&self) -> Duration { + self.quic_connection.rtt() + } + + /// Derive keying material from this connection's TLS session secrets. + /// + /// When both peers call this method with the same `label` and `context` + /// arguments and `output` buffers of equal length, they will get the + /// same sequence of bytes in `output`. These bytes are cryptographically + /// strong and pseudorandom, and are suitable for use as keying material. + /// + /// See [RFC5705](https://tools.ietf.org/html/rfc5705) for more information. + pub fn export_keying_material( + &self, + output: &mut [u8], + label: &[u8], + context: &[u8], + ) -> Result<(), ExportKeyingMaterialError> { + self.quic_connection + .export_keying_material(output, label, context) + .map_err(|_: quinn::crypto::ExportKeyingMaterialError| ExportKeyingMaterialError) + } + + /// Returns the peer's identity as a certificate chain if available. + /// + /// This function returns an `Option` containing a [`CertificateChain`]. If the peer's identity + /// is available, it is converted into a `CertificateChain` and returned. If the peer's identity + /// is not available, `None` is returned. + pub fn peer_identity(&self) -> Option { + self.quic_connection.peer_identity().map(|any| { + any.downcast::>>() + .expect("rustls certificate vector") + .into_iter() + .map(Certificate::from_rustls_pki) + .collect() + }) + } + + /// Retrieves handshake data associated with the connection. + pub fn handshake_data(&self) -> HandshakeData { + let hd = self + .quic_connection + .handshake_data() + .expect("fully established connection") + .downcast::() + .expect("valid downcast"); + + HandshakeData { + alpn: hd.protocol, + server_name: hd.server_name, + } + } + + /// Returns a reference to the inner QUIC connection. + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + #[inline(always)] + pub fn quic_connection(&self) -> &quinn::Connection { + &self.quic_connection + } + + /// Returns a mutable reference to the inner QUIC connection. + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + #[inline(always)] + pub fn quic_connection_mut(&mut self) -> &mut quinn::Connection { + &mut self.quic_connection + } +} diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/datagram.rs b/joule-code/joule-code-rs/vendor/wtransport/src/datagram.rs new file mode 100644 index 000000000..f9c87d442 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/src/datagram.rs @@ -0,0 +1,76 @@ +use crate::SessionId; +use bytes::Bytes; +use std::ops::Deref; +use wtransport_proto::datagram::Datagram as H3Datagram; +use wtransport_proto::error::ErrorCode; +use wtransport_proto::ids::QStreamId; + +/// An application Datagram. +#[derive(Debug)] +pub struct Datagram { + quic_dgram: Bytes, + payload_offset: usize, + session_id: SessionId, +} + +impl Datagram { + /// Returns the datagram payload. + #[inline(always)] + pub fn payload(&self) -> Bytes { + self.quic_dgram.slice(self.payload_offset..) + } + + pub(crate) fn read(quic_dgram: Bytes) -> Result { + let h3dgram = H3Datagram::read(&quic_dgram)?; + let payload_offset = quic_dgram.len() - h3dgram.payload().len(); + let session_id = h3dgram.qstream_id().into_session_id(); + + Ok(Self { + quic_dgram, + payload_offset, + session_id, + }) + } + + pub(crate) fn write(session_id: SessionId, payload: &[u8]) -> Self { + let h3dgram = H3Datagram::new(QStreamId::from_session_id(session_id), payload); + + let mut buffer = vec![0; h3dgram.write_size()].into_boxed_slice(); + h3dgram.write(&mut buffer).expect("Preallocated capacity"); + + let quic_dgram = Bytes::from(buffer); + + let payload_offset = quic_dgram.len() - payload.len(); + + Self { + quic_dgram, + payload_offset, + session_id, + } + } + + #[inline(always)] + pub(crate) fn header_size(session_id: SessionId) -> usize { + H3Datagram::header_size(QStreamId::from_session_id(session_id)) + } + + /// Returns the associated [`SessionId`]. + #[inline(always)] + pub fn session_id(&self) -> SessionId { + self.session_id + } + + #[inline(always)] + pub(crate) fn into_quic_bytes(self) -> Bytes { + self.quic_dgram + } +} + +impl Deref for Datagram { + type Target = [u8]; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + &self.quic_dgram[self.payload_offset..] + } +} diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/driver/mod.rs b/joule-code/joule-code-rs/vendor/wtransport/src/driver/mod.rs new file mode 100644 index 000000000..dc68f0491 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/src/driver/mod.rs @@ -0,0 +1,677 @@ +use crate::datagram::Datagram; +use crate::driver::streams::biremote::StreamBiRemoteH3; +use crate::driver::streams::biremote::StreamBiRemoteWT; +use crate::driver::streams::session::StreamSession; +use crate::driver::streams::uniremote::StreamUniRemoteWT; +use crate::driver::streams::Stream; +use crate::driver::utils::bichannel; +use crate::driver::utils::shared_result; +use crate::driver::utils::SendError; +use crate::driver::utils::SharedResultGet; +use crate::driver::utils::SharedResultSet; +use crate::error::ApplicationClose; +use crate::error::SendDatagramError; +use crate::stream::OpeningBiStream; +use crate::stream::OpeningUniStream; +use crate::SessionId; +use tokio::sync::mpsc; +use tokio::sync::Mutex; +use tracing::debug; +use tracing::debug_span; +use tracing::instrument; +use tracing::trace; +use tracing::Instrument; +use utils::BiChannelEndpoint; +use wtransport_proto::error::ErrorCode; +use wtransport_proto::frame::Frame; +use wtransport_proto::session::SessionRequest; +use wtransport_proto::settings::Settings; + +#[derive(Clone, Debug)] +pub enum DriverError { + Proto(ErrorCode), + ApplicationClosed(ApplicationClose), + NotConnected, +} + +#[derive(Debug)] +pub struct Driver { + quic_connection: quinn::Connection, + ready_settings: Mutex>, + ready_sessions: BiChannelEndpoint, + ready_uni_wt_streams: Mutex>, + ready_bi_wt_streams: Mutex>, + ready_datagrams: Mutex>, + driver_result: SharedResultGet, +} + +impl Driver { + pub fn init(quic_connection: quinn::Connection) -> Self { + let ready_settings = mpsc::channel(1); + let ready_sessions = bichannel(1); + let ready_uni_wt_streams = mpsc::channel(4); + let ready_bi_wt_streams = mpsc::channel(1); + let ready_datagrams = mpsc::channel(1); + let driver_result = shared_result(); + + tokio::spawn( + worker::Worker::new( + quic_connection.clone(), + ready_settings.0, + ready_sessions.0, + ready_uni_wt_streams.0, + ready_bi_wt_streams.0, + ready_datagrams.0, + driver_result.0, + ) + .run() + .instrument(debug_span!("Driver", quic_id = quic_connection.stable_id())), + ); + + Self { + quic_connection, + ready_settings: Mutex::new(ready_settings.1), + ready_sessions: ready_sessions.1, + ready_uni_wt_streams: Mutex::new(ready_uni_wt_streams.1), + ready_bi_wt_streams: Mutex::new(ready_bi_wt_streams.1), + ready_datagrams: Mutex::new(ready_datagrams.1), + driver_result: driver_result.1, + } + } + + pub async fn accept_settings(&self) -> Result { + let mut lock = self.ready_settings.lock().await; + + match lock.recv().await { + Some(settings) => Ok(settings), + None => Err(self.result().await), + } + } + + pub async fn accept_session(&self) -> Result { + match self.ready_sessions.recv().await { + Some(session) => Ok(session), + None => Err(self.result().await), + } + } + + pub async fn open_session( + &self, + session_request: SessionRequest, + ) -> Result { + let stream = Stream::open_bi(&self.quic_connection) + .await + .ok_or(DriverError::NotConnected)? + .upgrade() + .into_session(session_request); + + Ok(stream) + } + + pub async fn register_session(&self, stream_session: StreamSession) -> Result<(), DriverError> { + match self.ready_sessions.send(stream_session).await { + Ok(()) => Ok(()), + Err(SendError) => Err(self.result().await), + } + } + + pub async fn accept_uni( + &self, + session_id: SessionId, + ) -> Result { + let mut lock = self.ready_uni_wt_streams.lock().await; + + loop { + let Some(stream) = lock.recv().await else { + return Err(self.result().await); + }; + + if stream.session_id() == session_id { + return Ok(stream); + } + + debug!( + "Discarding WT stream (stream_id: {}, session_id: {})", + stream.id(), + stream.session_id() + ); + + stream + .into_stream() + .stop(ErrorCode::BufferedStreamRejected.to_code()) + .expect("Stream not already stopped"); + } + } + + pub async fn accept_bi(&self, session_id: SessionId) -> Result { + let mut lock = self.ready_bi_wt_streams.lock().await; + + loop { + let Some(stream) = lock.recv().await else { + return Err(self.result().await); + }; + + if stream.session_id() == session_id { + return Ok(stream); + } + + debug!( + "Discarding WT stream (stream_id: {}, session_id: {})", + stream.id(), + stream.session_id() + ); + + stream + .into_stream() + .1 + .stop(ErrorCode::BufferedStreamRejected.to_code()) + .expect("Stream not already stopped"); + } + } + + pub async fn receive_datagram(&self, session_id: SessionId) -> Result { + let mut lock = self.ready_datagrams.lock().await; + + loop { + let Some(datagram) = lock.recv().await else { + return Err(self.result().await); + }; + + if datagram.session_id() == session_id { + return Ok(datagram); + } + + debug!( + "Incoming datagram discarded (session_id: {})", + datagram.session_id() + ); + } + } + + pub async fn open_uni(&self, session_id: SessionId) -> Result { + let quic_stream = Stream::open_uni(&self.quic_connection) + .await + .ok_or(DriverError::NotConnected)?; + + Ok(OpeningUniStream::new(session_id, quic_stream)) + } + + pub async fn open_bi(&self, session_id: SessionId) -> Result { + let quic_stream = Stream::open_bi(&self.quic_connection) + .await + .ok_or(DriverError::NotConnected)?; + + Ok(OpeningBiStream::new(session_id, quic_stream)) + } + + pub fn send_datagram( + &self, + session_id: SessionId, + payload: &[u8], + ) -> Result<(), SendDatagramError> { + let quic_datagram = Datagram::write(session_id, payload).into_quic_bytes(); + + match self.quic_connection.send_datagram(quic_datagram) { + Ok(()) => Ok(()), + Err(quinn::SendDatagramError::UnsupportedByPeer) => { + Err(SendDatagramError::UnsupportedByPeer) + } + Err(quinn::SendDatagramError::Disabled) => { + unreachable!() + } + + Err(quinn::SendDatagramError::TooLarge) => Err(SendDatagramError::TooLarge), + Err(quinn::SendDatagramError::ConnectionLost(_)) => { + Err(SendDatagramError::NotConnected) + } + } + } + + async fn result(&self) -> DriverError { + match self.driver_result.result().await { + Some(error) => error, + None => panic!("Driver worker panic!"), + } + } +} + +mod worker { + use super::*; + use crate::driver::streams::qpack::RemoteQPackDecStream; + use crate::driver::streams::qpack::RemoteQPackEncStream; + use crate::driver::streams::settings::LocalSettingsStream; + use crate::driver::streams::settings::RemoteSettingsStream; + use crate::driver::streams::uniremote::StreamUniRemoteH3; + use crate::driver::streams::ProtoReadError; + use crate::driver::streams::ProtoWriteError; + use crate::driver::utils::TrySendError; + use streams::connect::ConnectStream; + use utils::varint_w2q; + use wtransport_proto::frame::FrameKind; + use wtransport_proto::headers::Headers; + use wtransport_proto::session::HeadersParseError; + use wtransport_proto::stream_header::StreamHeader; + use wtransport_proto::stream_header::StreamKind; + + pub struct Worker { + quic_connection: quinn::Connection, + ready_settings: mpsc::Sender, + ready_sessions: BiChannelEndpoint, + ready_uni_wt_streams: mpsc::Sender, + ready_bi_wt_streams: mpsc::Sender, + ready_datagrams: mpsc::Sender, + driver_result: SharedResultSet, + local_settings_stream: LocalSettingsStream, + remote_settings_stream: RemoteSettingsStream, + remote_qpack_enc_stream: RemoteQPackEncStream, + remote_qpack_dec_stream: RemoteQPackDecStream, + connect_stream: ConnectStream, + } + + impl Worker { + pub fn new( + quic_connection: quinn::Connection, + ready_settings: mpsc::Sender, + ready_sessions: BiChannelEndpoint, + ready_uni_wt_streams: mpsc::Sender, + ready_bi_wt_streams: mpsc::Sender, + ready_datagrams: mpsc::Sender, + driver_result: SharedResultSet, + ) -> Self { + Self { + quic_connection, + ready_settings, + ready_sessions, + ready_uni_wt_streams, + ready_bi_wt_streams, + ready_datagrams, + driver_result, + local_settings_stream: LocalSettingsStream::empty(), + remote_settings_stream: RemoteSettingsStream::empty(), + remote_qpack_enc_stream: RemoteQPackEncStream::empty(), + remote_qpack_dec_stream: RemoteQPackDecStream::empty(), + connect_stream: ConnectStream::empty(), + } + } + + pub async fn run(mut self) { + debug!("Started"); + + let error = self + .run_impl() + .await + .expect_err("Worker must return an error"); + + debug!("Ended with error: {:?}", error); + + match &error { + DriverError::ApplicationClosed(_) => { + // Termination procedure + // TODO Reset send sides of all streams with SessionGone error + self.quic_connection + .close(varint_w2q(ErrorCode::NoError.to_code()), b""); + } + DriverError::Proto(error_code) => { + self.quic_connection + .close(varint_w2q(error_code.to_code()), b""); + } + DriverError::NotConnected => (), + } + + self.driver_result.set(error); + } + + async fn run_impl(&mut self) -> Result<(), DriverError> { + let mut remote_settings_watcher = self.remote_settings_stream.subscribe(); + let mut ready_uni_h3_streams = mpsc::channel(4); + let mut ready_bi_h3_streams = mpsc::channel(1); + + self.open_and_send_settings().await?; + + loop { + tokio::select! { + result = Self::accept_uni(&self.quic_connection, + &ready_uni_h3_streams.0, + &self.ready_uni_wt_streams) => { + result?; + } + + result = Self::accept_bi(&self.quic_connection, + &ready_bi_h3_streams.0, + &self.ready_bi_wt_streams) => { + result?; + } + + result = Self::accept_datagram(&self.quic_connection, + &self.ready_datagrams) => { + result?; + } + + uni_h3_stream = ready_uni_h3_streams.1.recv() => { + let uni_h3_stream = uni_h3_stream.expect("Sender cannot be dropped")?; + self.handle_uni_h3_stream(uni_h3_stream)?; + } + + bi_h3_stream = ready_bi_h3_streams.1.recv() => { + let (bi_h3_stream, first_frame) = bi_h3_stream.expect("Sender cannot be dropped")?; + self.handle_bi_h3_stream(bi_h3_stream, first_frame)?; + } + + + settings = remote_settings_watcher.accept_settings() => { + let settings = settings.expect("Channel cannot be dropped"); + self.handle_remote_settings(settings)?; + } + + stream_session = self.ready_sessions.recv() => { + match stream_session { + Some(stream_session) => { + if self.connect_stream.is_empty() { + self.connect_stream.set_stream(stream_session); + } + } + None => return Err(DriverError::NotConnected), + }; + } + + error = Self::run_control_streams(&mut self.local_settings_stream, + &mut self.remote_settings_stream, + &mut self.remote_qpack_enc_stream, + &mut self.remote_qpack_dec_stream, + &mut self.connect_stream) => { + return Err(error); + } + + () = self.driver_result.closed() => { + return Err(DriverError::NotConnected); + } + } + } + } + + async fn open_and_send_settings(&mut self) -> Result<(), DriverError> { + assert!(self.local_settings_stream.is_empty()); + + let stream = match Stream::open_uni(&self.quic_connection) + .await + .ok_or(DriverError::NotConnected)? + .upgrade(StreamHeader::new_control()) + .await + { + Ok(h3_stream) => h3_stream, + Err(ProtoWriteError::NotConnected) => return Err(DriverError::NotConnected), + Err(ProtoWriteError::Stopped) => { + return Err(DriverError::Proto(ErrorCode::ClosedCriticalStream)); + } + }; + + self.local_settings_stream.set_stream(stream); + self.local_settings_stream.send_settings().await + } + + async fn accept_uni( + quic_connection: &quinn::Connection, + ready_uni_h3_streams: &mpsc::Sender>, + ready_uni_wt_streams: &mpsc::Sender, + ) -> Result<(), DriverError> { + trace!("H3 uni queue capacity: {}", ready_uni_h3_streams.capacity()); + let h3_slot = ready_uni_h3_streams + .clone() + .reserve_owned() + .await + .expect("Receiver cannot be dropped"); + + let wt_slot = match ready_uni_wt_streams.clone().reserve_owned().await { + Ok(wt_slot) => wt_slot, + Err(mpsc::error::SendError(_)) => return Err(DriverError::NotConnected), + }; + + let stream_quic = Stream::accept_uni(quic_connection) + .await + .ok_or(DriverError::NotConnected)?; + + let stream_id = stream_quic.id(); + debug!("New incoming uni stream ({})", stream_id); + + tokio::spawn( + async move { + let stream_h3 = match stream_quic.upgrade().await { + Ok(stream_h3) => stream_h3, + Err(ProtoReadError::H3(error_code)) => { + h3_slot.send(Err(DriverError::Proto(error_code))); + return; + } + Err(ProtoReadError::IO(_)) => { + return; + } + }; + + let stream_kind = stream_h3.kind(); + debug!("Type: {:?}", stream_kind); + + if matches!(stream_kind, StreamKind::WebTransport) { + let stream_wt = stream_h3.upgrade(); + wt_slot.send(stream_wt); + } else { + h3_slot.send(Ok(stream_h3)); + } + } + .instrument(debug_span!("Stream", "id={}", stream_id)), + ); + + Ok(()) + } + + async fn accept_bi( + quic_connection: &quinn::Connection, + ready_bi_h3_streams: &mpsc::Sender< + Result<(StreamBiRemoteH3, Frame<'static>), DriverError>, + >, + ready_bi_wt_streams: &mpsc::Sender, + ) -> Result<(), DriverError> { + trace!("H3 bi queue capacity: {}", ready_bi_h3_streams.capacity()); + let h3_slot = ready_bi_h3_streams + .clone() + .reserve_owned() + .await + .expect("Receiver cannot be dropped"); + + let wt_slot = match ready_bi_wt_streams.clone().reserve_owned().await { + Ok(wt_slot) => wt_slot, + Err(mpsc::error::SendError(_)) => return Err(DriverError::NotConnected), + }; + + let stream_quic = Stream::accept_bi(quic_connection) + .await + .ok_or(DriverError::NotConnected)?; + + let stream_id = stream_quic.id(); + debug!("New incoming bi stream ({})", stream_id); + + tokio::spawn( + async move { + let mut stream_h3 = stream_quic.upgrade(); + + let frame = loop { + match stream_h3.read_frame().await { + Ok(frame) => { + debug!("Frame kind: {:?}", frame.kind()); + if !matches!(frame.kind(), FrameKind::Exercise(_)) { + break frame; + } + } + Err(ProtoReadError::H3(error_code)) => { + h3_slot.send(Err(DriverError::Proto(error_code))); + return; + } + Err(ProtoReadError::IO(_)) => { + return; + } + } + }; + + debug!("First frame: {:?}", frame); + + match frame.session_id() { + Some(session_id) => { + let stream_wt = stream_h3.upgrade(session_id); + wt_slot.send(stream_wt); + } + None => { + h3_slot.send(Ok((stream_h3, frame))); + } + } + } + .instrument(debug_span!("Stream", "id={}", stream_id)), + ); + + Ok(()) + } + + async fn accept_datagram( + quic_connection: &quinn::Connection, + ready_datagrams: &mpsc::Sender, + ) -> Result<(), DriverError> { + let slot = match ready_datagrams.reserve().await { + Ok(slot) => slot, + Err(mpsc::error::SendError(_)) => return Err(DriverError::NotConnected), + }; + + let quic_dgram = quic_connection + .read_datagram() + .await + .map_err(|_| DriverError::NotConnected)?; + + let datagram = match Datagram::read(quic_dgram) { + Ok(datagram) => datagram, + Err(error_code) => return Err(DriverError::Proto(error_code)), + }; + + trace!( + "New incoming datagram (session_id: {})", + datagram.session_id() + ); + + slot.send(datagram); + + Ok(()) + } + + fn handle_uni_h3_stream(&mut self, stream: StreamUniRemoteH3) -> Result<(), DriverError> { + match stream.kind() { + StreamKind::Control => { + if !self.remote_settings_stream.is_empty() { + return Err(DriverError::Proto(ErrorCode::StreamCreation)); + } + + self.remote_settings_stream.set_stream(stream); + } + StreamKind::QPackEncoder => { + if !self.remote_qpack_enc_stream.is_empty() { + return Err(DriverError::Proto(ErrorCode::StreamCreation)); + } + + self.remote_qpack_enc_stream.set_stream(stream); + } + StreamKind::QPackDecoder => { + if !self.remote_qpack_dec_stream.is_empty() { + return Err(DriverError::Proto(ErrorCode::StreamCreation)); + } + + self.remote_qpack_dec_stream.set_stream(stream); + } + StreamKind::WebTransport => unreachable!(), + StreamKind::Exercise(_) => {} + } + + Ok(()) + } + + #[instrument(skip_all, name = "Stream", fields(id = %stream.id()))] + fn handle_bi_h3_stream( + &mut self, + mut stream: StreamBiRemoteH3, + first_frame: Frame<'static>, + ) -> Result<(), DriverError> { + match first_frame.kind() { + FrameKind::Data => { + return Err(DriverError::Proto(ErrorCode::FrameUnexpected)); + } + FrameKind::Headers => { + let headers = match Headers::with_frame(&first_frame) { + Ok(headers) => headers, + Err(error_code) => return Err(DriverError::Proto(error_code)), + }; + + debug!("Headers: {:?}", headers); + + let stream_session = match SessionRequest::try_from(headers) { + Ok(session_request) => stream.into_session(session_request), + Err(HeadersParseError::MethodNotConnect) => { + stream + .stop(ErrorCode::RequestRejected.to_code()) + .expect("Stream not already stopped"); + return Ok(()); + } + // TODO(biagio): we might have more granularity with errors + Err(_) => { + stream + .stop(ErrorCode::Message.to_code()) + .expect("Stream not already stopped"); + return Ok(()); + } + }; + + match self.ready_sessions.try_send(stream_session) { + Ok(()) => {} + Err(TrySendError::Full(mut stream)) => { + debug!("Discarding session request: sessions queue is full"); + stream + .stop(ErrorCode::RequestRejected.to_code()) + .expect("Stream not already stopped"); + } + Err(TrySendError::Closed(_)) => return Err(DriverError::NotConnected), + } + } + FrameKind::Settings => { + return Err(DriverError::Proto(ErrorCode::FrameUnexpected)); + } + FrameKind::WebTransport => unreachable!(), + FrameKind::Exercise(_) => {} + } + + Ok(()) + } + + async fn run_control_streams( + local_settings: &mut LocalSettingsStream, + remote_settings: &mut RemoteSettingsStream, + remote_qpack_enc: &mut RemoteQPackEncStream, + remote_qpack_dec: &mut RemoteQPackDecStream, + connect_stream: &mut ConnectStream, + ) -> DriverError { + tokio::select! { + error = connect_stream.run() => error, + error = local_settings.run() => error, + error = remote_settings.run() => error, + error = remote_qpack_enc.run() => error, + error = remote_qpack_dec.run() => error, + } + } + + fn handle_remote_settings(&mut self, settings: Settings) -> Result<(), DriverError> { + debug!("Received: {:?}", settings); + + match self.ready_settings.try_send(settings) { + Ok(()) => Ok(()), + Err(mpsc::error::TrySendError::Closed(_)) => Err(DriverError::NotConnected), + Err(mpsc::error::TrySendError::Full(_)) => { + unreachable!("No more than 1 setting frame can be processed") + } + } + } + } +} + +pub(crate) mod streams; +pub(crate) mod utils; diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/connect.rs b/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/connect.rs new file mode 100644 index 000000000..bce6db105 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/connect.rs @@ -0,0 +1,105 @@ +use tracing::debug; +use wtransport_proto::bytes::IoReadError; +use wtransport_proto::capsule::capsules; +use wtransport_proto::capsule::Capsule; +use wtransport_proto::capsule::{self}; +use wtransport_proto::error::ErrorCode; +use wtransport_proto::frame::FrameKind; +use wtransport_proto::varint::VarInt; + +use super::session::StreamSession; +use super::ProtoReadError; +use crate::driver::DriverError; +use crate::error::ApplicationClose; +use std::future::pending; + +pub struct ConnectStream { + stream: Option, +} + +impl ConnectStream { + pub fn empty() -> Self { + Self { stream: None } + } + + pub fn is_empty(&self) -> bool { + self.stream.is_none() + } + + pub fn set_stream(&mut self, stream: StreamSession) { + self.stream = Some(stream); + } + + pub async fn run(&mut self) -> DriverError { + let stream = match self.stream.as_mut() { + Some(stream) => stream, + None => pending().await, + }; + + loop { + return match stream.read_frame().await { + Ok(frame) => { + if !matches!(frame.kind(), FrameKind::Data) { + debug!("Skipping non-data frame of kind {:?}", frame.kind()); + continue; + } + + let capsule = match Capsule::with_frame(&frame) { + Some(capsule) + if matches!( + capsule.kind(), + capsule::CapsuleKind::CloseWebTransportSession + ) => + { + capsule + } + // Unknown capsule, skip it + _ => { + debug!( + "Unknown capsule of kind {:?} of {}B", + frame.kind(), + frame.payload().len() + ); + + continue; + } + }; + + let close_session = + match capsules::CloseWebTransportSession::with_capsule(&capsule) { + Ok(close_session) => close_session, + Err(error_code) => return DriverError::Proto(error_code), + }; + + // reset right away to avoid receiving additional data which requires resetting with ErrorCode::Message. + self.stream + .take() + .unwrap() + .reset(ErrorCode::NoError.to_code()); + + DriverError::ApplicationClosed(ApplicationClose::new( + close_session.error_code(), + close_session + .reason() + .as_bytes() + .to_vec() + .into_boxed_slice(), + )) + } + Err(ProtoReadError::H3(error_code)) => DriverError::Proto(error_code), + Err(ProtoReadError::IO(io_error)) => match io_error { + // Cleanly terminating a CONNECT stream without a CLOSE_WEBTRANSPORT_SESSION + // capsule SHALL be semantically equivalent to terminating it with a + // CLOSE_WEBTRANSPORT_SESSION capsule that has an error code of 0 and an empty error string. + IoReadError::ImmediateFin => DriverError::ApplicationClosed( + ApplicationClose::new(VarInt::from_u32(0), Box::new([])), + ), + IoReadError::UnexpectedFin | IoReadError::Reset => { + DriverError::Proto(ErrorCode::ClosedCriticalStream) + } + IoReadError::NotConnected => DriverError::NotConnected, + }, + }; + } + } +} diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/mod.rs b/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/mod.rs new file mode 100644 index 000000000..7382c4835 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/mod.rs @@ -0,0 +1,588 @@ +use crate::driver::utils::streamid_q2w; +use crate::driver::utils::varint_q2w; +use crate::driver::utils::varint_w2q; +use crate::error::ClosedStream; +use crate::error::StreamReadError; +use crate::error::StreamReadExactError; +use crate::error::StreamWriteError; +use crate::SessionId; +use crate::StreamId; +use crate::VarInt; +use std::pin::Pin; +use std::task::ready; +use std::task::Context; +use std::task::Poll; +use tokio::io::ReadBuf; +use wtransport_proto::frame::Frame; +use wtransport_proto::session::SessionRequest; +use wtransport_proto::stream as stream_proto; +use wtransport_proto::stream::Stream as StreamProto; +use wtransport_proto::stream_header::StreamHeader; +use wtransport_proto::stream_header::StreamKind; + +pub type ProtoReadError = wtransport_proto::stream::IoReadError; +pub type ProtoWriteError = wtransport_proto::stream::IoWriteError; + +#[derive(Debug)] +pub struct AlreadyStop; + +#[derive(Debug)] +pub struct QuicSendStream(quinn::SendStream); + +impl QuicSendStream { + #[inline(always)] + pub async fn write(&mut self, buf: &[u8]) -> Result { + let written = self.0.write(buf).await?; + Ok(written) + } + + #[inline(always)] + pub async fn write_all(&mut self, buf: &[u8]) -> Result<(), StreamWriteError> { + self.0.write_all(buf).await?; + Ok(()) + } + + #[inline(always)] + pub async fn finish(&mut self) -> Result<(), StreamWriteError> { + let _ = self.0.finish(); + let result = self.stopped().await; + if matches!(result, StreamWriteError::Closed) { + Ok(()) + } else { + Err(result) + } + } + + #[inline(always)] + pub fn set_priority(&self, priority: i32) { + let _ = self.0.set_priority(priority); + } + + #[inline(always)] + /// # Panics + /// + /// If `reset` was called. + pub fn priority(&self) -> i32 { + self.0.priority().expect("Stream has been reset") + } + + pub async fn stopped(&mut self) -> StreamWriteError { + match self.0.stopped().await { + Ok(None) => StreamWriteError::Closed, + Ok(Some(code)) => StreamWriteError::Stopped(varint_q2w(code)), + Err(quinn::StoppedError::ConnectionLost(_)) => StreamWriteError::NotConnected, + Err(quinn::StoppedError::ZeroRttRejected) => StreamWriteError::QuicProto, + } + } + + #[inline(always)] + pub fn reset(&mut self, error_code: VarInt) -> Result<(), ClosedStream> { + self.0 + .reset(varint_w2q(error_code)) + .map_err(|_| ClosedStream) + } + + #[inline(always)] + pub fn id(&self) -> StreamId { + streamid_q2w(self.0.id()) + } + + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + #[inline(always)] + pub fn quic_stream(&self) -> &quinn::SendStream { + &self.0 + } + + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + #[inline(always)] + pub fn quic_stream_mut(&mut self) -> &mut quinn::SendStream { + &mut self.0 + } +} + +impl wtransport_proto::bytes::AsyncWrite for QuicSendStream { + #[inline(always)] + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + tokio::io::AsyncWrite::poll_write(Pin::new(&mut self.0), cx, buf) + } +} + +impl tokio::io::AsyncWrite for QuicSendStream { + #[inline(always)] + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + tokio::io::AsyncWrite::poll_write(Pin::new(&mut self.0), cx, buf) + } + + #[inline(always)] + fn poll_flush( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + tokio::io::AsyncWrite::poll_flush(Pin::new(&mut self.0), cx) + } + + #[inline(always)] + fn poll_shutdown( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + tokio::io::AsyncWrite::poll_shutdown(Pin::new(&mut self.0), cx) + } + + #[inline(always)] + fn poll_write_vectored( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[std::io::IoSlice<'_>], + ) -> Poll> { + tokio::io::AsyncWrite::poll_write_vectored(Pin::new(&mut self.0), cx, bufs) + } + + fn is_write_vectored(&self) -> bool { + tokio::io::AsyncWrite::is_write_vectored(&self.0) + } +} + +#[derive(Debug)] +pub struct QuicRecvStream(quinn::RecvStream); + +impl QuicRecvStream { + #[inline(always)] + pub async fn read(&mut self, buf: &mut [u8]) -> Result, StreamReadError> { + match self.0.read(buf).await? { + Some(read) => Ok(Some(read)), + None => Ok(None), + } + } + + #[inline(always)] + pub async fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), StreamReadExactError> { + self.0 + .read_exact(buf) + .await + .map_err(|quic_error| match quic_error { + quinn::ReadExactError::FinishedEarly(read) => { + StreamReadExactError::FinishedEarly(read) + } + quinn::ReadExactError::ReadError(read) => StreamReadExactError::Read(read.into()), + }) + } + + #[inline(always)] + pub fn stop(&mut self, error_code: VarInt) -> Result<(), AlreadyStop> { + self.0.stop(varint_w2q(error_code)).map_err(|_| AlreadyStop) + } + + #[inline(always)] + pub fn id(&self) -> StreamId { + streamid_q2w(self.0.id()) + } + + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + #[inline(always)] + pub fn quic_stream(&self) -> &quinn::RecvStream { + &self.0 + } + + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + #[inline(always)] + pub fn quic_stream_mut(&mut self) -> &mut quinn::RecvStream { + &mut self.0 + } +} + +impl wtransport_proto::bytes::AsyncRead for QuicRecvStream { + #[inline(always)] + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll> { + let mut buffer = ReadBuf::new(buf); + + match ready!(tokio::io::AsyncRead::poll_read( + Pin::new(&mut self.0), + cx, + &mut buffer + )) { + Ok(()) => Poll::Ready(Ok(buffer.filled().len())), + Err(io_error) => Poll::Ready(Err(io_error)), + } + } +} + +impl tokio::io::AsyncRead for QuicRecvStream { + #[inline(always)] + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + tokio::io::AsyncRead::poll_read(Pin::new(&mut self.0), cx, buf) + } +} + +#[derive(Debug)] +pub struct Stream { + stream: S, + proto: P, +} + +pub mod biremote { + use super::*; + + pub type StreamBiRemoteQuic = + Stream<(QuicSendStream, QuicRecvStream), stream_proto::biremote::StreamBiRemoteQuic>; + + pub type StreamBiRemoteH3 = + Stream<(QuicSendStream, QuicRecvStream), stream_proto::biremote::StreamBiRemoteH3>; + + pub type StreamBiRemoteWT = + Stream<(QuicSendStream, QuicRecvStream), stream_proto::biremote::StreamBiRemoteWT>; + + impl StreamBiRemoteQuic { + pub async fn accept_bi(quic_connection: &quinn::Connection) -> Option { + let stream = quic_connection.accept_bi().await.ok()?; + Some(Self { + stream: (QuicSendStream(stream.0), QuicRecvStream(stream.1)), + proto: StreamProto::accept_bi(), + }) + } + + pub fn upgrade(self) -> StreamBiRemoteH3 { + StreamBiRemoteH3 { + stream: self.stream, + proto: self.proto.upgrade(), + } + } + + #[inline(always)] + pub fn id(&self) -> StreamId { + self.stream.0.id() + } + } + + impl StreamBiRemoteH3 { + pub async fn read_frame<'a>(&mut self) -> Result, ProtoReadError> { + self.proto.read_frame_async(&mut self.stream.1).await + } + + pub fn stop(&mut self, error_code: VarInt) -> Result<(), AlreadyStop> { + self.stream.1.stop(error_code) + } + + pub fn upgrade(self, session_id: SessionId) -> StreamBiRemoteWT { + StreamBiRemoteWT { + stream: self.stream, + proto: self.proto.upgrade(session_id), + } + } + + pub fn id(&self) -> StreamId { + self.stream.0.id() + } + + pub fn into_session(self, session_request: SessionRequest) -> session::StreamSession { + session::StreamSession { + stream: self.stream, + proto: self.proto.into_session(session_request), + } + } + } + + impl StreamBiRemoteWT { + #[inline(always)] + pub fn session_id(&self) -> SessionId { + self.proto.session_id() + } + + #[inline(always)] + pub fn id(&self) -> StreamId { + self.stream.0.id() + } + + #[inline(always)] + pub fn into_stream(self) -> (QuicSendStream, QuicRecvStream) { + self.stream + } + } +} + +pub mod bilocal { + use super::*; + + pub type StreamBiLocalQuic = + Stream<(QuicSendStream, QuicRecvStream), stream_proto::bilocal::StreamBiLocalQuic>; + + pub type StreamBiLocalH3 = + Stream<(QuicSendStream, QuicRecvStream), stream_proto::bilocal::StreamBiLocalH3>; + + pub type StreamBiLocalWT = + Stream<(QuicSendStream, QuicRecvStream), stream_proto::bilocal::StreamBiLocalWT>; + + impl StreamBiLocalQuic { + pub async fn open_bi(quic_connection: &quinn::Connection) -> Option { + let stream = quic_connection.open_bi().await.ok()?; + Some(Self { + stream: (QuicSendStream(stream.0), QuicRecvStream(stream.1)), + proto: StreamProto::open_bi(), + }) + } + + pub fn upgrade(self) -> StreamBiLocalH3 { + StreamBiLocalH3 { + stream: self.stream, + proto: self.proto.upgrade(), + } + } + } + + impl StreamBiLocalH3 { + pub async fn upgrade( + mut self, + session_id: SessionId, + ) -> Result { + let proto = self + .proto + .upgrade_async(session_id, &mut self.stream.0) + .await?; + + Ok(StreamBiLocalWT { + stream: self.stream, + proto, + }) + } + + pub fn into_session(self, session_request: SessionRequest) -> session::StreamSession { + session::StreamSession { + stream: self.stream, + proto: self.proto.into_session(session_request), + } + } + } + + impl StreamBiLocalWT { + pub fn into_stream(self) -> (QuicSendStream, QuicRecvStream) { + self.stream + } + } +} + +pub mod uniremote { + use super::*; + + pub type StreamUniRemoteQuic = + Stream; + + pub type StreamUniRemoteH3 = Stream; + + pub type StreamUniRemoteWT = Stream; + + impl StreamUniRemoteQuic { + pub async fn accept_uni(quic_connection: &quinn::Connection) -> Option { + let stream = quic_connection.accept_uni().await.ok()?; + Some(Self { + stream: QuicRecvStream(stream), + proto: StreamProto::accept_uni(), + }) + } + + pub async fn upgrade(mut self) -> Result { + let proto = self.proto.upgrade_async(&mut self.stream).await?; + Ok(StreamUniRemoteH3 { + stream: self.stream, + proto, + }) + } + + #[inline(always)] + pub fn id(&self) -> StreamId { + self.stream.id() + } + } + + impl StreamUniRemoteH3 { + pub async fn read_frame<'a>(&mut self) -> Result, ProtoReadError> { + self.proto.read_frame_async(&mut self.stream).await + } + + pub fn kind(&self) -> StreamKind { + self.proto.kind() + } + + pub fn upgrade(self) -> StreamUniRemoteWT { + StreamUniRemoteWT { + stream: self.stream, + proto: self.proto.upgrade(), + } + } + + pub fn stream_mut(&mut self) -> &mut QuicRecvStream { + &mut self.stream + } + } + + impl StreamUniRemoteWT { + #[inline(always)] + pub fn session_id(&self) -> SessionId { + self.proto.session_id() + } + + #[inline(always)] + pub fn id(&self) -> StreamId { + self.stream.id() + } + + #[inline(always)] + pub fn into_stream(self) -> QuicRecvStream { + self.stream + } + } +} + +pub mod unilocal { + use super::*; + + pub type StreamUniLocalQuic = + Stream; + + pub type StreamUniLocalH3 = Stream; + + pub type StreamUniLocalWT = Stream; + + impl StreamUniLocalQuic { + pub async fn open_uni(quic_connection: &quinn::Connection) -> Option { + let stream = quic_connection.open_uni().await.ok()?; + Some(Self { + stream: QuicSendStream(stream), + proto: StreamProto::open_uni(), + }) + } + + pub async fn upgrade( + mut self, + stream_header: StreamHeader, + ) -> Result { + let proto = self + .proto + .upgrade_async(stream_header, &mut self.stream) + .await?; + + Ok(StreamUniLocalH3 { + stream: self.stream, + proto, + }) + } + } + + impl StreamUniLocalH3 { + pub async fn write_frame(&mut self, frame: Frame<'_>) -> Result<(), ProtoWriteError> { + self.proto.write_frame_async(frame, &mut self.stream).await + } + + pub fn kind(&self) -> StreamKind { + self.proto.kind() + } + + pub async fn stopped(&mut self) -> StreamWriteError { + self.stream.stopped().await + } + + pub fn upgrade(self) -> StreamUniLocalWT { + StreamUniLocalWT { + stream: self.stream, + proto: self.proto.upgrade(), + } + } + } + + impl StreamUniLocalWT { + pub fn into_stream(self) -> QuicSendStream { + self.stream + } + } +} + +pub mod session { + use super::*; + + pub type StreamSession = + Stream<(QuicSendStream, QuicRecvStream), stream_proto::session::StreamSession>; + + impl StreamSession { + pub async fn read_frame<'a>(&mut self) -> Result, ProtoReadError> { + self.proto.read_frame_async(&mut self.stream.1).await + } + + pub async fn write_frame(&mut self, frame: Frame<'_>) -> Result<(), ProtoWriteError> { + self.proto + .write_frame_async(frame, &mut self.stream.0) + .await + } + + pub fn stop(&mut self, error_code: VarInt) -> Result<(), AlreadyStop> { + self.stream.1.stop(error_code) + } + + pub fn id(&self) -> StreamId { + self.stream.0.id() + } + + pub fn session_id(&self) -> SessionId { + SessionId::try_from_session_stream(self.id()).expect("Session stream must be valid") + } + + pub fn request(&self) -> &SessionRequest { + self.proto.request() + } + + pub async fn finish(&mut self) { + let _ = self.stream.0.finish().await; + } + + pub fn reset(&mut self, error_code: VarInt) { + let _ = self.stream.0.reset(error_code); + } + } +} + +impl From for StreamWriteError { + fn from(error: quinn::WriteError) -> Self { + match error { + quinn::WriteError::Stopped(code) => StreamWriteError::Stopped(varint_q2w(code)), + quinn::WriteError::ConnectionLost(_) | quinn::WriteError::ClosedStream => { + StreamWriteError::NotConnected + } + quinn::WriteError::ZeroRttRejected => StreamWriteError::QuicProto, + } + } +} + +impl From for StreamReadError { + fn from(error: quinn::ReadError) -> Self { + match error { + quinn::ReadError::Reset(code) => StreamReadError::Reset(varint_q2w(code)), + quinn::ReadError::ConnectionLost(_) | quinn::ReadError::ClosedStream => { + StreamReadError::NotConnected + } + quinn::ReadError::IllegalOrderedRead => StreamReadError::QuicProto, + quinn::ReadError::ZeroRttRejected => StreamReadError::QuicProto, + } + } +} + +pub mod connect; +pub mod qpack; +pub mod settings; diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/qpack.rs b/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/qpack.rs new file mode 100644 index 000000000..3589837ca --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/qpack.rs @@ -0,0 +1,106 @@ +use crate::driver::streams::uniremote::StreamUniRemoteH3; +use crate::driver::DriverError; +use crate::error::StreamReadError; +use crate::error::StreamReadExactError; +use std::future::pending; +use wtransport_proto::error::ErrorCode; +use wtransport_proto::stream_header::StreamKind; + +pub struct RemoteQPackEncStream { + stream: Option, + buffer: Box<[u8]>, +} + +impl RemoteQPackEncStream { + pub fn empty() -> Self { + let buffer = vec![0; 64].into_boxed_slice(); + + Self { + stream: None, + buffer, + } + } + + pub fn is_empty(&self) -> bool { + self.stream.is_none() + } + + pub fn set_stream(&mut self, stream: StreamUniRemoteH3) { + assert!(matches!(stream.kind(), StreamKind::QPackEncoder)); + self.stream = Some(stream); + } + + pub async fn run(&mut self) -> DriverError { + let stream = match self.stream.as_mut() { + Some(stream) => stream, + None => pending().await, + }; + + loop { + match stream.stream_mut().read_exact(&mut self.buffer).await { + Ok(()) => {} + Err(StreamReadExactError::FinishedEarly(_)) => { + return DriverError::Proto(ErrorCode::ClosedCriticalStream); + } + Err(StreamReadExactError::Read(StreamReadError::NotConnected)) => { + return DriverError::NotConnected; + } + Err(StreamReadExactError::Read(StreamReadError::Reset(_))) => { + return DriverError::Proto(ErrorCode::ClosedCriticalStream); + } + Err(StreamReadExactError::Read(StreamReadError::QuicProto)) => { + return DriverError::Proto(ErrorCode::ClosedCriticalStream); + } + } + } + } +} + +pub struct RemoteQPackDecStream { + stream: Option, + buffer: Box<[u8]>, +} + +impl RemoteQPackDecStream { + pub fn empty() -> Self { + let buffer = vec![0; 64].into_boxed_slice(); + Self { + stream: None, + buffer, + } + } + + pub fn is_empty(&self) -> bool { + self.stream.is_none() + } + + pub fn set_stream(&mut self, stream: StreamUniRemoteH3) { + assert!(matches!(stream.kind(), StreamKind::QPackDecoder)); + self.stream = Some(stream); + } + + pub async fn run(&mut self) -> DriverError { + let stream = match self.stream.as_mut() { + Some(stream) => stream, + None => pending().await, + }; + + loop { + match stream.stream_mut().read_exact(&mut self.buffer).await { + Ok(()) => {} + Err(StreamReadExactError::FinishedEarly(_)) => { + return DriverError::Proto(ErrorCode::ClosedCriticalStream); + } + Err(StreamReadExactError::Read(StreamReadError::NotConnected)) => { + return DriverError::NotConnected; + } + Err(StreamReadExactError::Read(StreamReadError::Reset(_))) => { + return DriverError::Proto(ErrorCode::ClosedCriticalStream); + } + Err(StreamReadExactError::Read(StreamReadError::QuicProto)) => { + return DriverError::Proto(ErrorCode::ClosedCriticalStream); + } + } + } + } +} diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/settings.rs b/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/settings.rs new file mode 100644 index 000000000..8b02b6e71 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/settings.rs @@ -0,0 +1,161 @@ +use crate::driver::streams::unilocal::StreamUniLocalH3; +use crate::driver::streams::uniremote::StreamUniRemoteH3; +use crate::driver::streams::ProtoReadError; +use crate::driver::streams::ProtoWriteError; +use crate::driver::DriverError; +use crate::error::StreamWriteError; +use crate::VarInt; +use std::future::pending; +use tokio::sync::watch; +use wtransport_proto::bytes; +use wtransport_proto::error::ErrorCode; +use wtransport_proto::frame::Frame; +use wtransport_proto::frame::FrameKind; +use wtransport_proto::settings::Settings; +use wtransport_proto::stream_header::StreamKind; + +pub struct LocalSettingsStream { + stream: Option, + settings: Settings, +} + +impl LocalSettingsStream { + pub fn empty() -> Self { + let settings = Settings::builder() + .qpack_max_table_capacity(VarInt::from_u32(0)) + .qpack_blocked_streams(VarInt::from_u32(0)) + .enable_connect_protocol() // TODO(biagio): it would be nice to have this only for server + .enable_webtransport() + .enable_h3_datagrams() + .webtransport_max_sessions(VarInt::from_u32(1)) + .build(); + + Self { + stream: None, + settings, + } + } + + pub fn is_empty(&self) -> bool { + self.stream.is_none() + } + + pub fn set_stream(&mut self, stream: StreamUniLocalH3) { + assert!(matches!(stream.kind(), StreamKind::Control)); + self.stream = Some(stream); + } + + pub async fn send_settings(&mut self) -> Result<(), DriverError> { + match self + .stream + .as_mut() + .expect("Cannot send settings on empty stream") + .write_frame(self.settings.generate_frame()) + .await + { + Ok(()) => Ok(()), + Err(ProtoWriteError::NotConnected) => Err(DriverError::NotConnected), + Err(ProtoWriteError::Stopped) => { + Err(DriverError::Proto(ErrorCode::ClosedCriticalStream)) + } + } + } + + pub async fn run(&mut self) -> DriverError { + match self.stream.as_mut() { + Some(stream) => match stream.stopped().await { + StreamWriteError::NotConnected => DriverError::NotConnected, + StreamWriteError::Closed | StreamWriteError::Stopped(_) => { + DriverError::Proto(ErrorCode::ClosedCriticalStream) + } + StreamWriteError::QuicProto => DriverError::Proto(ErrorCode::ClosedCriticalStream), + }, + None => pending().await, + } + } +} + +pub struct RemoteSettingsStream { + stream: Option, + settings: watch::Sender>, +} + +impl RemoteSettingsStream { + pub fn empty() -> Self { + Self { + stream: None, + settings: watch::channel(None).0, + } + } + + pub fn is_empty(&self) -> bool { + self.stream.is_none() + } + + pub fn set_stream(&mut self, stream: StreamUniRemoteH3) { + assert!(matches!(stream.kind(), StreamKind::Control)); + self.stream = Some(stream); + } + + pub fn subscribe(&self) -> RemoteSettingsWatcher { + RemoteSettingsWatcher(self.settings.subscribe()) + } + + pub async fn run(&mut self) -> DriverError { + loop { + let frame = match self.read_frame().await { + Ok(frame) => frame, + Err(driver_error) => return driver_error, + }; + + if self.settings.borrow().is_none() { + if !matches!(frame.kind(), FrameKind::Settings) { + return DriverError::Proto(ErrorCode::MissingSettings); + } + + let settings = match Settings::with_frame(&frame) { + Ok(settings) => settings, + Err(error_code) => return DriverError::Proto(error_code), + }; + + self.settings.send_replace(Some(settings)); + } else if !matches!(frame.kind(), FrameKind::Exercise(_)) { + return DriverError::Proto(ErrorCode::FrameUnexpected); + } + } + } + + async fn read_frame<'a>(&mut self) -> Result, DriverError> { + let Some(stream) = self.stream.as_mut() else { + return pending().await; + }; + + match stream.read_frame().await { + Ok(frame) => Ok(frame), + Err(ProtoReadError::H3(error_code)) => Err(DriverError::Proto(error_code)), + Err(ProtoReadError::IO(io_error)) => match io_error { + bytes::IoReadError::ImmediateFin + | bytes::IoReadError::UnexpectedFin + | bytes::IoReadError::Reset => { + Err(DriverError::Proto(ErrorCode::ClosedCriticalStream)) + } + bytes::IoReadError::NotConnected => Err(DriverError::NotConnected), + }, + } + } +} + +pub struct RemoteSettingsWatcher(watch::Receiver>); + +impl RemoteSettingsWatcher { + pub async fn accept_settings(&mut self) -> Option { + self.0.changed().await.ok()?; + + Some( + self.0 + .borrow() + .clone() + .expect("On change settings must be set"), + ) + } +} diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/driver/utils.rs b/joule-code/joule-code-rs/vendor/wtransport/src/driver/utils.rs new file mode 100644 index 000000000..50e854aa7 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/src/driver/utils.rs @@ -0,0 +1,236 @@ +use crate::StreamId; +use crate::VarInt; +use std::sync::Arc; +use tokio::sync::mpsc; +use tokio::sync::watch; +use tokio::sync::Mutex; + +#[inline(always)] +pub fn varint_q2w(varint: quinn::VarInt) -> VarInt { + // SAFETY: varint conversion + unsafe { + debug_assert!(varint.into_inner() <= VarInt::MAX.into_inner()); + VarInt::from_u64_unchecked(varint.into_inner()) + } +} + +#[inline(always)] +pub fn varint_w2q(varint: VarInt) -> quinn::VarInt { + // SAFETY: varint conversion + unsafe { + debug_assert!(varint.into_inner() <= quinn::VarInt::MAX.into_inner()); + quinn::VarInt::from_u64_unchecked(varint.into_inner()) + } +} + +#[inline(always)] +pub fn streamid_q2w(stream_id: quinn::StreamId) -> StreamId { + let value = quinn::VarInt::from(stream_id).into_inner(); + + let varint = unsafe { + debug_assert!(value <= VarInt::MAX.into_inner()); + VarInt::from_u64_unchecked(value) + }; + + StreamId::new(varint) +} + +pub fn shared_result() -> (SharedResultSet, SharedResultGet) +where + T: Clone, +{ + let set = SharedResultSet::new(); + let get = set.subscribe(); + (set, get) +} + +#[derive(Debug, Clone)] +pub struct SharedResultSet(Arc>>); + +impl SharedResultSet +where + T: Clone, +{ + #[inline(always)] + pub fn new() -> Self { + Self(Arc::new(watch::channel(None).0)) + } + + /// Sets the shared result in thread safe manner. + /// + /// The first call will be able to actually set the inner value, + /// successive calls end up into being no-op. + /// + /// Returns `true` if the inner result is actually set. + pub fn set(&self, result: T) -> bool { + self.0.send_if_modified(|state| { + if state.is_none() { + *state = Some(result); + true + } else { + false + } + }) + } + + /// Awaits all subscribers are dead. + #[inline(always)] + pub async fn closed(&self) { + self.0.closed().await; + } + + /// Generates a new subscriber. + /// + /// A subscriber is able to be notified when the shared result + /// will be set. + #[inline(always)] + pub fn subscribe(&self) -> SharedResultGet { + SharedResultGet(Mutex::new(self.0.subscribe())) + } +} + +#[derive(Debug)] +pub struct SharedResultGet(Mutex>>); + +impl SharedResultGet +where + T: Clone, +{ + /// Awaits the shared result is set by any setter. + /// + /// Once the shared result is set, this method will always + /// return that value (i.e., `Some(T)`). + /// + /// If all setters are dead before setting any result, this will + /// return `None`. And all successive calls will return `None`. + pub async fn result(&self) -> Option { + let mut lock = self.0.lock().await; + + loop { + if let Some(result) = lock.borrow().clone() { + return Some(result); + } + + if lock.changed().await.is_err() { + return None; + } + } + } +} + +pub struct SendError; + +pub enum TrySendError { + Full(T), + Closed(T), +} + +#[derive(Debug)] +pub struct BiChannelEndpoint { + sender: mpsc::Sender, + receiver: Mutex>, +} + +impl BiChannelEndpoint { + #[inline(always)] + pub async fn send(&self, value: T) -> Result<(), SendError> { + self.sender.send(value).await.map_err(|_| SendError) + } + + pub fn try_send(&self, value: T) -> Result<(), TrySendError> { + self.sender.try_send(value).map_err(|error| match error { + mpsc::error::TrySendError::Full(value) => TrySendError::Full(value), + mpsc::error::TrySendError::Closed(value) => TrySendError::Closed(value), + }) + } + + #[inline(always)] + pub async fn recv(&self) -> Option { + self.receiver.lock().await.recv().await + } +} + +pub fn bichannel(capacity: usize) -> (BiChannelEndpoint, BiChannelEndpoint) { + let c1 = mpsc::channel(capacity); + let c2 = mpsc::channel(capacity); + + ( + BiChannelEndpoint { + sender: c1.0, + receiver: Mutex::new(c2.1), + }, + BiChannelEndpoint { + sender: c2.0, + receiver: Mutex::new(c1.1), + }, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use utils::poll_once; + + #[tokio::test] + async fn shared_result_double_set() { + let set = SharedResultSet::new(); + assert!(set.set(1)); + assert!(!set.set(2)); + + let get = set.subscribe(); + assert!(matches!(get.result().await, Some(1))); + } + + #[tokio::test] + async fn shared_result_get_drop() { + let set = SharedResultSet::<()>::new(); + let get = set.subscribe(); + drop(set); + assert!(get.result().await.is_none()); + assert!(get.result().await.is_none()); + } + + #[tokio::test] + async fn shared_result_get() { + let set = SharedResultSet::new(); + let get = set.subscribe(); + + assert!(poll_once(get.result()).await.is_none()); + + set.set(1); + drop(set); + + assert!(matches!(poll_once(get.result()).await.unwrap(), Some(1))); + assert!(matches!(poll_once(get.result()).await.unwrap(), Some(1))); + } + + mod utils { + use std::future::Future; + use std::pin::Pin; + use std::task::Context; + use std::task::Poll; + + pub async fn poll_once(future: F) -> Option + where + F: Future, + { + PollOnce(Box::pin(future)).await + } + + struct PollOnce(Pin>); + + impl Future for PollOnce + where + F: Future, + { + type Output = Option; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match Future::poll(self.0.as_mut(), cx) { + Poll::Ready(result) => Poll::Ready(Some(result)), + Poll::Pending => Poll::Ready(None), + } + } + } + } +} diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/endpoint.rs b/joule-code/joule-code-rs/vendor/wtransport/src/endpoint.rs new file mode 100644 index 000000000..0e7d9e672 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/src/endpoint.rs @@ -0,0 +1,797 @@ +use crate::config::ClientConfig; +use crate::config::DnsResolver; +use crate::config::ServerConfig; +use crate::connection::Connection; +use crate::driver::streams::session::StreamSession; +use crate::driver::streams::ProtoReadError; +use crate::driver::streams::ProtoWriteError; +use crate::driver::utils::varint_w2q; +use crate::driver::Driver; +use crate::error::ConnectingError; +use crate::error::ConnectionError; +use crate::VarInt; +use quinn::TokioRuntime; +use std::collections::HashMap; +use std::future::Future; +use std::future::IntoFuture; +use std::marker::PhantomData; +use std::net::SocketAddr; +use std::net::SocketAddrV4; +use std::net::SocketAddrV6; +use std::pin::Pin; +use std::sync::Arc; +use std::task::Context; +use std::task::Poll; +use tracing::debug; +use url::Host; +use url::Url; +use wtransport_proto::error::ErrorCode; +use wtransport_proto::frame::FrameKind; +use wtransport_proto::headers::Headers; +use wtransport_proto::session::ReservedHeader; +use wtransport_proto::session::SessionRequest as SessionRequestProto; +use wtransport_proto::session::SessionResponse as SessionResponseProto; + +/// Helper structure for Endpoint types. +pub mod endpoint_side { + use super::*; + + /// Type of endpoint accepting multiple WebTransport connections. + /// + /// Use [`Endpoint::server`] to create and server-endpoint. + pub struct Server { + pub(super) _marker: PhantomData<()>, + } + + /// Type of endpoint opening a WebTransport connection. + /// + /// Use [`Endpoint::client`] to create and client-endpoint. + pub struct Client { + pub(super) dns_resolver: Arc, + } +} + +/// Entrypoint for creating client or server connections. +/// +/// A single endpoint can be used to accept or connect multiple connections. +/// Each endpoint internally binds an UDP socket. +/// +/// # Server +/// Use [`Endpoint::server`] for creating a server-side endpoint. +/// Afterwards use the method [`Endpoint::accept`] for awaiting on incoming session request. +/// +/// ```no_run +/// # use anyhow::Result; +/// # use wtransport::ServerConfig; +/// # use wtransport::Identity; +/// use wtransport::Endpoint; +/// +/// # async fn run() -> Result<()> { +/// # let config = ServerConfig::builder() +/// # .with_bind_default(4433) +/// # .with_identity(Identity::self_signed(["doc"]).unwrap()) +/// # .build(); +/// let server = Endpoint::server(config)?; +/// loop { +/// let incoming_session = server.accept().await; +/// // Spawn task that handles client incoming session... +/// } +/// # Ok(()) +/// # } +/// ``` +/// +/// # Client +/// Use [`Endpoint::client`] for creating a client-side endpoint and use [`Endpoint::connect`] +/// to connect to a server specifying the URL. +/// +/// ```no_run +/// # use anyhow::Result; +/// use wtransport::ClientConfig; +/// use wtransport::Endpoint; +/// +/// # async fn run() -> Result<()> { +/// let connection = Endpoint::client(ClientConfig::default())? +/// .connect("https://localhost:4433") +/// .await?; +/// # Ok(()) +/// # } +/// ``` +pub struct Endpoint { + endpoint: quinn::Endpoint, + side: Side, +} + +impl Endpoint { + /// Closes all of this endpoint's connections immediately and cease accepting new connections. + pub fn close(&self, error_code: VarInt, reason: &[u8]) { + self.endpoint.close(varint_w2q(error_code), reason); + } + + /// Waits for all connections on the endpoint to be cleanly shut down. + pub async fn wait_idle(&self) { + self.endpoint.wait_idle().await; + } + + /// Gets the local [`SocketAddr`] the underlying socket is bound to. + pub fn local_addr(&self) -> std::io::Result { + self.endpoint.local_addr() + } + + /// Get the number of connections that are currently open. + pub fn open_connections(&self) -> usize { + self.endpoint.open_connections() + } +} + +impl Endpoint { + /// Constructs a *server* endpoint. + pub fn server(server_config: ServerConfig) -> std::io::Result { + let endpoint_config = server_config.endpoint_config; + let quic_config = server_config.quic_config; + let socket = server_config.bind_address_config.bind_socket()?; + let runtime = Arc::new(TokioRuntime); + + let endpoint = quinn::Endpoint::new(endpoint_config, Some(quic_config), socket, runtime)?; + + Ok(Self { + endpoint, + side: endpoint_side::Server { + _marker: PhantomData, + }, + }) + } + + /// Get the next incoming connection attempt from a client. + pub async fn accept(&self) -> IncomingSession { + let quic_incoming = self + .endpoint + .accept() + .await + .expect("Endpoint cannot be closed"); + + debug!("New incoming QUIC connection"); + + IncomingSession(quic_incoming) + } + + /// Reloads the server configuration. + /// + /// Useful for e.g. refreshing TLS certificates without disrupting existing connections. + /// + /// # Arguments + /// + /// * `server_config` - The new configuration for the server. + /// * `rebind` - A boolean indicating whether the server should rebind its socket. + /// If `true`, the server will bind to a new socket with the provided configuration. + /// If `false`, the bind address configuration will be ignored. + pub fn reload_config(&self, server_config: ServerConfig, rebind: bool) -> std::io::Result<()> { + if rebind { + let socket = server_config.bind_address_config.bind_socket()?; + self.endpoint.rebind(socket)?; + } + + let quic_config = server_config.quic_config; + self.endpoint.set_server_config(Some(quic_config)); + + Ok(()) + } +} + +impl Endpoint { + /// Constructs a *client* endpoint. + pub fn client(client_config: ClientConfig) -> std::io::Result { + let endpoint_config = client_config.endpoint_config; + let quic_config = client_config.quic_config; + let socket = client_config.bind_address_config.bind_socket()?; + let runtime = Arc::new(TokioRuntime); + + let mut endpoint = quinn::Endpoint::new(endpoint_config, None, socket, runtime)?; + + endpoint.set_default_client_config(quic_config); + + Ok(Self { + endpoint, + side: endpoint_side::Client { + dns_resolver: client_config.dns_resolver, + }, + }) + } + + /// Establishes a WebTransport connection to a specified URL. + /// + /// This method initiates a WebTransport connection to the specified URL. + /// It validates the URL, and performs necessary steps to establish a secure connection. + /// + /// # Arguments + /// + /// * `options` - Connection options specifying the URL and additional headers. + /// It can be simply an [URL](https://en.wikipedia.org/wiki/URL) string representing + /// the WebTransport endpoint to connect to. It must have an `https` scheme. + /// The URL can specify either an IP address or a hostname. + /// When specifying a hostname, the method will internally perform DNS resolution, + /// configured with + /// [`ClientConfigBuilder::dns_resolver`](crate::config::ClientConfigBuilder::dns_resolver). + /// + /// # Examples + /// + /// Connect using a URL with a hostname (DNS resolution is performed): + /// + /// ```no_run + /// # use anyhow::Result; + /// # use wtransport::endpoint::endpoint_side::Client; + /// # async fn example(endpoint: wtransport::Endpoint) -> Result<()> { + /// let url = "https://example.com:4433/webtransport"; + /// let connection = endpoint.connect(url).await?; + /// # Ok(()) + /// # } + /// ``` + /// + /// Connect using a URL with an IP address: + /// + /// ```no_run + /// # use anyhow::Result; + /// # use wtransport::endpoint::endpoint_side::Client; + /// # async fn example(endpoint: wtransport::Endpoint) -> Result<()> { + /// let url = "https://127.0.0.1:4343/webtransport"; + /// let connection = endpoint.connect(url).await?; + /// # Ok(()) + /// # } + /// ``` + /// + /// Connect adding an additional header: + /// + /// ```no_run + /// # use anyhow::Result; + /// # use wtransport::endpoint::endpoint_side::Client; + /// # use wtransport::endpoint::ConnectOptions; + /// # async fn example(endpoint: wtransport::Endpoint) -> Result<()> { + /// let options = ConnectOptions::builder("https://example.com:4433/webtransport") + /// .add_header("Authorization", "AuthToken") + /// .build(); + /// let connection = endpoint.connect(options).await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn connect(&self, options: O) -> Result + where + O: IntoConnectOptions, + { + let options = options.into_options(); + + let url = Url::parse(&options.url) + .map_err(|parse_error| ConnectingError::InvalidUrl(parse_error.to_string()))?; + + if url.scheme() != "https" { + return Err(ConnectingError::InvalidUrl( + "WebTransport URL scheme must be 'https'".to_string(), + )); + } + + let host = url.host().expect("https scheme must have an host"); + let port = url.port().unwrap_or(443); + + let (socket_address, server_name) = match host { + Host::Domain(domain) => { + let socket_address = self + .side + .dns_resolver + .resolve(&format!("{domain}:{port}")) + .await + .map_err(ConnectingError::DnsLookup)? + .ok_or(ConnectingError::DnsNotFound)?; + + (socket_address, domain.to_string()) + } + Host::Ipv4(address) => { + let socket_address = SocketAddr::V4(SocketAddrV4::new(address, port)); + (socket_address, address.to_string()) + } + Host::Ipv6(address) => { + let socket_address = SocketAddr::V6(SocketAddrV6::new(address, port, 0, 0)); + (socket_address, address.to_string()) + } + }; + + let quic_connection = self + .endpoint + .connect(socket_address, &server_name) + .map_err(ConnectingError::with_connect_error)? + .await + .map_err(|connection_error| { + ConnectingError::ConnectionError(connection_error.into()) + })?; + + let driver = Driver::init(quic_connection.clone()); + + let _settings = driver.accept_settings().await.map_err(|driver_error| { + ConnectingError::ConnectionError(ConnectionError::with_driver_error( + driver_error, + &quic_connection, + )) + })?; + + // TODO(biagio): validate settings + + let mut session_request_proto = + SessionRequestProto::new(url.as_ref()).expect("Url has been already validate"); + + for (k, v) in options.additional_headers { + session_request_proto + .insert(k.clone(), v) + .map_err(|ReservedHeader| ConnectingError::ReservedHeader(k))?; + } + + let mut stream_session = match driver.open_session(session_request_proto).await { + Ok(stream_session) => stream_session, + Err(driver_error) => { + return Err(ConnectingError::ConnectionError( + ConnectionError::with_driver_error(driver_error, &quic_connection), + )) + } + }; + + let session_id = stream_session.session_id(); + + match stream_session + .write_frame(stream_session.request().headers().generate_frame()) + .await + { + Ok(()) => {} + Err(ProtoWriteError::Stopped) => { + return Err(ConnectingError::SessionRejected); + } + Err(ProtoWriteError::NotConnected) => { + return Err(ConnectingError::with_no_connection(&quic_connection)); + } + } + + let frame = loop { + let frame = match stream_session.read_frame().await { + Ok(frame) => frame, + Err(ProtoReadError::H3(error_code)) => { + quic_connection.close(varint_w2q(error_code.to_code()), b""); + return Err(ConnectingError::ConnectionError( + ConnectionError::local_h3_error(error_code), + )); + } + Err(ProtoReadError::IO(_io_error)) => { + return Err(ConnectingError::with_no_connection(&quic_connection)); + } + }; + + if let FrameKind::Exercise(_) = frame.kind() { + continue; + } + break frame; + }; + + if !matches!(frame.kind(), FrameKind::Headers) { + quic_connection.close(varint_w2q(ErrorCode::FrameUnexpected.to_code()), b""); + return Err(ConnectingError::ConnectionError( + ConnectionError::local_h3_error(ErrorCode::FrameUnexpected), + )); + } + + let headers = match Headers::with_frame(&frame) { + Ok(headers) => headers, + Err(error_code) => { + quic_connection.close(varint_w2q(error_code.to_code()), b""); + return Err(ConnectingError::ConnectionError( + ConnectionError::local_h3_error(error_code), + )); + } + }; + + let Ok(session_response) = SessionResponseProto::try_from(headers) else { + quic_connection.close(varint_w2q(ErrorCode::Message.to_code()), b""); + return Err(ConnectingError::ConnectionError( + ConnectionError::local_h3_error(ErrorCode::Message), + )); + }; + + if session_response.code().is_successful() { + match driver.register_session(stream_session).await { + Ok(()) => {} + Err(driver_error) => { + return Err(ConnectingError::ConnectionError( + ConnectionError::with_driver_error(driver_error, &quic_connection), + )) + } + } + } else { + return Err(ConnectingError::SessionRejected); + } + + Ok(Connection::new(quic_connection, driver, session_id)) + } +} + +/// Options for establishing a client WebTransport connection. +/// +/// Used in [`Endpoint::connect`]. +/// +/// # Examples +/// +/// ```no_run +/// # use anyhow::Result; +/// # use wtransport::endpoint::endpoint_side::Client; +/// # use wtransport::endpoint::ConnectOptions; +/// # async fn example(endpoint: wtransport::Endpoint) -> Result<()> { +/// let options = ConnectOptions::builder("https://example.com:4433/webtransport") +/// .add_header("Authorization", "AuthToken") +/// .build(); +/// let connection = endpoint.connect(options).await?; +/// # Ok(()) +/// # } +/// ``` +#[derive(Debug, Clone)] +pub struct ConnectOptions { + url: String, + additional_headers: HashMap, +} + +impl ConnectOptions { + /// Creates a new `ConnectOptions` using a builder pattern. + /// + /// # Arguments + /// + /// * `url` - A [URL](https://en.wikipedia.org/wiki/URL) string representing the WebTransport + /// endpoint to connect to. It must have an `https` scheme. + /// The URL can specify either an IP address or a hostname. + /// When specifying a hostname, the method will internally perform DNS resolution, + /// configured with + /// [`ClientConfigBuilder::dns_resolver`](crate::config::ClientConfigBuilder::dns_resolver). + pub fn builder(url: S) -> ConnectRequestBuilder + where + S: ToString, + { + ConnectRequestBuilder { + url: url.to_string(), + additional_headers: Default::default(), + } + } + + /// Gets the URL which this will connect to. + pub fn url(&self) -> &str { + &self.url + } + + /// Gets the additional headers that will be passed when connecting. + pub fn additional_headers(&self) -> &HashMap { + &self.additional_headers + } +} + +/// A trait for converting types into `ConnectOptions`. +pub trait IntoConnectOptions { + /// Perform value-to-value conversion into [`ConnectOptions`]. + fn into_options(self) -> ConnectOptions; +} + +/// A builder for [`ConnectOptions`]. +/// +/// See [`ConnectOptions::builder`]. +pub struct ConnectRequestBuilder { + url: String, + additional_headers: HashMap, +} + +impl ConnectRequestBuilder { + /// Adds a header to the connection options. + /// + /// # Examples + /// + /// ```rust + /// use wtransport::endpoint::ConnectOptions; + /// + /// let options = ConnectOptions::builder("https://example.com:4433/webtransport") + /// .add_header("Authorization", "AuthToken") + /// .build(); + /// ``` + pub fn add_header(mut self, key: K, value: V) -> Self + where + K: ToString, + V: ToString, + { + self.additional_headers + .insert(key.to_string(), value.to_string()); + self + } + + /// Constructs the [`ConnectOptions`] from the builder configuration. + pub fn build(self) -> ConnectOptions { + ConnectOptions { + url: self.url, + additional_headers: self.additional_headers, + } + } +} + +impl IntoConnectOptions for ConnectRequestBuilder { + fn into_options(self) -> ConnectOptions { + self.build() + } +} + +impl IntoConnectOptions for ConnectOptions { + fn into_options(self) -> ConnectOptions { + self + } +} + +impl IntoConnectOptions for S +where + S: ToString, +{ + fn into_options(self) -> ConnectOptions { + ConnectOptions::builder(self).build() + } +} + +type DynFutureIncomingSession = + dyn Future> + Send + Sync; + +/// [`IntoFuture`] for an in-progress incoming connection attempt. +/// +/// Created by [`Endpoint::accept`]. +pub struct IncomingSession(quinn::Incoming); + +impl IncomingSession { + /// The peer's UDP address. + pub fn remote_address(&self) -> SocketAddr { + self.0.remote_address() + } + + /// Whether the socket address that is initiating this connection has been validated. + /// + /// This means that the sender of the initial packet has proved that they can receive traffic + /// sent to `self.remote_address()`. + pub fn remote_address_validated(&self) -> bool { + self.0.remote_address_validated() + } + + /// Respond with a retry packet, requiring the client to retry with address validation + /// + /// # Panics + /// + /// If `remote_address_validated()` is true. + pub fn retry(self) { + self.0.retry().expect("remote address already verified"); + } + + /// Reject this incoming connection attempt. + pub fn refuse(self) { + self.0.refuse(); + } + + /// Ignore this incoming connection attempt, not sending any packet in response. + pub fn ignore(self) { + self.0.ignore(); + } +} + +impl IntoFuture for IncomingSession { + type IntoFuture = IncomingSessionFuture; + type Output = Result; + + fn into_future(self) -> Self::IntoFuture { + IncomingSessionFuture::new(self.0) + } +} + +/// [`Future`] for an in-progress incoming connection attempt. +/// +/// Created by awaiting an [`IncomingSession`] +pub struct IncomingSessionFuture(Pin>); + +impl IncomingSessionFuture { + /// Creates a future from [`quinn::Incoming`]. + /// + /// The returned future resolves to a [`SessionRequest`] once the QUIC connection + /// is established and the WebTransport session has been accepted. + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn with_quic_incoming(quic_incoming: quinn::Incoming) -> Self { + Self::new(quic_incoming) + } + + /// Creates a future from [`quinn::Connecting`]. + /// + /// This is useful when a [`quinn::Connecting`] is already available and needs to be + /// driven to completion to accept an incoming WebTransport session. + /// + /// The returned future resolves to a [`SessionRequest`] once the QUIC connection + /// is established and the WebTransport session has been accepted. + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + pub fn with_quic_connecting(quic_connecting: quinn::Connecting) -> Self { + Self(Box::pin(async move { + let quic_connection = quic_connecting.await?; + Self::accept(quic_connection).await + })) + } + + fn new(quic_incoming: quinn::Incoming) -> Self { + Self(Box::pin(async move { + let quic_connection = quic_incoming.await?; + Self::accept(quic_connection).await + })) + } + + async fn accept(quic_connection: quinn::Connection) -> Result { + let driver = Driver::init(quic_connection.clone()); + + let _settings = driver.accept_settings().await.map_err(|driver_error| { + ConnectionError::with_driver_error(driver_error, &quic_connection) + })?; + + // TODO(biagio): validate settings + + let stream_session = driver.accept_session().await.map_err(|driver_error| { + ConnectionError::with_driver_error(driver_error, &quic_connection) + })?; + + Ok(SessionRequest::new(quic_connection, driver, stream_session)) + } +} + +impl Future for IncomingSessionFuture { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + Future::poll(self.0.as_mut(), cx) + } +} + +/// A incoming client session request. +/// +/// Server should use methods [`accept`](Self::accept), [`forbidden`](Self::forbidden), +/// or [`not_found`](Self::not_found) in order to validate or reject the client request. +pub struct SessionRequest { + quic_connection: quinn::Connection, + driver: Driver, + stream_session: StreamSession, +} + +impl SessionRequest { + pub(crate) fn new( + quic_connection: quinn::Connection, + driver: Driver, + stream_session: StreamSession, + ) -> Self { + Self { + quic_connection, + driver, + stream_session, + } + } + + /// Returns the peer's UDP address. + /// + /// **Note**: as QUIC supports migration, remote address may change + /// during connection. Furthermore, when IPv6 support is enabled, IPv4 + /// addresses may be mapped to IPv6. + #[inline(always)] + pub fn remote_address(&self) -> SocketAddr { + self.quic_connection.remote_address() + } + + /// Returns the `:authority` field of the request. + pub fn authority(&self) -> &str { + self.stream_session.request().authority() + } + + /// Returns the `:path` field of the request. + pub fn path(&self) -> &str { + self.stream_session.request().path() + } + + /// Returns the `origin` field of the request if present. + pub fn origin(&self) -> Option<&str> { + self.stream_session.request().origin() + } + + /// Returns the `user-agent` field of the request if present. + pub fn user_agent(&self) -> Option<&str> { + self.stream_session.request().user_agent() + } + + /// Returns all header fields associated with the request. + pub fn headers(&self) -> &HashMap { + self.stream_session.request().headers().as_ref() + } + + /// Accepts the client request with headers. + pub async fn accept_with_headers( + self, + headers: I, + ) -> Result + where + I: IntoIterator, + K: Into, + V: Into, + { + self.accept_impl( + headers + .into_iter() + .map(|(k, v)| (k.into(), v.into())) + .collect(), + ) + .await + } + + /// Accepts the client request and it establishes the WebTransport session. + pub async fn accept(self) -> Result { + self.accept_impl(HashMap::new()).await + } + + /// Rejects the client request by replying with `403` status code. + pub async fn forbidden(self) { + self.reject(SessionResponseProto::forbidden()).await; + } + + /// Rejects the client request by replying with `404` status code. + pub async fn not_found(self) { + self.reject(SessionResponseProto::not_found()).await; + } + + /// Rejects the client request by replying with `429` status code. + pub async fn too_many_requests(self) { + self.reject(SessionResponseProto::too_many_requests()).await; + } + + async fn accept_impl( + mut self, + headers: HashMap, + ) -> Result { + let mut response = SessionResponseProto::ok(); + + for (key, value) in headers { + response.add(key, value); + } + + self.send_response(response).await?; + + let session_id = self.stream_session.session_id(); + + self.driver + .register_session(self.stream_session) + .await + .map_err(|driver_error| { + ConnectionError::with_driver_error(driver_error, &self.quic_connection) + })?; + + Ok(Connection::new( + self.quic_connection, + self.driver, + session_id, + )) + } + + async fn reject(mut self, response: SessionResponseProto) { + let _ = self.send_response(response).await; + self.stream_session.finish().await; + } + + async fn send_response( + &mut self, + response: SessionResponseProto, + ) -> Result<(), ConnectionError> { + let frame = response.headers().generate_frame(); + + match self.stream_session.write_frame(frame).await { + Ok(()) => Ok(()), + Err(ProtoWriteError::NotConnected) => { + Err(ConnectionError::no_connect(&self.quic_connection)) + } + Err(ProtoWriteError::Stopped) => { + self.quic_connection + .close(varint_w2q(ErrorCode::ClosedCriticalStream.to_code()), b""); + + Err(ConnectionError::local_h3_error( + ErrorCode::ClosedCriticalStream, + )) + } + } + } +} diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/error.rs b/joule-code/joule-code-rs/vendor/wtransport/src/error.rs new file mode 100644 index 000000000..6be424ff8 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/src/error.rs @@ -0,0 +1,352 @@ +use crate::driver::utils::varint_q2w; +use crate::driver::DriverError; +use crate::VarInt; +use std::borrow::Cow; +use std::fmt::Display; +use std::net::SocketAddr; +use wtransport_proto::error::ErrorCode; + +/// An enumeration representing various errors that can occur during a WebTransport connection. +#[derive(thiserror::Error, Debug, Clone, Eq, PartialEq)] +pub enum ConnectionError { + /// The connection was aborted by the peer (protocol level). + #[error("connection aborted by peer: {0}")] + ConnectionClosed(ConnectionClose), + + /// The connection was closed by the peer (application level). + #[error("connection closed by peer: {0}")] + ApplicationClosed(ApplicationClose), + + /// The connection was locally closed. + #[error("connection locally closed")] + LocallyClosed, + + /// The connection was locally closed because an HTTP3 protocol violation. + #[error("connection locally aborted: {0}")] + LocalH3Error(H3Error), + + /// The connection timed out. + #[error("connection timed out")] + TimedOut, + + /// The connection was closed because a QUIC protocol error. + #[error("QUIC protocol error: {0}")] + QuicProto(QuicProtoError), + + /// The connection could not be created because not enough of the CID space is available + /// + /// Try using longer connection IDs. + #[error("CIDs exhausted")] + CidsExhausted, +} + +impl ConnectionError { + pub(crate) fn with_driver_error( + driver_error: DriverError, + quic_connection: &quinn::Connection, + ) -> Self { + match driver_error { + DriverError::Proto(error_code) => Self::local_h3_error(error_code), + DriverError::ApplicationClosed(close) => Self::ApplicationClosed(close), + DriverError::NotConnected => Self::no_connect(quic_connection), + } + } + + pub(crate) fn no_connect(quic_connection: &quinn::Connection) -> Self { + match quic_connection.close_reason() { + Some(reason) => reason.into(), + None => { + // Connection still alive but driver reported NotConnected. + // This race condition can occur when driver threads are processing + // incoming streams while connection cleanup is triggered. + ConnectionError::LocallyClosed + } + } + } + + pub(crate) fn local_h3_error(error_code: ErrorCode) -> Self { + ConnectionError::LocalH3Error(H3Error { code: error_code }) + } +} + +/// An enumeration representing various errors that can occur during a WebTransport client connecting. +#[derive(thiserror::Error, Debug)] +pub enum ConnectingError { + /// URL provided for connection is not valid. + #[error("invalid URL: {0}")] + InvalidUrl(String), + + /// Failure during DNS resolution. + #[error("cannot resolve domain: {0}")] + DnsLookup(std::io::Error), + + /// Cannot find any DNS. + #[error("cannot resolve domain")] + DnsNotFound, + + /// Connection error during handshaking. + #[error(transparent)] + ConnectionError(ConnectionError), + + /// Request rejected. + #[error("server rejected WebTransport session request")] + SessionRejected, + + /// Cannot use reserved key for additional headers. + #[error("additional header '{0}' is reserved")] + ReservedHeader(String), + + /// The endpoint can no longer create new connections + /// + /// Indicates that a necessary component of the endpoint has been dropped or otherwise disabled. + #[error("endpoint stopping")] + EndpointStopping, + + /// The connection could not be created because not enough of the CID space is available + /// + /// Try using longer connection IDs + #[error("CIDs exhausted")] + CidsExhausted, + + /// The server name supplied was malformed + #[error("invalid server name: {0}")] + InvalidServerName(String), + + /// The remote [`SocketAddr`] supplied was malformed. + /// + /// Examples include attempting to connect to port 0, or using an inappropriate address family. + #[error("invalid remote address: {0}")] + InvalidRemoteAddress(SocketAddr), +} + +impl ConnectingError { + pub(crate) fn with_no_connection(quic_connection: &quinn::Connection) -> Self { + ConnectingError::ConnectionError(match quic_connection.close_reason() { + Some(reason) => reason.into(), + None => { + // Connection still alive but error reported. + // This race condition can occur when driver threads are processing + // incoming streams while connection cleanup is triggered. + ConnectionError::LocallyClosed + } + }) + } + + pub(crate) fn with_connect_error(error: quinn::ConnectError) -> Self { + match error { + quinn::ConnectError::EndpointStopping => ConnectingError::EndpointStopping, + quinn::ConnectError::CidsExhausted => ConnectingError::CidsExhausted, + quinn::ConnectError::InvalidServerName(name) => { + ConnectingError::InvalidServerName(name) + } + quinn::ConnectError::InvalidRemoteAddress(socket_addr) => { + ConnectingError::InvalidRemoteAddress(socket_addr) + } + quinn::ConnectError::NoDefaultClientConfig => { + unreachable!("quic client config is internally provided") + } + quinn::ConnectError::UnsupportedVersion => { + unreachable!("quic version is internally configured") + } + } + } +} + +/// Error indicating that a stream has been already finished or reset. +#[derive(thiserror::Error, Debug)] +#[error("closed stream")] +pub struct ClosedStream; + +/// An error that arise from writing to a stream. +#[derive(thiserror::Error, Debug, Clone, Eq, PartialEq)] +pub enum StreamWriteError { + /// Connection has been dropped. + #[error("not connected")] + NotConnected, + + /// The stream was already finished or reset locally. + #[error("stream closed")] + Closed, + + /// The peer is no longer accepting data on this stream. + #[error("stream stopped (code: {0})")] + Stopped(VarInt), + + /// QUIC protocol error. + #[error("QUIC protocol error")] + QuicProto, +} + +/// An error that arise from reading from a stream. +#[derive(thiserror::Error, Debug, Clone, Eq, PartialEq)] +pub enum StreamReadError { + /// Connection has been dropped. + #[error("not connected")] + NotConnected, + + /// The peer abandoned transmitting data on this stream + #[error("stream reset (code: {0})")] + Reset(VarInt), + + /// QUIC protocol error. + #[error("QUIC protocol error")] + QuicProto, +} + +/// An error that arise from reading from a stream. +#[derive(thiserror::Error, Debug, Clone)] +pub enum StreamReadExactError { + /// The stream finished before all bytes were read. + #[error("stream finished too early ({0} bytes read)")] + FinishedEarly(usize), + + /// A read error occurred. + #[error(transparent)] + Read(StreamReadError), +} + +/// An error that arise from sending a datagram. +#[derive(thiserror::Error, Debug, Clone, Eq, PartialEq)] +pub enum SendDatagramError { + /// Connection has been dropped. + #[error("not connected")] + NotConnected, + + /// The peer does not support receiving datagram frames. + #[error("peer does not support datagrams")] + UnsupportedByPeer, + + /// The datagram is larger than the connection can currently accommodate. + #[error("datagram payload too large")] + TooLarge, +} + +/// An error that arise when opening a new stream. +#[derive(thiserror::Error, Debug, Clone, Eq, PartialEq)] +pub enum StreamOpeningError { + /// Connection has been dropped. + #[error("not connected")] + NotConnected, + + /// The peer refused the stream, stopping it during initialization. + #[error("opening stream refused")] + Refused, +} + +/// Reason given by an application for closing the connection +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct ApplicationClose { + code: VarInt, + reason: Box<[u8]>, +} + +impl Display for ApplicationClose { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.reason.is_empty() { + self.code.fmt(f)?; + } else { + f.write_str(&String::from_utf8_lossy(&self.reason))?; + f.write_str(" (code ")?; + self.code.fmt(f)?; + f.write_str(")")?; + } + Ok(()) + } +} + +impl ApplicationClose { + /// Creates a new application close reason. + pub(crate) fn new(code: VarInt, reason: Box<[u8]>) -> Self { + Self { code, reason } + } + + /// Application-specific code for close operation. + pub fn code(&self) -> VarInt { + self.code + } + + /// Data containing the reason for closing operation. + pub fn reason(&self) -> &[u8] { + &self.reason + } +} + +/// Reason given by the transport for closing the connection. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ConnectionClose(quinn::ConnectionClose); + +impl Display for ConnectionClose { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +/// A struct representing an error in the HTTP3 layer. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct H3Error { + code: ErrorCode, +} + +impl Display for H3Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.code.fmt(f) + } +} + +impl From for ConnectionError { + fn from(error: quinn::ConnectionError) -> Self { + match error { + quinn::ConnectionError::VersionMismatch => ConnectionError::QuicProto(QuicProtoError { + code: None, + reason: Cow::Borrowed("QUIC protocol version mismatched"), + }), + quinn::ConnectionError::TransportError(e) => { + ConnectionError::QuicProto(QuicProtoError { + code: VarInt::try_from_u64(e.code.into()).ok(), + reason: Cow::Owned(e.reason), + }) + } + quinn::ConnectionError::ConnectionClosed(close) => { + ConnectionError::ConnectionClosed(ConnectionClose(close)) + } + quinn::ConnectionError::ApplicationClosed(close) => { + ConnectionError::ApplicationClosed(ApplicationClose { + code: varint_q2w(close.error_code), + reason: close.reason.to_vec().into_boxed_slice(), + }) + } + quinn::ConnectionError::Reset => ConnectionError::QuicProto(QuicProtoError { + code: None, + reason: Cow::Borrowed("connection has been reset"), + }), + quinn::ConnectionError::TimedOut => ConnectionError::TimedOut, + quinn::ConnectionError::LocallyClosed => ConnectionError::LocallyClosed, + quinn::ConnectionError::CidsExhausted => ConnectionError::CidsExhausted, + } + } +} + +/// A complete specification of an error over QUIC protocol. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct QuicProtoError { + code: Option, + reason: Cow<'static, str>, +} + +impl Display for QuicProtoError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let code = self + .code + .map(|code| format!(" (code: {code})")) + .unwrap_or_default(); + + f.write_fmt(format_args!("{}{}", self.reason, code)) + } +} + +/// Error returned by [`Connection::export_keying_material`](crate::Connection::export_keying_material). +/// +/// This error occurs if the requested output length is too large. +#[derive(Debug, thiserror::Error, Clone, Eq, PartialEq)] +#[error("cannot derive keying material as requested output length is too large")] +pub struct ExportKeyingMaterialError; diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/lib.rs b/joule-code/joule-code-rs/vendor/wtransport/src/lib.rs new file mode 100644 index 000000000..a1915c1f1 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/src/lib.rs @@ -0,0 +1,148 @@ +//! WebTransport protocol implementation in *pure* Rust, *async-friendly*, and *API-simple*. +//! +//! For a quick start with this library, refer to [`Endpoint`]. +//! +//! ## About WebTransport +//! WebTransport is a modern protocol built on [QUIC](https://en.wikipedia.org/wiki/QUIC) +//! and [HTTP/3](https://en.wikipedia.org/wiki/HTTP/3), providing an alternative to +//! HTTP and WebSocket. +//! +//! It's designed for efficient client-server communication with *low latency* and +//! bi-directional *multistream* data exchange capabilities, making it suitable for a wide range of +//! applications. +//! +//! WebTransport guarantees *secure* and *reliable* communication by leveraging encryption +//! and authentication to protect your data during transmission. +//! +//! WebTransport offers two key communication channels: *streams* and *datagrams*. +//! +//! ### Streams +//! WebTransport streams are communication channels that provide *ordered* and +//! *reliable* data transfer. +//! +//! WebTransport streams allow sending multiple sets of data at once within a single session. +//! Each stream operates independently, ensuring that the order and reliability +//! of one stream do not affect the others. +//! +//! Streams can be: *uni-directional* or *bi-directional*. +//! +//! *Order Preserved, Guaranteed Delivery, Flow-Controlled, Secure (All Traffic Encrypted), +//! and Multiple Independent Streams*. +//! +//! ## Datagrams +//! WebTransport datagrams are lightweight and *unordered* communication channels, +//! prioritizing quick data exchange without guarantees of reliability or sequence. +//! +//! *Unordered, No Guaranteed Delivery, No Flow-Controlled, Secure (All Traffic Encrypted), +//! Independent Messages*. +//! +//! +//! # Examples +//! Explore operational server and client examples below. The elegantly simple yet potent +//! API empowers you to get started with minimal code. +//! +//! ## Server +//! ```no_run +//! # use anyhow::Result; +//! use wtransport::Endpoint; +//! use wtransport::Identity; +//! use wtransport::ServerConfig; +//! +//! #[tokio::main] +//! async fn main() -> Result<()> { +//! let config = ServerConfig::builder() +//! .with_bind_default(4433) +//! .with_identity(Identity::load_pemfiles("cert.pem", "key.pem").await?) +//! .build(); +//! +//! let server = Endpoint::server(config)?; +//! +//! loop { +//! let incoming_session = server.accept().await; +//! let incoming_request = incoming_session.await?; +//! let connection = incoming_request.accept().await?; +//! // ... +//! } +//! } +//! ``` +//! See [repository server example](https://github.com/BiagioFesta/wtransport/blob/master/wtransport/examples/server.rs) +//! for the complete code. +//! +//! ## Client +//! ```no_run +//! # use anyhow::Result; +//! use wtransport::ClientConfig; +//! use wtransport::Endpoint; +//! +//! #[tokio::main] +//! async fn main() -> Result<()> { +//! let connection = Endpoint::client(ClientConfig::default())? +//! .connect("https://localhost:4433") +//! .await?; +//! // ... +//! # Ok(()) +//! } +//! ``` +//! See [repository client example](https://github.com/BiagioFesta/wtransport/blob/master/wtransport/examples/client.rs) +//! for the complete code. +#![cfg_attr(docsrs, feature(doc_cfg))] +#![warn(missing_docs, clippy::doc_markdown)] + +/// Client and server configurations. +pub mod config; + +/// WebTransport connection. +pub mod connection; + +/// Endpoint module. +pub mod endpoint; + +/// Errors definitions module. +pub mod error; + +/// Interfaces for sending and receiving data. +pub mod stream; + +/// TLS specific configurations. +pub mod tls; + +/// Datagrams module. +pub mod datagram; + +#[doc(inline)] +pub use config::ClientConfig; + +#[doc(inline)] +pub use config::ServerConfig; + +#[doc(inline)] +pub use tls::Identity; + +#[doc(inline)] +pub use endpoint::Endpoint; + +#[doc(inline)] +pub use connection::Connection; + +#[doc(inline)] +pub use stream::RecvStream; + +#[doc(inline)] +pub use stream::SendStream; + +#[doc(inline)] +pub use wtransport_proto::varint::VarInt; + +#[doc(inline)] +pub use wtransport_proto::ids::StreamId; + +#[doc(inline)] +pub use wtransport_proto::ids::SessionId; + +pub use wtransport_proto as proto; + +#[cfg(feature = "quinn")] +#[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] +pub use quinn; + +mod driver; diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/stream.rs b/joule-code/joule-code-rs/vendor/wtransport/src/stream.rs new file mode 100644 index 000000000..4528157df --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/src/stream.rs @@ -0,0 +1,392 @@ +use crate::driver::streams::bilocal::StreamBiLocalQuic; +use crate::driver::streams::unilocal::StreamUniLocalQuic; +use crate::driver::streams::ProtoWriteError; +use crate::driver::streams::QuicRecvStream; +use crate::driver::streams::QuicSendStream; +use crate::error::ClosedStream; +use crate::error::StreamOpeningError; +use crate::error::StreamReadError; +use crate::error::StreamReadExactError; +use crate::error::StreamWriteError; +use crate::SessionId; +use crate::StreamId; +use crate::VarInt; +use std::future::Future; +use std::pin::Pin; +use std::task::Context; +use std::task::Poll; +use tokio::io::ReadBuf; +use wtransport_proto::stream_header::StreamHeader; + +/// A stream that can only be used to send data. +#[derive(Debug)] +pub struct SendStream(QuicSendStream); + +impl SendStream { + #[inline(always)] + pub(crate) fn new(stream: QuicSendStream) -> Self { + Self(stream) + } + + /// Writes bytes to the stream. + /// + /// On success, returns the number of bytes written. + /// Congestion and flow control may cause this to be shorter than `buf.len()`, + /// indicating that only a prefix of `buf` was written. + #[inline(always)] + pub async fn write(&mut self, buf: &[u8]) -> Result { + self.0.write(buf).await + } + + /// Convenience method to write an entire buffer to the stream. + #[inline(always)] + pub async fn write_all(&mut self, buf: &[u8]) -> Result<(), StreamWriteError> { + self.0.write_all(buf).await + } + + /// Shuts down the send stream gracefully. + /// + /// No new data may be written after calling this method. Completes when the peer has + /// acknowledged all sent data, retransmitting data as needed. + #[inline(always)] + pub async fn finish(&mut self) -> Result<(), StreamWriteError> { + self.0.finish().await + } + + /// Returns the [`StreamId`] associated. + #[inline(always)] + pub fn id(&self) -> StreamId { + self.0.id() + } + + /// Sets the priority of the send stream. + /// + /// Every send stream has an initial priority of 0. Locally buffered data from streams with + /// higher priority will be transmitted before data from streams with lower priority. Changing + /// the priority of a stream with pending data may only take effect after that data has been + /// transmitted. Using many different priority levels per connection may have a negative + /// impact on performance. + #[inline(always)] + pub fn set_priority(&self, priority: i32) { + self.0.set_priority(priority); + } + + /// Gets the priority of the send stream. + /// + /// # Panics + /// + /// If `reset` was called. + #[inline(always)] + pub fn priority(&self) -> i32 { + self.0.priority() + } + + /// Closes the send stream immediately. + /// + /// No new data can be written after calling this method. Locally buffered data is dropped, and + /// previously transmitted data will no longer be retransmitted if lost. If an attempt has + /// already been made to finish the stream, the peer may still receive all written data. + /// + /// If called more than once, subsequent calls will result in [`ClosedStream`] error. + #[inline(always)] + pub fn reset(&mut self, error_code: VarInt) -> Result<(), ClosedStream> { + self.0.reset(error_code) + } + + /// Passively waits for the send stream to be stopped for any reason. + /// + /// Returns [`StreamWriteError::Closed`] if the stream was already `finish`ed or `reset`. + /// + /// Otherwise returns [`StreamWriteError::Stopped`] with an error code from the peer. + #[inline(always)] + pub async fn stopped(&mut self) -> StreamWriteError { + self.0.stopped().await + } + + /// Returns a reference to the underlying QUIC stream. + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + #[inline(always)] + pub fn quic_stream(&self) -> &quinn::SendStream { + self.0.quic_stream() + } + + /// Returns a mutable reference to the underlying QUIC stream. + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + #[inline(always)] + pub fn quic_stream_mut(&mut self) -> &mut quinn::SendStream { + self.0.quic_stream_mut() + } +} + +/// A stream that can only be used to receive data. +#[derive(Debug)] +pub struct RecvStream(QuicRecvStream); + +impl RecvStream { + #[inline(always)] + pub(crate) fn new(stream: QuicRecvStream) -> Self { + Self(stream) + } + + /// Read data contiguously from the stream. + /// + /// On success, returns the number of bytes read into `buf`. + #[inline(always)] + pub async fn read(&mut self, buf: &mut [u8]) -> Result, StreamReadError> { + self.0.read(buf).await + } + + /// Reads an exact number of bytes contiguously from the stream. + /// + /// If the stream terminates before the entire length has been read, it + /// returns [`StreamReadExactError::FinishedEarly`]. + pub async fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), StreamReadExactError> { + self.0.read_exact(buf).await + } + + /// Stops accepting data on the stream. + /// + /// Discards unread data and notifies the peer to stop transmitting. + pub fn stop(mut self, error_code: VarInt) { + let _ = self.0.stop(error_code); + } + + /// Returns the [`StreamId`] associated. + #[inline(always)] + pub fn id(&self) -> StreamId { + self.0.id() + } + + /// Returns a reference to the underlying QUIC stream. + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + #[inline(always)] + pub fn quic_stream(&self) -> &quinn::RecvStream { + self.0.quic_stream() + } + + /// Returns a mutable reference to the underlying QUIC stream. + #[cfg(feature = "quinn")] + #[cfg_attr(docsrs, doc(cfg(feature = "quinn")))] + #[inline(always)] + pub fn quic_stream_mut(&mut self) -> &mut quinn::RecvStream { + self.0.quic_stream_mut() + } +} + +/// A bidirectional stream composed of [`SendStream`] and [`RecvStream`]. +/// +/// `BiStream` is a utility particularly useful in situations where a generic +/// function or method expects a single object that must implement both +/// [`AsyncRead`](tokio::io::AsyncRead) and [`AsyncWrite`](tokio::io::AsyncWrite). +/// +/// # Examples +/// +/// ``` +/// use tokio::io::AsyncRead; +/// use tokio::io::AsyncWrite; +/// use wtransport::stream::BiStream; +/// +/// async fn do_operation(io: T) +/// where +/// T: AsyncRead + AsyncWrite, +/// { +/// // ... +/// } +/// +/// # use wtransport::Connection; +/// # async fn run(connection: Connection) { +/// let bi_stream = BiStream::join(connection.accept_bi().await.unwrap()); +/// do_operation(bi_stream).await; +/// # } +/// ``` +#[derive(Debug)] +pub struct BiStream((SendStream, RecvStream)); + +impl BiStream { + /// Joins a sending stream and a receiving stream into a single `BiStream` object. + pub fn join(s: (SendStream, RecvStream)) -> Self { + Self(s) + } + + /// Splits the bidirectional stream into its sending and receiving stream handles. + pub fn split(self) -> (SendStream, RecvStream) { + self.0 + } + + /// Returns a reference to the inner [`SendStream`]. + pub fn send(&self) -> &SendStream { + &self.0 .0 + } + + /// Returns a mutable reference to the inner [`SendStream`]. + pub fn send_mut(&mut self) -> &mut SendStream { + &mut self.0 .0 + } + + /// Returns a reference to the inner [`RecvStream`]. + pub fn recv(&self) -> &RecvStream { + &self.0 .1 + } + + /// Returns a mutable reference to the inner [`RecvStream`]. + pub fn recv_mut(&mut self) -> &mut RecvStream { + &mut self.0 .1 + } +} + +impl From<(SendStream, RecvStream)> for BiStream { + fn from(value: (SendStream, RecvStream)) -> Self { + Self::join(value) + } +} + +impl From for (SendStream, RecvStream) { + fn from(value: BiStream) -> Self { + value.split() + } +} + +impl tokio::io::AsyncWrite for SendStream { + #[inline(always)] + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + tokio::io::AsyncWrite::poll_write(Pin::new(&mut self.0), cx, buf) + } + + #[inline(always)] + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + tokio::io::AsyncWrite::poll_flush(Pin::new(&mut self.0), cx) + } + + #[inline(always)] + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + tokio::io::AsyncWrite::poll_shutdown(Pin::new(&mut self.0), cx) + } + + #[inline(always)] + fn poll_write_vectored( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[std::io::IoSlice<'_>], + ) -> Poll> { + tokio::io::AsyncWrite::poll_write_vectored(Pin::new(&mut self.0), cx, bufs) + } + + #[inline(always)] + fn is_write_vectored(&self) -> bool { + tokio::io::AsyncWrite::is_write_vectored(&self.0) + } +} + +impl tokio::io::AsyncRead for RecvStream { + #[inline(always)] + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + tokio::io::AsyncRead::poll_read(Pin::new(&mut self.0), cx, buf) + } +} + +impl tokio::io::AsyncWrite for BiStream { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + tokio::io::AsyncWrite::poll_write(Pin::new(&mut self.0 .0), cx, buf) + } + + fn poll_flush( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + tokio::io::AsyncWrite::poll_flush(Pin::new(&mut self.0 .0), cx) + } + + fn poll_shutdown( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + tokio::io::AsyncWrite::poll_shutdown(Pin::new(&mut self.0 .0), cx) + } +} + +impl tokio::io::AsyncRead for BiStream { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + tokio::io::AsyncRead::poll_read(Pin::new(&mut self.0 .1), cx, buf) + } +} + +type DynFutureUniStream = dyn Future> + Send + Sync; + +/// [`Future`] for an in-progress opening unidirectional stream. +/// +/// See [`Connection::open_uni`](crate::Connection::open_uni). +pub struct OpeningUniStream(Pin>); + +impl OpeningUniStream { + pub(crate) fn new(session_id: SessionId, quic_stream: StreamUniLocalQuic) -> Self { + Self(Box::pin(async move { + match quic_stream + .upgrade(StreamHeader::new_webtransport(session_id)) + .await + { + Ok(stream) => Ok(SendStream(stream.upgrade().into_stream())), + Err(ProtoWriteError::NotConnected) => Err(StreamOpeningError::NotConnected), + Err(ProtoWriteError::Stopped) => Err(StreamOpeningError::Refused), + } + })) + } +} + +impl Future for OpeningUniStream { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + Future::poll(self.0.as_mut(), cx) + } +} + +type DynFutureBiStream = + dyn Future> + Send + Sync; + +/// [`Future`] for an in-progress opening bidirectional stream. +/// +/// See [`Connection::open_bi`](crate::Connection::open_bi). +pub struct OpeningBiStream(Pin>); + +impl OpeningBiStream { + pub(crate) fn new(session_id: SessionId, quic_stream: StreamBiLocalQuic) -> Self { + Self(Box::pin(async move { + match quic_stream.upgrade().upgrade(session_id).await { + Ok(stream) => { + let stream = stream.into_stream(); + Ok((SendStream::new(stream.0), RecvStream::new(stream.1))) + } + Err(ProtoWriteError::NotConnected) => Err(StreamOpeningError::NotConnected), + Err(ProtoWriteError::Stopped) => Err(StreamOpeningError::Refused), + } + })) + } +} + +impl Future for OpeningBiStream { + type Output = Result<(SendStream, RecvStream), StreamOpeningError>; + + #[inline(always)] + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + Future::poll(self.0.as_mut(), cx) + } +} diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/tls.rs b/joule-code/joule-code-rs/vendor/wtransport/src/tls.rs new file mode 100644 index 000000000..dd2baec57 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/src/tls.rs @@ -0,0 +1,1373 @@ +use error::InvalidCertificate; +use error::InvalidDigest; +use error::PemLoadError; +use pem::encode as pem_encode; +use pem::Pem; +use rustls::RootCertStore; +use rustls_pki_types::pem::PemObject; +use rustls_pki_types::CertificateDer; +use rustls_pki_types::PrivateKeyDer; +use rustls_pki_types::PrivatePkcs8KeyDer; +use sha2::Digest; +use sha2::Sha256; +use std::fmt::Debug; +use std::fmt::Display; +use std::path::Path; +use std::str::FromStr; +use std::sync::Arc; +use x509_parser::certificate::X509Certificate; +use x509_parser::prelude::FromDer; + +pub use wtransport_proto::WEBTRANSPORT_ALPN; + +/// Represents an X.509 certificate. +#[derive(Clone)] +pub struct Certificate(CertificateDer<'static>); + +impl Certificate { + pub(crate) fn from_rustls_pki(cert: CertificateDer<'static>) -> Self { + Self(cert) + } + + /// Constructs a new `Certificate` from DER-encoded binary data. + pub fn from_der(der: Vec) -> Result { + X509Certificate::from_der(&der).map_err(|error| InvalidCertificate(error.to_string()))?; + Ok(Self(CertificateDer::from(der))) + } + + /// Loads the *first* certificate found in a PEM-encoded file. + /// + /// Filters out any PEM sections that are not certificate. + /// + /// Returns a [`PemLoadError::NoCertificateSection`] if no certificate is found in the file. + pub async fn load_pemfile(filepath: impl AsRef) -> Result { + let file_data = tokio::fs::read(filepath.as_ref()) + .await + .map_err(|io_error| PemLoadError::FileError { + file: filepath.as_ref().to_path_buf(), + error: io_error, + })?; + + let der = CertificateDer::from_pem_slice(&file_data).map_err(|error| { + if let rustls_pki_types::pem::Error::NoItemsFound = error { + PemLoadError::NoCertificateSection + } else { + PemLoadError::FileError { + file: filepath.as_ref().to_path_buf(), + error: std::io::Error::other(error), + } + } + })?; + + Self::from_der(der.to_vec()).map_err(|invalid_certificate| { + PemLoadError::InvalidCertificateChain { + index: 0, + error: invalid_certificate, + } + }) + } + + /// Stores the certificate in PEM format into a file asynchronously. + /// + /// If the file does not exist, it will be created. If the file exists, its contents + /// will be truncated before writing. + pub async fn store_pemfile(&self, filepath: impl AsRef) -> std::io::Result<()> { + use tokio::io::AsyncWriteExt; + + let mut file = tokio::fs::File::create(filepath).await?; + file.write_all(self.to_pem().as_bytes()).await?; + + Ok(()) + } + + /// Returns a reference to the DER-encoded binary data of the certificate. + pub fn der(&self) -> &[u8] { + &self.0 + } + + /// Retrieves the serial number of the certificate as a string. + pub fn serial(&self) -> String { + X509Certificate::from_der(&self.0) + .expect("valid der") + .1 + .raw_serial_as_string() + } + + /// Converts the X.509 certificate to the PEM (Privacy-Enhanced Mail) format. + /// + /// # Returns + /// A `String` containing the PEM-encoded representation of the certificate. + pub fn to_pem(&self) -> String { + pem_encode(&Pem::new("CERTIFICATE", self.der())) + } + + /// Computes certificate's *hash*. + /// + /// The hash is the *SHA-256* of the DER encoding of the certificate. + /// + /// This function can be used to make a *web* client accept a self-signed + /// certificate by using the [`WebTransportOptions.serverCertificateHashes`] W3C API. + /// + /// [`WebTransportOptions.serverCertificateHashes`]: https://www.w3.org/TR/webtransport/#dom-webtransportoptions-servercertificatehashes + pub fn hash(&self) -> Sha256Digest { + // TODO(biagio): you might consider use crypto provider from new rustls version + Sha256Digest(Sha256::digest(self.der()).into()) + } +} + +impl Debug for Certificate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Certificate") + .field("serial", &self.serial()) + .finish() + } +} + +/// Represents a private key. +#[derive(Debug)] +pub struct PrivateKey(PrivateKeyDer<'static>); + +impl PrivateKey { + /// Constructs a new `PrivateKey` from DER-encoded PKCS#8 binary data. + pub fn from_der_pkcs8(der: Vec) -> Self { + PrivateKey(PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(der))) + } + + /// Loads the *first* private key found in a PEM-encoded file. + /// + /// Filters out any PEM sections that are not private key. + /// + /// Returns a [`PemLoadError::NoPrivateKeySection`] if no private key is found in the file. + pub async fn load_pemfile(filepath: impl AsRef) -> Result { + let file_data = tokio::fs::read(filepath.as_ref()) + .await + .map_err(|io_error| PemLoadError::FileError { + file: filepath.as_ref().to_path_buf(), + error: io_error, + })?; + + let private_key = PrivateKeyDer::from_pem_slice(&file_data).map_err(|error| { + if let rustls_pki_types::pem::Error::NoItemsFound = error { + PemLoadError::NoPrivateKeySection + } else { + PemLoadError::FileError { + file: filepath.as_ref().to_path_buf(), + error: std::io::Error::other(error), + } + } + })?; + + Ok(Self(private_key)) + } + + /// Stores the private key in PEM format into a file asynchronously. + /// + /// If the file does not exist, it will be created. If the file exists, + /// its contents will be truncated before writing. + pub async fn store_secret_pemfile(&self, filepath: impl AsRef) -> std::io::Result<()> { + tokio::fs::write(filepath, self.to_secret_pem()).await + } + + /// Returns a reference to the DER-encoded binary data of the private key. + pub fn secret_der(&self) -> &[u8] { + self.0.secret_der() + } + + /// Converts the private key to PEM format. + pub fn to_secret_pem(&self) -> String { + pem_encode(&Pem::new("PRIVATE KEY", self.secret_der())) + } + + /// Clones this private key. + /// + /// # Note + /// `PrivateKey` does not implement `Clone` directly to ensure that sensitive information + /// is not cloned inadvertently. + /// Implementing `Clone` directly could potentially lead to unintended cloning of sensitive data. + pub fn clone_key(&self) -> Self { + Self(self.0.clone_key()) + } +} + +/// A collection of [`Certificate`]. +#[derive(Clone, Debug)] +pub struct CertificateChain(Vec); + +impl CertificateChain { + /// Constructs a new `CertificateChain` from a vector of certificates. + pub fn new(certificates: Vec) -> Self { + Self(certificates) + } + + /// Constructs a new `CertificateChain` with a single certificate. + pub fn single(certificate: Certificate) -> Self { + Self::new(vec![certificate]) + } + + /// Loads a certificate chain from a PEM-encoded file. + /// + /// Filters out any PEM sections that are not certificates and yields error + /// if a problem occurs while trying to parse any certificate. + /// + /// *Note*: if the PEM file does not contain any certificate section this + /// will return an empty chain. + pub async fn load_pemfile(filepath: impl AsRef) -> Result { + let file_data = tokio::fs::read(filepath.as_ref()) + .await + .map_err(|io_error| PemLoadError::FileError { + file: filepath.as_ref().to_path_buf(), + error: io_error, + })?; + + let certificates = CertificateDer::pem_slice_iter(&file_data) + .enumerate() + .map(|(index, maybe_der)| match maybe_der { + Ok(der) => Certificate::from_der(der.to_vec()).map_err(|invalid_certificate| { + PemLoadError::InvalidCertificateChain { + index, + error: invalid_certificate, + } + }), + Err(error) => Err(PemLoadError::FileError { + file: filepath.as_ref().to_path_buf(), + error: std::io::Error::other(error), + }), + }) + .collect::, _>>()?; + + Ok(Self(certificates)) + } + + /// Stores the certificate chain in PEM format into a file asynchronously. + /// + /// If the file does not exist, it will be created. If the file exists, its contents + /// will be truncated before writing. + pub async fn store_pemfile(&self, filepath: impl AsRef) -> std::io::Result<()> { + use tokio::io::AsyncWriteExt; + + let mut file = tokio::fs::File::create(filepath).await?; + + for cert in self.0.iter() { + file.write_all(cert.to_pem().as_bytes()).await?; + } + + Ok(()) + } + + /// Returns a slice containing references to the certificates in the chain. + pub fn as_slice(&self) -> &[Certificate] { + &self.0 + } +} + +impl AsRef<[Certificate]> for CertificateChain { + fn as_ref(&self) -> &[Certificate] { + self.as_slice() + } +} + +impl FromIterator for CertificateChain { + fn from_iter>(iter: T) -> Self { + Self(iter.into_iter().collect()) + } +} + +/// Represents an TLS identity consisting of a certificate chain and a private key. +#[derive(Debug)] +pub struct Identity { + certificate_chain: CertificateChain, + private_key: PrivateKey, +} + +impl Identity { + /// Constructs a new `Identity` with the given certificate chain and private key. + pub fn new(certificate_chain: CertificateChain, private_key: PrivateKey) -> Self { + Self { + certificate_chain, + private_key, + } + } + + /// Loads an identity from PEM-encoded certificate and private key files. + pub async fn load_pemfiles( + cert_pemfile: impl AsRef, + private_key_pemfile: impl AsRef, + ) -> Result { + let certificate_chain = CertificateChain::load_pemfile(cert_pemfile).await?; + let private_key = PrivateKey::load_pemfile(private_key_pemfile).await?; + + Ok(Self::new(certificate_chain, private_key)) + } + + /// Generates a self-signed certificate and private key for new identity. + /// + /// The certificate conforms to the W3C WebTransport specifications as follows: + /// + /// - The certificate MUST be an *X.509v3* certificate as defined in *RFC5280*. + /// - The key used in the Subject Public Key field MUST be one of the allowed public key algorithms. + /// This function uses the `ECDSA P-256` algorithm. + /// - The current time MUST be within the validity period of the certificate as defined + /// in Section 4.1.2.5 of *RFC5280*. + /// - The total length of the validity period MUST NOT exceed two weeks. + /// + /// # Arguments + /// + /// * `subject_alt_names` - An iterator of strings representing subject alternative names (SANs). + /// They can be both hostnames or IP addresses. An error is returned + /// if DNS are not valid ASN.1 strings. + /// + /// # Examples + /// + /// ``` + /// use wtransport::Identity; + /// + /// let identity = Identity::self_signed(&["localhost", "127.0.0.1", "::1"]).unwrap(); + /// ``` + /// + /// The following example will return an error as *subject alternative name* is an invalid + /// string. + /// ```should_panic + /// use wtransport::Identity; + /// + /// Identity::self_signed(&["❤️"]).unwrap(); + /// ``` + /// + /// # Features + /// + /// This function is only available when the `self-signed` feature is enabled. + #[cfg(feature = "self-signed")] + #[cfg_attr(docsrs, doc(cfg(feature = "self-signed")))] + pub fn self_signed(subject_alt_names: I) -> Result + where + I: IntoIterator, + S: AsRef, + { + self_signed::SelfSignedIdentityBuilder::new() + .subject_alt_names(subject_alt_names) + .from_now_utc() + .validity_days(14) + .build() + } + + /// Creates a new [`SelfSignedIdentityBuilder`][1] instance. + /// + /// This function provides a convenient way to create a self-signed identity + /// builder, and thus generate a custom self-signed identity. + /// + /// # Example + /// + /// ``` + /// use wtransport::Identity; + /// + /// let identity = Identity::self_signed_builder() + /// .subject_alt_names(&["localhost", "127.0.0.1", "::1"]) + /// .from_now_utc() + /// .validity_days(14) + /// .build() + /// .unwrap(); + /// ``` + /// + /// # Features + /// + /// This function is only available when the `self-signed` feature is enabled. + /// + /// [1]: self_signed::SelfSignedIdentityBuilder + #[cfg(feature = "self-signed")] + #[cfg_attr(docsrs, doc(cfg(feature = "self-signed")))] + pub fn self_signed_builder( + ) -> self_signed::SelfSignedIdentityBuilder { + self_signed::SelfSignedIdentityBuilder::new() + } + + /// Returns a reference to the certificate chain associated with the identity. + pub fn certificate_chain(&self) -> &CertificateChain { + &self.certificate_chain + } + + /// Returns a reference to the private key associated with the identity. + pub fn private_key(&self) -> &PrivateKey { + &self.private_key + } + + /// Clones this identity. + /// + /// # Note + /// `Identity` does not implement `Clone` directly to ensure that sensitive information, + /// specifically the inner *private key*, is not cloned inadvertently. + /// Implementing `Clone` directly could potentially lead to unintended cloning of sensitive data. + pub fn clone_identity(&self) -> Self { + Self::new(self.certificate_chain.clone(), self.private_key.clone_key()) + } +} + +/// Represents the formatting options for displaying a SHA-256 digest. +#[derive(Debug, Copy, Clone)] +pub enum Sha256DigestFmt { + /// Represents the SHA-256 digest as an array of bytes. + /// + /// The string-format is as follows: `"[b0, b1, b2, ..., b31]"`, + /// where `b` is the byte represented as a *decimal* integer. + BytesArray, + + /// Represents the SHA-256 digest as a dotted hexadecimal string. + /// + /// The string-format is as follows: `"x0:x1:x2:...:x31"` where `x` + /// is the byte represented as a *hexadecimal* integer. + DottedHex, +} + +/// Represents a *SHA-256* digest, which is a fixed-size array of 32 bytes. +/// +/// For example, you can obtain the certificate digest with [`Certificate::hash`]. +#[derive(Debug, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)] +pub struct Sha256Digest([u8; 32]); + +impl Sha256Digest { + /// Creates a new instance from a given array of 32 bytes. + /// + /// # Examples + /// + /// ``` + /// use wtransport::tls::Sha256Digest; + /// + /// // Create a new SHA-256 digest instance. + /// let digest = Sha256Digest::new([97; 32]); + /// ``` + pub fn new(bytes: [u8; 32]) -> Self { + Self(bytes) + } + + /// Attempts to create a new instance from a string representation. + /// + /// This method parses the string representation of the digest according to the provided + /// format (`fmt`) and constructs a new `Sha256Digest` instance if the parsing is successful. + /// + /// # Examples + /// + /// ``` + /// use wtransport::tls::Sha256Digest; + /// use wtransport::tls::Sha256DigestFmt; + /// + /// const HASH_ARRAY: &str = "[234, 204, 110, 153, 82, 177, 87, 232, 180, 125, \ + /// 234, 158, 66, 129, 212, 250, 217, 48, 47, 32, 83, \ + /// 133, 23, 44, 79, 198, 70, 118, 25, 153, 146, 142]"; + /// + /// let digest_bytes_array = Sha256Digest::from_str_fmt(HASH_ARRAY, Sha256DigestFmt::BytesArray); + /// assert!(digest_bytes_array.is_ok()); + /// + /// const HASH_HEX: &str = "e3:4e:c7:de:b8:da:2d:b8:3c:86:a0:11:76:40:75:b3:\ + /// b9:ba:9d:00:e0:04:b3:38:72:cd:a1:af:4e:e3:11:26"; + /// + /// let digest_dotted_hex = Sha256Digest::from_str_fmt(HASH_HEX, Sha256DigestFmt::DottedHex); + /// assert!(digest_dotted_hex.is_ok()); + /// + /// let invalid_digest = Sha256Digest::from_str_fmt("invalid_digest", Sha256DigestFmt::BytesArray); + /// assert!(invalid_digest.is_err()); + /// ``` + pub fn from_str_fmt(s: S, fmt: Sha256DigestFmt) -> Result + where + S: AsRef, + { + let bytes = match fmt { + Sha256DigestFmt::BytesArray => s + .as_ref() + .trim_start_matches('[') + .trim_end_matches(']') + .split(',') + .map(|byte| byte.trim().parse::().map_err(|_| InvalidDigest)) + .collect::, _>>()?, + + Sha256DigestFmt::DottedHex => s + .as_ref() + .split(':') + .map(|hex| u8::from_str_radix(hex.trim(), 16).map_err(|_| InvalidDigest)) + .collect::, _>>()?, + }; + + Ok(Self(bytes.try_into().map_err(|_| InvalidDigest)?)) + } + + /// Formats the digest into a human-readable representation based on the specified format. + /// + /// # Examples + /// + /// ``` + /// use wtransport::tls::Sha256Digest; + /// use wtransport::tls::Sha256DigestFmt; + /// + /// let digest = Sha256Digest::new([ + /// 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, + /// 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 64, + /// ]); + /// + /// // Represent the digest as a byte array string. + /// let bytes_array_fmt = digest.fmt(Sha256DigestFmt::BytesArray); + /// assert_eq!( + /// bytes_array_fmt, + /// "[97, 98, 99, 100, 101, 102, 103, 104, 105, 106, \ + /// 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, \ + /// 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, \ + /// 127, 64]" + /// ); + /// + /// // Represent the digest as a dotted hexadecimal string. + /// let dotted_hex_fmt = digest.fmt(Sha256DigestFmt::DottedHex); + /// assert_eq!( + /// dotted_hex_fmt, + /// "61:62:63:64:65:66:67:68:69:6a:6b:6c:6d:6e:6f:70:\ + /// 71:72:73:74:75:76:77:78:79:7a:7b:7c:7d:7e:7f:40" + /// ); + /// ``` + pub fn fmt(&self, fmt: Sha256DigestFmt) -> String { + match fmt { + Sha256DigestFmt::BytesArray => { + format!("{:?}", self.0) + } + Sha256DigestFmt::DottedHex => self + .0 + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::>() + .join(":"), + } + } +} + +impl From<[u8; 32]> for Sha256Digest { + fn from(value: [u8; 32]) -> Self { + Self::new(value) + } +} + +impl AsRef<[u8; 32]> for Sha256Digest { + fn as_ref(&self) -> &[u8; 32] { + &self.0 + } +} + +impl Display for Sha256Digest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.fmt(Sha256DigestFmt::DottedHex), f) + } +} + +/// Represents data related to a TLS handshake process. +#[derive(Clone, Debug)] +pub struct HandshakeData { + pub(crate) alpn: Option>, + pub(crate) server_name: Option, +} + +impl HandshakeData { + /// Application-Layer Protocol Negotiation (ALPN) data. + pub fn alpn(&self) -> Option<&[u8]> { + self.alpn.as_deref() + } + + /// The server name associated with the handshake data. + pub fn server_name(&self) -> Option<&str> { + self.server_name.as_deref() + } +} + +impl FromStr for Sha256Digest { + type Err = InvalidDigest; + + fn from_str(s: &str) -> Result { + Sha256Digest::from_str_fmt(s, Sha256DigestFmt::BytesArray) + .or_else(|_| Sha256Digest::from_str_fmt(s, Sha256DigestFmt::DottedHex)) + } +} + +/// Builds [`rustls::RootCertStore`] by using platform’s native certificate store. +/// +/// This function works on a *best-effort* basis, attempting to load every possible +/// root certificate found on the platform. It skips anchors that produce errors during loading. +/// Therefore, it might produce an *empty* root store. +/// +/// +/// It's important to note that this function can be expensive, as it might involve loading +/// all root certificates from the filesystem platform. Therefore, it's advisable to use it +/// sporadically and judiciously. +pub fn build_native_cert_store() -> RootCertStore { + let _vars_restore_guard = utils::remove_vars_tmp(["SSL_CERT_FILE", "SSL_CERT_DIR"]); + + let mut root_store = RootCertStore::empty(); + + let rustls_native_certs::CertificateResult { certs, .. } = + rustls_native_certs::load_native_certs(); + + for c in certs { + let _ = root_store.add(c); + } + + root_store +} + +fn default_crypto_provider() -> rustls::crypto::CryptoProvider { + #[cfg(feature = "ring")] + { + rustls::crypto::ring::default_provider() + } + + #[cfg(not(feature = "ring"))] + { + rustls::crypto::aws_lc_rs::default_provider() + } +} + +/// TLS configurations and utilities server-side. +pub mod server { + use super::*; + use rustls::ServerConfig as TlsServerConfig; + + /// Builds a default TLS server configuration with safe defaults. + /// + /// This function constructs a TLS server configuration with safe defaults. + /// The configuration utilizes the identity (certificate and private key) specified + /// in the input argument of the function. + /// + /// Client authentication is not required in this configuration. + /// + /// # Arguments + /// + /// - `identity`: A reference to the identity containing the certificate chain and private key. + pub fn build_default_tls_config(identity: Identity) -> TlsServerConfig { + let provider = Arc::new(default_crypto_provider()); + + let certificates = identity + .certificate_chain + .0 + .into_iter() + .map(|cert| cert.0) + .collect(); + + let private_key = identity.private_key.0; + + let mut tls_config = TlsServerConfig::builder_with_provider(provider) + .with_protocol_versions(&[&rustls::version::TLS13]) + .expect("valid version") + .with_no_client_auth() + .with_single_cert(certificates, private_key) + .expect("Certificate and private key should be already validated"); + + tls_config.alpn_protocols = [WEBTRANSPORT_ALPN.to_vec()].to_vec(); + + tls_config + } +} + +/// TLS configurations and utilities client-side. +pub mod client { + use super::*; + use rustls::client::danger::ServerCertVerified; + use rustls::client::danger::ServerCertVerifier; + use rustls::crypto::WebPkiSupportedAlgorithms; + use rustls::ClientConfig as TlsClientConfig; + + /// Builds a default TLS client configuration with safe defaults. + /// + /// This function constructs a TLS client configuration with safe defaults. + /// It utilizes the provided `RootCertStore` to validate server certificates during the TLS handshake. + /// + /// If a custom [`ServerCertVerifier`] is provided, it will be used for certificate validation; otherwise, + /// it will use the standard safe default mechanism (using the Web PKI mechanism). + /// + /// Client authentication is not required in this configuration. + /// + /// # Arguments + /// + /// - `root_store`: An `Arc` containing the [`RootCertStore`] with trusted root certificates. + /// To obtain a `RootCertStore`, one can use the [`build_native_cert_store`] + /// function, which loads the platform's certificate authorities (CAs). + /// - `custom_verifier`: An optional `Arc` containing a custom implementation of the [`ServerCertVerifier`] + /// trait for custom certificate verification. If `None` is provided, the default + /// Web PKI mechanism will be used. + /// + /// # Note + /// + /// If a custom `ServerCertVerifier` is provided, exercise caution as it could potentially compromise the + /// certificate validation process if not implemented correctly. + pub fn build_default_tls_config( + root_store: Arc, + custom_verifier: Option>, + ) -> TlsClientConfig { + let provider = Arc::new(default_crypto_provider()); + + let mut config = TlsClientConfig::builder_with_provider(provider) + .with_protocol_versions(&[&rustls::version::TLS13]) + .expect("valid version") + .with_root_certificates(root_store) + .with_no_client_auth(); + + if let Some(custom_verifier) = custom_verifier { + config.dangerous().set_certificate_verifier(custom_verifier); + } + + config.alpn_protocols = [WEBTRANSPORT_ALPN.to_vec()].to_vec(); + config + } + + /// A custom **insecure** [`ServerCertVerifier`] implementation. + /// + /// This verifier is configured to skip all server's certificate validation. + /// + /// Therefore, it's advisable to use it judiciously, and avoid using it in + /// production environments. + #[derive(Debug)] + pub struct NoServerVerification { + supported_algorithms: WebPkiSupportedAlgorithms, + } + + impl Default for NoServerVerification { + fn default() -> Self { + Self::new() + } + } + + impl NoServerVerification { + /// Creates a new instance of `NoServerVerification`. + pub fn new() -> NoServerVerification { + NoServerVerification { + supported_algorithms: default_crypto_provider().signature_verification_algorithms, + } + } + } + + impl ServerCertVerifier for NoServerVerification { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer, + _intermediates: &[CertificateDer], + _server_name: &rustls_pki_types::ServerName, + _ocsp_response: &[u8], + _now: rustls_pki_types::UnixTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls12_signature(message, cert, dss, &self.supported_algorithms) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls13_signature(message, cert, dss, &self.supported_algorithms) + } + + fn supported_verify_schemes(&self) -> Vec { + self.supported_algorithms.supported_schemes() + } + } + + /// A custom [`ServerCertVerifier`] implementation. + /// + /// Configures the client to skip *some* server certificates validation. + /// + /// This verifier is configured to accept server certificates + /// whose digests match the specified *SHA-256* hashes and fulfill + /// some additional constraints (*see notes below*). + /// + /// This is useful for scenarios where clients need to accept known + /// self-signed certificates or certificates from non-standard authorities. + /// + /// # Notes + /// + /// - The current time MUST be within the validity period of the certificate. + /// - The total length of the validity period MUST NOT exceed *two* weeks. + /// - Only certificates for which the public key algorithm is *ECDSA* with the *secp256r1* are accepted. + #[derive(Debug)] + pub struct ServerHashVerification { + hashes: std::collections::BTreeSet, + supported_algorithms: WebPkiSupportedAlgorithms, + } + + impl ServerHashVerification { + const SELF_MAX_VALIDITY: time::Duration = time::Duration::days(14); + + /// Creates a new instance of `ServerHashVerification`. + /// + /// # Arguments + /// + /// - `hashes`: An iterator yielding `Sha256Digest` instances representing the + /// accepted certificate hashes. + pub fn new(hashes: H) -> Self + where + H: IntoIterator, + { + use std::collections::BTreeSet; + + Self { + hashes: BTreeSet::from_iter(hashes), + supported_algorithms: default_crypto_provider().signature_verification_algorithms, + } + } + + /// Adds a `digest` to the list of accepted certificates. + pub fn add(&mut self, digest: Sha256Digest) { + self.hashes.insert(digest); + } + } + + impl FromIterator for ServerHashVerification { + fn from_iter>(iter: T) -> Self { + Self::new(iter) + } + } + + impl ServerCertVerifier for ServerHashVerification { + fn verify_server_cert( + &self, + end_entity: &CertificateDer, + _intermediates: &[CertificateDer], + _server_name: &rustls_pki_types::ServerName, + _ocsp_response: &[u8], + now: rustls_pki_types::UnixTime, + ) -> Result { + use time::OffsetDateTime; + use x509_parser::oid_registry::OID_EC_P256; + use x509_parser::oid_registry::OID_KEY_TYPE_EC_PUBLIC_KEY; + use x509_parser::time::ASN1Time; + + let now = ASN1Time::new( + now.as_secs() + .try_into() + .ok() + .and_then(|time| OffsetDateTime::from_unix_timestamp(time).ok()) + .expect("time overflow"), + ); + + let x509 = X509Certificate::from_der(end_entity.as_ref()) + .map_err(|_| rustls::CertificateError::BadEncoding)? + .1; + + match x509.validity() { + x if now < x.not_before => { + return Err(rustls::CertificateError::NotValidYet.into()); + } + x if now > x.not_after => { + return Err(rustls::CertificateError::Expired.into()); + } + _ => {} + } + + let validity_period = x509.validity().not_after - x509.validity.not_before; + if !matches!(validity_period, Some(x) if x <= Self::SELF_MAX_VALIDITY) { + return Err(rustls::CertificateError::UnknownIssuer.into()); + } + + if x509.public_key().algorithm.algorithm != OID_KEY_TYPE_EC_PUBLIC_KEY { + return Err(rustls::CertificateError::UnknownIssuer.into()); + } + + if !matches!(x509.public_key().algorithm.parameters.as_ref().map(|any| any.as_oid()), + Some(Ok(oid)) if oid == OID_EC_P256) + { + return Err(rustls::CertificateError::UnknownIssuer.into()); + } + + // TODO: Duplicates logic in `Certificate::from_der`, to avoid allocating + X509Certificate::from_der(end_entity.as_ref()) + .map_err(|_| rustls::CertificateError::BadEncoding)?; + // TODO: Duplicates logic in `Certificate::hash`, to avoid allocating + let end_entity_hash = Sha256Digest(Sha256::digest(end_entity.as_ref()).into()); + + if self.hashes.contains(&end_entity_hash) { + Ok(ServerCertVerified::assertion()) + } else { + Err(rustls::CertificateError::UnknownIssuer.into()) + } + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls12_signature(message, cert, dss, &self.supported_algorithms) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls13_signature(message, cert, dss, &self.supported_algorithms) + } + + fn supported_verify_schemes(&self) -> Vec { + self.supported_algorithms.supported_schemes() + } + } +} + +/// TLS errors definitions module. +pub mod error { + use std::path::PathBuf; + + /// Represents an error indicating an invalid certificate parsing. + #[derive(Debug, thiserror::Error)] + #[error("invalid certificate: {0}")] + pub struct InvalidCertificate(pub(super) String); + + /// Represents an error failure to parse a string as a [`Sha256Digest`](super::Sha256Digest). + /// + /// See [`Sha256Digest::from_str_fmt`](super::Sha256Digest::from_str_fmt). + #[derive(Debug, thiserror::Error)] + #[error("cannot parse string as sha256 digest")] + pub struct InvalidDigest; + + /// Error during PEM load operation. + #[derive(Debug, thiserror::Error)] + pub enum PemLoadError { + /// Invalid certificate during chain load. + #[error("invalid certificate in the PEM chain (index: {}): {}", .index, .error)] + InvalidCertificateChain { + /// The index of the certificate in the PEM file. + index: usize, + /// Additional error information. + error: InvalidCertificate, + }, + + /// Cannot load the private key as the PEM file does not contain it. + #[error("no private key section found in PEM file")] + NoPrivateKeySection, + + /// Cannot load the certificate as the PEM file does not contain it. + #[error("no certificate section found in PEM file")] + NoCertificateSection, + + /// I/O operation encoding/decoding PEM file failed. + #[error("error on file '{}': {}", .file.display(), error)] + FileError { + /// Filename of the operation. + file: PathBuf, + + /// io error details. + error: std::io::Error, + }, + } + + /// Certificate SANs are not valid DNS. + /// + /// This error might happen during self signed certificate generation + /// [`Identity::self_signed`](super::Identity::self_signed). + /// In particular, *Subject Alternative Names* passed for the generation of the + /// certificate are not valid DNS *IA5* strings. + /// + /// DNS strings support the [International Alphabet No. 5 (IA5)] character encoding, i.e. + /// the 128 characters of the ASCII alphabet. + /// + /// [International Alphabet No. 5 (IA5)]: https://en.wikipedia.org/wiki/T.50_(standard) + #[cfg(feature = "self-signed")] + #[cfg_attr(docsrs, doc(cfg(feature = "self-signed")))] + #[derive(Debug, thiserror::Error)] + #[error("invalid SANs for the self certificate")] + pub struct InvalidSan; +} + +/// Module for generating self-signed [`Identity`]. +/// +/// This module provides a builder pattern for constructing self-signed +/// identities. These certificates are primarily used for local testing or +/// development where a full certificate authority (CA) infrastructure isn't +/// necessary. +/// +/// The feature is enabled when the `self-signed` feature flag is active. +#[cfg(feature = "self-signed")] +#[cfg_attr(docsrs, doc(cfg(feature = "self-signed")))] +pub mod self_signed { + use super::*; + use error::InvalidSan; + use rcgen::CertificateParams; + use rcgen::DistinguishedName; + use rcgen::DnType; + use rcgen::KeyPair; + use rcgen::PKCS_ECDSA_P256_SHA256; + use time::OffsetDateTime; + + /// The builder for creating self-signed [`Identity`]. + /// + /// This struct uses state-based typing to enforce that the appropriate methods + /// are called in the right order. + pub struct SelfSignedIdentityBuilder(State); + + impl SelfSignedIdentityBuilder { + /// Creates a new `SelfSignedIdentityBuilder` instance. + /// + /// The builder starts in the `WantsSans` state, meaning it requires subject + /// alternative names (SANs) to be specified before continuing. + /// + /// # Example + /// + /// ``` + /// use wtransport::tls::self_signed::SelfSignedIdentityBuilder; + /// + /// let builder = SelfSignedIdentityBuilder::new(); + /// ``` + /// + /// **Note**: You can conveniently create a new builder with [`Identity::self_signed_builder()`]. + /// + /// # Example + /// + /// ``` + /// use wtransport::Identity; + /// + /// let builder = Identity::self_signed_builder(); + /// ``` + pub fn new() -> Self { + Self(states::WantsSans {}) + } + + /// Specifies the subject alternative names (SANs) for the certificate. + /// + /// The SANs can be provided as a collection of strings, such as hostnames or IP addresses. + /// + /// # Arguments + /// + /// * `subject_alt_names` - An iterator of strings representing subject alternative names (SANs). + /// They can be both hostnames or IP addresses. + /// + /// # Example + /// + /// ``` + /// use wtransport::tls::self_signed::SelfSignedIdentityBuilder; + /// + /// let builder = + /// SelfSignedIdentityBuilder::new().subject_alt_names(&["localhost", "127.0.0.1", "::1"]); + /// ``` + pub fn subject_alt_names( + self, + subject_alt_names: I, + ) -> SelfSignedIdentityBuilder + where + I: IntoIterator, + S: AsRef, + { + let sans = subject_alt_names + .into_iter() + .map(|s| s.as_ref().to_string()) + .collect(); + + SelfSignedIdentityBuilder(states::WantsValidityPeriod { sans }) + } + } + + impl SelfSignedIdentityBuilder { + /// Sets the certificate's `not_before` time to the current UTC time. + /// + /// After this, the builder is in the `WantsNotAfter` state, requiring a `not_after` time to be set. + #[allow(clippy::wrong_self_convention)] + pub fn from_now_utc(self) -> SelfSignedIdentityBuilder { + let not_before = OffsetDateTime::now_utc(); + self.not_before(not_before) + } + + /// Sets the `not_before` time of the certificate. + /// + /// # Parameters + /// + /// - `not_before`: The starting time of the certificate's validity period. + /// + /// After this, the builder is in the `WantsNotAfter` state, requiring a `not_after` time to be set. + pub fn not_before( + self, + not_before: OffsetDateTime, + ) -> SelfSignedIdentityBuilder { + SelfSignedIdentityBuilder(states::WantsNotAfter { + sans: self.0.sans, + not_before, + }) + } + + /// Specifies the validity period for the certificate. + /// + /// # Parameters + /// + /// - `not_before`: The starting time of the certificate's validity period. + /// - `not_after`: The ending time of the certificate's validity period. + /// + /// # Example + /// + /// ``` + /// use wtransport::tls::self_signed::time::OffsetDateTime; + /// use wtransport::tls::self_signed::SelfSignedIdentityBuilder; + /// + /// let builder = SelfSignedIdentityBuilder::new() + /// .subject_alt_names(&["localhost"]) + /// .validity_period( + /// OffsetDateTime::now_utc(), + /// OffsetDateTime::now_utc() + time::Duration::days(7), + /// ); + /// ``` + pub fn validity_period( + self, + not_before: OffsetDateTime, + not_after: OffsetDateTime, + ) -> SelfSignedIdentityBuilder { + self.not_before(not_before).not_after(not_after) + } + } + + impl SelfSignedIdentityBuilder { + /// Specifies the `not_after` time of the certificate. + /// + /// # Parameters + /// + /// - `not_after`: The ending time of the certificate's validity. + pub fn not_after( + self, + not_after: OffsetDateTime, + ) -> SelfSignedIdentityBuilder { + SelfSignedIdentityBuilder(states::ReadyToBuild { + sans: self.0.sans, + not_before: self.0.not_before, + not_after, + }) + } + + /// Sets the `not_after` time of the certificate to an offset from the `not_before` time. + /// + /// # Parameters + /// + /// - `offset`: A time duration that specifies how far `not_after` should be from `not_before`. + /// + /// # Example + /// + /// ``` + /// use wtransport::tls::self_signed::time::OffsetDateTime; + /// use wtransport::tls::self_signed::SelfSignedIdentityBuilder; + /// + /// let builder = SelfSignedIdentityBuilder::new() + /// .subject_alt_names(&["localhost"]) + /// .not_before(OffsetDateTime::now_utc()) + /// .offset_from_not_before(time::Duration::days(7)); // now + 7 days + /// ``` + pub fn offset_from_not_before( + self, + offset: time::Duration, + ) -> SelfSignedIdentityBuilder { + let not_after = self.0.not_before + offset; + self.not_after(not_after) + } + + /// Sets the certificate's validity to a specified number of days from `not_before`. + /// + /// # Parameters + /// + /// - `days`: The number of days for which the certificate should be valid. + /// + /// # Example + /// + /// ``` + /// use wtransport::tls::self_signed::SelfSignedIdentityBuilder; + /// + /// let builder = SelfSignedIdentityBuilder::new() + /// .subject_alt_names(&["localhost"]) + /// .from_now_utc() + /// .validity_days(14); + /// ``` + pub fn validity_days(self, days: u32) -> SelfSignedIdentityBuilder { + self.offset_from_not_before(time::Duration::days(days as i64)) + } + } + + impl SelfSignedIdentityBuilder { + /// Generates a self-signed certificate and private key for new identity. + /// + /// # Returns + /// + /// Returns an [`Identity`] containing the certificate and private key, + /// or an error if the SANs are invalid (they are not valid *ASN.1* strings). + /// + /// # Specifications + /// + /// The certificate will be conforms to the following specifications: + /// + /// - The certificate is an *X.509v3* certificate as defined in *RFC5280*. + /// - The key used in the Subject Public Key field uses `ECDSA P-256` algorithm. + /// + /// # Example + /// + /// ``` + /// use wtransport::tls::self_signed::SelfSignedIdentityBuilder; + /// + /// let identity = SelfSignedIdentityBuilder::new() + /// .subject_alt_names(&["localhost", "127.0.0.1"]) + /// .from_now_utc() + /// .validity_days(7) + /// .build() + /// .unwrap(); + /// ``` + pub fn build(self) -> Result { + let mut dname = DistinguishedName::new(); + dname.push(DnType::CommonName, "wtransport self-signed"); + + let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256) + .expect("algorithm for key pair is supported"); + + let mut cert_params = CertificateParams::new(self.0.sans).map_err(|_| InvalidSan)?; + cert_params.distinguished_name = dname; + cert_params.not_before = self.0.not_before; + cert_params.not_after = self.0.not_after; + let cert = cert_params + .self_signed(&key_pair) + .expect("inner params are valid"); + + Ok(Identity::new( + CertificateChain::single(Certificate(cert.der().clone())), + PrivateKey::from_der_pkcs8(key_pair.serialize_der()), + )) + } + } + + impl Default for SelfSignedIdentityBuilder { + fn default() -> Self { + Self::new() + } + } + + /// State-types for [`SelfSignedIdentityBuilder`] builder. + pub mod states { + use super::*; + + /// Initial state, requiring subject alternative names (SANs). + pub struct WantsSans {} + + /// State after SANs have been set, requiring a validity period. + pub struct WantsValidityPeriod { + pub(super) sans: Vec, + } + + /// State after `not_before` is set, requiring `not_after` to be specified. + pub struct WantsNotAfter { + pub(super) sans: Vec, + pub(super) not_before: OffsetDateTime, + } + + /// Final state where all data is ready and the certificate can be built. + pub struct ReadyToBuild { + pub(super) sans: Vec, + pub(super) not_before: OffsetDateTime, + pub(super) not_after: OffsetDateTime, + } + } + + pub use time; +} + +pub use rustls; + +mod utils { + use std::env; + use std::ffi::OsStr; + use std::ffi::OsString; + + pub struct VarsRestoreGuard(Vec<(OsString, Option)>); + + impl Drop for VarsRestoreGuard { + fn drop(&mut self) { + for (k, v) in std::mem::take(&mut self.0) { + if let Some(v) = v { + env::set_var(k, v); + } + } + } + } + + pub fn remove_vars_tmp(keys: I) -> VarsRestoreGuard + where + I: IntoIterator, + K: AsRef, + { + let table = keys + .into_iter() + .map(|k| { + let k = k.as_ref().to_os_string(); + let v = env::var_os(&k); + env::remove_var(&k); + (k, v) + }) + .collect(); + + VarsRestoreGuard(table) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn invalid_certificate() { + assert!(matches!( + Certificate::from_der(b"invalid-certificate".to_vec()), + Err(InvalidCertificate(_)) + )); + } + + #[test] + fn idempotence_digest() { + let digest = Sha256Digest::new([ + 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, + 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 64, + ]); + + let d = Sha256Digest::from_str_fmt( + digest.fmt(Sha256DigestFmt::BytesArray), + Sha256DigestFmt::BytesArray, + ) + .unwrap(); + + assert_eq!(d, digest); + + let d = Sha256Digest::from_str_fmt( + digest.fmt(Sha256DigestFmt::DottedHex), + Sha256DigestFmt::DottedHex, + ) + .unwrap(); + + assert_eq!(d, digest); + } + + #[test] + fn digest_from_str() { + assert!(matches!( + "invalid".parse::(), + Err(InvalidDigest) + )); + + assert!(matches!( + "[97, 98, 99, 100, 101, 102, 103, 104, 105, 106, \ + 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, \ + 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, \ + 127, 64]" + .parse::(), + + Ok(digest) if digest == Sha256Digest::new([ + 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, + 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 64 + ]))); + + assert!(matches!( + "61:62:63:64:65:66:67:68:69:6a:6b:6c:6d:6e:6f:70: \ + 71:72:73:74:75:76:77:78:79:7a:7b:7c:7d:7e:7f:40" + .parse::(), + + Ok(digest) if digest == Sha256Digest::new([ + 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, + 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 64 + ]))); + } +} diff --git a/scripts/check-lockfiles.sh b/scripts/check-lockfiles.sh index 30dfb8403..a116cc94c 100755 --- a/scripts/check-lockfiles.sh +++ b/scripts/check-lockfiles.sh @@ -33,6 +33,10 @@ SKIP=( # that workspace, against that workspace's lock; its own lock is the # upstream's, kept with the vendored source. "jouledesk/jouledesk-rs/vendor/libwebrtc" + # A vendored crate, patched into the joule-code workspace and into joule-moq-room. + # Cargo resolves it as part of each, against that workspace's lock; its own lock is + # the upstream's, kept with the vendored source. + "joule-code/joule-code-rs/vendor/wtransport" ) skipped() { local d; for d in "${SKIP[@]}"; do [ "$d" = "$1" ] && return 0; done; return 1; } -- 2.54.0 (Apple Git-157) From 7e6c0ccc4a9f50c5b0357b582bece0c3f7115f3d Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 20 Oct 2026 08:30:35 -0400 Subject: [PATCH 2/9] vendored wtransport: close a WebTransport session with WT_CLOSE_SESSION MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Connection::close_session(code, reason) has the driver's worker write one DATA frame holding a WT_CLOSE_SESSION capsule (draft-ietf-webtrans-http3-16 §6) on the CONNECT stream at the highest send priority, FIN it, and wait at most two seconds for the peer to close its side before closing the QUIC connection with H3_NO_ERROR, so the connection's close cannot overtake the capsule. The reason is cut to 1024 bytes at a character boundary. After it, this end opens no stream and sends no datagram; a close asked for just as the last handle drops still goes out. A session the peer closes is ConnectionError::SessionClosed (the release reported it as ApplicationClosed, the variant of the QUIC connection's close, whose code under HTTP/3 is an HTTP/3 error code), and Connection::closed() reports the session's end as the driver saw it. Unit tests of the capsule's bytes against the draft's layout. The upstream's examples and dev-dependencies are left out so the crate's unit tests run from the workspace (cargo test -p wtransport --lib). VENDORED.md lists every change. Co-Authored-By: Claude Opus 5.5 --- .../vendor/wtransport/Cargo.toml | 39 +- .../vendor/wtransport/VENDORED.md | 62 ++ .../vendor/wtransport/examples/client.rs | 20 - .../vendor/wtransport/examples/full.rs | 558 ------------------ .../vendor/wtransport/examples/gencert.rs | 45 -- .../vendor/wtransport/examples/server.rs | 111 ---- .../vendor/wtransport/src/connection.rs | 32 +- .../vendor/wtransport/src/driver/mod.rs | 89 ++- .../wtransport/src/driver/streams/connect.rs | 125 ++++ .../wtransport/src/driver/streams/mod.rs | 4 + .../vendor/wtransport/src/error.rs | 12 +- 11 files changed, 319 insertions(+), 778 deletions(-) create mode 100644 joule-code/joule-code-rs/vendor/wtransport/VENDORED.md delete mode 100644 joule-code/joule-code-rs/vendor/wtransport/examples/client.rs delete mode 100644 joule-code/joule-code-rs/vendor/wtransport/examples/full.rs delete mode 100644 joule-code/joule-code-rs/vendor/wtransport/examples/gencert.rs delete mode 100644 joule-code/joule-code-rs/vendor/wtransport/examples/server.rs diff --git a/joule-code/joule-code-rs/vendor/wtransport/Cargo.toml b/joule-code/joule-code-rs/vendor/wtransport/Cargo.toml index 5b8c6f268..45151cf7b 100644 --- a/joule-code/joule-code-rs/vendor/wtransport/Cargo.toml +++ b/joule-code/joule-code-rs/vendor/wtransport/Cargo.toml @@ -104,26 +104,6 @@ self-signed = ["dep:rcgen"] name = "wtransport" path = "src/lib.rs" -[[example]] -name = "client" -path = "examples/client.rs" -required-features = ["dangerous-configuration"] - -[[example]] -name = "full" -path = "examples/full.rs" -required-features = ["self-signed"] - -[[example]] -name = "gencert" -path = "examples/gencert.rs" -required-features = ["self-signed"] - -[[example]] -name = "server" -path = "examples/server.rs" -required-features = ["self-signed"] - [dependencies.bytes] version = "1.4.0" @@ -168,6 +148,7 @@ features = [ "macros", "fs", "io-util", + "time", ] default-features = false @@ -184,24 +165,6 @@ features = ["async"] [dependencies.x509-parser] version = "0.18.1" -[dev-dependencies.anyhow] -version = "1.0.71" - -[dev-dependencies.axum] -version = "0.7.1" - -[dev-dependencies.hyper] -version = "1.0.1" - -[dev-dependencies.tokio] -version = "1.28.1" -features = ["rt-multi-thread"] -default-features = false - -[dev-dependencies.tracing-subscriber] -version = "0.3.17" -features = ["env-filter"] - [lints.clippy] manual_is_variant_and = "warn" manual_let_else = "warn" diff --git a/joule-code/joule-code-rs/vendor/wtransport/VENDORED.md b/joule-code/joule-code-rs/vendor/wtransport/VENDORED.md new file mode 100644 index 000000000..2fd6db1d2 --- /dev/null +++ b/joule-code/joule-code-rs/vendor/wtransport/VENDORED.md @@ -0,0 +1,62 @@ +# wtransport 0.7.2, vendored + +**Upstream.** The `wtransport` crate, version 0.7.2, exactly as published on crates.io: the +`.crate` file's SHA-256 is `b4273ce3157a3262a68665f8d3f20a0ac0c5b8a69ffd67f05ae986832ebec036`, +the checksum the joule-code lock held for it, and `.cargo_vcs_info.json` names the upstream +commit it was packaged from (`d022a6ad5f6260cb526e06d8ca42d09516ce6440`, directory +`wtransport`). `LICENSE-MIT` and `LICENSE-APACHE` are the upstream's, from that commit; the +crate is MIT OR Apache-2.0, and these changes are offered under the same terms. The +crate's companion, `wtransport-proto` 0.7.2, is not vendored: nothing in it changes. + +**Where it is used.** `[patch.crates-io]` in `joule-code/joule-code-rs/Cargo.toml` and in +`crates/joule-moq-room/Cargo.toml` (a workspace of its own that builds the relay). Its +`Cargo.lock` and `Cargo.toml.orig` are the upstream's, kept with the source; cargo reads +neither, and `scripts/check-lockfiles.sh` skips the lock. + +## Why + +A MOQT session over WebTransport is closed by closing the WebTransport session +(`draft-ietf-moq-transport-22` §6.6, which calls the capsule by its earlier name, +CLOSE_WEBTRANSPORT_SESSION): a WT_CLOSE_SESSION capsule on the session's CONNECT stream +carries a 32-bit application error code and a UTF-8 reason of at most 1024 bytes, followed +by a FIN (`draft-ietf-webtrans-http3-16` §6). The release reads that capsule but cannot +send it: its CONNECT stream belongs to the driver's worker, and `Connection::close` closes +the QUIC connection, whose code a browser's WebTransport API does not expose. A page whose +session joule-moq-relay refused saw only that the session was lost. + +## What changed + +- `Connection::close_session(error_code: u32, reason: &str)`. It hands the driver's worker + a request (`Driver::close_session`); the worker writes one HTTP/3 DATA frame holding the + capsule (`close_session_capsule`, in `src/driver/streams/connect.rs`) on the CONNECT + stream at the highest send priority, sends the FIN, and then reads that stream until the + peer's FIN or reset, at most two seconds (`SESSION_CLOSE_LINGER`), before it closes the + QUIC connection with H3_NO_ERROR: §6 asks an endpoint to wait for the peer to close its + CONNECT stream first, so the connection's close cannot overtake the capsule. The reason + is cut to 1024 bytes at a UTF-8 character boundary, as §6 requires of a sender that + truncates. +- After `close_session` this end opens no stream and sends no datagram (§6): `open_uni`, + `open_bi` and `send_datagram` fail at once. +- A close requested just as the session's last handle is dropped still goes out: the + worker's loop can see the handles gone first, so `Worker::run` takes a pending request + before it ends. +- `ConnectionError::SessionClosed`, for a session the peer closed (the capsule's code and + reason, or code 0 and no reason for a FIN alone, §6). The release reported that as + `ApplicationClosed`, the variant it also uses for the QUIC connection's close, whose code + under HTTP/3 is an HTTP/3 error code; the two could not be told apart. +- `Connection::closed()` resolves with how the session ended, as the driver saw it, rather + than with how the QUIC connection closed after: `SessionClosed` with the peer's code and + reason, or `LocallyClosed` as soon as this end's capsule is queued. The release's + `closed()` gave the QUIC close, so a peer's capsule was never reported there. +- The tokio feature `time` is named, for the wait's bound (it was on already through quinn). +- The upstream's examples and dev-dependencies are left out, so that the crate's unit tests, + the upstream's and those of the capsule's layout, run from the joule-code workspace: + `cargo test --locked -p wtransport --lib` (cargo tests a non-member crate only when it + has no dev-dependencies). The CI job of joule-moq-relay runs them. + +## What did not change + +The streams of a session this end closes are not reset one by one with WT_SESSION_GONE, as +§6 asks of an endpoint whose connection carries on; the driver does not track them. They end +with the QUIC connection, at most two seconds later. joule-moq-relay serves one session per +connection, and the session ends at the capsule for both ends. diff --git a/joule-code/joule-code-rs/vendor/wtransport/examples/client.rs b/joule-code/joule-code-rs/vendor/wtransport/examples/client.rs deleted file mode 100644 index ed8450737..000000000 --- a/joule-code/joule-code-rs/vendor/wtransport/examples/client.rs +++ /dev/null @@ -1,20 +0,0 @@ -use wtransport::ClientConfig; -use wtransport::Endpoint; - -#[tokio::main] -async fn main() { - let config = ClientConfig::builder() - .with_bind_default() - .with_no_cert_validation() - .build(); - - let connection = Endpoint::client(config) - .unwrap() - .connect("https://[::1]:4433") - .await - .unwrap(); - - let mut stream = connection.open_bi().await.unwrap().await.unwrap(); - stream.0.write_all(b"HELLO").await.unwrap(); - stream.0.finish().await.unwrap(); -} diff --git a/joule-code/joule-code-rs/vendor/wtransport/examples/full.rs b/joule-code/joule-code-rs/vendor/wtransport/examples/full.rs deleted file mode 100644 index fef22bd0e..000000000 --- a/joule-code/joule-code-rs/vendor/wtransport/examples/full.rs +++ /dev/null @@ -1,558 +0,0 @@ -use anyhow::Context; -use anyhow::Result; -use http::HttpServer; -use tracing::error; -use tracing::info; -use tracing::info_span; -use tracing::Instrument; -use webtransport::WebTransportServer; -use wtransport::tls::Sha256Digest; -use wtransport::tls::Sha256DigestFmt; -use wtransport::Identity; - -#[tokio::main] -async fn main() -> Result<()> { - utils::init_logging(); - - let identity = Identity::self_signed(["localhost", "127.0.0.1", "::1"]).unwrap(); - let cert_digest = identity.certificate_chain().as_slice()[0].hash(); - - let webtransport_server = WebTransportServer::new(identity)?; - let http_server = HttpServer::new(&cert_digest, webtransport_server.local_port()).await?; - - info!( - "Open the browser and go to: http://127.0.0.1:{}", - http_server.local_port() - ); - - tokio::select! { - result = http_server.serve() => { - error!("HTTP server: {:?}", result); - } - result = webtransport_server.serve() => { - error!("WebTransport server: {:?}", result); - } - } - - Ok(()) -} - -mod webtransport { - use super::*; - use std::time::Duration; - use wtransport::endpoint::endpoint_side::Server; - use wtransport::endpoint::IncomingSession; - use wtransport::Endpoint; - use wtransport::ServerConfig; - - pub struct WebTransportServer { - endpoint: Endpoint, - } - - impl WebTransportServer { - pub fn new(identity: Identity) -> Result { - let config = ServerConfig::builder() - .with_bind_default(0) - .with_identity(identity) - .keep_alive_interval(Some(Duration::from_secs(3))) - .build(); - - let endpoint = Endpoint::server(config)?; - - Ok(Self { endpoint }) - } - - pub fn local_port(&self) -> u16 { - self.endpoint.local_addr().unwrap().port() - } - - pub async fn serve(self) -> Result<()> { - info!("Server running on port {}", self.local_port()); - - for id in 0.. { - let incoming_session = self.endpoint.accept().await; - - tokio::spawn( - Self::handle_incoming_session(incoming_session) - .instrument(info_span!("Connection", id)), - ); - } - - Ok(()) - } - - async fn handle_incoming_session(incoming_session: IncomingSession) { - async fn handle_incoming_session_impl(incoming_session: IncomingSession) -> Result<()> { - let mut buffer = vec![0; 65536].into_boxed_slice(); - - info!("Waiting for session request..."); - - let session_request = incoming_session.await?; - - info!( - "New session: Authority: '{}', Path: '{}'", - session_request.authority(), - session_request.path() - ); - - let connection = session_request.accept().await?; - - info!("Waiting for data from client..."); - - loop { - tokio::select! { - stream = connection.accept_bi() => { - let mut stream = stream?; - info!("Accepted BI stream"); - - let Some(bytes_read) = stream.1.read(&mut buffer).await? else { - continue; - }; - - let str_data = std::str::from_utf8(&buffer[..bytes_read])?; - - info!("Received (bi) '{str_data}' from client"); - - stream.0.write_all(b"ACK").await?; - } - stream = connection.accept_uni() => { - let mut stream = stream?; - info!("Accepted UNI stream"); - - let Some(bytes_read) = stream.read(&mut buffer).await? else { - continue; - }; - - let str_data = std::str::from_utf8(&buffer[..bytes_read])?; - - info!("Received (uni) '{str_data}' from client"); - - let mut stream = connection.open_uni().await?.await?; - stream.write_all(b"ACK").await?; - } - dgram = connection.receive_datagram() => { - let dgram = dgram?; - let str_data = std::str::from_utf8(&dgram)?; - - info!("Received (dgram) '{str_data}' from client"); - - connection.send_datagram(b"ACK")?; - } - } - } - } - - let result = handle_incoming_session_impl(incoming_session).await; - info!("Result: {:?}", result); - } - } -} - -mod http { - use super::*; - use axum::http::header::CONTENT_TYPE; - use axum::response::Html; - use axum::routing::get; - use axum::serve; - use axum::serve::Serve; - use axum::Router; - use std::net::Ipv4Addr; - use std::net::SocketAddr; - use tokio::net::TcpListener; - - pub struct HttpServer { - serve: Serve, - local_port: u16, - } - - impl HttpServer { - const PORT: u16 = 8080; - - pub async fn new(cert_digest: &Sha256Digest, webtransport_port: u16) -> Result { - let router = Self::build_router(cert_digest, webtransport_port); - - let listener = - TcpListener::bind(SocketAddr::new(Ipv4Addr::LOCALHOST.into(), Self::PORT)) - .await - .context("Cannot bind TCP listener for HTTP server")?; - - let local_port = listener - .local_addr() - .context("Cannot get local port")? - .port(); - - Ok(HttpServer { - serve: serve(listener, router), - local_port, - }) - } - - pub fn local_port(&self) -> u16 { - self.local_port - } - - pub async fn serve(self) -> Result<()> { - info!("Server running on port {}", self.local_port()); - - self.serve.await.context("HTTP server error")?; - - Ok(()) - } - - fn build_router(cert_digest: &Sha256Digest, webtransport_port: u16) -> Router { - let cert_digest = cert_digest.fmt(Sha256DigestFmt::BytesArray); - - let root = move || async move { - Html( - http_data::INDEX_DATA - .replace("${WEBTRANSPORT_PORT}", &webtransport_port.to_string()), - ) - }; - - let style = - move || async move { ([(CONTENT_TYPE, "text/css")], http_data::STYLE_DATA) }; - - let client = move || async move { - ( - [(CONTENT_TYPE, "application/javascript")], - http_data::CLIENT_DATA.replace("${CERT_DIGEST}", &cert_digest), - ) - }; - - Router::new() - .route("/", get(root)) - .route("/style.css", get(style)) - .route("/client.js", get(client)) - } - } -} - -mod utils { - use tracing_subscriber::filter::LevelFilter; - use tracing_subscriber::EnvFilter; - - pub fn init_logging() { - let env_filter = EnvFilter::builder() - .with_default_directive(LevelFilter::INFO.into()) - .from_env_lossy(); - - tracing_subscriber::fmt() - .with_target(true) - .with_level(true) - .with_env_filter(env_filter) - .init(); - } -} - -mod http_data { - - pub const INDEX_DATA: &str = r#" - - - WTransport-Example - - - - - - -

WTransport Example

- -
-

Establish WebTransport connection

-
- - - -
-
- -
-

Send data over WebTransport

-
- -
- - -
-
- - -
-
- - -
- -
-
- -
-

Event log

-
    -
-
- - - -"#; - - pub const STYLE_DATA: &str = r#" -body { - font-family: sans-serif; -} - -h1 { - margin: 0 auto; - width: fit-content; -} - -h2 { - border-bottom: 1px dotted #333; - font-size: 120%; - font-weight: normal; - padding-bottom: 0.2em; - padding-top: 0.5em; -} - -code { - background-color: #eee; -} - -input[type=text], textarea { - font-family: monospace; -} - -#top { - display: flex; - flex-direction: row-reverse; - flex-wrap: wrap; - justify-content: center; -} - -#explanation { - border: 1px dotted black; - font-size: 90%; - height: fit-content; - margin-bottom: 1em; - padding: 1em; - width: 13em; -} - -#tool { - flex-grow: 1; - margin: 0 auto; - max-width: 26em; - padding: 0 1em; - width: 26em; -} - -.input-line { - display: flex; -} - -.input-line input[type=text] { - flex-grow: 1; - margin: 0 0.5em; -} - -textarea { - height: 3em; - width: 100%; -} - -#send { - margin-top: 0.5em; - width: 15em; -} - -#event-log { - border: 1px dotted black; - font-family: monospace; - height: 12em; - overflow: scroll; - padding-bottom: 1em; - padding-top: 1em; -} - -.log-error { - color: darkred; -} - -#explanation ul { - padding-left: 1em; -} -"#; - - pub const CLIENT_DATA: &str = r#" -// Adds an entry to the event log on the page, optionally applying a specified -// CSS class. - -const HASH = new Uint8Array(${CERT_DIGEST}); - -let currentTransport, streamNumber, currentTransportDatagramWriter; - -// "Connect" button handler. -async function connect() { - const url = document.getElementById('url').value; - try { - var transport = new WebTransport(url, { serverCertificateHashes: [ { algorithm: "sha-256", value: HASH.buffer } ] } ); - addToEventLog('Initiating connection...'); - } catch (e) { - addToEventLog('Failed to create connection object. ' + e, 'error'); - return; - } - - try { - await transport.ready; - addToEventLog('Connection ready.'); - } catch (e) { - addToEventLog('Connection failed. ' + e, 'error'); - return; - } - - transport.closed - .then(() => { - addToEventLog('Connection closed normally.'); - }) - .catch(() => { - addToEventLog('Connection closed abruptly.', 'error'); - }); - - currentTransport = transport; - streamNumber = 1; - try { - currentTransportDatagramWriter = transport.datagrams.writable.getWriter(); - addToEventLog('Datagram writer ready.'); - } catch (e) { - addToEventLog('Sending datagrams not supported: ' + e, 'error'); - return; - } - readDatagrams(transport); - acceptUnidirectionalStreams(transport); - document.forms.sending.elements.send.disabled = false; - document.getElementById('connect').disabled = true; -} - -// "Send data" button handler. -async function sendData() { - let form = document.forms.sending.elements; - let encoder = new TextEncoder('utf-8'); - let rawData = sending.data.value; - let data = encoder.encode(rawData); - let transport = currentTransport; - try { - switch (form.sendtype.value) { - case 'datagram': - await currentTransportDatagramWriter.write(data); - addToEventLog('Sent datagram: ' + rawData); - break; - case 'unidi': { - let stream = await transport.createUnidirectionalStream(); - let writer = stream.getWriter(); - await writer.write(data); - await writer.close(); - addToEventLog('Sent a unidirectional stream with data: ' + rawData); - break; - } - case 'bidi': { - let stream = await transport.createBidirectionalStream(); - let number = streamNumber++; - readFromIncomingStream(stream.readable, number); - - let writer = stream.writable.getWriter(); - await writer.write(data); - await writer.close(); - addToEventLog( - 'Opened bidirectional stream #' + number + - ' with data: ' + rawData); - break; - } - } - } catch (e) { - addToEventLog('Error while sending data: ' + e, 'error'); - } -} - -// Reads datagrams from |transport| into the event log until EOF is reached. -async function readDatagrams(transport) { - try { - var reader = transport.datagrams.readable.getReader(); - addToEventLog('Datagram reader ready.'); - } catch (e) { - addToEventLog('Receiving datagrams not supported: ' + e, 'error'); - return; - } - let decoder = new TextDecoder('utf-8'); - try { - while (true) { - const { value, done } = await reader.read(); - if (done) { - addToEventLog('Done reading datagrams!'); - return; - } - let data = decoder.decode(value); - addToEventLog('Datagram received: ' + data); - } - } catch (e) { - addToEventLog('Error while reading datagrams: ' + e, 'error'); - } -} - -async function acceptUnidirectionalStreams(transport) { - let reader = transport.incomingUnidirectionalStreams.getReader(); - try { - while (true) { - const { value, done } = await reader.read(); - if (done) { - addToEventLog('Done accepting unidirectional streams!'); - return; - } - let stream = value; - let number = streamNumber++; - addToEventLog('New incoming unidirectional stream #' + number); - readFromIncomingStream(stream, number); - } - } catch (e) { - addToEventLog('Error while accepting streams: ' + e, 'error'); - } -} - -async function readFromIncomingStream(stream, number) { - let decoder = new TextDecoderStream('utf-8'); - let reader = stream.pipeThrough(decoder).getReader(); - try { - while (true) { - const { value, done } = await reader.read(); - if (done) { - addToEventLog('Stream #' + number + ' closed'); - return; - } - let data = value; - addToEventLog('Received data on stream #' + number + ': ' + data); - } - } catch (e) { - addToEventLog( - 'Error while reading from stream #' + number + ': ' + e, 'error'); - addToEventLog(' ' + e.message); - } -} - -function addToEventLog(text, severity = 'info') { - let log = document.getElementById('event-log'); - let mostRecentEntry = log.lastElementChild; - let entry = document.createElement('li'); - entry.innerText = text; - entry.className = 'log-' + severity; - log.appendChild(entry); - - // If the most recent entry in the log was visible, scroll the log to the - // newly added element. - if (mostRecentEntry != null && - mostRecentEntry.getBoundingClientRect().top < - log.getBoundingClientRect().bottom) { - entry.scrollIntoView(); - } -} -"#; -} diff --git a/joule-code/joule-code-rs/vendor/wtransport/examples/gencert.rs b/joule-code/joule-code-rs/vendor/wtransport/examples/gencert.rs deleted file mode 100644 index 3e48e30a6..000000000 --- a/joule-code/joule-code-rs/vendor/wtransport/examples/gencert.rs +++ /dev/null @@ -1,45 +0,0 @@ -use anyhow::Context; -use anyhow::Result; -use wtransport::tls::Sha256DigestFmt; -use wtransport::Identity; - -const CERT_FILE: &str = "cert.pem"; -const KEY_FILE: &str = "key.pem"; - -#[tokio::main] -async fn main() -> Result<()> { - println!("Generating self signed certificate for WebTransport"); - - let identity = - Identity::self_signed(["localhost"]).context("cannot create self signed identity")?; - - println!("Storing certificate to file: '{CERT_FILE}'"); - - identity - .certificate_chain() - .store_pemfile(CERT_FILE) - .await - .context("cannot store certificate chain")?; - - println!("Storing private key to file: '{KEY_FILE}'"); - - identity - .private_key() - .store_secret_pemfile(KEY_FILE) - .await - .context("cannot store private key")?; - - println!( - "Certificate serial: {}", - identity.certificate_chain().as_slice()[0].serial() - ); - - println!( - "Certificate hash: {}", - identity.certificate_chain().as_slice()[0] - .hash() - .fmt(Sha256DigestFmt::BytesArray) - ); - - Ok(()) -} diff --git a/joule-code/joule-code-rs/vendor/wtransport/examples/server.rs b/joule-code/joule-code-rs/vendor/wtransport/examples/server.rs deleted file mode 100644 index 212bed68c..000000000 --- a/joule-code/joule-code-rs/vendor/wtransport/examples/server.rs +++ /dev/null @@ -1,111 +0,0 @@ -use anyhow::Result; -use std::time::Duration; -use tracing::error; -use tracing::info; -use tracing::info_span; -use tracing::Instrument; -use tracing_subscriber::filter::LevelFilter; -use tracing_subscriber::EnvFilter; -use wtransport::endpoint::IncomingSession; -use wtransport::Endpoint; -use wtransport::Identity; -use wtransport::ServerConfig; - -#[tokio::main] -async fn main() -> Result<()> { - init_logging(); - - let config = ServerConfig::builder() - .with_bind_default(4433) - .with_identity(Identity::self_signed(["localhost"]).unwrap()) - .keep_alive_interval(Some(Duration::from_secs(3))) - .build(); - - let server = Endpoint::server(config)?; - - info!("Server ready!"); - - for id in 0.. { - let incoming_session = server.accept().await; - tokio::spawn(handle_connection(incoming_session).instrument(info_span!("Connection", id))); - } - - Ok(()) -} - -async fn handle_connection(incoming_session: IncomingSession) { - let result = handle_connection_impl(incoming_session).await; - error!("{:?}", result); -} - -async fn handle_connection_impl(incoming_session: IncomingSession) -> Result<()> { - let mut buffer = vec![0; 65536].into_boxed_slice(); - - info!("Waiting for session request..."); - - let session_request = incoming_session.await?; - - info!( - "New session: Authority: '{}', Path: '{}'", - session_request.authority(), - session_request.path() - ); - - let connection = session_request.accept().await?; - - info!("Waiting for data from client..."); - - loop { - tokio::select! { - stream = connection.accept_bi() => { - let mut stream = stream?; - info!("Accepted BI stream"); - - let Some(bytes_read) = stream.1.read(&mut buffer).await? else { - continue; - }; - - let str_data = std::str::from_utf8(&buffer[..bytes_read])?; - - info!("Received (bi) '{str_data}' from client"); - - stream.0.write_all(b"ACK").await?; - } - stream = connection.accept_uni() => { - let mut stream = stream?; - info!("Accepted UNI stream"); - - let Some(bytes_read) = stream.read(&mut buffer).await? else { - continue; - }; - - let str_data = std::str::from_utf8(&buffer[..bytes_read])?; - - info!("Received (uni) '{str_data}' from client"); - - let mut stream = connection.open_uni().await?.await?; - stream.write_all(b"ACK").await?; - } - dgram = connection.receive_datagram() => { - let dgram = dgram?; - let str_data = std::str::from_utf8(&dgram)?; - - info!("Received (dgram) '{str_data}' from client"); - - connection.send_datagram(b"ACK")?; - } - } - } -} - -fn init_logging() { - let env_filter = EnvFilter::builder() - .with_default_directive(LevelFilter::INFO.into()) - .from_env_lossy(); - - tracing_subscriber::fmt() - .with_target(true) - .with_level(true) - .with_env_filter(env_filter) - .init(); -} diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/connection.rs b/joule-code/joule-code-rs/vendor/wtransport/src/connection.rs index 60f642579..6cdd9ab7e 100644 --- a/joule-code/joule-code-rs/vendor/wtransport/src/connection.rs +++ b/joule-code/joule-code-rs/vendor/wtransport/src/connection.rs @@ -302,13 +302,41 @@ impl Connection { } /// Closes the connection immediately. + /// + /// This closes the QUIC connection: the peer learns no more than that the session is + /// gone. To close the session with an application error code and reason the peer can + /// read, use [`close_session`](Self::close_session). pub fn close(&self, error_code: VarInt, reason: &[u8]) { self.quic_connection.close(varint_w2q(error_code), reason); } - /// Waits for the connection to be closed for any reason. + /// Closes the WebTransport session with a 32-bit application error code and a reason + /// (draft-ietf-webtrans-http3-16 §6), then the connection, which carries this session + /// alone. + /// + /// A WT_CLOSE_SESSION capsule carrying `error_code` and `reason` (cut to 1024 bytes at + /// a character boundary) goes out on the session's CONNECT stream, with a FIN, ahead of + /// anything else queued. The session is over from here: this end opens no stream and + /// sends no datagram, and [`closed`](Self::closed) resolves with + /// [`ConnectionError::LocallyClosed`]. The connection is closed with H3_NO_ERROR once + /// the peer has closed its side of the CONNECT stream, as it must on reading the + /// capsule, or after two seconds, so that the connection's close cannot overtake the + /// capsule. The streams of the session end with the connection. + pub fn close_session(&self, error_code: u32, reason: &str) { + self.driver.close_session(error_code, reason); + } + + /// Waits for the session to be closed for any reason. + /// + /// A session the peer closed resolves with [`ConnectionError::SessionClosed`], its code + /// and reason, rather than with how the connection closed after it. pub async fn closed(&self) -> ConnectionError { - self.quic_connection.closed().await.into() + match self.driver.closed().await { + Some(driver_error) => { + ConnectionError::with_driver_error(driver_error, &self.quic_connection) + } + None => self.quic_connection.closed().await.into(), + } } /// Returns the WebTransport session identifier. diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/driver/mod.rs b/joule-code/joule-code-rs/vendor/wtransport/src/driver/mod.rs index dc68f0491..ff05e4fe5 100644 --- a/joule-code/joule-code-rs/vendor/wtransport/src/driver/mod.rs +++ b/joule-code/joule-code-rs/vendor/wtransport/src/driver/mod.rs @@ -14,6 +14,9 @@ use crate::error::SendDatagramError; use crate::stream::OpeningBiStream; use crate::stream::OpeningUniStream; use crate::SessionId; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; +use std::time::Duration; use tokio::sync::mpsc; use tokio::sync::Mutex; use tracing::debug; @@ -27,10 +30,16 @@ use wtransport_proto::frame::Frame; use wtransport_proto::session::SessionRequest; use wtransport_proto::settings::Settings; +/// How long a session closed by this end waits for the peer to close its side of the +/// CONNECT stream before the connection is closed (see [`Driver::close_session`]). +const SESSION_CLOSE_LINGER: Duration = Duration::from_secs(2); + #[derive(Clone, Debug)] pub enum DriverError { Proto(ErrorCode), ApplicationClosed(ApplicationClose), + /// This endpoint closed the session ([`Driver::close_session`]). + LocallyClosed, NotConnected, } @@ -43,6 +52,8 @@ pub struct Driver { ready_bi_wt_streams: Mutex>, ready_datagrams: Mutex>, driver_result: SharedResultGet, + close_requests: mpsc::UnboundedSender<(u32, String)>, + session_closing: AtomicBool, } impl Driver { @@ -53,6 +64,7 @@ impl Driver { let ready_bi_wt_streams = mpsc::channel(1); let ready_datagrams = mpsc::channel(1); let driver_result = shared_result(); + let close_requests = mpsc::unbounded_channel(); tokio::spawn( worker::Worker::new( @@ -63,6 +75,7 @@ impl Driver { ready_bi_wt_streams.0, ready_datagrams.0, driver_result.0, + close_requests.1, ) .run() .instrument(debug_span!("Driver", quic_id = quic_connection.stable_id())), @@ -76,9 +89,30 @@ impl Driver { ready_bi_wt_streams: Mutex::new(ready_bi_wt_streams.1), ready_datagrams: Mutex::new(ready_datagrams.1), driver_result: driver_result.1, + close_requests: close_requests.0, + session_closing: AtomicBool::new(false), } } + /// Closes the session from this end: the worker sends WT_CLOSE_SESSION with `error_code` + /// and `reason` on the CONNECT stream, then closes the connection (see + /// [`Connection::close_session`](crate::Connection::close_session)). From here this end + /// opens no stream and sends no datagram (draft-ietf-webtrans-http3-16 §6). + pub fn close_session(&self, error_code: u32, reason: &str) { + self.session_closing.store(true, Ordering::Release); + // An error means the worker has ended, and the session with it. + let _ = self.close_requests.send((error_code, reason.to_owned())); + } + + /// Why the worker ended, once it has; `None` if it stopped without saying. + pub async fn closed(&self) -> Option { + self.driver_result.result().await + } + + fn closing(&self) -> bool { + self.session_closing.load(Ordering::Acquire) + } + pub async fn accept_settings(&self) -> Result { let mut lock = self.ready_settings.lock().await; @@ -189,6 +223,10 @@ impl Driver { } pub async fn open_uni(&self, session_id: SessionId) -> Result { + if self.closing() { + return Err(DriverError::LocallyClosed); + } + let quic_stream = Stream::open_uni(&self.quic_connection) .await .ok_or(DriverError::NotConnected)?; @@ -197,6 +235,10 @@ impl Driver { } pub async fn open_bi(&self, session_id: SessionId) -> Result { + if self.closing() { + return Err(DriverError::LocallyClosed); + } + let quic_stream = Stream::open_bi(&self.quic_connection) .await .ok_or(DriverError::NotConnected)?; @@ -209,6 +251,10 @@ impl Driver { session_id: SessionId, payload: &[u8], ) -> Result<(), SendDatagramError> { + if self.closing() { + return Err(SendDatagramError::NotConnected); + } + let quic_datagram = Datagram::write(session_id, payload).into_quic_bytes(); match self.quic_connection.send_datagram(quic_datagram) { @@ -266,6 +312,7 @@ mod worker { remote_qpack_enc_stream: RemoteQPackEncStream, remote_qpack_dec_stream: RemoteQPackDecStream, connect_stream: ConnectStream, + close_requests: mpsc::UnboundedReceiver<(u32, String)>, } impl Worker { @@ -277,6 +324,7 @@ mod worker { ready_bi_wt_streams: mpsc::Sender, ready_datagrams: mpsc::Sender, driver_result: SharedResultSet, + close_requests: mpsc::UnboundedReceiver<(u32, String)>, ) -> Self { Self { quic_connection, @@ -291,21 +339,32 @@ mod worker { remote_qpack_enc_stream: RemoteQPackEncStream::empty(), remote_qpack_dec_stream: RemoteQPackDecStream::empty(), connect_stream: ConnectStream::empty(), + close_requests, } } pub async fn run(mut self) { debug!("Started"); - let error = self + let mut error = self .run_impl() .await .expect_err("Worker must return an error"); + // A close asked for as the session's last handle was dropped: the loop can see + // the handles gone first. The close still goes out. + if matches!(error, DriverError::NotConnected) + && self.quic_connection.close_reason().is_none() + { + if let Ok((error_code, reason)) = self.close_requests.try_recv() { + error = self.close_session(error_code, &reason).await; + } + } + debug!("Ended with error: {:?}", error); match &error { - DriverError::ApplicationClosed(_) => { + DriverError::ApplicationClosed(_) | DriverError::LocallyClosed => { // Termination procedure // TODO Reset send sides of all streams with SessionGone error self.quic_connection @@ -382,6 +441,10 @@ mod worker { return Err(error); } + Some((error_code, reason)) = self.close_requests.recv() => { + return Err(self.close_session(error_code, &reason).await); + } + () = self.driver_result.closed() => { return Err(DriverError::NotConnected); } @@ -389,6 +452,28 @@ mod worker { } } + /// Closes the session from this end (draft-ietf-webtrans-http3-16 §6): WT_CLOSE_SESSION + /// and a FIN on the CONNECT stream, then a wait of at most [`SESSION_CLOSE_LINGER`] + /// for the peer to close its side. The session is over as soon as the capsule is + /// queued; [`run`](Self::run) closes the connection after the wait. + async fn close_session(&mut self, error_code: u32, reason: &str) -> DriverError { + // `register_session` hands the CONNECT stream over before the session's + // `Connection` exists, but this loop may not have taken it yet. + if self.connect_stream.is_empty() { + let registered = + tokio::time::timeout(SESSION_CLOSE_LINGER, self.ready_sessions.recv()).await; + if let Ok(Some(stream)) = registered { + self.connect_stream.set_stream(stream); + } + } + + self.driver_result.set(DriverError::LocallyClosed); + self.connect_stream + .close(error_code, reason, SESSION_CLOSE_LINGER) + .await; + DriverError::LocallyClosed + } + async fn open_and_send_settings(&mut self) -> Result<(), DriverError> { assert!(self.local_settings_stream.is_empty()); diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/connect.rs b/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/connect.rs index bce6db105..520c2e280 100644 --- a/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/connect.rs +++ b/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/connect.rs @@ -1,9 +1,12 @@ +use std::time::Duration; use tracing::debug; +use wtransport_proto::bytes::BytesWriter; use wtransport_proto::bytes::IoReadError; use wtransport_proto::capsule::capsules; use wtransport_proto::capsule::Capsule; use wtransport_proto::capsule::{self}; use wtransport_proto::error::ErrorCode; +use wtransport_proto::frame::Frame; use wtransport_proto::frame::FrameKind; use wtransport_proto::varint::VarInt; @@ -30,6 +33,29 @@ impl ConnectStream { self.stream = Some(stream); } + /// Closes the session from this end (draft-ietf-webtrans-http3-16 §6): one DATA frame + /// carrying a WT_CLOSE_SESSION capsule, then a FIN, on the CONNECT stream, ahead of + /// anything else queued on the connection. Then it waits, at most `linger`, for the + /// peer to close its side of the CONNECT stream, as it must on receiving the capsule: + /// the connection's close, which comes after, cannot then overtake the capsule. + pub async fn close(&mut self, error_code: u32, reason: &str, linger: Duration) { + let Some(mut stream) = self.stream.take() else { + return; + }; + let frame = Frame::new_data(close_session_capsule(error_code, reason).into()); + + let _ = tokio::time::timeout(linger, async move { + stream.set_priority(i32::MAX); + if stream.write_frame(frame).await.is_err() { + return; + } + stream.finish().await; + // Until the peer's FIN or reset, or the connection's end. + while stream.read_frame().await.is_ok() {} + }) + .await; + } + pub async fn run(&mut self) -> DriverError { let stream = match self.stream.as_mut() { Some(stream) => stream, @@ -103,3 +129,102 @@ impl ConnectStream { } } } + +/// The longest Application Error Message a WT_CLOSE_SESSION capsule may carry, in bytes +/// (draft-ietf-webtrans-http3-16 §6). +pub const MAX_CLOSE_REASON: usize = 1024; + +/// The payload of the DATA frame that carries a WT_CLOSE_SESSION capsule +/// (draft-ietf-webtrans-http3-16 §6, in the capsule format of RFC 9297 §3.2): +/// +/// ```text +/// WT_CLOSE_SESSION Capsule { +/// Type (i) = WT_CLOSE_SESSION (0x2843), +/// Length (i), +/// Application Error Code (32), +/// Application Error Message (..8192), +/// } +/// ``` +/// +/// A reason longer than [`MAX_CLOSE_REASON`] bytes is cut at the last UTF-8 character +/// boundary within them, as the draft requires of a sender that truncates. +pub fn close_session_capsule(error_code: u32, reason: &str) -> Vec { + let mut end = reason.len().min(MAX_CLOSE_REASON); + while !reason.is_char_boundary(end) { + end -= 1; + } + let reason = &reason.as_bytes()[..end]; + + let mut capsule = Vec::with_capacity(2 + 2 + 4 + reason.len()); + let length = VarInt::from_u32(4 + reason.len() as u32); + capsule + .put_varint(VarInt::from_u32(0x2843)) + .and_then(|()| capsule.put_varint(length)) + .and_then(|()| capsule.put_bytes(&error_code.to_be_bytes())) + .and_then(|()| capsule.put_bytes(reason)) + .expect("a Vec grows to fit"); + capsule +} + +#[cfg(test)] +mod tests { + use super::*; + use wtransport_proto::capsule::Capsule; + + /// The frame's bytes as they go on the CONNECT stream. + fn frame_bytes(error_code: u32, reason: &str) -> Vec { + let mut out = Vec::new(); + Frame::new_data(close_session_capsule(error_code, reason).into()) + .write(&mut out) + .unwrap(); + out + } + + #[test] + fn the_capsule_is_laid_out_as_the_draft_gives() { + // DATA (0x00) of 18 bytes: WT_CLOSE_SESSION, 0x2843 as a two-byte varint (0x68 0x43); + // its length, 4 + 11; the code, 32 bits big-endian; the message. + let mut want = vec![0x00, 0x12, 0x68, 0x43, 0x0f, 0x00, 0x00, 0x00, 0x02]; + want.extend_from_slice(b"pop_missing"); + assert_eq!(frame_bytes(2, "pop_missing"), want); + + // Code 0 and no message: the capsule is the type, a length of 4, and the code. + assert_eq!( + frame_bytes(0, ""), + [0x00, 0x07, 0x68, 0x43, 0x04, 0x00, 0x00, 0x00, 0x00] + ); + // The whole 32-bit code space. + assert_eq!( + &frame_bytes(u32::MAX, "")[5..], + [0xff, 0xff, 0xff, 0xff] + ); + } + + #[test] + fn a_long_message_is_cut_at_a_character_boundary_within_1024_bytes() { + // 1023 bytes of ASCII, then a two-byte character that would end at byte 1025. + let reason = format!("{}é tail", "a".repeat(1023)); + let capsule = close_session_capsule(7, &reason); + // The type (2 bytes), then a length of 4 + 1023 = 1027 as a two-byte varint. + assert_eq!(&capsule[..4], [0x68, 0x43, 0x44, 0x03]); + assert_eq!(&capsule[4..8], 7u32.to_be_bytes()); + assert_eq!(&capsule[8..], "a".repeat(1023).as_bytes()); + + let exact = "b".repeat(1024); + assert_eq!(close_session_capsule(7, &exact).len(), 2 + 2 + 4 + 1024); + assert_eq!(close_session_capsule(7, &"c".repeat(5000)).len(), 2 + 2 + 4 + 1024); + } + + #[test] + fn the_capsule_reads_back_as_the_code_and_message() { + let frame = Frame::new_data(close_session_capsule(0x10, "GOAWAY timeout").into()); + let capsule = Capsule::with_frame(&frame).expect("a capsule"); + assert!(matches!( + capsule.kind(), + capsule::CapsuleKind::CloseWebTransportSession + )); + let close = capsules::CloseWebTransportSession::with_capsule(&capsule).expect("a close"); + assert_eq!(close.error_code(), VarInt::from_u32(0x10)); + assert_eq!(close.reason(), "GOAWAY timeout"); + } +} diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/mod.rs b/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/mod.rs index 7382c4835..57e035667 100644 --- a/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/mod.rs +++ b/joule-code/joule-code-rs/vendor/wtransport/src/driver/streams/mod.rs @@ -555,6 +555,10 @@ pub mod session { pub fn reset(&mut self, error_code: VarInt) { let _ = self.stream.0.reset(error_code); } + + pub fn set_priority(&self, priority: i32) { + self.stream.0.set_priority(priority); + } } } diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/error.rs b/joule-code/joule-code-rs/vendor/wtransport/src/error.rs index 6be424ff8..86002fd1c 100644 --- a/joule-code/joule-code-rs/vendor/wtransport/src/error.rs +++ b/joule-code/joule-code-rs/vendor/wtransport/src/error.rs @@ -13,7 +13,14 @@ pub enum ConnectionError { #[error("connection aborted by peer: {0}")] ConnectionClosed(ConnectionClose), - /// The connection was closed by the peer (application level). + /// The peer closed the WebTransport session: with a WT_CLOSE_SESSION capsule, whose + /// application error code and reason this carries, or with a FIN on the CONNECT stream + /// alone, which reads as code 0 and no reason (draft-ietf-webtrans-http3-16 §6). + #[error("session closed by peer: {0}")] + SessionClosed(ApplicationClose), + + /// The connection was closed by the peer (application level). Under HTTP/3 the code is + /// an HTTP/3 error code, not the session's. #[error("connection closed by peer: {0}")] ApplicationClosed(ApplicationClose), @@ -47,7 +54,8 @@ impl ConnectionError { ) -> Self { match driver_error { DriverError::Proto(error_code) => Self::local_h3_error(error_code), - DriverError::ApplicationClosed(close) => Self::ApplicationClosed(close), + DriverError::ApplicationClosed(close) => Self::SessionClosed(close), + DriverError::LocallyClosed => Self::LocallyClosed, DriverError::NotConnected => Self::no_connect(quic_connection), } } -- 2.54.0 (Apple Git-157) From ce7a30b7be29224be491a68ac3e9d13f8604d4e5 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 20 Oct 2026 08:30:35 -0400 Subject: [PATCH 3/9] joule-moq-relay: close a WebTransport session with its MOQT code and reason MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WtConnection::close closes the WebTransport session (moq-transport-22 §6.6): the MOQT code is the WT_CLOSE_SESSION capsule's 32-bit application error code, unmapped, with the reason. closed() reads a peer's capsule as its code and reason (by_peer); a QUIC close with the session still open gives no MOQT code (u64::MAX), since its code is HTTP/3's. tests/webtransport_close.rs: the relay refusing a grant without its proof, refusing a session past its count, and ending one at its time limit, each read by the client as the exact code and reason; a client's close recorded as the peer's in the relay's summary. The browser e2e check of a bare grant now requires code 2 and the reason over WebTransport too. CI: the vendored crate's unit tests in the relay job; the browser and room workflows also run on a change under vendor/. README and CHANGELOG. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/joule-code-conformance.yml | 9 ++ .github/workflows/joule-moq-room.yml | 1 + .github/workflows/joule-moq-web.yml | 2 + joule-code/CHANGELOG.md | 26 +++++ .../crates/joule-moq-relay/README.md | 24 +++- .../src/transport/webtransport.rs | 31 +++++- .../tests/webtransport_close.rs | 103 ++++++++++++++++++ .../crates/joule-moq-web/e2e/room.e2e.mjs | 9 +- 8 files changed, 193 insertions(+), 12 deletions(-) create mode 100644 joule-code/joule-code-rs/crates/joule-moq-relay/tests/webtransport_close.rs diff --git a/.github/workflows/joule-code-conformance.yml b/.github/workflows/joule-code-conformance.yml index 129b18f02..aad2f6bbb 100644 --- a/.github/workflows/joule-code-conformance.yml +++ b/.github/workflows/joule-code-conformance.yml @@ -204,6 +204,15 @@ jobs: shell: bash run: cargo test --locked -p joule-moq-relay --all-targets + # The relay's WebTransport layer, wtransport 0.7.2 vendored with a session close + # (vendor/wtransport/VENDORED.md): its unit tests, the WT_CLOSE_SESSION capsule's bytes + # against the layout of draft-ietf-webtrans-http3-16 §6 among them. It is not a member + # of the workspace, so `--all-targets` above does not reach it. + - name: The vendored wtransport's unit tests + working-directory: joule-code/joule-code-rs + shell: bash + run: cargo test --locked -p wtransport --lib + # The WHIP and WHEP gateway. Every test is on loopback, against a relay that checks # every receipt at its door: a WHIP publisher (str0m as the client) of H.264 and Opus # whose frames a relay subscriber receives whole with every receipt verified, and DELETE diff --git a/.github/workflows/joule-moq-room.yml b/.github/workflows/joule-moq-room.yml index d97393977..faa3f5e13 100644 --- a/.github/workflows/joule-moq-room.yml +++ b/.github/workflows/joule-moq-room.yml @@ -23,6 +23,7 @@ on: - 'joule-code/joule-code-rs/crates/joule-moq/**' - 'joule-code/joule-code-rs/crates/joule-moq-relay/**' - 'joule-code/joule-code-rs/crates/jwp/**' + - 'joule-code/joule-code-rs/vendor/**' - 'wai/wai-rs/**' - 'wai/wai-ops/**' - '.github/workflows/joule-moq-room.yml' diff --git a/.github/workflows/joule-moq-web.yml b/.github/workflows/joule-moq-web.yml index 85f079ccb..fe48ff55c 100644 --- a/.github/workflows/joule-moq-web.yml +++ b/.github/workflows/joule-moq-web.yml @@ -43,6 +43,7 @@ on: - 'joule-code/joule-code-rs/crates/jwp/**' - 'joule-code/joule-code-rs/Cargo.toml' - 'joule-code/joule-code-rs/Cargo.lock' + - 'joule-code/joule-code-rs/vendor/**' - 'wai/wai-live-web/**' - 'wai/wai-rs/**' - 'wai/wai-ops/**' @@ -57,6 +58,7 @@ on: - 'joule-code/joule-code-rs/crates/jwp/**' - 'joule-code/joule-code-rs/Cargo.toml' - 'joule-code/joule-code-rs/Cargo.lock' + - 'joule-code/joule-code-rs/vendor/**' - 'wai/wai-live-web/**' - 'wai/wai-rs/**' - 'wai/wai-ops/**' diff --git a/joule-code/CHANGELOG.md b/joule-code/CHANGELOG.md index 9bc63fce0..8c8616c3f 100644 --- a/joule-code/CHANGELOG.md +++ b/joule-code/CHANGELOG.md @@ -18,6 +18,32 @@ All notable changes to the Joule Code standard are recorded here. The format fol - **§6 / `spec/receipt.md` §2 — a vendor counter is a hardware reading only while it is demonstrably live.** A reading from a vendor energy counter (RAPL, NVML, Apple SMC/IOReport) MAY be tagged `HwShunt`, and supply `measured_uj`, only while the counter has been observed advancing under load for the domain the figure claims (for IOReport: the CPU energy rail itself, in at least 5 sample intervals, at least 4 of every 5, over at least 300 ms of CPU burn). A counter that is present but not live — frozen, or republished in lumps — MUST NOT produce either; the reading falls back to the estimator. A figure MUST NOT claim coverage of a domain its counter does not read. No wire or conformance-vector change. +### Changed — `joule-moq-relay`: a session over WebTransport closes with its code and reason + +- **The relay closes a WebTransport session, not the QUIC connection under it** + (`draft-ietf-moq-transport-22` §6.6): a WT_CLOSE_SESSION capsule on the CONNECT stream + carries the MOQT session termination code as the session's 32-bit application error code, + unmapped, and the reason (`draft-ietf-webtrans-http3-16` §6); the QUIC connection closes + after the peer has closed its side of that stream, or two seconds on. A page now reads the + relay's code and reason over WebTransport as it does over the WebSocket binding: a grant + presented without its proof, for one, is `UNAUTHORIZED` (2) with its live-rooms §10.5 + reason, where the page saw only that the session was lost. +- **A peer's close is read the same way.** `closed()` over WebTransport gives the code and + reason of the peer's WT_CLOSE_SESSION (`by_peer: true`), so a page's close code reaches + the session's signed summary; a QUIC connection closed with the session still open gives + no MOQT code (its code is HTTP/3's). +- **`wtransport` 0.7.2 is vendored** (`joule-code-rs/vendor/wtransport`, MIT OR + Apache-2.0, patched in for the joule-code workspace and `joule-moq-room`), because the + release reads WT_CLOSE_SESSION but cannot send it. The change gives it + `Connection::close_session` and `ConnectionError::SessionClosed`, and makes `closed()` + report the session's end; `VENDORED.md` lists every change and why. Its unit tests, the + capsule's bytes against the draft's layout among them, run in the relay's CI job. +- **Tests.** `tests/webtransport_close.rs`: the relay refusing a grant without its proof, + refusing a session past its session count, and ending one at its time limit, each read + by the client as the exact code and reason; and a client's own close, recorded as the + peer's in the relay's summary. The browser end-to-end check of a bare grant now requires + code 2 and the reason over WebTransport too. + ### Added — the public room (L5c-2): `joule-moq-room`, room authorities at the relay - **`joule-moq-room`**, a room service (a workspace of its own, on `wai-rs`'s room, event diff --git a/joule-code/joule-code-rs/crates/joule-moq-relay/README.md b/joule-code/joule-code-rs/crates/joule-moq-relay/README.md index e2a39eb0a..66688a371 100644 --- a/joule-code/joule-code-rs/crates/joule-moq-relay/README.md +++ b/joule-code/joule-code-rs/crates/joule-moq-relay/README.md @@ -39,9 +39,9 @@ a room grant signs (below, *Tokens and room grants*). Native QUIC and WebTransport share one quinn endpoint: the relay reads the ALPN of each connection and either runs MOQT on it directly or hands it to the WebTransport layer. -**The WebTransport implementation** is `wtransport` 0.7 (MIT OR Apache-2.0). It was -chosen because it runs on the same quinn 0.11 as `joule-moq`, so one UDP endpoint serves -both ALPNs; it gives the server the CONNECT request's headers and lets it answer with its +**The WebTransport implementation** is `wtransport` 0.7.2 (MIT OR Apache-2.0), vendored +(below). It was chosen because it runs on the same quinn 0.11 as `joule-moq`, so one UDP +endpoint serves both ALPNs; it gives the server the CONNECT request's headers and lets it answer with its own, which is what `WT-Available-Protocols` and `WT-Protocol` need; and its client checks a certificate by hash under the rules a browser applies to `serverCertificateHashes` (ECDSA P-256, a validity of fourteen days at most), so the relay's tests connect the way a @@ -49,6 +49,24 @@ page does. Stream bytes are read and written on the quinn streams underneath; ap error codes on WebTransport streams are mapped into the HTTP/3 error space as `draft-ietf-webtrans-http3-16` §4.4 gives. +**Closing a WebTransport session.** A MOQT session over WebTransport is closed by closing +the WebTransport session (`draft-ietf-moq-transport-22` §6.6). The relay sends a +`WT_CLOSE_SESSION` capsule (`0x2843`) on the session's CONNECT stream, then a FIN +(`draft-ietf-webtrans-http3-16` §6): the MOQT session termination code (§12.2) is the +capsule's 32-bit application error code as it stands, not mapped into the HTTP/3 space as +stream codes are, and the reason, at most 1,024 bytes of UTF-8, is its message. The QUIC +connection is closed with `H3_NO_ERROR` once the peer has closed its side of the CONNECT +stream, or two seconds after the capsule, so the connection's close cannot overtake it. A +page reads the code and reason from `WebTransport.closed` (`closeCode`, `reason`), as it +reads them from the WebSocket binding's `WT_CLOSE_SESSION`. A session the client closes is +read the same way: the relay's `closed()` gives the client's code and reason, and the +session's summary records the code as the peer's. A QUIC connection that closes with the +session still open gives no MOQT code; its code is an HTTP/3 error code. + +The release of `wtransport` reads `WT_CLOSE_SESSION` but cannot send it, so the joule-code +workspace vendors 0.7.2 with a session close added (`vendor/wtransport`, patched in with +`[patch.crates-io]`; `vendor/wtransport/VENDORED.md` lists what changed and why). + ## How a browser connects **WebTransport, in development.** Start the relay with `--dev` (or with no certificate diff --git a/joule-code/joule-code-rs/crates/joule-moq-relay/src/transport/webtransport.rs b/joule-code/joule-code-rs/crates/joule-moq-relay/src/transport/webtransport.rs index 926947add..cb19fa85d 100644 --- a/joule-code/joule-code-rs/crates/joule-moq-relay/src/transport/webtransport.rs +++ b/joule-code/joule-code-rs/crates/joule-moq-relay/src/transport/webtransport.rs @@ -10,12 +10,19 @@ //! bytes are read and written on the quinn stream. Application error codes on WebTransport //! streams are 32-bit and ride RESET_STREAM and STOP_SENDING mapped into the HTTP/3 error //! space (webtrans-http3 §4.4); [`wt_to_h3`] and [`h3_to_wt`] are that mapping. +//! +//! The session closes by closing the WebTransport session (moq-transport-22 §6.6): a +//! WT_CLOSE_SESSION capsule on the CONNECT stream carries the MOQT session termination code +//! as the session's 32-bit application error code, unmapped, and the reason +//! (webtrans-http3 §6). The vendored wtransport sends it (`vendor/wtransport/VENDORED.md`), +//! and reports the one a peer sends, so a browser reads the relay's code and reason as it +//! does over the WebSocket binding. The QUIC connection closes after; its code is an HTTP/3 +//! error code, not the session's. use super::quic::{conn_err, READ_CHUNK}; use super::{BoxRecv, BoxSend, Connection, RecvStream, SendStream, TResult, TransportError, TransportKind}; use async_trait::async_trait; use bytes::Bytes; -use wtransport::VarInt; /// The first HTTP/3 error code of the WebTransport application range. const WT_FIRST: u64 = 0x52e4_a40f_a8db; @@ -85,11 +92,25 @@ impl WtConnection { fn wt_conn_err(e: &wtransport::error::ConnectionError) -> TransportError { use wtransport::error::ConnectionError as E; match e { - E::ApplicationClosed(c) => TransportError::Closed { + // The peer closed the session (WT_CLOSE_SESSION, or a FIN on the CONNECT stream, + // which reads as code 0): the code is the MOQT session's. + E::SessionClosed(c) => TransportError::Closed { code: c.code().into_inner(), reason: String::from_utf8_lossy(c.reason()).into_owned(), by_peer: true, }, + // The connection closed with the session still open: an HTTP/3 error code, which + // says nothing of MOQT's. + E::ApplicationClosed(c) => TransportError::Closed { + code: u64::MAX, + reason: format!( + "the connection closed without closing the session (HTTP/3 error {:#x}){}{}", + c.code().into_inner(), + if c.reason().is_empty() { "" } else { ": " }, + String::from_utf8_lossy(c.reason()) + ), + by_peer: true, + }, E::LocallyClosed => TransportError::Closed { code: 0, reason: "closed by this endpoint".into(), by_peer: false }, other => TransportError::Closed { code: u64::MAX, reason: other.to_string(), by_peer: true }, } @@ -179,9 +200,11 @@ impl Connection for WtConnection { let d = self.conn.receive_datagram().await.map_err(|e| wt_conn_err(&e))?; Ok(d.payload()) } + /// WT_CLOSE_SESSION with the MOQT code as the session's application error code (a code + /// past 32 bits as `u32::MAX`) and the reason, then the connection's close (module docs). fn close(&self, code: u64, reason: &[u8]) { - let code = VarInt::try_from_u64(code).unwrap_or(VarInt::MAX); - self.conn.close(code, reason); + let code = u32::try_from(code).unwrap_or(u32::MAX); + self.conn.close_session(code, &String::from_utf8_lossy(reason)); } async fn closed(&self) -> TransportError { wt_conn_err(&self.conn.closed().await) diff --git a/joule-code/joule-code-rs/crates/joule-moq-relay/tests/webtransport_close.rs b/joule-code/joule-code-rs/crates/joule-moq-relay/tests/webtransport_close.rs new file mode 100644 index 000000000..f7a6a7b23 --- /dev/null +++ b/joule-code/joule-code-rs/crates/joule-moq-relay/tests/webtransport_close.rs @@ -0,0 +1,103 @@ +//! A session over WebTransport is closed by closing the WebTransport session +//! (`draft-ietf-moq-transport-22` §6.6): the MOQT session termination code and the reason +//! ride a WT_CLOSE_SESSION capsule on the CONNECT stream (`draft-ietf-webtrans-http3-16` +//! §6), and the other end reads both exactly, as it does over the WebSocket binding. A QUIC +//! connection close alone carries an HTTP/3 error code, which no end reads as MOQT's. +//! +//! The relay closes a session three ways here: refusing a room grant presented without its +//! proof, after SETUP; refusing a session past its session count, before any MOQT message; +//! and ending an established session at its time limit. And a client closes one, whose code +//! the relay's signed summary records as the peer's. + +mod common; + +use common::*; +use ed25519_dalek::SigningKey; +use joule_moq::draft22::{SessionError, Token}; +use joule_moq_relay::client::ClientOptions; +use joule_moq_relay::config::Grant; +use joule_moq_relay::transport::{webtransport, Connection, TransportError}; +use joule_moq_relay::RelayServer; +use std::time::Duration; +use tokio::time::timeout; + +/// A record shaped as a room grant (`WRG1`, version 1, a room, the principal), which is +/// all the relay reads of it: its rule names the record by its SHA-256. +fn grant_record(room: u8, principal: &SigningKey) -> Vec { + let mut g = b"WRG1\x01".to_vec(); + g.extend_from_slice(&[room; 32]); + g.extend_from_slice(&principal.verifying_key().to_bytes()); + g.extend_from_slice(&[0x5a; 160]); + g.extend_from_slice(&[0x77; 64]); + g +} + +async fn closed(conn: &dyn Connection, what: &str) -> TransportError { + timeout(STEP, conn.closed()).await.unwrap_or_else(|_| panic!("{what}: not closed in time")) +} + +fn by_relay(code: SessionError, reason: &str) -> TransportError { + TransportError::Closed { code: code.0, reason: reason.into(), by_peer: true } +} + +/// A WebTransport session to `server` with no MOQT on it yet. +async fn session(server: &RelayServer) -> webtransport::WtConnection { + let url = format!("https://127.0.0.1:{}{}", server.quic_addr.unwrap().port(), server.relay().config().path); + timeout(STEP, webtransport::connect(&url, Some(server.identity.leaf_sha256), loopback())) + .await + .expect("in time") + .expect("a WebTransport session") +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn a_session_over_webtransport_closes_with_its_code_and_reason() { + timeout(Duration::from_secs(120), run()).await.expect("the test finishes in time"); +} + +async fn run() { + // A room grant presented as a bare token: the relay refuses the session after SETUP, + // UNAUTHORIZED (WAI live-rooms §10.5.2). + let host = SigningKey::from_bytes(&[0x51; 32]); + let record = grant_record(3, &host); + let hash = hex::encode(joule_moq_relay::auth::sha256(&record)); + let rooms = relay(|c| { + c.auth.required = true; + c.auth.grants = vec![Grant { prefix: vec!["room".into()], token_sha256: hash, publish: true, subscribe: true, proof: true, unlimited: false }]; + }) + .await; + let bare = ClientOptions { tokens: vec![Token::UseValue { token_type: 0, value: record }], ..ClientOptions::default() }; + let (c, _) = connect(&rooms, Via::WebTransport, bare).await; + assert_eq!(closed(c.conn.as_ref(), "a grant with no proof").await, by_relay(SessionError::UNAUTHORIZED, "live-rooms §10.5: pop_missing")); + + // A client closes its session with a code of its own: the relay reads it as the peer's, + // and its summary records it; the client's own end reads its close as local. + let (c, _) = connect(&rooms, Via::WebTransport, ClientOptions::default()).await; + c.close(SessionError::PROTOCOL_VIOLATION.0, "a client's own close"); + assert_eq!( + closed(c.conn.as_ref(), "the client's close").await, + TransportError::Closed { code: 0, reason: "closed by this endpoint".into(), by_peer: false } + ); + let recorded = until(STEP, || { + rooms.relay().summaries().iter().any(|s| s.summary.transport == "webtransport" && s.summary.close_code == Some(SessionError::PROTOCOL_VIOLATION.0)) + }) + .await; + let codes: Vec<_> = rooms.relay().summaries().iter().map(|s| (s.summary.transport.clone(), s.summary.close_code)).collect(); + assert!(recorded, "the relay records the client's code: {codes:?}"); + rooms.shutdown().await; + + // A relay that holds one session at a time, each for three seconds at most. + let limited = relay(|c| { + c.limits.max_sessions = 1; + c.limits.max_session_ms = 3_000; + }) + .await; + let (held, _) = connect(&limited, Via::WebTransport, ClientOptions::default()).await; + assert!(until(STEP, || limited.relay().session_diagnostics().len() == 1).await, "the first session is held"); + // Past its session count: refused before any MOQT message, INTERNAL_ERROR. + let refused = session(&limited).await; + assert_eq!(closed(&refused, "a session past the count").await, by_relay(SessionError::INTERNAL_ERROR, "the relay is at its session limit")); + // At its time limit, an established session is ended with NO_ERROR and the reason, which + // a FIN on the CONNECT stream alone (code 0, no reason) would not carry. + assert_eq!(closed(held.conn.as_ref(), "a session at its time limit").await, by_relay(SessionError::NO_ERROR, "the relay's session limit of 3 s")); + limited.shutdown().await; +} diff --git a/joule-code/joule-code-rs/crates/joule-moq-web/e2e/room.e2e.mjs b/joule-code/joule-code-rs/crates/joule-moq-web/e2e/room.e2e.mjs index 7e75a27f1..ba828cccb 100644 --- a/joule-code/joule-code-rs/crates/joule-moq-web/e2e/room.e2e.mjs +++ b/joule-code/joule-code-rs/crates/joule-moq-web/e2e/room.e2e.mjs @@ -295,13 +295,12 @@ async function scenarioRoom(run, transport) { fs.writeFileSync(path.join(dir, "receipts-sizes.json"), JSON.stringify(sv, null, 1)); // A grant presented as a bare token, with no proof of its key: the relay refuses the - // session (live-rooms §10.5.2), UNAUTHORIZED. Over the WebSocket binding the page reads - // the code (2) and the reason; over WebTransport the relay's transport closes the QUIC - // connection rather than the WebTransport session, so the page sees the session lost, - // and the relay's log gives the reason. + // session (live-rooms §10.5.2), UNAUTHORIZED. The page reads the code (2) and the reason + // over either transport: over WebTransport the relay closes the WebTransport session + // with them (WT_CLOSE_SESSION, moq-transport-22 §6.6), as over the WebSocket binding. const bare = await alice.evaluate(() => window.roomTest.tryBareGrant()); const refusals = relay.lines.filter((l) => l.includes("without a proof that passes")); - const coded = bare.transport === "websocket" ? !!bare.closed && bare.closed.code === 2 : !!bare.closed; + const coded = !!bare.closed && bare.closed.code === 2 && /pop_missing/.test(bare.closed.reason); check(`${label}: a grant presented without its proof is refused the session`, coded && refusals.some((l) => l.includes("pop_missing")), `${JSON.stringify(bare).slice(0, 200)}; relay: ${(refusals[0] || "").replace(/^.*INFO /, "").slice(0, 200)}`); check(`${label}: the relay refused that session and no other`, refusals.length === 1, `${refusals.length} refused`); -- 2.54.0 (Apple Git-157) From 020dd7d8530b5feee17553d27c80f3d02798b3da Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 20 Oct 2026 08:31:03 -0400 Subject: [PATCH 4/9] joule-code CHANGELOG: the WebTransport close entry at the top, above DVR's Co-Authored-By: Claude Opus 5.5 --- joule-code/CHANGELOG.md | 52 ++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/joule-code/CHANGELOG.md b/joule-code/CHANGELOG.md index f3a616442..d5181e8a9 100644 --- a/joule-code/CHANGELOG.md +++ b/joule-code/CHANGELOG.md @@ -4,6 +4,32 @@ All notable changes to the Joule Code standard are recorded here. The format fol ## [Unreleased] +### Changed — `joule-moq-relay`: a session over WebTransport closes with its code and reason + +- **The relay closes a WebTransport session, not the QUIC connection under it** + (`draft-ietf-moq-transport-22` §6.6): a WT_CLOSE_SESSION capsule on the CONNECT stream + carries the MOQT session termination code as the session's 32-bit application error code, + unmapped, and the reason (`draft-ietf-webtrans-http3-16` §6); the QUIC connection closes + after the peer has closed its side of that stream, or two seconds on. A page now reads the + relay's code and reason over WebTransport as it does over the WebSocket binding: a grant + presented without its proof, for one, is `UNAUTHORIZED` (2) with its live-rooms §10.5 + reason, where the page saw only that the session was lost. +- **A peer's close is read the same way.** `closed()` over WebTransport gives the code and + reason of the peer's WT_CLOSE_SESSION (`by_peer: true`), so a page's close code reaches + the session's signed summary; a QUIC connection closed with the session still open gives + no MOQT code (its code is HTTP/3's). +- **`wtransport` 0.7.2 is vendored** (`joule-code-rs/vendor/wtransport`, MIT OR + Apache-2.0, patched in for the joule-code workspace and `joule-moq-room`), because the + release reads WT_CLOSE_SESSION but cannot send it. The change gives it + `Connection::close_session` and `ConnectionError::SessionClosed`, and makes `closed()` + report the session's end; `VENDORED.md` lists every change and why. Its unit tests, the + capsule's bytes against the draft's layout among them, run in the relay's CI job. +- **Tests.** `tests/webtransport_close.rs`: the relay refusing a grant without its proof, + refusing a session past its session count, and ending one at its time limit, each read + by the client as the exact code and reason; and a client's own close, recorded as the + peer's in the relay's summary. The browser end-to-end check of a bare grant now requires + code 2 and the reason over WebTransport too. + ### Added — DVR on the relay (L7b): `joule-moq-relay`'s DVR store, the seek power, the room page's Seek back - **`joule-moq-relay` keeps a DVR window on disk** (`[dvr]`, `[[dvr.window]]`; WAI @@ -59,32 +85,6 @@ All notable changes to the Joule Code standard are recorded here. The format fol - **§6 / `spec/receipt.md` §2 — a vendor counter is a hardware reading only while it is demonstrably live.** A reading from a vendor energy counter (RAPL, NVML, Apple SMC/IOReport) MAY be tagged `HwShunt`, and supply `measured_uj`, only while the counter has been observed advancing under load for the domain the figure claims (for IOReport: the CPU energy rail itself, in at least 5 sample intervals, at least 4 of every 5, over at least 300 ms of CPU burn). A counter that is present but not live — frozen, or republished in lumps — MUST NOT produce either; the reading falls back to the estimator. A figure MUST NOT claim coverage of a domain its counter does not read. No wire or conformance-vector change. -### Changed — `joule-moq-relay`: a session over WebTransport closes with its code and reason - -- **The relay closes a WebTransport session, not the QUIC connection under it** - (`draft-ietf-moq-transport-22` §6.6): a WT_CLOSE_SESSION capsule on the CONNECT stream - carries the MOQT session termination code as the session's 32-bit application error code, - unmapped, and the reason (`draft-ietf-webtrans-http3-16` §6); the QUIC connection closes - after the peer has closed its side of that stream, or two seconds on. A page now reads the - relay's code and reason over WebTransport as it does over the WebSocket binding: a grant - presented without its proof, for one, is `UNAUTHORIZED` (2) with its live-rooms §10.5 - reason, where the page saw only that the session was lost. -- **A peer's close is read the same way.** `closed()` over WebTransport gives the code and - reason of the peer's WT_CLOSE_SESSION (`by_peer: true`), so a page's close code reaches - the session's signed summary; a QUIC connection closed with the session still open gives - no MOQT code (its code is HTTP/3's). -- **`wtransport` 0.7.2 is vendored** (`joule-code-rs/vendor/wtransport`, MIT OR - Apache-2.0, patched in for the joule-code workspace and `joule-moq-room`), because the - release reads WT_CLOSE_SESSION but cannot send it. The change gives it - `Connection::close_session` and `ConnectionError::SessionClosed`, and makes `closed()` - report the session's end; `VENDORED.md` lists every change and why. Its unit tests, the - capsule's bytes against the draft's layout among them, run in the relay's CI job. -- **Tests.** `tests/webtransport_close.rs`: the relay refusing a grant without its proof, - refusing a session past its session count, and ending one at its time limit, each read - by the client as the exact code and reason; and a client's own close, recorded as the - peer's in the relay's summary. The browser end-to-end check of a bare grant now requires - code 2 and the reason over WebTransport too. - ### Added — the public room (L5c-2): `joule-moq-room`, room authorities at the relay - **`joule-moq-room`**, a room service (a workspace of its own, on `wai-rs`'s room, event -- 2.54.0 (Apple Git-157) From c357d5d123358a5e043b71cec85116d7542b52bc Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 20 Oct 2026 08:32:05 -0400 Subject: [PATCH 5/9] vendored wtransport: the close_session doc says when the session's result is set Co-Authored-By: Claude Opus 5.5 --- joule-code/joule-code-rs/vendor/wtransport/src/driver/mod.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/joule-code/joule-code-rs/vendor/wtransport/src/driver/mod.rs b/joule-code/joule-code-rs/vendor/wtransport/src/driver/mod.rs index ff05e4fe5..0c16359ee 100644 --- a/joule-code/joule-code-rs/vendor/wtransport/src/driver/mod.rs +++ b/joule-code/joule-code-rs/vendor/wtransport/src/driver/mod.rs @@ -454,8 +454,9 @@ mod worker { /// Closes the session from this end (draft-ietf-webtrans-http3-16 §6): WT_CLOSE_SESSION /// and a FIN on the CONNECT stream, then a wait of at most [`SESSION_CLOSE_LINGER`] - /// for the peer to close its side. The session is over as soon as the capsule is - /// queued; [`run`](Self::run) closes the connection after the wait. + /// for the peer to close its side. The session is over from here: its result is set + /// before the capsule is written; [`run`](Self::run) closes the connection after the + /// wait. async fn close_session(&mut self, error_code: u32, reason: &str) -> DriverError { // `register_session` hands the CONNECT stream over before the session's // `Connection` exists, but this loop may not have taken it yet. -- 2.54.0 (Apple Git-157) From b3497d2c04eccc67a499484f78f39be3a86d5bf3 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 20 Oct 2026 08:34:21 -0400 Subject: [PATCH 6/9] joule-moq-relay tests/webtransport_close.rs: a grant rule's seek field, after the DVR merge Co-Authored-By: Claude Opus 5.5 --- .../crates/joule-moq-relay/tests/webtransport_close.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/joule-code/joule-code-rs/crates/joule-moq-relay/tests/webtransport_close.rs b/joule-code/joule-code-rs/crates/joule-moq-relay/tests/webtransport_close.rs index f7a6a7b23..b6d797b9d 100644 --- a/joule-code/joule-code-rs/crates/joule-moq-relay/tests/webtransport_close.rs +++ b/joule-code/joule-code-rs/crates/joule-moq-relay/tests/webtransport_close.rs @@ -62,7 +62,7 @@ async fn run() { let hash = hex::encode(joule_moq_relay::auth::sha256(&record)); let rooms = relay(|c| { c.auth.required = true; - c.auth.grants = vec![Grant { prefix: vec!["room".into()], token_sha256: hash, publish: true, subscribe: true, proof: true, unlimited: false }]; + c.auth.grants = vec![Grant { prefix: vec!["room".into()], token_sha256: hash, publish: true, subscribe: true, proof: true, unlimited: false, seek: false }]; }) .await; let bare = ClientOptions { tokens: vec![Token::UseValue { token_type: 0, value: record }], ..ClientOptions::default() }; -- 2.54.0 (Apple Git-157) From d6abac7c4be029d685f156f28baa77cfc94309ca Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 20 Oct 2026 13:21:33 -0400 Subject: [PATCH 7/9] joule-moq-relay: hold a closing WebTransport session's streams until its connection closes A stream dropped once the relay has asked to close a WebTransport session would send its FIN or STOP_SENDING at once, ahead of the WT_CLOSE_SESSION capsule. In a browser on the build host, a page whose stream was stopped just before the close lost its renderer (reproduced on loopback: a page writing a stream the relay refused crashed in most runs). The binding now keeps such streams (WtSend, WtRecv hold the connection's Closing) until the QUIC connection has closed, at most ten seconds, when they end with nothing more on the wire; read_setup keeps the stream it refuses in `early`, so it too is dropped after the close. tests/webtransport_close.rs: a stream that breaks a rule before SETUP is refused with PROTOCOL_VIOLATION, and the client reads the close and no STOP_SENDING ahead of it. Co-Authored-By: Claude Opus 5.5 --- .../crates/joule-moq-relay/src/session.rs | 33 ++++-- .../src/transport/webtransport.rs | 112 +++++++++++++++--- .../tests/webtransport_close.rs | 24 +++- 3 files changed, 138 insertions(+), 31 deletions(-) diff --git a/joule-code/joule-code-rs/crates/joule-moq-relay/src/session.rs b/joule-code/joule-code-rs/crates/joule-moq-relay/src/session.rs index 9256bb804..815910865 100644 --- a/joule-code/joule-code-rs/crates/joule-moq-relay/src/session.rs +++ b/joule-code/joule-code-rs/crates/joule-moq-relay/src/session.rs @@ -120,24 +120,33 @@ pub fn fetch_range(f: Option, largest: Location) -> Option<(Loca // ── The session ──────────────────────────────────────────────────────────── +/// The client's SETUP, from its control stream; data streams that come first wait in +/// `early`. A stream that breaks a rule is put there too, so that it ends after the +/// session's close, not before it (`transport::webtransport` holds what is dropped then). async fn read_setup(conn: &SharedConnection, max_payload: u64, early: &mut Vec) -> Result<(Setup, UniReader), String> { loop { let recv = conn.accept_uni().await.map_err(|e| e.to_string())?; let mut r = UniReader::new(recv, max_payload); - match r.start().await.map_err(|e| e.to_string())? { - StreamHeader::Control => { - return match r.next().await.map_err(|e| e.to_string())? { - Some(UniEvent::Control(ControlMessage::Setup(s))) => Ok((s, r)), - _ => Err("the control stream does not begin with SETUP".into()), - }; - } - _ => { - if early.len() >= EARLY_STREAMS { - return Err("too many data streams before SETUP".into()); - } + let first = match r.start().await.map_err(|e| e.to_string()) { + Ok(StreamHeader::Control) => r.next().await.map_err(|e| e.to_string()), + Ok(_) if early.len() < EARLY_STREAMS => { early.push(r); + continue; } - } + Ok(_) => Err("too many data streams before SETUP".into()), + Err(e) => Err(e), + }; + return match first { + Ok(Some(UniEvent::Control(ControlMessage::Setup(s)))) => Ok((s, r)), + Ok(_) => { + early.push(r); + Err("the control stream does not begin with SETUP".into()) + } + Err(e) => { + early.push(r); + Err(e) + } + }; } } diff --git a/joule-code/joule-code-rs/crates/joule-moq-relay/src/transport/webtransport.rs b/joule-code/joule-code-rs/crates/joule-moq-relay/src/transport/webtransport.rs index cb19fa85d..c6a6ace25 100644 --- a/joule-code/joule-code-rs/crates/joule-moq-relay/src/transport/webtransport.rs +++ b/joule-code/joule-code-rs/crates/joule-moq-relay/src/transport/webtransport.rs @@ -18,11 +18,24 @@ //! and reports the one a peer sends, so a browser reads the relay's code and reason as it //! does over the WebSocket binding. The QUIC connection closes after; its code is an HTTP/3 //! error code, not the session's. +//! +//! The capsule must reach the peer before any of the session's streams ends: §6 ends them +//! after the session, and a page whose stream was stopped just ahead of the close was seen +//! to lose its renderer. A stream dropped once the session is closing would send its FIN or +//! STOP_SENDING at once, so it is held ([`Closing`]) until the QUIC connection has closed, +//! when it ends with nothing more on the wire. use super::quic::{conn_err, READ_CHUNK}; use super::{BoxRecv, BoxSend, Connection, RecvStream, SendStream, TResult, TransportError, TransportKind}; use async_trait::async_trait; use bytes::Bytes; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +/// How long the streams of a closed session are held at most: past the vendored driver's +/// wait for the peer (two seconds) with room to spare. +const HOLD: Duration = Duration::from_secs(10); /// The first HTTP/3 error code of the WebTransport application range. const WT_FIRST: u64 = 0x52e4_a40f_a8db; @@ -75,11 +88,34 @@ pub fn select_protocol(header: Option<&str>) -> Option<&'static str> { pub struct WtConnection { conn: wtransport::Connection, target: Option, + closing: Arc, +} + +/// The streams dropped while a session this end closed is closing, held until its QUIC +/// connection has closed (module docs). +#[derive(Default)] +struct Closing { + holding: AtomicBool, + held: Mutex>, +} + +enum Held { + Send(wtransport::SendStream), + Recv(wtransport::RecvStream), +} + +impl Closing { + /// Keep `h` while the session is closing; otherwise it is dropped here, as usual. + fn drop_or_hold(&self, h: Held) { + if self.holding.load(Ordering::Acquire) { + self.held.lock().unwrap().push(h); + } + } } impl WtConnection { pub fn new(conn: wtransport::Connection) -> Self { - WtConnection { conn, target: None } + WtConnection { conn, target: None, closing: Arc::default() } } /// The `//authority/path` of the CONNECT that opened the session. @@ -116,13 +152,47 @@ fn wt_conn_err(e: &wtransport::error::ConnectionError) -> TransportError { } } -pub struct WtSend(wtransport::SendStream); -pub struct WtRecv(wtransport::RecvStream); +pub struct WtSend(Option, Arc); +pub struct WtRecv(Option, Arc); + +impl WtSend { + fn new(s: wtransport::SendStream, closing: &Arc) -> Self { + WtSend(Some(s), closing.clone()) + } + fn quic(&mut self) -> &mut quinn::SendStream { + self.0.as_mut().expect("held until dropped").quic_stream_mut() + } +} + +impl WtRecv { + fn new(r: wtransport::RecvStream, closing: &Arc) -> Self { + WtRecv(Some(r), closing.clone()) + } + fn quic(&mut self) -> &mut quinn::RecvStream { + self.0.as_mut().expect("held until dropped").quic_stream_mut() + } +} + +impl Drop for WtSend { + fn drop(&mut self) { + if let Some(s) = self.0.take() { + self.1.drop_or_hold(Held::Send(s)); + } + } +} + +impl Drop for WtRecv { + fn drop(&mut self) { + if let Some(r) = self.0.take() { + self.1.drop_or_hold(Held::Recv(r)); + } + } +} #[async_trait] impl SendStream for WtSend { async fn write_all(&mut self, buf: &[u8]) -> TResult<()> { - match self.0.quic_stream_mut().write_all(buf).await { + match self.quic().write_all(buf).await { Ok(()) => Ok(()), Err(quinn::WriteError::Stopped(code)) => Err(TransportError::Stopped(h3_to_moqt(code.into_inner()))), Err(quinn::WriteError::ConnectionLost(c)) => Err(conn_err(&c)), @@ -130,16 +200,16 @@ impl SendStream for WtSend { } } fn finish(&mut self) -> TResult<()> { - self.0.quic_stream_mut().finish().map_err(TransportError::other) + self.quic().finish().map_err(TransportError::other) } fn reset(&mut self, code: u64) { - let _ = self.0.quic_stream_mut().reset(moqt_code_to_h3(code)); + let _ = self.quic().reset(moqt_code_to_h3(code)); } fn set_priority(&mut self, priority: i32) { - self.0.set_priority(priority); + let _ = self.quic().set_priority(priority); } async fn delivered(&mut self) -> TResult> { - match self.0.quic_stream_mut().stopped().await { + match self.quic().stopped().await { Ok(None) => Ok(None), Ok(Some(code)) => Ok(Some(h3_to_moqt(code.into_inner()))), Err(quinn::StoppedError::ConnectionLost(c)) => Err(conn_err(&c)), @@ -151,7 +221,7 @@ impl SendStream for WtSend { #[async_trait] impl RecvStream for WtRecv { async fn read_chunk(&mut self) -> TResult> { - match self.0.quic_stream_mut().read_chunk(READ_CHUNK, true).await { + match self.quic().read_chunk(READ_CHUNK, true).await { Ok(Some(c)) => Ok(Some(c.bytes)), Ok(None) => Ok(None), Err(quinn::ReadError::Reset(code)) => Err(TransportError::Reset(h3_to_moqt(code.into_inner()))), @@ -160,7 +230,7 @@ impl RecvStream for WtRecv { } } fn stop(&mut self, code: u64) { - let _ = self.0.quic_stream_mut().stop(moqt_code_to_h3(code)); + let _ = self.quic().stop(moqt_code_to_h3(code)); } } @@ -178,20 +248,20 @@ impl Connection for WtConnection { async fn open_uni(&self) -> TResult { let opening = self.conn.open_uni().await.map_err(|e| wt_conn_err(&e))?; let s = opening.await.map_err(TransportError::other)?; - Ok(Box::new(WtSend(s))) + Ok(Box::new(WtSend::new(s, &self.closing))) } async fn open_bi(&self) -> TResult<(BoxSend, BoxRecv)> { let opening = self.conn.open_bi().await.map_err(|e| wt_conn_err(&e))?; let (s, r) = opening.await.map_err(TransportError::other)?; - Ok((Box::new(WtSend(s)), Box::new(WtRecv(r)))) + Ok((Box::new(WtSend::new(s, &self.closing)), Box::new(WtRecv::new(r, &self.closing)))) } async fn accept_uni(&self) -> TResult { let r = self.conn.accept_uni().await.map_err(|e| wt_conn_err(&e))?; - Ok(Box::new(WtRecv(r))) + Ok(Box::new(WtRecv::new(r, &self.closing))) } async fn accept_bi(&self) -> TResult<(BoxSend, BoxRecv)> { let (s, r) = self.conn.accept_bi().await.map_err(|e| wt_conn_err(&e))?; - Ok((Box::new(WtSend(s)), Box::new(WtRecv(r)))) + Ok((Box::new(WtSend::new(s, &self.closing)), Box::new(WtRecv::new(r, &self.closing)))) } fn send_datagram(&self, payload: Bytes) -> TResult<()> { self.conn.send_datagram(&payload).map_err(TransportError::other) @@ -201,8 +271,20 @@ impl Connection for WtConnection { Ok(d.payload()) } /// WT_CLOSE_SESSION with the MOQT code as the session's application error code (a code - /// past 32 bits as `u32::MAX`) and the reason, then the connection's close (module docs). + /// past 32 bits as `u32::MAX`) and the reason, then the connection's close; the streams + /// dropped from here are held until the connection has closed (module docs). fn close(&self, code: u64, reason: &[u8]) { + if let Ok(rt) = tokio::runtime::Handle::try_current() + && !self.closing.holding.swap(true, Ordering::AcqRel) + { + let quic = self.conn.quic_connection().clone(); + let closing = self.closing.clone(); + rt.spawn(async move { + let _ = tokio::time::timeout(HOLD, quic.closed()).await; + closing.holding.store(false, Ordering::Release); + closing.held.lock().unwrap().clear(); + }); + } let code = u32::try_from(code).unwrap_or(u32::MAX); self.conn.close_session(code, &String::from_utf8_lossy(reason)); } diff --git a/joule-code/joule-code-rs/crates/joule-moq-relay/tests/webtransport_close.rs b/joule-code/joule-code-rs/crates/joule-moq-relay/tests/webtransport_close.rs index b6d797b9d..6691e4600 100644 --- a/joule-code/joule-code-rs/crates/joule-moq-relay/tests/webtransport_close.rs +++ b/joule-code/joule-code-rs/crates/joule-moq-relay/tests/webtransport_close.rs @@ -4,10 +4,11 @@ //! §6), and the other end reads both exactly, as it does over the WebSocket binding. A QUIC //! connection close alone carries an HTTP/3 error code, which no end reads as MOQT's. //! -//! The relay closes a session three ways here: refusing a room grant presented without its -//! proof, after SETUP; refusing a session past its session count, before any MOQT message; -//! and ending an established session at its time limit. And a client closes one, whose code -//! the relay's signed summary records as the peer's. +//! The relay closes a session four ways here: refusing a room grant presented without its +//! proof, after SETUP; refusing a stream that breaks a rule before SETUP, which it stops only +//! after the session's close; refusing a session past its session count, before any MOQT +//! message; and ending an established session at its time limit. And a client closes one, +//! whose code the relay's signed summary records as the peer's. mod common; @@ -69,6 +70,21 @@ async fn run() { let (c, _) = connect(&rooms, Via::WebTransport, bare).await; assert_eq!(closed(c.conn.as_ref(), "a grant with no proof").await, by_relay(SessionError::UNAUTHORIZED, "live-rooms §10.5: pop_missing")); + // A stream that breaks a rule before SETUP: refused, PROTOCOL_VIOLATION. The relay ends + // that stream only after the session's close, so the client reads the close and never + // a STOP_SENDING ahead of it. + let raw = session(&rooms).await; + let mut s = timeout(STEP, raw.open_uni()).await.expect("in time").expect("a stream"); + s.write_all(&[0x3f; 4]).await.expect("written"); + match closed(&raw, "a stream that breaks a rule").await { + TransportError::Closed { code, reason, by_peer: true } if code == SessionError::PROTOCOL_VIOLATION.0 => assert!(reason.contains("invalid flags"), "{reason}"), + other => panic!("a stream that breaks a rule: {other:?}"), + } + match timeout(STEP, s.delivered()).await.expect("in time") { + Err(TransportError::Closed { .. }) => {} + other => panic!("the relay stopped the stream ahead of the session's close: {other:?}"), + } + // A client closes its session with a code of its own: the relay reads it as the peer's, // and its summary records it; the client's own end reads its close as local. let (c, _) = connect(&rooms, Via::WebTransport, ClientOptions::default()).await; -- 2.54.0 (Apple Git-157) From d8ae7cdb569ddb77ab3402022242d3cc5ab1bf98 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 20 Oct 2026 13:25:08 -0400 Subject: [PATCH 8/9] joule-moq-relay: hold a closing session's streams as boxed values (no dead fields) Co-Authored-By: Claude Opus 5.5 --- .../src/transport/webtransport.rs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/joule-code/joule-code-rs/crates/joule-moq-relay/src/transport/webtransport.rs b/joule-code/joule-code-rs/crates/joule-moq-relay/src/transport/webtransport.rs index c6a6ace25..288c20d5e 100644 --- a/joule-code/joule-code-rs/crates/joule-moq-relay/src/transport/webtransport.rs +++ b/joule-code/joule-code-rs/crates/joule-moq-relay/src/transport/webtransport.rs @@ -22,7 +22,7 @@ //! The capsule must reach the peer before any of the session's streams ends: §6 ends them //! after the session, and a page whose stream was stopped just ahead of the close was seen //! to lose its renderer. A stream dropped once the session is closing would send its FIN or -//! STOP_SENDING at once, so it is held ([`Closing`]) until the QUIC connection has closed, +//! STOP_SENDING at once, so it is held (`Closing`) until the QUIC connection has closed, //! when it ends with nothing more on the wire. use super::quic::{conn_err, READ_CHUNK}; @@ -96,17 +96,12 @@ pub struct WtConnection { #[derive(Default)] struct Closing { holding: AtomicBool, - held: Mutex>, -} - -enum Held { - Send(wtransport::SendStream), - Recv(wtransport::RecvStream), + held: Mutex>>, } impl Closing { - /// Keep `h` while the session is closing; otherwise it is dropped here, as usual. - fn drop_or_hold(&self, h: Held) { + /// Keep a stream while the session is closing; otherwise it is dropped here, as usual. + fn drop_or_hold(&self, h: Box) { if self.holding.load(Ordering::Acquire) { self.held.lock().unwrap().push(h); } @@ -176,7 +171,7 @@ impl WtRecv { impl Drop for WtSend { fn drop(&mut self) { if let Some(s) = self.0.take() { - self.1.drop_or_hold(Held::Send(s)); + self.1.drop_or_hold(Box::new(s)); } } } @@ -184,7 +179,7 @@ impl Drop for WtSend { impl Drop for WtRecv { fn drop(&mut self) { if let Some(r) = self.0.take() { - self.1.drop_or_hold(Held::Recv(r)); + self.1.drop_or_hold(Box::new(r)); } } } -- 2.54.0 (Apple Git-157) From 1eb03e05134b989574acf6b73015cf363810eff3 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 20 Oct 2026 13:38:19 -0400 Subject: [PATCH 9/9] joule-moq-relay README and the changelog: a closing session's streams are held until its connection closes Co-Authored-By: Claude Opus 5.5 --- joule-code/CHANGELOG.md | 10 ++++++++-- .../joule-code-rs/crates/joule-moq-relay/README.md | 10 +++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/joule-code/CHANGELOG.md b/joule-code/CHANGELOG.md index d5181e8a9..036c69fb4 100644 --- a/joule-code/CHANGELOG.md +++ b/joule-code/CHANGELOG.md @@ -14,6 +14,11 @@ All notable changes to the Joule Code standard are recorded here. The format fol relay's code and reason over WebTransport as it does over the WebSocket binding: a grant presented without its proof, for one, is `UNAUTHORIZED` (2) with its live-rooms §10.5 reason, where the page saw only that the session was lost. +- **Nothing of the session overtakes its close.** A stream the relay lets go once a + WebTransport session is closing is held until the QUIC connection has closed, so no FIN or + STOP_SENDING reaches the page ahead of the capsule (a page whose stream was stopped just + before the close was seen to lose its renderer); a stream the relay refuses before SETUP + is kept until then too. - **A peer's close is read the same way.** `closed()` over WebTransport gives the code and reason of the peer's WT_CLOSE_SESSION (`by_peer: true`), so a page's close code reaches the session's signed summary; a QUIC connection closed with the session still open gives @@ -25,8 +30,9 @@ All notable changes to the Joule Code standard are recorded here. The format fol report the session's end; `VENDORED.md` lists every change and why. Its unit tests, the capsule's bytes against the draft's layout among them, run in the relay's CI job. - **Tests.** `tests/webtransport_close.rs`: the relay refusing a grant without its proof, - refusing a session past its session count, and ending one at its time limit, each read - by the client as the exact code and reason; and a client's own close, recorded as the + refusing a stream that breaks a rule before SETUP (with no STOP_SENDING ahead of the + close), refusing a session past its session count, and ending one at its time limit, each + read by the client as the exact code and reason; and a client's own close, recorded as the peer's in the relay's summary. The browser end-to-end check of a bare grant now requires code 2 and the reason over WebTransport too. diff --git a/joule-code/joule-code-rs/crates/joule-moq-relay/README.md b/joule-code/joule-code-rs/crates/joule-moq-relay/README.md index 20a356711..855868d1b 100644 --- a/joule-code/joule-code-rs/crates/joule-moq-relay/README.md +++ b/joule-code/joule-code-rs/crates/joule-moq-relay/README.md @@ -56,9 +56,13 @@ the WebTransport session (`draft-ietf-moq-transport-22` §6.6). The relay sends capsule's 32-bit application error code as it stands, not mapped into the HTTP/3 space as stream codes are, and the reason, at most 1,024 bytes of UTF-8, is its message. The QUIC connection is closed with `H3_NO_ERROR` once the peer has closed its side of the CONNECT -stream, or two seconds after the capsule, so the connection's close cannot overtake it. A -page reads the code and reason from `WebTransport.closed` (`closeCode`, `reason`), as it -reads them from the WebSocket binding's `WT_CLOSE_SESSION`. A session the client closes is +stream, or two seconds after the capsule, so the connection's close cannot overtake it. +Nothing else of the session overtakes it either: a stream the relay lets go once the session +is closing (the client's control stream of a refused session, say) is held until the QUIC +connection has closed, rather than ended at once with a FIN or STOP_SENDING that would reach +the page before the capsule; a page whose stream was stopped just ahead of the close was +seen to lose its renderer. A page reads the code and reason from `WebTransport.closed` +(`closeCode`, `reason`), as it reads them from the WebSocket binding's `WT_CLOSE_SESSION`. A session the client closes is read the same way: the relay's `closed()` gives the client's code and reason, and the session's summary records the code as the peer's. A QUIC connection that closes with the session still open gives no MOQT code; its code is an HTTP/3 error code. -- 2.54.0 (Apple Git-157)