Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 50f96a5fe6 |
36 changed files with 1095 additions and 594 deletions
|
|
@ -7,12 +7,16 @@ on:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
NIX_CONFIG: experimental-features = nix-command flakes
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
checks:
|
checks:
|
||||||
name: checks
|
name: checks
|
||||||
runs-on: nixos
|
runs-on: nixos
|
||||||
steps:
|
steps:
|
||||||
|
- name: Install node
|
||||||
|
run: nix profile add nixpkgs#nodejs_24
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: https://data.forgejo.org/actions/checkout@v7
|
uses: https://data.forgejo.org/actions/checkout@v7
|
||||||
|
|
||||||
|
|
@ -29,7 +33,6 @@ jobs:
|
||||||
cargo machete
|
cargo machete
|
||||||
|
|
||||||
pnpm install --frozen-lockfile
|
pnpm install --frozen-lockfile
|
||||||
pnpm add --save-dev --save-exact --workspace-root jscpd-linux-x64-gnu@5.0.14
|
|
||||||
pnpm run dup
|
pnpm run dup
|
||||||
|
|
||||||
RUSTFLAGS="--cfg web_sys_unstable_apis" wasm-pack test --node wasm
|
RUSTFLAGS="--cfg web_sys_unstable_apis" wasm-pack test --node wasm
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,16 @@ on:
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
|
env:
|
||||||
|
NIX_CONFIG: experimental-features = nix-command flakes
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: nixos
|
runs-on: nixos
|
||||||
steps:
|
steps:
|
||||||
|
- name: Install node & bun
|
||||||
|
run: nix profile add nixpkgs#nodejs_24 nixpkgs#bun
|
||||||
|
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: https://data.forgejo.org/actions/checkout@v7
|
uses: https://data.forgejo.org/actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
|
|
@ -26,6 +32,9 @@ jobs:
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install
|
run: bun install
|
||||||
|
|
||||||
|
- name: Install cc linker, sed & jq
|
||||||
|
run: nix profile add nixpkgs#stdenv.cc nixpkgs#gnused nixpkgs#jq
|
||||||
|
|
||||||
- name: Build all
|
- name: Build all
|
||||||
run: bun build:all
|
run: bun build:all
|
||||||
|
|
||||||
|
|
|
||||||
154
Cargo.lock
generated
154
Cargo.lock
generated
|
|
@ -90,9 +90,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.92"
|
version = "0.1.91"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
|
checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -221,9 +221,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.4.3"
|
version = "1.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d"
|
checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"jobserver",
|
"jobserver",
|
||||||
|
|
@ -561,7 +561,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -596,9 +596,9 @@ checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.11"
|
version = "0.1.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
|
checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
|
|
@ -629,9 +629,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.34"
|
version = "0.3.33"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"
|
checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
|
@ -660,9 +660,9 @@ checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-executor"
|
name = "futures-executor"
|
||||||
version = "0.3.34"
|
version = "0.3.33"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
|
checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
|
|
@ -764,9 +764,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.4.16"
|
version = "0.4.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27"
|
checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic-waker",
|
"atomic-waker",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
@ -895,9 +895,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http-body-util"
|
name = "http-body-util"
|
||||||
version = "0.1.5"
|
version = "0.1.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
|
checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
|
@ -966,9 +966,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_collections"
|
name = "icu_collections"
|
||||||
version = "2.3.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
|
checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"potential_utf",
|
"potential_utf",
|
||||||
|
|
@ -980,9 +980,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_locale_core"
|
name = "icu_locale_core"
|
||||||
version = "2.3.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
|
checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"litemap",
|
"litemap",
|
||||||
|
|
@ -993,9 +993,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_normalizer"
|
name = "icu_normalizer"
|
||||||
version = "2.3.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
|
checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"icu_collections",
|
"icu_collections",
|
||||||
"icu_normalizer_data",
|
"icu_normalizer_data",
|
||||||
|
|
@ -1007,17 +1007,16 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_normalizer_data"
|
name = "icu_normalizer_data"
|
||||||
version = "2.3.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
|
checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_properties"
|
name = "icu_properties"
|
||||||
version = "2.3.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
|
checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
|
||||||
"icu_collections",
|
"icu_collections",
|
||||||
"icu_locale_core",
|
"icu_locale_core",
|
||||||
"icu_properties_data",
|
"icu_properties_data",
|
||||||
|
|
@ -1028,15 +1027,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_properties_data"
|
name = "icu_properties_data"
|
||||||
version = "2.3.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
|
checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_provider"
|
name = "icu_provider"
|
||||||
version = "2.3.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92a7ed671a6aad807a8651a2e1782a6598fda9ce5185dd8158549e95a91c6428"
|
checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"icu_locale_core",
|
"icu_locale_core",
|
||||||
|
|
@ -1154,9 +1153,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.104"
|
version = "0.3.103"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
|
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
|
@ -1174,9 +1173,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "keccak"
|
name = "keccak"
|
||||||
version = "0.2.1"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ffd9697dc4a9a62e2da93389f34400b77a28f0287711263cabb203b3ccb9c0e4"
|
checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures 0.3.0",
|
"cpufeatures 0.3.0",
|
||||||
|
|
@ -1202,9 +1201,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litemap"
|
name = "litemap"
|
||||||
version = "0.8.3"
|
version = "0.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
|
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
|
|
@ -1235,9 +1234,9 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minicov"
|
name = "minicov"
|
||||||
version = "0.3.9"
|
version = "0.3.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c3aa3aa12b448ac225b3102217d1ac5cc717908f02722926524b0599c933c7a0"
|
checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
|
|
@ -1666,9 +1665,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkg-config"
|
name = "pkg-config"
|
||||||
version = "0.3.34"
|
version = "0.3.33"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
|
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "poly1305"
|
name = "poly1305"
|
||||||
|
|
@ -1701,9 +1700,9 @@ checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "potential_utf"
|
name = "potential_utf"
|
||||||
version = "0.1.6"
|
version = "0.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
|
checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
@ -1746,9 +1745,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quinn-proto"
|
name = "quinn-proto"
|
||||||
version = "0.11.17"
|
version = "0.11.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83"
|
checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-rs",
|
"aws-lc-rs",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
@ -1780,7 +1779,7 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1951,7 +1950,7 @@ dependencies = [
|
||||||
"security-framework",
|
"security-framework",
|
||||||
"security-framework-sys",
|
"security-framework-sys",
|
||||||
"webpki-root-certs",
|
"webpki-root-certs",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2121,7 +2120,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09057cb2149ad4cbd2da1e26b351f9a4c354219421229c69c3063e6f61947c4a"
|
checksum = "09057cb2149ad4cbd2da1e26b351f9a4c354219421229c69c3063e6f61947c4a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"digest 0.11.3",
|
"digest 0.11.3",
|
||||||
"keccak 0.2.1",
|
"keccak 0.2.0",
|
||||||
"sponge-cursor",
|
"sponge-cursor",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2346,9 +2345,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinystr"
|
name = "tinystr"
|
||||||
version = "0.8.4"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
|
checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"zerovec",
|
"zerovec",
|
||||||
|
|
@ -2420,14 +2419,13 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.19"
|
version = "0.7.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
|
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"libc",
|
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
@ -2572,9 +2570,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.127"
|
version = "0.2.126"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
|
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
|
@ -2585,9 +2583,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-futures"
|
name = "wasm-bindgen-futures"
|
||||||
version = "0.4.77"
|
version = "0.4.76"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950"
|
checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
|
|
@ -2595,9 +2593,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.127"
|
version = "0.2.126"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
|
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
|
|
@ -2605,9 +2603,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro-support"
|
name = "wasm-bindgen-macro-support"
|
||||||
version = "0.2.127"
|
version = "0.2.126"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
|
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
|
@ -2618,18 +2616,18 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-shared"
|
name = "wasm-bindgen-shared"
|
||||||
version = "0.2.127"
|
version = "0.2.126"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
|
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-test"
|
name = "wasm-bindgen-test"
|
||||||
version = "0.3.77"
|
version = "0.3.76"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "895a2607575412a4eda1df892084a375ea10dfeadc4d7d2ab87b854e4ddc7ba1"
|
checksum = "2a0d555ca874445df8d314f94f5c948a4e74e5418f332c89f660a3d8310a96f4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"cast",
|
"cast",
|
||||||
|
|
@ -2649,9 +2647,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-test-macro"
|
name = "wasm-bindgen-test-macro"
|
||||||
version = "0.3.77"
|
version = "0.3.76"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4288cb0ebe215033bf949ae1fd046726daa4c32a157f24b9dc6ac387a52aa759"
|
checksum = "94eb68555b95bcea5e8cf4abe280b529049479fa995bfc23734af96a6aedc120"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -2660,9 +2658,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-test-shared"
|
name = "wasm-bindgen-test-shared"
|
||||||
version = "0.2.127"
|
version = "0.2.126"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "33ff1c1b360982e93b6d8ea9c04836f71dba0817a16f91e229cf3a51bdd9d987"
|
checksum = "c31d56021e873866c968588ed85ccdf56db5c426e44afdb4618c39895104b920"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-tracing"
|
name = "wasm-tracing"
|
||||||
|
|
@ -2700,7 +2698,7 @@ version = "0.1.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2793,9 +2791,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "writeable"
|
name = "writeable"
|
||||||
version = "0.6.4"
|
version = "0.6.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
|
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wtransport"
|
name = "wtransport"
|
||||||
|
|
@ -2940,9 +2938,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerotrie"
|
name = "zerotrie"
|
||||||
version = "0.2.5"
|
version = "0.2.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
|
checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"yoke",
|
"yoke",
|
||||||
|
|
@ -2951,9 +2949,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerovec"
|
name = "zerovec"
|
||||||
version = "0.11.7"
|
version = "0.11.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94b5c6b5976d66c1d703c4fd17d3f5e43c8cedaacf604961b171adc7130896d8"
|
checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"yoke",
|
"yoke",
|
||||||
"zerofrom",
|
"zerofrom",
|
||||||
|
|
@ -2962,13 +2960,13 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerovec-derive"
|
name = "zerovec-derive"
|
||||||
version = "0.11.5"
|
version = "0.11.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f212a141d820099d57ffafb9569be9617a6f27d3dc881fbee8fb56642f917a9"
|
checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.3",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
23
README.md
23
README.md
|
|
@ -47,29 +47,18 @@ Feature summary:
|
||||||
|
|
||||||
| Feature | Pulls in | Enables |
|
| Feature | Pulls in | Enables |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `serde` | Crypto serialization support | Serde implementations for crypto key types |
|
| `crypto` | `mtp::crypto` | AEAD, signatures, KEM, KDF, hashing |
|
||||||
| `crypto` | `mtp::crypto` | AEAD, signatures, KEM, KDF, hashing, and connection authentication support |
|
| `host` | `mtp::host`, codec registry | QUIC host and version negotiation |
|
||||||
| `host` | `mtp::host` | Native QUIC host and version negotiation |
|
| `client` | `mtp::client` | QUIC client connections |
|
||||||
| `client` | `mtp::client` | Native QUIC client connections |
|
| `webserver` | `mtp::webserver` | HTTPS server with HTTP/1.1, HTTP/2, HTTP/3, and WebTransport MTP sessions |
|
||||||
| `transport` | `mtp-transport` dependency | Low-level transport support; enabled automatically by `host` and `client` |
|
|
||||||
| `pipes` | Pipe support in transport, host, client, and web server | Raw and encrypted byte streams |
|
|
||||||
| `files` | `mtp::files` | `.mk` keyrings and `.mpkb` public bundles; also enables `crypto` |
|
|
||||||
| `raw` | Raw file APIs | Legacy plaintext keyring migration APIs |
|
|
||||||
| `web-server` | `mtp::webserver` | HTTPS server with HTTP/1.1, HTTP/2, HTTP/3, and WebTransport MTP sessions |
|
|
||||||
| `full-server` | Native host and web-server surface | `host`, `web-server`, `crypto`, and `pipes` together |
|
|
||||||
| `tls` | `mtp::crypto::tls` | Development self-signed certificate generation |
|
|
||||||
| `insecure-tls` | Lower-level transport | Development-only certificate verification bypass, gated by `MTP_INSECURE_TLS=1` |
|
|
||||||
|
|
||||||
The core modules always available from the facade are `codec`, `common`, and
|
The core crates are always available: `codec`, `transport`, `common`, and `type_map`. See the [native client](./docs/NATIVE-CLIENT.md) and [native host](./docs/NATIVE-HOST.md)
|
||||||
`type_map`. Native `client` and `host` modules re-export the transport policy
|
|
||||||
types; the low-level transport crate is not exposed as `mtp::transport`. See the [native client](./docs/NATIVE-CLIENT.md) and [native host](./docs/NATIVE-HOST.md)
|
|
||||||
guides for configuration and usage. See [Security](./docs/SECURITY.md) for security boundaries.
|
guides for configuration and usage. See [Security](./docs/SECURITY.md) for security boundaries.
|
||||||
|
|
||||||
## Sub-crates
|
## Sub-crates
|
||||||
|
|
||||||
The `mtp` facade re-exports the following modules:
|
The `mtp` facade re-exports the following modules:
|
||||||
`mtp::codec`, `mtp::common`, `mtp::type_map`, `mtp::crypto`, `mtp::host`,
|
`mtp::codec`, `mtp::transport`, `mtp::common`, `mtp::type_map`, `mtp::crypto`, `mtp::host`, and `mtp::client`.
|
||||||
`mtp::client`, `mtp::files`, and `mtp::webserver` when their features are enabled.
|
|
||||||
|
|
||||||
### Codec
|
### Codec
|
||||||
|
|
||||||
|
|
|
||||||
16
deny.toml
16
deny.toml
|
|
@ -8,23 +8,9 @@ ignore = []
|
||||||
|
|
||||||
[bans]
|
[bans]
|
||||||
# Flag multiple versions of the same crate so duplicate trees are visible.
|
# Flag multiple versions of the same crate so duplicate trees are visible.
|
||||||
multiple-versions = "deny"
|
multiple-versions = "warn"
|
||||||
wildcards = "deny"
|
wildcards = "deny"
|
||||||
|
|
||||||
# These versions are required by incompatible upstream dependency lines:
|
|
||||||
# - pem/rcgen/wtransport still use base64 0.22.
|
|
||||||
# - ring and wasm-bindgen still use getrandom 0.2.
|
|
||||||
# - current displaydoc/serde/thiserror/tokio and wasm-bindgen trees span syn 2
|
|
||||||
# and syn 3.
|
|
||||||
# - ring still uses windows-sys 0.52 while the Tokio/QUIC tree uses 0.61.
|
|
||||||
# Keep the duplicate-version policy strict for every other crate/version.
|
|
||||||
skip = [
|
|
||||||
{ name = "base64", version = "0.22.1" },
|
|
||||||
{ name = "getrandom", version = "0.2.17" },
|
|
||||||
{ name = "syn", version = "2.0.119" },
|
|
||||||
{ name = "windows-sys", version = "0.52.0" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[licenses]
|
[licenses]
|
||||||
# Allowlist of licenses acceptable for this project's dependencies.
|
# Allowlist of licenses acceptable for this project's dependencies.
|
||||||
allow = [
|
allow = [
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,19 @@
|
||||||
# MTP Connections
|
# MTP Connections
|
||||||
|
|
||||||
Native clients and server-side hosts expose parallel connection handles after the
|
Native clients and hosts share the same connection shape after the opening handshake. The client creates the connection; the host receives it from `accept()`.
|
||||||
opening handshake. The client creates its handle; the host receives one from
|
|
||||||
`accept()`.
|
|
||||||
|
|
||||||
| Member | Native client | Native host | Web host (`WebMTPConnection`) |
|
| Member | Native client | Native host | Web host (`WebMTPConnection`) |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `version` | Compiled client version accepted by the host | Version selected by the registry | Version selected by the registry |
|
| `version` | Compiled client version accepted by the host | Version selected by the registry | Version selected by the registry |
|
||||||
| `sender` | Sends `CommunicationValue` frames | Sends `CommunicationValue` frames | Sends `CommunicationValue` frames |
|
| `sender` | Sends `CommunicationValue` frames | Sends `CommunicationValue` frames | Sends `CommunicationValue` frames |
|
||||||
| `receiver` | Underlying receiver; use `receive()` for application frames | Underlying receiver; use `receive()` for application frames | Underlying receiver; use `receive()` for application frames |
|
| `receiver` | Receives application frames | Receives application frames | Receives application frames |
|
||||||
| `description` | Optional label sent during setup | Optional label received from the client | Optional label received from the client |
|
| `description` | Optional label sent during setup | Optional label received from the client | Optional label received from the client |
|
||||||
| `client_id` | Confirmed or assigned ID with `crypto` | Authenticated or guest client ID with `crypto` | Authenticated or guest client ID with `crypto` |
|
| `client_id` | Confirmed or assigned ID with `crypto` | Authenticated or guest client ID with `crypto` | Authenticated or guest client ID with `crypto` |
|
||||||
| `auth_state` | Authentication result with `crypto` | Authentication result with `crypto` | Authentication result with `crypto` |
|
| `auth_state` | Authentication result with `crypto` | Authentication result with `crypto` | Authentication result with `crypto` |
|
||||||
| `path` | — | Native hosts use `/` | WebTransport CONNECT path (e.g. `/mtp`) |
|
| `request_path` | / | / | WebTransport CONNECT path (e.g. `/mtp`) |
|
||||||
| `remote_addr` | Server `SocketAddr` when available | Peer `SocketAddr` | Peer `SocketAddr` |
|
| `remote_addr` | Server `SocketAddr` when available | Peer `SocketAddr` | Peer `SocketAddr` |
|
||||||
|
|
||||||
`WebMTPConnection`, returned by `MTPWebServer::accept()`, exposes the same
|
`WebMTPConnection`, returned by `MTPWebServer::accept()`, exposes the same members as the native host connection plus `request_path`, which contains the HTTP/3 path used for the WebTransport extended CONNECT request.
|
||||||
server-side members as the native host connection. Its `path` contains the
|
|
||||||
HTTP/3 path used for the WebTransport extended CONNECT request.
|
|
||||||
|
|
||||||
Server-side MTP connections expose `remote_addr`, the peer address observed by
|
Server-side MTP connections expose `remote_addr`, the peer address observed by
|
||||||
QUIC. HTTP route handlers receive the peer address as `HttpRequest::remote_addr`.
|
QUIC. HTTP route handlers receive the peer address as `HttpRequest::remote_addr`.
|
||||||
|
|
|
||||||
|
|
@ -4,28 +4,23 @@ This file documents the connection and version negotiation logic.
|
||||||
|
|
||||||
## Registry
|
## Registry
|
||||||
|
|
||||||
The `registry` module provides a multi-version `Registry` used by the host for
|
The `registry` module provides a multi-version `Registry` used by the host for version negotiation. Accessed through the `mtp` facade (requires the `host` feature):
|
||||||
version negotiation. Accessed through the `mtp` facade (requires the `host`
|
|
||||||
feature). In this repository, `Registry::builtin()` is generated from
|
|
||||||
[`example/type-maps.yaml`](../example/type-maps.yaml), which currently contains
|
|
||||||
protocol version 3.0 only. Downstream projects can register additional versions
|
|
||||||
in their own YAML configuration.
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use mtp::codec::{Version, registry::Registry};
|
use mtp::codec::registry::Registry;
|
||||||
|
|
||||||
let registry = Registry::builtin(); // loads all TypeMaps from the build config
|
let registry = Registry::builtin(); // loads all TypeMaps from config
|
||||||
|
|
||||||
// Check if a version is supported
|
// Check if a version is supported
|
||||||
assert!(registry.supports(&Version(3, 0)));
|
assert!(registry.supports(&Version(1, 0)));
|
||||||
|
|
||||||
// Find highest mutual version for a client
|
// Find highest mutual version for a client
|
||||||
let client_versions = &[Version(2, 0), Version(3, 0)];
|
let client_versions = &[Version(0, 0), Version(1, 0)];
|
||||||
let negotiated = registry.negotiate(client_versions);
|
let negotiated = registry.negotiate(client_versions);
|
||||||
assert_eq!(negotiated, Some(Version(3, 0)));
|
assert_eq!(negotiated, Some(Version(1, 0)));
|
||||||
|
|
||||||
// Look up a version's TypeMap
|
// Look up a version's TypeMap
|
||||||
let tm = registry.get(&Version(3, 0)).unwrap();
|
let tm = registry.get(&Version(2, 0)).unwrap();
|
||||||
```
|
```
|
||||||
|
|
||||||
The `Registry::builtin()` constructor uses the `TypeMap::vX_Y()` methods generated from the config.
|
The `Registry::builtin()` constructor uses the `TypeMap::vX_Y()` methods generated from the config.
|
||||||
|
|
@ -59,9 +54,9 @@ let mut host = MTPHost::new(config).await?;
|
||||||
while let Some(conn) = host.accept().await? {
|
while let Some(conn) = host.accept().await? {
|
||||||
// conn.version is the negotiated version
|
// conn.version is the negotiated version
|
||||||
// conn.codec is a VersionedCodec scoped to that version
|
// conn.codec is a VersionedCodec scoped to that version
|
||||||
// conn.sender / conn.receive() for application CommunicationValue I/O
|
// conn.sender / conn.receiver for raw CommunicationValue I/O
|
||||||
|
|
||||||
let msg = conn.receive().await?;
|
let msg = conn.receiver.receive().await?;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -99,31 +94,29 @@ The client's `PROTOCOL_VERSION` constant is set by `protocol_version` in `type-m
|
||||||
## Version Negotiation Flow
|
## Version Negotiation Flow
|
||||||
|
|
||||||
```
|
```
|
||||||
Client (v3.0) Host (v3.0)
|
Client (v2.0) Host (v0.0, v1.0, v2.0)
|
||||||
| |
|
| |
|
||||||
| QUIC connect |
|
| QUIC connect |
|
||||||
|----------------------->|
|
|----------------------->|
|
||||||
| |
|
| |
|
||||||
| CommValue{ Ident. } |
|
| CommValue{ Ident. } |
|
||||||
| Version -> "3.0" |
|
| Version -> "2.0" |
|
||||||
| Id -> 8765 |
|
| Id -> 8765 |
|
||||||
| (unsigned hello; auth |
|
| (unsigned hello; auth |
|
||||||
| challenge follows) |
|
| challenge follows) |
|
||||||
|----------------------->|
|
|----------------------->|
|
||||||
| | registry.negotiate(&[Version(3,0)])
|
| | registry.negotiate(&[Version(2,0)])
|
||||||
| | -> Some(Version(3,0))
|
| | -> Some(Version(2,0))
|
||||||
| |
|
| |
|
||||||
| Response | selected v3.0 TypeMap
|
| Response | selected v2.0 TypeMap
|
||||||
|<-----------------------|
|
|<-----------------------|
|
||||||
| Status, version |
|
| Status, version |
|
||||||
| |
|
| |
|
||||||
| subsequent messages |
|
| subsequent messages |
|
||||||
| use v3.0 TypeMap |
|
| use v2.0 TypeMap |
|
||||||
```
|
```
|
||||||
|
|
||||||
If the client sends an unsupported version (for example, v2.0 to the current
|
If the client sends an unsupported version (e.g. v3.0 when the host only knows up to v2.0), `negotiate` returns `None` and the connection is closed.
|
||||||
repository builtin host), `negotiate` returns `None` and the connection is
|
|
||||||
closed.
|
|
||||||
|
|
||||||
## Protocol Ping and Pong
|
## Protocol Ping and Pong
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,6 @@ MTP reports codec failures separately from connection and transport failures.
|
||||||
| `ReservedCommunicationType` | An application attempted to use a reserved communication type ID. |
|
| `ReservedCommunicationType` | An application attempted to use a reserved communication type ID. |
|
||||||
| `InvalidEncoding` | Bytes do not match the MTP value or frame format. |
|
| `InvalidEncoding` | Bytes do not match the MTP value or frame format. |
|
||||||
| `TooManyEntries` | A serialized value or frame exceeds its representable size. |
|
| `TooManyEntries` | A serialized value or frame exceeds its representable size. |
|
||||||
| `MissingTypeMap` | A versioned codec was asked to encode a value without a retained negotiated type map. |
|
|
||||||
| `TypeMapMismatch` | A value was created with a different protocol type map from the codec or peer operation. |
|
|
||||||
| `CryptoFailed` | Signing, verification, encryption, or decryption failed while encoding or decoding. |
|
| `CryptoFailed` | Signing, verification, encryption, or decryption failed while encoding or decoding. |
|
||||||
| `MissingField` | A required typed field is absent. |
|
| `MissingField` | A required typed field is absent. |
|
||||||
|
|
||||||
|
|
@ -45,4 +43,4 @@ Native builds may expose additional variants wrapping QUIC and WebTransport erro
|
||||||
|
|
||||||
## Authentication Rejections
|
## Authentication Rejections
|
||||||
|
|
||||||
The host reports unsupported or missing protocol versions through `AcceptError`. Authentication failures return `AcceptError::AuthenticationFailed` after the host sends a rejected handshake response; a handshake that exceeds the configured limit returns `AcceptError::AuthenticationTimedOut`. The authentication flow and its signed fields are defined in [Security](SECURITY.md).
|
The host reports unsupported or missing protocol versions through `AcceptError`. Authentication failures return `AcceptError::AuthenticationFailed` after the host sends a rejected handshake response. The authentication flow and its signed fields are defined in [Security](SECURITY.md).
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ let request = CommunicationValue::new(CommunicationType::Ping).with_id(1);
|
||||||
conn.sender.send(&request).await?;
|
conn.sender.send(&request).await?;
|
||||||
let response = conn.receive().await?;
|
let response = conn.receive().await?;
|
||||||
println!("received {:?}", response.id());
|
println!("received {:?}", response.id());
|
||||||
conn.sender.close().await;
|
conn.sender.close();
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
@ -230,7 +230,7 @@ let response = conn
|
||||||
|
|
||||||
Requests are routed by id through the connection's receive dispatcher. Frames with other ids remain available through `conn.receive()`.
|
Requests are routed by id through the connection's receive dispatcher. Frames with other ids remain available through `conn.receive()`.
|
||||||
|
|
||||||
Two send modes (configured via `mtp::client::Policy`):
|
Two send modes (configured via `mtp::transport::Policy`):
|
||||||
- `PersistentStream` (default): reuses one QUIC unidirectional stream
|
- `PersistentStream` (default): reuses one QUIC unidirectional stream
|
||||||
- `SingleStreamPerMessage`: opens a new stream per message
|
- `SingleStreamPerMessage`: opens a new stream per message
|
||||||
|
|
||||||
|
|
@ -248,16 +248,12 @@ Inbound frames are queued internally. The `receive()` method returns the next av
|
||||||
### Close
|
### Close
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
conn.sender.close().await;
|
conn.sender.close();
|
||||||
// or
|
// or
|
||||||
conn.receiver.close();
|
conn.receiver.close();
|
||||||
```
|
```
|
||||||
|
|
||||||
`Sender::close().await` gracefully finishes the active send stream, sends the
|
Sends a close frame and signals the peer. The `Sender::close()` spawns an async task that sends the frame, waits for `force_close_delay` (default 300ms), then force-closes the QUIC connection if the peer has not already done so.
|
||||||
MTP close frame, and waits for `force_close_delay` (default 300ms) before
|
|
||||||
force-closing the QUIC connection if necessary. `Sender::close_immediate()` is
|
|
||||||
the fire-and-forget variant. `Receiver::close()` closes the local receive
|
|
||||||
handle without performing the sender's graceful close sequence.
|
|
||||||
|
|
||||||
### Pipes
|
### Pipes
|
||||||
|
|
||||||
|
|
@ -313,7 +309,7 @@ For `public_signer`, call `verify` and `into_verified` before calling `decrypt`;
|
||||||
The `Policy` struct controls transport behaviour:
|
The `Policy` struct controls transport behaviour:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use mtp::client::{Policy, SendMode};
|
use mtp::transport::{Policy, SendMode};
|
||||||
|
|
||||||
let policy = Policy {
|
let policy = Policy {
|
||||||
send_mode: SendMode::PersistentStream,
|
send_mode: SendMode::PersistentStream,
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@ while let Some(connection) = server.accept().await? {
|
||||||
```
|
```
|
||||||
> `MTPWebServer::new` consumes a `HostConfig` (not an `MTPHost` instance). It creates its own QUIC endpoint and does not share a port with a running `MTPHost`.
|
> `MTPWebServer::new` consumes a `HostConfig` (not an `MTPHost` instance). It creates its own QUIC endpoint and does not share a port with a running `MTPHost`.
|
||||||
|
|
||||||
`server.accept()` returns `Option<WebMTPConnection>` for each WebTransport session. Ordinary HTTP routes do not surface through `accept()` because the server dispatches them internally. `WebMTPConnection` retains the negotiated version, codec, `path`, remote address, description, sender, and receiver used by native MTP connections.
|
`server.accept()` returns `Option<WebMTPConnection>` for each WebTransport session. Ordinary HTTP routes do not surface through `accept()` because the server dispatches them internally. `WebMTPConnection` retains the negotiated version, codec, request path, remote address, description, sender, and receiver used by native MTP connections.
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
|
|
@ -138,7 +138,7 @@ For direct browser access, leave `serve_tcp_https(true)` enabled. The server adv
|
||||||
|
|
||||||
When a reverse proxy or another process owns TCP, use `WebServerConfig::new().serve_tcp_https(false)`. This retains the UDP HTTP/3/WebTransport endpoint and its shared router without claiming the TCP port.
|
When a reverse proxy or another process owns TCP, use `WebServerConfig::new().serve_tcp_https(false)`. This retains the UDP HTTP/3/WebTransport endpoint and its shared router without claiming the TCP port.
|
||||||
|
|
||||||
With port `0` and TCP enabled, construction binds TCP first and binds UDP to the selected TCP port, so `local_addr()` reports the common address. With TCP disabled, Quinn selects the UDP port as before. `shutdown().await` stops both accept loops, gracefully finishes active HTTP requests until `drain_timeout`, closes Quinn, and then aborts remaining work. `close().await` and dropping the server stop both listeners immediately.
|
With port `0` and TCP enabled, construction binds TCP first and binds UDP to the selected TCP port, so `local_addr()` reports the common address. With TCP disabled, Quinn selects the UDP port as before. `shutdown()` stops both accept loops, gracefully finishes active HTTP requests until `drain_timeout`, closes Quinn, and then aborts remaining work. `close()` and dropping the server stop both listeners immediately.
|
||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
|
|
||||||
|
|
@ -164,18 +164,12 @@ On success, the connection has `AuthState::Authenticated`, the assigned `client_
|
||||||
|
|
||||||
## Errors
|
## Errors
|
||||||
|
|
||||||
`MTPWebServer::new` returns `CommunicationError` for certificate parsing,
|
`MTPWebServer::new` returns `CommunicationError` for certificate parsing, certificate loading, bind failures, and rejected authentication policy.
|
||||||
certificate loading, and bind failures. Authentication policy is evaluated when
|
|
||||||
WebTransport sessions are accepted, not rejected during construction.
|
|
||||||
`accept()` returns `AcceptError` for a missing or unsupported version, a receive failure, or a send failure during the WebTransport opening handshake. HTTP route failures are reported through `WebServerMetrics::error_occurred` when metrics are configured. See [Errors](ERRORS.md) for shared error variants.
|
`accept()` returns `AcceptError` for a missing or unsupported version, a receive failure, or a send failure during the WebTransport opening handshake. HTTP route failures are reported through `WebServerMetrics::error_occurred` when metrics are configured. See [Errors](ERRORS.md) for shared error variants.
|
||||||
|
|
||||||
`WebServerMetrics` has these callbacks:
|
`WebServerMetrics` has these callbacks:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
fn connection_accepted(&self)
|
|
||||||
fn connection_closed(&self, duration: Duration, reason: &str)
|
|
||||||
fn request_started(&self, path: &str)
|
fn request_started(&self, path: &str)
|
||||||
fn request_completed(&self, path: &str, status: u16, duration: Duration)
|
fn request_completed(&self, path: &str, status: u16, duration: Duration)
|
||||||
fn error_occurred(&self, error: &WebServerError)
|
fn error_occurred(&self, error: &WebServerError)
|
||||||
|
|
|
||||||
|
|
@ -83,18 +83,18 @@ network metadata, not an authenticated client identity.
|
||||||
|
|
||||||
## Version Negotiation
|
## Version Negotiation
|
||||||
|
|
||||||
`accept()` uses the version-bearing opening frame and registry flow in [Connector](CONNECTOR.md). The host registry is built from the type maps in [`example/type-maps.yaml`](../example/type-maps.yaml) by `Registry::builtin()` in this repository; downstream builds can provide their own `MTP_TYPE_MAPS` configuration.
|
`accept()` uses the version-bearing opening frame and registry flow in [Connector](CONNECTOR.md). The host registry is built from the type maps in `type-maps.yaml` by `Registry::builtin()`.
|
||||||
|
|
||||||
### Registry
|
### Registry
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use mtp::codec::Version;
|
use mtp::codec::registry::Registry;
|
||||||
|
|
||||||
let registry = host.registry();
|
let registry = host.registry();
|
||||||
assert!(registry.supports(&Version(3, 0)));
|
assert!(registry.supports(&Version(2, 0)));
|
||||||
|
|
||||||
let negotiated = registry.negotiate(&[Version(2, 0), Version(3, 0)]);
|
let negotiated = registry.negotiate(&[Version(1, 0), Version(2, 0)]);
|
||||||
// -> Some(Version(3, 0)) for this repository's builtin map
|
// -> Some(Version(2, 0)) if both versions are registered
|
||||||
```
|
```
|
||||||
|
|
||||||
## Authentication Flow
|
## Authentication Flow
|
||||||
|
|
@ -105,15 +105,13 @@ After a successful handshake, `MTPConnection` exposes `AuthState::Authenticated`
|
||||||
|
|
||||||
## Handling Messages
|
## Handling Messages
|
||||||
|
|
||||||
Use `conn.sender` and `conn.receive()` for bidirectional message exchange. The
|
Use `conn.sender` and `conn.receiver` for bidirectional message exchange:
|
||||||
connection dispatcher owns the underlying receiver, especially when `pipes` is
|
|
||||||
enabled:
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
while let Some(conn) = host.accept().await? {
|
while let Some(conn) = host.accept().await? {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
match conn.receive().await {
|
match conn.receiver.receive().await {
|
||||||
Ok(msg) => {
|
Ok(msg) => {
|
||||||
let response = process_message(&msg, &conn);
|
let response = process_message(&msg, &conn);
|
||||||
conn.sender.send(&response).await.ok();
|
conn.sender.send(&response).await.ok();
|
||||||
|
|
|
||||||
|
|
@ -39,4 +39,4 @@ Back up host keyrings and client keyrings as protected secrets. Test restoring a
|
||||||
|
|
||||||
### Graceful Shutdown
|
### Graceful Shutdown
|
||||||
|
|
||||||
Stop accepting new connections, reject new work at the application layer, and allow active requests and pipe writers to finish. For `MTPWebServer`, call `shutdown().await`; its `drain_timeout` controls graceful TCP HTTP completion and the QUIC drain period before remaining connection tasks are terminated.
|
Stop accepting new connections, reject new work at the application layer, and allow active requests and pipe writers to finish. For `MTPWebServer`, call `shutdown()`; its `drain_timeout` controls graceful TCP HTTP completion and the QUIC drain period before remaining connection tasks are terminated.
|
||||||
|
|
|
||||||
|
|
@ -167,9 +167,6 @@ if let Some(writer) = handle.wait().await? {
|
||||||
```rust
|
```rust
|
||||||
// Host
|
// Host
|
||||||
use mtp_transport::{PipeSessionParameters, accept_pipe_session};
|
use mtp_transport::{PipeSessionParameters, accept_pipe_session};
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
|
|
||||||
// The streaming digest below requires `sha2` as a direct application dependency.
|
|
||||||
|
|
||||||
while let Ok(request) = conn.receive_pipe().await {
|
while let Ok(request) = conn.receive_pipe().await {
|
||||||
if request.description() != "file-upload" {
|
if request.description() != "file-upload" {
|
||||||
|
|
@ -185,7 +182,7 @@ while let Ok(request) = conn.receive_pipe().await {
|
||||||
let mut reader = accept_pipe_session(
|
let mut reader = accept_pipe_session(
|
||||||
reader.into_inner(), ¶ms, &own_keyring, &client_public_bundle,
|
reader.into_inner(), ¶ms, &own_keyring, &client_public_bundle,
|
||||||
).await?;
|
).await?;
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = sha2::Sha256::new();
|
||||||
while let Some(chunk) = reader.read_record().await? {
|
while let Some(chunk) = reader.read_record().await? {
|
||||||
hasher.update(&chunk);
|
hasher.update(&chunk);
|
||||||
process_chunk(&chunk).await?;
|
process_chunk(&chunk).await?;
|
||||||
|
|
|
||||||
|
|
@ -112,9 +112,4 @@ identity-specific response for deployments where IDs are public.
|
||||||
|
|
||||||
The client sends one compiled-in protocol version. The host compares it with the versions in its registry and returns the selected version in the opening response. Subsequent frames use that version's type map. An unsupported version closes the connection with `AcceptError::UnsupportedVersion`.
|
The client sends one compiled-in protocol version. The host compares it with the versions in its registry and returns the selected version in the opening response. Subsequent frames use that version's type map. An unsupported version closes the connection with `AcceptError::UnsupportedVersion`.
|
||||||
|
|
||||||
The current self-delimiting `DataValue` codec and three-bit communication header
|
The self-delimiting `DataValue` codec and the three-bit communication header begin at protocol version `3.0`. A peer offering an older codec version is rejected during version negotiation; the new decoder does not attempt legacy flag, ID, or crypto-container fallbacks.
|
||||||
are used by the repository's protocol 3.0 map. The checked-in builtin registry
|
|
||||||
contains only 3.0, so its native clients and hosts do not provide legacy map
|
|
||||||
fallbacks. Type-map versions are configuration-driven; a custom registry may
|
|
||||||
register another version number, but its map must use the current codec format
|
|
||||||
and is not a fallback for a different legacy wire format.
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ For rotation, publish the replacement certificate or key before changing the ser
|
||||||
|
|
||||||
### Development Certificates
|
### Development Certificates
|
||||||
|
|
||||||
The `tls` feature exposes `mtp_crypto::tls::generate_self_signed_cert`. It creates an ECDSA P-256 server certificate for the requested domain, `127.0.0.1`, and `::1`; the certificate is valid for 13 days. The lower-level `mtp_transport::HostConfig::self_signed` provides a transport-level self-signed setup without the crypto certificate helper.
|
The `tls` feature exposes `mtp_crypto::tls::generate_self_signed_cert`. It creates an ECDSA P-256 server certificate for the requested domain, `127.0.0.1`, and `::1`; the certificate is valid for 13 days. `HostConfig::self_signed` provides a transport-level self-signed setup without the crypto certificate helper.
|
||||||
|
|
||||||
Self-signed certificates are for development. Production deployments should use a certificate trusted by the client or an explicitly pinned certificate.
|
Self-signed certificates are for development. Production deployments should use a certificate trusted by the client or an explicitly pinned certificate.
|
||||||
|
|
||||||
|
|
@ -212,7 +212,6 @@ The crate's feature groups are:
|
||||||
| `serde` | Serialization support for key types |
|
| `serde` | Serialization support for key types |
|
||||||
| `wasm` | `getrandom` support for WebAssembly |
|
| `wasm` | `getrandom` support for WebAssembly |
|
||||||
| `tls` | Development certificate generation |
|
| `tls` | Development certificate generation |
|
||||||
| `password-kdf` | Argon2id password derivation for protected keyring files |
|
|
||||||
|
|
||||||
The main types are `Keyring`, `PublicKeyBundle`, `EncryptionType`, `HybridKem`, `XChaCha20Poly1305` (with the legacy `ChaCha20Poly1305` alias), `Aes256Gcm`, `Ed25519Signer`, and `MlDsaSigner`. Hashing and KDF helpers include `sha256`, `sha256_double`, `hkdf_extract`, `hkdf_expand`, and `derive_encryption_key`. Handshake payload builders are in `mtp_crypto::auth`.
|
The main types are `Keyring`, `PublicKeyBundle`, `EncryptionType`, `HybridKem`, `XChaCha20Poly1305` (with the legacy `ChaCha20Poly1305` alias), `Aes256Gcm`, `Ed25519Signer`, and `MlDsaSigner`. Hashing and KDF helpers include `sha256`, `sha256_double`, `hkdf_extract`, `hkdf_expand`, and `derive_encryption_key`. Handshake payload builders are in `mtp_crypto::auth`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ When `require_pq` is true, both Ed25519 and ML-DSA-65 keys and signatures must b
|
||||||
|
|
||||||
**Prevention:** Treat generated type maps as versioned build artifacts.
|
**Prevention:** Treat generated type maps as versioned build artifacts.
|
||||||
|
|
||||||
`CodecError::UnknownVersion` means the codec was created for a version absent from its registry. `UnknownCommunicationType` and `UnknownDataType` mean the selected `TypeMap` has no mapping for the value being encoded. `MissingTypeMap` means a versioned value lost its retained negotiated map; `TypeMapMismatch` means it was combined with a value or codec for another version. Select the negotiated type map and do not send an unmapped variant.
|
`CodecError::UnknownVersion` means the codec was created for a version absent from its registry. `UnknownCommunicationType` and `UnknownDataType` mean the selected `TypeMap` has no mapping for the value being encoded. Select the negotiated type map and do not send an unmapped variant.
|
||||||
|
|
||||||
`ReservedCommunicationType` means application code attempted to use a reserved wire ID. Use generated communication types instead of assigning protocol IDs manually. `MissingField` means a required typed field was not present.
|
`ReservedCommunicationType` means application code attempted to use a reserved wire ID. Use generated communication types instead of assigning protocol IDs manually. `MissingField` means a required typed field was not present.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,8 @@
|
||||||
# Type Map
|
# Type Map
|
||||||
|
|
||||||
This file documents the type-map and registry configuration used by MTP. The
|
This file documents the Type Map & Registry configuration used by the MTP protocol. It will assume you are working with the [example-type-maps.yaml](./../example-type-maps.yaml).
|
||||||
repository workspace uses [`example/type-maps.yaml`](../example/type-maps.yaml)
|
A type musn't be the version of MTP, it stays independant.
|
||||||
through [`.cargo/config.toml`](../.cargo/config.toml); that map currently
|
MTP version defines the codec. The Type-Map version defines the available Types.
|
||||||
selects protocol version 3.0. The root [`example-type-maps.yaml`](../example-type-maps.yaml)
|
|
||||||
is a separate illustrative multi-version configuration used by the manual WASM
|
|
||||||
build script. Downstream applications should provide their own map.
|
|
||||||
|
|
||||||
The protocol version selects the generated codec/type-map build, while the
|
|
||||||
type-map entries define the available application types and their IDs.
|
|
||||||
|
|
||||||
## Binary Frame Format
|
## Binary Frame Format
|
||||||
|
|
||||||
|
|
@ -137,7 +131,7 @@ After editing the config and rebuilding, `CommunicationType` and `DataType` enum
|
||||||
use mtp::type_map::{CommunicationType, DataType, TypeMap};
|
use mtp::type_map::{CommunicationType, DataType, TypeMap};
|
||||||
|
|
||||||
let tm = TypeMap::v3_0();
|
let tm = TypeMap::v3_0();
|
||||||
let id = tm.data_id_enum(DataType::ExampleText).unwrap();
|
let id = tm.data_id_enum(DataType::SomeType).unwrap();
|
||||||
```
|
```
|
||||||
|
|
||||||
For native builds with the `registry` feature, the enums are a **union across
|
For native builds with the `registry` feature, the enums are a **union across
|
||||||
|
|
@ -151,31 +145,30 @@ compiled by the Vite plugin.
|
||||||
Encoding/decoding uses a `TypeMap` to resolve type names to wire IDs:
|
Encoding/decoding uses a `TypeMap` to resolve type names to wire IDs:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
use mtp::codec::{encode, decode, DataValue};
|
||||||
use mtp::type_map::TypeMap;
|
use mtp::type_map::TypeMap;
|
||||||
|
|
||||||
let tm = TypeMap::v3_0();
|
let tm = TypeMap::v2_0();
|
||||||
let value = CommunicationValue::new_with_type_map(CommunicationType::Ping, &tm)
|
let value = DataValue::Str("hello".into());
|
||||||
.add_typed(DataType::Description, &tm, DataValue::Str("hello".into()));
|
|
||||||
|
|
||||||
let bytes = value.to_bytes().unwrap();
|
let bytes = encode(&value, &tm).unwrap();
|
||||||
let decoded = CommunicationValue::from_bytes_with(&bytes, &tm).unwrap();
|
let decoded = decode(&bytes, &tm).unwrap();
|
||||||
```
|
```
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
let tm_v3 = TypeMap::v3_0();
|
let tm_v3 = TypeMap::v3_0();
|
||||||
assert!(tm_v3.data_id_enum(DataType::ExampleText).is_some());
|
assert!(tm_v3.data_id_enum(DataType::SomeType).is_some());
|
||||||
```
|
```
|
||||||
|
|
||||||
When communicating with a peer on another version, encode only variants that map in the negotiated version. If an incoming frame names a type absent from the selected map, reject it as a protocol or type-map compatibility error; do not reinterpret its wire ID using another version's map. The current repository map uses the self-delimiting codec format for protocol version `3.0`; a custom registry may register other version numbers, but those maps are not legacy wire-format fallbacks.
|
When communicating with a peer on another version, encode only variants that map in the negotiated version. If an incoming frame names a type absent from the selected map, reject it as a protocol or type-map compatibility error; do not reinterpret its wire ID using another version's map. The self-delimiting codec begins at protocol version `3.0`; older versions are not codec fallbacks.
|
||||||
|
|
||||||
### Forward/Backward Compatibility Between Versions
|
### Forward/Backward Compatibility Between Versions
|
||||||
|
|
||||||
Because enums are a union of all types across versions, a variant might exist that has no wire mapping in the *negotiated* version:
|
Because enums are a union of all types across versions, a variant might exist that has no wire mapping in the *negotiated* version:
|
||||||
|
|
||||||
```
|
```
|
||||||
v3.0 client sends DataType::ExampleText → host encodes with v3.0 TypeMap → wire ID 43
|
v3.0 client sends DataType::SomeType → host encodes with v3.0 TypeMap → wire ID 32
|
||||||
v3.0 host receives a version absent from the registry → version negotiation error
|
v3.0 host receives an unsupported pre-v3.0 peer → version negotiation error
|
||||||
```
|
```
|
||||||
|
|
||||||
Encoding a frame with an unmapped communication or data type returns `CodecError::UnknownCommunicationType` or `CodecError::UnknownDataType`. Select a mapped variant from the compiled-in version before sending it.
|
Encoding a frame with an unmapped communication or data type returns `CodecError::UnknownCommunicationType` or `CodecError::UnknownDataType`. Select a mapped variant from the compiled-in version before sending it.
|
||||||
|
|
|
||||||
|
|
@ -602,19 +602,8 @@ The SDK logger receives parsed events:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
type MTPLogEvent =
|
type MTPLogEvent =
|
||||||
| {
|
| { hint: "info" | "warning"; type: string; data: unknown }
|
||||||
hint: "info" | "warning";
|
| { hint: "error"; type: string | "error"; error: string };
|
||||||
type: string;
|
|
||||||
data: unknown;
|
|
||||||
direction?: "send" | "recv";
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
hint: "error";
|
|
||||||
type: string | "error";
|
|
||||||
error: string;
|
|
||||||
data?: unknown;
|
|
||||||
direction?: "send" | "recv";
|
|
||||||
};
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Incoming non-error frames and sent frames are logged as `info`. Error frames and transport errors are logged as `error`.
|
Incoming non-error frames and sent frames are logged as `info`. Error frames and transport errors are logged as `error`.
|
||||||
|
|
|
||||||
3
example/Cargo.lock
generated
3
example/Cargo.lock
generated
|
|
@ -1314,7 +1314,6 @@ dependencies = [
|
||||||
name = "mtp-crypto"
|
name = "mtp-crypto"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
|
|
@ -1338,6 +1337,7 @@ dependencies = [
|
||||||
name = "mtp-files"
|
name = "mtp-files"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"argon2",
|
||||||
"mtp-crypto",
|
"mtp-crypto",
|
||||||
"rand",
|
"rand",
|
||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
|
|
@ -1353,7 +1353,6 @@ dependencies = [
|
||||||
"mtp-crypto",
|
"mtp-crypto",
|
||||||
"mtp-transport",
|
"mtp-transport",
|
||||||
"rand",
|
"rand",
|
||||||
"thiserror 2.0.20",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"wtransport",
|
"wtransport",
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ pub fn build_demo_message(
|
||||||
)
|
)
|
||||||
.add_typed_default(
|
.add_typed_default(
|
||||||
DataType::Timestamp,
|
DataType::Timestamp,
|
||||||
DataValue::UnsignedNumber(timestamp),
|
DataValue::UnsignedNumber(timestamp as u128),
|
||||||
)
|
)
|
||||||
.add_typed_default(DataType::Data, DataValue::Str("Hello, MTP!".into()))
|
.add_typed_default(DataType::Data, DataValue::Str("Hello, MTP!".into()))
|
||||||
.add_typed_default(DataType::Flags, DataValue::BoolTrue)
|
.add_typed_default(DataType::Flags, DataValue::BoolTrue)
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,6 @@ pub struct ClientMetrics {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientMetrics {
|
impl ClientMetrics {
|
||||||
#[cfg(test)]
|
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
sessions: Vec::new(),
|
sessions: Vec::new(),
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ fn pong(tm: &TypeMap, data: impl Into<String>) -> Result<CommunicationValue, Str
|
||||||
CommunicationValue::from_comm(CommunicationType::Pong, tm)
|
CommunicationValue::from_comm(CommunicationType::Pong, tm)
|
||||||
.add_data(desc_id, DataValue::Str("MTP example response".into()))
|
.add_data(desc_id, DataValue::Str("MTP example response".into()))
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?
|
||||||
.add_data(ts_id, DataValue::UnsignedNumber(now))
|
.add_data(ts_id, DataValue::UnsignedNumber(now as u128))
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?
|
||||||
.add_data(data_id, DataValue::Str(data.into()))
|
.add_data(data_id, DataValue::Str(data.into()))
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
|
|
@ -388,7 +388,7 @@ pub fn process_and_respond(
|
||||||
let response = CommunicationValue::from_comm(CommunicationType::Pong, tm)
|
let response = CommunicationValue::from_comm(CommunicationType::Pong, tm)
|
||||||
.add_data(desc_id, description)
|
.add_data(desc_id, description)
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?
|
||||||
.add_data(ts_id, DataValue::UnsignedNumber(now))
|
.add_data(ts_id, DataValue::UnsignedNumber(now as u128))
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?
|
||||||
.add_data(
|
.add_data(
|
||||||
data_id,
|
data_id,
|
||||||
|
|
|
||||||
|
|
@ -100,11 +100,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
serde_json::to_string_pretty(&*db).ok()
|
serde_json::to_string_pretty(&*db).ok()
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(json) = json
|
if let Some(json) = json {
|
||||||
&& let Err(error) = tokio::fs::write("clients.json", json).await
|
if let Err(error) = tokio::fs::write("clients.json", json).await {
|
||||||
{
|
|
||||||
eprintln!("Failed to persist clients.json: {error}");
|
eprintln!("Failed to persist clients.json: {error}");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
println!("Registered new client with ID: {id}");
|
println!("Registered new client with ID: {id}");
|
||||||
id
|
id
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,6 @@ pub struct ServerMetrics {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerMetrics {
|
impl ServerMetrics {
|
||||||
#[cfg(test)]
|
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
inner: Mutex::new(Inner {
|
inner: Mutex::new(Inner {
|
||||||
|
|
@ -219,7 +218,6 @@ impl ServerMetrics {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub fn snapshot(&self) -> ServerMetricsFile {
|
pub fn snapshot(&self) -> ServerMetricsFile {
|
||||||
let inner = self.inner.lock().unwrap();
|
let inner = self.inner.lock().unwrap();
|
||||||
self.to_file(&inner)
|
self.to_file(&inner)
|
||||||
|
|
@ -555,11 +553,11 @@ mod tests {
|
||||||
let metrics = ServerMetrics::new();
|
let metrics = ServerMetrics::new();
|
||||||
|
|
||||||
for i in 0..3 {
|
for i in 0..3 {
|
||||||
let mut session = metrics.start_session(1000 + i, format!("session {i}"));
|
let mut session = metrics.start_session(1000 + i as u64, format!("session {i}"));
|
||||||
for _ in 0..(i + 1) * 2 {
|
for _ in 0..(i + 1) * 2 {
|
||||||
session.record_message(Duration::from_millis(1 + i), true);
|
session.record_message(Duration::from_millis(1 + i), true);
|
||||||
}
|
}
|
||||||
session.record_pipe((i + 1) * 1000);
|
session.record_pipe((i as u64 + 1) * 1000);
|
||||||
session.finish(format!("exit {i}"));
|
session.finish(format!("exit {i}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -251,8 +251,6 @@ pub struct HostConfig {
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
pub authentication_policy: AuthenticationPolicy,
|
pub authentication_policy: AuthenticationPolicy,
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
authentication_policy_explicit: bool,
|
|
||||||
#[cfg(feature = "crypto")]
|
|
||||||
pub auth_timeout: Duration,
|
pub auth_timeout: Duration,
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
pub require_pq: bool,
|
pub require_pq: bool,
|
||||||
|
|
@ -290,8 +288,6 @@ impl HostConfig {
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
authentication_policy: AuthenticationPolicy::Unauthenticated,
|
authentication_policy: AuthenticationPolicy::Unauthenticated,
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
authentication_policy_explicit: false,
|
|
||||||
#[cfg(feature = "crypto")]
|
|
||||||
auth_timeout: Duration::from_secs(30),
|
auth_timeout: Duration::from_secs(30),
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
require_pq: true,
|
require_pq: true,
|
||||||
|
|
@ -345,9 +341,7 @@ impl HostConfig {
|
||||||
get_existing_client: GetExistingClient,
|
get_existing_client: GetExistingClient,
|
||||||
complete_register: CompleteRegister,
|
complete_register: CompleteRegister,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
if !self.authentication_policy_explicit {
|
|
||||||
self.authentication_policy = AuthenticationPolicy::ForceAuthentication;
|
self.authentication_policy = AuthenticationPolicy::ForceAuthentication;
|
||||||
}
|
|
||||||
self.host_keyring = host_keyring;
|
self.host_keyring = host_keyring;
|
||||||
self.get_existing_client = Box::new(get_existing_client);
|
self.get_existing_client = Box::new(get_existing_client);
|
||||||
self.complete_register = Box::new(complete_register);
|
self.complete_register = Box::new(complete_register);
|
||||||
|
|
@ -357,7 +351,6 @@ impl HostConfig {
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
pub fn with_authentication_policy(mut self, policy: AuthenticationPolicy) -> Self {
|
pub fn with_authentication_policy(mut self, policy: AuthenticationPolicy) -> Self {
|
||||||
self.authentication_policy = policy;
|
self.authentication_policy = policy;
|
||||||
self.authentication_policy_explicit = true;
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -448,78 +441,4 @@ mod tests {
|
||||||
.expect("repeated registration decision")
|
.expect("repeated registration decision")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_keyring() -> mtp_crypto::Keyring {
|
|
||||||
mtp_crypto::Keyring::new(
|
|
||||||
mtp_crypto::KemPublicKey::new(Vec::new()),
|
|
||||||
mtp_crypto::KemPrivateKey::new(Vec::new()),
|
|
||||||
mtp_crypto::SignaturePqPublicKey::new(Vec::new()),
|
|
||||||
mtp_crypto::SignaturePqPrivateKey::new(Vec::new()),
|
|
||||||
mtp_crypto::SignaturePublicKey::new(Vec::new()),
|
|
||||||
mtp_crypto::SignaturePrivateKey::new(Vec::new()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_get_existing_client() -> GetExistingClient {
|
|
||||||
Box::new(|_, _| Box::pin(async { None }))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_complete_register() -> CompleteRegister {
|
|
||||||
Box::new(|_, _| Box::pin(async { 1 }))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_config() -> HostConfig {
|
|
||||||
HostConfig::new(
|
|
||||||
IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
|
|
||||||
4433,
|
|
||||||
Vec::new(),
|
|
||||||
Vec::new(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn with_authentication_defaults_to_force_authentication() {
|
|
||||||
let config = test_config().with_authentication(
|
|
||||||
test_keyring(),
|
|
||||||
test_get_existing_client(),
|
|
||||||
test_complete_register(),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
config.authentication_policy,
|
|
||||||
AuthenticationPolicy::ForceAuthentication
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn explicit_authentication_policy_before_with_authentication_is_preserved() {
|
|
||||||
let config = test_config()
|
|
||||||
.with_authentication_policy(AuthenticationPolicy::AllowAuthentication)
|
|
||||||
.with_authentication(
|
|
||||||
test_keyring(),
|
|
||||||
test_get_existing_client(),
|
|
||||||
test_complete_register(),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
config.authentication_policy,
|
|
||||||
AuthenticationPolicy::AllowAuthentication
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn explicit_authentication_policy_after_with_authentication_is_preserved() {
|
|
||||||
let config = test_config()
|
|
||||||
.with_authentication(
|
|
||||||
test_keyring(),
|
|
||||||
test_get_existing_client(),
|
|
||||||
test_complete_register(),
|
|
||||||
)
|
|
||||||
.with_authentication_policy(AuthenticationPolicy::AllowAuthentication);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
config.authentication_policy,
|
|
||||||
AuthenticationPolicy::AllowAuthentication
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
30
host/src/engine.rs
Executable file → Normal file
30
host/src/engine.rs
Executable file → Normal file
|
|
@ -217,12 +217,6 @@ impl HandshakeEngine {
|
||||||
_authentication_context: &AuthenticationContext,
|
_authentication_context: &AuthenticationContext,
|
||||||
) -> Result<HandshakeResult, AcceptError> {
|
) -> Result<HandshakeResult, AcceptError> {
|
||||||
let mut first_msg = receiver.receive().await.map_err(AcceptError::Receive)?;
|
let mut first_msg = receiver.receive().await.map_err(AcceptError::Receive)?;
|
||||||
tracing::debug!(
|
|
||||||
message_type = ?first_msg.get_type(),
|
|
||||||
version = ?first_msg.get_str(DataType::Version),
|
|
||||||
client_id = ?first_msg.get_data(DataType::Id),
|
|
||||||
"received MTP opening message"
|
|
||||||
);
|
|
||||||
|
|
||||||
let version_str = match first_msg.get_data(DataType::Version) {
|
let version_str = match first_msg.get_data(DataType::Version) {
|
||||||
Some(DataValue::Str(s)) => s.clone(),
|
Some(DataValue::Str(s)) => s.clone(),
|
||||||
|
|
@ -277,11 +271,6 @@ impl HandshakeEngine {
|
||||||
return Err(AcceptError::UnsupportedVersion(client_version));
|
return Err(AcceptError::UnsupportedVersion(client_version));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
tracing::debug!(
|
|
||||||
client_version = %client_version,
|
|
||||||
negotiated_version = %negotiated,
|
|
||||||
"MTP protocol version negotiated"
|
|
||||||
);
|
|
||||||
|
|
||||||
let codec = VersionedCodec::for_version(self.registry.clone(), negotiated.clone())
|
let codec = VersionedCodec::for_version(self.registry.clone(), negotiated.clone())
|
||||||
.ok_or_else(|| AcceptError::UnsupportedVersion(negotiated.clone()))?;
|
.ok_or_else(|| AcceptError::UnsupportedVersion(negotiated.clone()))?;
|
||||||
|
|
@ -309,12 +298,6 @@ impl HandshakeEngine {
|
||||||
) || registration
|
) || registration
|
||||||
|| first_msg.get_data(DataType::PublicKeys).is_some()
|
|| first_msg.get_data(DataType::PublicKeys).is_some()
|
||||||
|| claimed_client_id.is_some_and(|client_id| client_id != 0);
|
|| claimed_client_id.is_some_and(|client_id| client_id != 0);
|
||||||
tracing::info!(
|
|
||||||
claimed_client_id = ?claimed_client_id,
|
|
||||||
registration,
|
|
||||||
authentication_requested,
|
|
||||||
"classified MTP opening authentication mode"
|
|
||||||
);
|
|
||||||
if authentication_requested {
|
if authentication_requested {
|
||||||
let attempt = crate::config::AuthenticationAttempt {
|
let attempt = crate::config::AuthenticationAttempt {
|
||||||
peer_network_identity: _authentication_context.peer_network_identity.clone(),
|
peer_network_identity: _authentication_context.peer_network_identity.clone(),
|
||||||
|
|
@ -572,7 +555,6 @@ impl HandshakeEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown or zero ID: fall back to guest
|
// Unknown or zero ID: fall back to guest
|
||||||
tracing::info!("allocating MTP guest identity");
|
|
||||||
let guest_id_lease = match self.assign_guest_id().await {
|
let guest_id_lease = match self.assign_guest_id().await {
|
||||||
Ok(lease) => lease,
|
Ok(lease) => lease,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
|
|
@ -581,7 +563,6 @@ impl HandshakeEngine {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let guest_id = guest_id_lease.id;
|
let guest_id = guest_id_lease.id;
|
||||||
tracing::info!(guest_id, "allocated MTP guest identity");
|
|
||||||
send_accepted_generic(sender, &negotiated, tm, Some(guest_id))
|
send_accepted_generic(sender, &negotiated, tm, Some(guest_id))
|
||||||
.await
|
.await
|
||||||
.map_err(AcceptError::Send)?;
|
.map_err(AcceptError::Send)?;
|
||||||
|
|
@ -1124,12 +1105,6 @@ async fn send_rejection_generic<S: HandshakeSender>(
|
||||||
.add_typed_default(DataType::Connected, DataValue::BoolFalse)
|
.add_typed_default(DataType::Connected, DataValue::BoolFalse)
|
||||||
.add_typed_default(DataType::ErrorMessage, DataValue::Str(reason.to_string())),
|
.add_typed_default(DataType::ErrorMessage, DataValue::Str(reason.to_string())),
|
||||||
};
|
};
|
||||||
tracing::debug!(
|
|
||||||
reason = %reason,
|
|
||||||
response_type = ?response.get_type(),
|
|
||||||
has_version = response.get_data(DataType::Version).is_some(),
|
|
||||||
"sending MTP handshake rejection"
|
|
||||||
);
|
|
||||||
let _ = sender.send(&response).await;
|
let _ = sender.send(&response).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1163,11 +1138,6 @@ async fn send_accepted_generic<S: HandshakeSender>(
|
||||||
if let Some(id) = assigned_id {
|
if let Some(id) = assigned_id {
|
||||||
response = response.add_typed_default(DataType::Id, DataValue::UnsignedNumber(id as u128));
|
response = response.add_typed_default(DataType::Id, DataValue::UnsignedNumber(id as u128));
|
||||||
}
|
}
|
||||||
tracing::debug!(
|
|
||||||
version = %version,
|
|
||||||
assigned_id = ?assigned_id,
|
|
||||||
"sending accepted MTP handshake response"
|
|
||||||
);
|
|
||||||
sender.send(&response).await?;
|
sender.send(&response).await?;
|
||||||
sender.finish_stream().await
|
sender.finish_stream().await
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -143,11 +143,6 @@ pub(crate) async fn run_driver(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
tracing::debug!(
|
|
||||||
remote = %remote_addr,
|
|
||||||
session_id = ?session.session_id(),
|
|
||||||
"accepted WebTransport MTP session"
|
|
||||||
);
|
|
||||||
tokio::spawn(run_session_requests(
|
tokio::spawn(run_session_requests(
|
||||||
session.clone(),
|
session.clone(),
|
||||||
router.clone(),
|
router.clone(),
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,6 @@ pub struct H3TransportSender {
|
||||||
|
|
||||||
pub struct H3TransportReceiver {
|
pub struct H3TransportReceiver {
|
||||||
stream: H3RecvStream,
|
stream: H3RecvStream,
|
||||||
quinn: quinn::Connection,
|
|
||||||
read_exact_calls: u64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl H3TransportConnection {
|
impl H3TransportConnection {
|
||||||
|
|
@ -78,43 +76,18 @@ impl TransportSendStream for H3TransportSender {
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl TransportRecvStream for H3TransportReceiver {
|
impl TransportRecvStream for H3TransportReceiver {
|
||||||
async fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), CommunicationError> {
|
async fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), CommunicationError> {
|
||||||
let first_read = self.read_exact_calls == 0;
|
|
||||||
self.read_exact_calls += 1;
|
|
||||||
self.stream
|
self.stream
|
||||||
.read_exact(buf)
|
.read_exact(buf)
|
||||||
.await
|
.await
|
||||||
.map(|_| {
|
.map(|_| ())
|
||||||
if first_read {
|
|
||||||
tracing::debug!(
|
|
||||||
remote = %self.quinn.remote_address(),
|
|
||||||
bytes = buf.len(),
|
|
||||||
header = ?buf,
|
|
||||||
"received first bytes from WebTransport MTP stream"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map_err(|error| {
|
.map_err(|error| {
|
||||||
if error.kind() == std::io::ErrorKind::UnexpectedEof
|
if error.kind() == std::io::ErrorKind::UnexpectedEof {
|
||||||
|| self.quinn.close_reason().is_some()
|
// Browser control frames are sent on one-frame uni streams.
|
||||||
{
|
// Reaching FIN while looking for another frame is normal.
|
||||||
/*
|
|
||||||
* Reaching FIN, or losing the enclosing QUIC connection,
|
|
||||||
* is a normal stream-closure path. Do not turn it into a
|
|
||||||
* frame-header failure and close the connection again.
|
|
||||||
*/
|
|
||||||
return CommunicationError::StreamClosed;
|
return CommunicationError::StreamClosed;
|
||||||
}
|
}
|
||||||
error!(
|
error!("[mtp-webserver] receive stream read_exact failed ({} bytes): {error}", buf.len());
|
||||||
"[mtp-webserver] receive stream read_exact failed ({} bytes): {error}",
|
tracing::warn!(len = buf.len(), %error, "WebTransport receive stream read_exact failed");
|
||||||
buf.len()
|
|
||||||
);
|
|
||||||
tracing::warn!(
|
|
||||||
remote = %self.quinn.remote_address(),
|
|
||||||
first_read,
|
|
||||||
len = buf.len(),
|
|
||||||
%error,
|
|
||||||
"WebTransport receive stream read_exact failed"
|
|
||||||
);
|
|
||||||
CommunicationError::StreamError
|
CommunicationError::StreamError
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -128,9 +101,6 @@ impl TransportRecvStream for H3TransportReceiver {
|
||||||
Ok(Some(buf))
|
Ok(Some(buf))
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
if self.quinn.close_reason().is_some() {
|
|
||||||
return Err(CommunicationError::StreamClosed);
|
|
||||||
}
|
|
||||||
error!(
|
error!(
|
||||||
"[mtp-webserver] receive stream read failed (max {} bytes): {error}",
|
"[mtp-webserver] receive stream read failed (max {} bytes): {error}",
|
||||||
max
|
max
|
||||||
|
|
@ -197,27 +167,10 @@ impl TransportConnection for H3TransportConnection {
|
||||||
loop {
|
loop {
|
||||||
match self.session.accept_uni().await {
|
match self.session.accept_uni().await {
|
||||||
Ok(Some((id, stream))) if id == self.session.session_id() => {
|
Ok(Some((id, stream))) if id == self.session.session_id() => {
|
||||||
let stream_id = h3::quic::RecvStream::recv_id(&stream);
|
return Ok(H3TransportReceiver { stream });
|
||||||
tracing::debug!(
|
|
||||||
remote = %self.quinn.remote_address(),
|
|
||||||
session_id = ?self.session.session_id(),
|
|
||||||
stream_id = ?stream_id,
|
|
||||||
"accepted WebTransport MTP receive stream"
|
|
||||||
);
|
|
||||||
return Ok(H3TransportReceiver {
|
|
||||||
stream,
|
|
||||||
quinn: self.quinn.clone(),
|
|
||||||
read_exact_calls: 0,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
Ok(Some((stream_session_id, _stream))) => {
|
Ok(Some(_)) => {
|
||||||
consecutive_errors = 0;
|
consecutive_errors = 0;
|
||||||
tracing::debug!(
|
|
||||||
remote = %self.quinn.remote_address(),
|
|
||||||
session_id = ?self.session.session_id(),
|
|
||||||
stream_session_id = ?stream_session_id,
|
|
||||||
"ignored WebTransport receive stream belonging to another session"
|
|
||||||
);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Ok(None) => return Err(CommunicationError::StreamClosed),
|
Ok(None) => return Err(CommunicationError::StreamClosed),
|
||||||
|
|
@ -353,18 +306,7 @@ async fn accept_web_connection_inner(
|
||||||
connection_id,
|
connection_id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await;
|
.await?;
|
||||||
#[cfg(feature = "crypto")]
|
|
||||||
if let Err(error) = &result {
|
|
||||||
tracing::warn!(
|
|
||||||
remote = %remote_addr,
|
|
||||||
connection_id,
|
|
||||||
%error,
|
|
||||||
"WebTransport MTP handshake failed"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
#[cfg(feature = "crypto")]
|
|
||||||
let result = result?;
|
|
||||||
#[cfg(not(feature = "crypto"))]
|
#[cfg(not(feature = "crypto"))]
|
||||||
let result = engine.accept(&sender, &receiver).await?;
|
let result = engine.accept(&sender, &receiver).await?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@
|
||||||
"pack": "pnpm run release:web",
|
"pack": "pnpm run release:web",
|
||||||
"release:web": "node create-web-release.mjs",
|
"release:web": "node create-web-release.mjs",
|
||||||
"build:all": "nix run .#build-all",
|
"build:all": "nix run .#build-all",
|
||||||
"dup": "jscpd --pattern '**/*.{rs,ts}' --ignore 'target/**' --ignore 'wasm/pkg/**' --min-lines 8 --min-tokens 80 --threshold 4 --reporters console --no-tips .",
|
"dup": "jscpd --pattern '**/*.{rs,ts}' --ignore 'target/**' --ignore 'wasm/pkg/**' --ignore '.git/**' --min-lines 8 --min-tokens 80 --threshold 4 --reporters console --noTips .",
|
||||||
"test:e2e": "tsc && node test/e2ee.mjs",
|
"test:e2e": "tsc && node test/e2ee.mjs",
|
||||||
"test:secrets": "tsc && node --test --test-isolation=none test/encrypted-secret.mjs",
|
"test:secrets": "tsc && node --test --test-isolation=none test/encrypted-secret.mjs",
|
||||||
"test:wasm-init": "tsc && node --test test/wasm-init.mjs",
|
"test:wasm-init": "tsc && node --test test/wasm-init.mjs",
|
||||||
|
|
@ -72,7 +72,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^26.0.1",
|
"@types/node": "^26.0.1",
|
||||||
"jscpd": "5.0.15",
|
"jscpd": "5.0.14",
|
||||||
"typescript": "^7.0.0"
|
"typescript": "^7.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
935
pnpm-lock.yaml
generated
935
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -9,7 +9,7 @@ use crate::{
|
||||||
connection::{DecodeRejectionCounters, RuntimePolicy, classify_decode_error},
|
connection::{DecodeRejectionCounters, RuntimePolicy, classify_decode_error},
|
||||||
framing::{RetryClassifier, write_frame},
|
framing::{RetryClassifier, write_frame},
|
||||||
};
|
};
|
||||||
use mtp_codec::{CommunicationValue, DataType, DecodeLimits, TypeMap};
|
use mtp_codec::{CommunicationValue, DecodeLimits, TypeMap};
|
||||||
use mtp_common::CommunicationError;
|
use mtp_common::CommunicationError;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
@ -67,15 +67,6 @@ impl<C: TransportConnection> GenericSender<C> {
|
||||||
if self.connection.close_reason().is_some() {
|
if self.connection.close_reason().is_some() {
|
||||||
return Err(CommunicationError::StreamClosed);
|
return Err(CommunicationError::StreamClosed);
|
||||||
}
|
}
|
||||||
if let Some(version) = value.get_str(DataType::Version) {
|
|
||||||
tracing::debug!(
|
|
||||||
message_type = ?value.get_type(),
|
|
||||||
version,
|
|
||||||
connected = ?value.get_data(DataType::Connected),
|
|
||||||
client_id = ?value.get_data(DataType::Id),
|
|
||||||
"sending MTP handshake response frame"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
match self.policy.send_mode {
|
match self.policy.send_mode {
|
||||||
crate::SendMode::SingleStreamPerMessage => {
|
crate::SendMode::SingleStreamPerMessage => {
|
||||||
let mut stream = self.open().await?;
|
let mut stream = self.open().await?;
|
||||||
|
|
@ -342,18 +333,6 @@ impl<C: TransportConnection> GenericReceiver<C> {
|
||||||
break 'stream;
|
break 'stream;
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
if frames == 0 {
|
|
||||||
tracing::warn!(
|
|
||||||
timeout = ?policy.read_timeout,
|
|
||||||
"MTP receive stream timed out before its first complete frame"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
tracing::debug!(
|
|
||||||
frames,
|
|
||||||
timeout = ?policy.read_timeout,
|
|
||||||
"MTP receive stream idle timeout"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -397,9 +376,6 @@ impl<C: TransportConnection> GenericReceiver<C> {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
if !matches!(&body_read, Ok(Ok(()))) {
|
if !matches!(&body_read, Ok(Ok(()))) {
|
||||||
if matches!(&body_read, Ok(Err(CommunicationError::StreamClosed))) {
|
|
||||||
break 'stream;
|
|
||||||
}
|
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
pipe_chunk_len = chunk_len,
|
pipe_chunk_len = chunk_len,
|
||||||
?body_read,
|
?body_read,
|
||||||
|
|
@ -432,12 +408,6 @@ impl<C: TransportConnection> GenericReceiver<C> {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
tracing::debug!(
|
|
||||||
frames,
|
|
||||||
frame_len,
|
|
||||||
message_type = ?message.get_type(),
|
|
||||||
"decoded MTP receive frame"
|
|
||||||
);
|
|
||||||
let negotiated_type_map = type_map.read().await.clone();
|
let negotiated_type_map = type_map.read().await.clone();
|
||||||
message.set_type_map(&negotiated_type_map);
|
message.set_type_map(&negotiated_type_map);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,6 @@ use crate::config::ConnectionConfig;
|
||||||
use crate::error::js_error;
|
use crate::error::js_error;
|
||||||
use crate::transport::WasmTransport;
|
use crate::transport::WasmTransport;
|
||||||
|
|
||||||
fn server_rejection_message(outcome: &CommunicationValue) -> Option<&str> {
|
|
||||||
(outcome.get_data(DataType::Connected) == Some(&DataValue::BoolFalse)).then(|| {
|
|
||||||
outcome
|
|
||||||
.get_str(DataType::ErrorMessage)
|
|
||||||
.unwrap_or("host rejected the connection")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
impl WasmClient {
|
impl WasmClient {
|
||||||
|
|
@ -87,31 +79,10 @@ impl WasmClient {
|
||||||
.unwrap_or("host does not support this protocol version"),
|
.unwrap_or("host does not support this protocol version"),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic host rejections are IdentificationResponse frames with
|
|
||||||
// Connected=false. They intentionally do not carry a negotiated
|
|
||||||
// Version because negotiation never completed. Check this before
|
|
||||||
// reading Version, otherwise a useful server error such as an
|
|
||||||
// authentication timeout is reported as the misleading
|
|
||||||
// "host omitted a valid negotiated protocol version".
|
|
||||||
if let Some(message) = server_rejection_message(&outcome) {
|
|
||||||
self.set_state_if_current(generation, ConnectionState::Disconnected);
|
|
||||||
return Err(js_error(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
let missing_version = || {
|
|
||||||
js_error(format!(
|
|
||||||
"host omitted a valid negotiated protocol version (response_type={:?}, connected={:?}, frame_len={})",
|
|
||||||
outcome.get_type(),
|
|
||||||
outcome.get_data(DataType::Connected),
|
|
||||||
outcome_bytes.len(),
|
|
||||||
))
|
|
||||||
};
|
|
||||||
|
|
||||||
let negotiated_version = match outcome.get_data(DataType::Version) {
|
let negotiated_version = match outcome.get_data(DataType::Version) {
|
||||||
Some(DataValue::Str(version)) => mtp_codec::Version::parse(version)
|
Some(DataValue::Str(version)) => mtp_codec::Version::parse(version)
|
||||||
.ok_or_else(|| missing_version())?,
|
.ok_or_else(|| js_error("host omitted a valid negotiated protocol version"))?,
|
||||||
_ => return Err(missing_version()),
|
_ => return Err(js_error("host omitted a valid negotiated protocol version")),
|
||||||
};
|
};
|
||||||
if negotiated_version != PROTOCOL_VERSION {
|
if negotiated_version != PROTOCOL_VERSION {
|
||||||
return Err(js_error(
|
return Err(js_error(
|
||||||
|
|
@ -657,24 +628,3 @@ impl WasmClient {
|
||||||
Ok(server_challenge)
|
Ok(server_challenge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::server_rejection_message;
|
|
||||||
use mtp_codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn reports_rejection_reason_without_a_negotiated_version() {
|
|
||||||
let response = CommunicationValue::new(CommunicationType::IdentificationResponse)
|
|
||||||
.add_typed_default(DataType::Connected, DataValue::BoolFalse)
|
|
||||||
.add_typed_default(
|
|
||||||
DataType::ErrorMessage,
|
|
||||||
DataValue::Str("authentication handshake timed out".into()),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
server_rejection_message(&response),
|
|
||||||
Some("authentication handshake timed out")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,6 @@ impl JsDataValueEncodeContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn js_to_data_value(value: &JsValue, tm: &TypeMap) -> Result<DataValue, JsValue> {
|
pub(crate) fn js_to_data_value(value: &JsValue, tm: &TypeMap) -> Result<DataValue, JsValue> {
|
||||||
js_to_data_value_with_limits(value, tm, EncodeLimits::default())
|
js_to_data_value_with_limits(value, tm, EncodeLimits::default())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -380,7 +380,6 @@ pub fn build_protected_frame_with_keyring_with_limits(
|
||||||
/// Read the claimed, unverified signer ID after decrypting the protected
|
/// Read the claimed, unverified signer ID after decrypting the protected
|
||||||
/// payload. The result may only select trusted keys for the same signer ID.
|
/// payload. The result may only select trusted keys for the same signer ID.
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
#[allow(deprecated)]
|
|
||||||
#[deprecated(note = "use protected_claimed_signer_id_with_limits")]
|
#[deprecated(note = "use protected_claimed_signer_id_with_limits")]
|
||||||
pub fn protected_claimed_signer_id(
|
pub fn protected_claimed_signer_id(
|
||||||
frame: &[u8],
|
frame: &[u8],
|
||||||
|
|
|
||||||
|
|
@ -296,7 +296,6 @@ impl WasmVerifiedRelayContent {
|
||||||
/// versioned relay metadata parser in the JavaScript SDK. The caller must bind
|
/// versioned relay metadata parser in the JavaScript SDK. The caller must bind
|
||||||
/// this value as the expected signer during the subsequent verification call.
|
/// this value as the expected signer during the subsequent verification call.
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
#[allow(deprecated)]
|
|
||||||
#[deprecated(note = "use relay_metadata_claimed_signer_id_with_limits")]
|
#[deprecated(note = "use relay_metadata_claimed_signer_id_with_limits")]
|
||||||
pub fn relay_metadata_claimed_signer_id(frame: &[u8], keyrings: JsValue) -> Result<u64, JsValue> {
|
pub fn relay_metadata_claimed_signer_id(frame: &[u8], keyrings: JsValue) -> Result<u64, JsValue> {
|
||||||
relay_metadata_claimed_signer_id_impl(frame, keyrings, JsValue::UNDEFINED)
|
relay_metadata_claimed_signer_id_impl(frame, keyrings, JsValue::UNDEFINED)
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,8 @@ pub struct WasmTransport {
|
||||||
buffer: Rc<RefCell<Vec<u8>>>,
|
buffer: Rc<RefCell<Vec<u8>>>,
|
||||||
/// Set to `true` when `open_next_stream` succeeds; cleared after the first frame is parsed.
|
/// Set to `true` when `open_next_stream` succeeds; cleared after the first frame is parsed.
|
||||||
new_stream_frame: Rc<Cell<bool>>,
|
new_stream_frame: Rc<Cell<bool>>,
|
||||||
|
/// A single ordered browser send stream shared by all cloned transports.
|
||||||
|
outgoing_writer: Rc<RefCell<Option<JsValue>>>,
|
||||||
/// Serializes stream creation and writes across concurrent callers.
|
/// Serializes stream creation and writes across concurrent callers.
|
||||||
send_lock: Rc<AsyncMutex<()>>,
|
send_lock: Rc<AsyncMutex<()>>,
|
||||||
type_map: Rc<RefCell<TypeMap>>,
|
type_map: Rc<RefCell<TypeMap>>,
|
||||||
|
|
@ -207,6 +209,7 @@ impl WasmTransport {
|
||||||
stream_reader: Rc::new(RefCell::new(None)),
|
stream_reader: Rc::new(RefCell::new(None)),
|
||||||
buffer: Rc::new(RefCell::new(Vec::new())),
|
buffer: Rc::new(RefCell::new(Vec::new())),
|
||||||
new_stream_frame: Rc::new(Cell::new(false)),
|
new_stream_frame: Rc::new(Cell::new(false)),
|
||||||
|
outgoing_writer: Rc::new(RefCell::new(None)),
|
||||||
send_lock: Rc::new(AsyncMutex::new(())),
|
send_lock: Rc::new(AsyncMutex::new(())),
|
||||||
type_map: Rc::new(RefCell::new(TypeMap::latest())),
|
type_map: Rc::new(RefCell::new(TypeMap::latest())),
|
||||||
decode_limits: Rc::new(RefCell::new(decode_limits)),
|
decode_limits: Rc::new(RefCell::new(decode_limits)),
|
||||||
|
|
@ -244,12 +247,9 @@ impl WasmTransport {
|
||||||
return Err(js_error("message too large"));
|
return Err(js_error("message too large"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use one WebTransport uni-stream per MTP frame. Chromium reliably
|
let writer_val = if let Some(writer) = self.outgoing_writer.borrow().clone() {
|
||||||
// publishes a browser-created uni-stream to the peer when it is
|
writer
|
||||||
// closed; leaving a shared stream open can leave the server waiting
|
} else {
|
||||||
// in accept_uni() until the authentication deadline. The bytes are
|
|
||||||
// already the canonical MTP self-framed value, so no extra stream
|
|
||||||
// length prefix is added here.
|
|
||||||
let create_stream = js_sys::Reflect::get(
|
let create_stream = js_sys::Reflect::get(
|
||||||
&self.inner,
|
&self.inner,
|
||||||
&JsValue::from_str("createUnidirectionalStream"),
|
&JsValue::from_str("createUnidirectionalStream"),
|
||||||
|
|
@ -262,12 +262,15 @@ impl WasmTransport {
|
||||||
.map_err(|_| js_error("createUnidirectionalStream did not return a Promise"))?;
|
.map_err(|_| js_error("createUnidirectionalStream did not return a Promise"))?;
|
||||||
let stream = JsFuture::from(stream_promise).await?;
|
let stream = JsFuture::from(stream_promise).await?;
|
||||||
let writable_or_stream = resolve_stream_writable(&stream)?;
|
let writable_or_stream = resolve_stream_writable(&stream)?;
|
||||||
let writer_val = js_sys::Reflect::get(&writable_or_stream, &JsValue::from_str("getWriter"))
|
let writer = js_sys::Reflect::get(&writable_or_stream, &JsValue::from_str("getWriter"))
|
||||||
.map_err(|_| js_error("missing getWriter"))?
|
.map_err(|_| js_error("missing getWriter"))?
|
||||||
.dyn_into::<js_sys::Function>()
|
.dyn_into::<js_sys::Function>()
|
||||||
.map_err(|_| js_error("getWriter not a function"))?
|
.map_err(|_| js_error("getWriter not a function"))?
|
||||||
.call0(&writable_or_stream)
|
.call0(&writable_or_stream)
|
||||||
.map_err(|_| js_error("getWriter call failed"))?;
|
.map_err(|_| js_error("getWriter call failed"))?;
|
||||||
|
*self.outgoing_writer.borrow_mut() = Some(writer.clone());
|
||||||
|
writer
|
||||||
|
};
|
||||||
|
|
||||||
let chunk = js_sys::Uint8Array::from(frame);
|
let chunk = js_sys::Uint8Array::from(frame);
|
||||||
|
|
||||||
|
|
@ -280,24 +283,11 @@ impl WasmTransport {
|
||||||
.map_err(|e| js_error(format!("write failed: {:?}", e)))?;
|
.map_err(|e| js_error(format!("write failed: {:?}", e)))?;
|
||||||
if let Err(e) = JsFuture::from(write_promise.unchecked_into::<js_sys::Promise>()).await {
|
if let Err(e) = JsFuture::from(write_promise.unchecked_into::<js_sys::Promise>()).await {
|
||||||
log_stream_error_code(&e, "send_frame write");
|
log_stream_error_code(&e, "send_frame write");
|
||||||
|
self.outgoing_writer.borrow_mut().take();
|
||||||
release_writer_lock(&writer_val);
|
release_writer_lock(&writer_val);
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
let close_fn = js_sys::Reflect::get(&writer_val, &JsValue::from_str("close"))
|
|
||||||
.map_err(|_| js_error("missing close"))?
|
|
||||||
.dyn_into::<js_sys::Function>()
|
|
||||||
.map_err(|_| js_error("close not a function"))?;
|
|
||||||
let close_promise = close_fn
|
|
||||||
.call0(&writer_val)
|
|
||||||
.map_err(|e| js_error(format!("close failed: {:?}", e)))?;
|
|
||||||
if let Err(e) = JsFuture::from(close_promise.unchecked_into::<js_sys::Promise>()).await {
|
|
||||||
// The frame was already written; do not retry it merely because
|
|
||||||
// FIN failed, as that would duplicate the MTP frame.
|
|
||||||
log_stream_error_code(&e, "send_frame close");
|
|
||||||
}
|
|
||||||
release_writer_lock(&writer_val);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -687,6 +677,12 @@ impl WasmTransport {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn close(&self) {
|
pub fn close(&self) {
|
||||||
|
if let Some(writer) = self.outgoing_writer.borrow_mut().take() {
|
||||||
|
// The WebTransport session close below terminates the stream. The
|
||||||
|
// lock must be released first so dropping it is not interpreted as
|
||||||
|
// an application abort.
|
||||||
|
release_writer_lock(&writer);
|
||||||
|
}
|
||||||
// Release reader locks before closing so they aren't treated as cancels.
|
// Release reader locks before closing so they aren't treated as cancels.
|
||||||
if let Some(reader) = self.stream_reader.borrow_mut().take() {
|
if let Some(reader) = self.stream_reader.borrow_mut().take() {
|
||||||
release_reader_lock(&reader);
|
release_reader_lock(&reader);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue