tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 587809605b225c874963fc36f40cfb961145599d
parent fa74376649f99202902e1bac72a7b7d5f53b8a58
Author: Mark Hammond <mhammond@skippinet.com.au>
Date:   Thu,  8 Jan 2026 22:28:33 +0000

Bug 2008875 - Update ohttp/bhttp crates to 0.7.2. r=valentin,necko-reviewers,supply-chain-reviewers,toolkit-telemetry-reviewers

Differential Revision: https://phabricator.services.mozilla.com/D278076

Diffstat:
M.cargo/config.toml.in | 5-----
MCargo.lock | 18++++++++++--------
MCargo.toml | 6------
Mnetwerk/protocol/http/binary_http/Cargo.toml | 2+-
Mnetwerk/protocol/http/oblivious_http/Cargo.toml | 2+-
Msupply-chain/audits.toml | 2+-
Msupply-chain/config.toml | 8--------
Msupply-chain/imports.lock | 10+++++-----
Mthird_party/rust/bhttp/.cargo-checksum.json | 4++--
Athird_party/rust/bhttp/Cargo.lock | 521+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mthird_party/rust/bhttp/Cargo.toml | 39++++++++++++++++++++-------------------
Mthird_party/rust/bhttp/src/err.rs | 18++++++++----------
Mthird_party/rust/bhttp/src/lib.rs | 344+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Mthird_party/rust/bhttp/src/parse.rs | 23++++++++++++-----------
Mthird_party/rust/bhttp/src/rw.rs | 40++++++++++++++--------------------------
Athird_party/rust/bhttp/src/stream/int.rs | 258+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/rust/bhttp/src/stream/mod.rs | 589+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/rust/bhttp/src/stream/vec.rs | 221+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mthird_party/rust/bhttp/tests/test.rs | 2+-
Mthird_party/rust/ohttp/.cargo-checksum.json | 4++--
Athird_party/rust/ohttp/Cargo.lock | 961+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mthird_party/rust/ohttp/Cargo.toml | 79++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Mthird_party/rust/ohttp/build.rs | 24+++++++++++++++++-------
Mthird_party/rust/ohttp/src/config.rs | 42++++++++++++++++++++++++++++--------------
Athird_party/rust/ohttp/src/crypto.rs | 13+++++++++++++
Mthird_party/rust/ohttp/src/err.rs | 16++++++++++++++--
Mthird_party/rust/ohttp/src/lib.rs | 234+++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mthird_party/rust/ohttp/src/nss/aead.rs | 133+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mthird_party/rust/ohttp/src/nss/err.rs | 7++++---
Mthird_party/rust/ohttp/src/nss/hkdf.rs | 14++++++++------
Mthird_party/rust/ohttp/src/nss/hpke.rs | 63++++++++++++++++++++++++++++++++++++++++++++-------------------
Mthird_party/rust/ohttp/src/nss/mod.rs | 12+++++-------
Mthird_party/rust/ohttp/src/nss/p11.rs | 54+++++++++++++++++++++++++++---------------------------
Mthird_party/rust/ohttp/src/rand.rs | 4++--
Mthird_party/rust/ohttp/src/rh/aead.rs | 82++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Mthird_party/rust/ohttp/src/rh/hkdf.rs | 7++++---
Mthird_party/rust/ohttp/src/rh/hpke.rs | 34++++++++++++++++++++++++----------
Athird_party/rust/ohttp/src/stream.rs | 1109+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtoolkit/components/glean/Cargo.toml | 4++--
39 files changed, 4525 insertions(+), 483 deletions(-)

diff --git a/.cargo/config.toml.in b/.cargo/config.toml.in @@ -75,11 +75,6 @@ git = "https://github.com/jfkthame/mapped_hyph.git" rev = "eff105f6ad7ec9b79816cfc1985a28e5340ad14b" replace-with = "vendored-sources" -[source."git+https://github.com/martinthomson/ohttp.git?rev=bf6a983845cc0b540effb3a615e92d914dfcfd0b"] -git = "https://github.com/martinthomson/ohttp.git" -rev = "bf6a983845cc0b540effb3a615e92d914dfcfd0b" -replace-with = "vendored-sources" - [source."git+https://github.com/mozilla/application-services?rev=0376c542e4a31cde8d33dd0e8da17dcfbc6c58b2"] git = "https://github.com/mozilla/application-services" rev = "0376c542e4a31cde8d33dd0e8da17dcfbc6c58b2" diff --git a/Cargo.lock b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "aa-stroke" @@ -425,10 +425,11 @@ dependencies = [ [[package]] name = "bhttp" -version = "0.6.1" -source = "git+https://github.com/martinthomson/ohttp.git?rev=bf6a983845cc0b540effb3a615e92d914dfcfd0b#bf6a983845cc0b540effb3a615e92d914dfcfd0b" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d305a54bcb99974213b4c78a486c34091e83c5d6d6572f7f4331c904ea9d127" dependencies = [ - "thiserror 1.999.999", + "thiserror 2.0.12", ] [[package]] @@ -5227,8 +5228,9 @@ dependencies = [ [[package]] name = "ohttp" -version = "0.6.1" -source = "git+https://github.com/martinthomson/ohttp.git?rev=bf6a983845cc0b540effb3a615e92d914dfcfd0b#bf6a983845cc0b540effb3a615e92d914dfcfd0b" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a03aaaf57495c75ce66aee6a7c3b21abf046c9d4cca3d45b22cdbf0de1bfba" dependencies = [ "bindgen 0.72.0", "byteorder", @@ -5237,8 +5239,8 @@ dependencies = [ "mozbuild", "serde", "serde_derive", - "thiserror 1.999.999", - "toml 0.5.999", + "thiserror 2.0.12", + "toml 0.9.8", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml @@ -266,12 +266,6 @@ objc = { git = "https://github.com/glandium/rust-objc", rev = "4de89f5aa9851ceca # allocator-api2 + f95e3419ce41883904fcb2279b52aa35b5f04d76 + fdd92751afa7ce34408b677004b429d597e72c90 allocator-api2 = { git = "https://github.com/glandium/allocator-api2", rev = "ad5f3d56a5a4519eff52af4ff85293431466ef5c" } -# patch ohttp - app-services relies on a version yet to be released. However, even if a new version was released it -# will have bumped the `toml` crate to a version which would cause dupes here. We should remove this and revert back -# to a published version when we can upgrade both a-s and m-c to the same version. -ohttp = { git = "https://github.com/martinthomson/ohttp.git", rev = "bf6a983845cc0b540effb3a615e92d914dfcfd0b" } -bhttp = { git = "https://github.com/martinthomson/ohttp.git", rev = "bf6a983845cc0b540effb3a615e92d914dfcfd0b" } - # application-services overrides to make updating them all simpler. context_id = { git = "https://github.com/mozilla/application-services", rev = "0376c542e4a31cde8d33dd0e8da17dcfbc6c58b2" } error-support = { git = "https://github.com/mozilla/application-services", rev = "0376c542e4a31cde8d33dd0e8da17dcfbc6c58b2" } diff --git a/netwerk/protocol/http/binary_http/Cargo.toml b/netwerk/protocol/http/binary_http/Cargo.toml @@ -6,6 +6,6 @@ edition = "2021" [dependencies] nserror = { path = "../../../../xpcom/rust/nserror" } nsstring = { path = "../../../../xpcom/rust/nsstring" } -bhttp = "0.6" +bhttp = "0.7.2" thin-vec = { version = "0.2.1", features = ["gecko-ffi"] } xpcom = { path = "../../../../xpcom/rust/xpcom" } diff --git a/netwerk/protocol/http/oblivious_http/Cargo.toml b/netwerk/protocol/http/oblivious_http/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] nserror = { path = "../../../../xpcom/rust/nserror" } -ohttp = { version = "0.6", default-features = false, features = ["gecko", "nss", "client", "server"] } +ohttp = { version = "0.7.2", default-features = false, features = ["gecko", "nss", "client", "server"] } rand = "0.8" thin-vec = { version = "0.2.1", features = ["gecko-ffi"] } xpcom = { path = "../../../../xpcom/rust/xpcom" } diff --git a/supply-chain/audits.toml b/supply-chain/audits.toml @@ -315,7 +315,7 @@ end = "2025-08-30" [[wildcard-audits.hawk]] who = "Ryan Safaeian <rsafaeian@mozilla.com>" criteria = "safe-to-deploy" -user-id = 158511 # Yarik (lotas) +user-id = 158511 # Yaraslau Kurmyza (lotas) start = "2022-05-05" end = "2026-04-24" notes = "Hawk is written and maintained by mozilla employees." diff --git a/supply-chain/config.toml b/supply-chain/config.toml @@ -31,10 +31,6 @@ notes = "This is the upstream code plus the ARM intrinsics workaround from qcms, audit-as-crates-io = true notes = "This is the upstream code plus a few local fixes, see bug 1685697." -[policy.bhttp] -audit-as-crates-io = true -notes = "This is upstream code in between released versions." - [policy."bindgen:0.72.0@git:9366e0af8da529c958b4cd4fcbe492d951c86f5c"] audit-as-crates-io = true notes = "This is the upstream code not yet released" @@ -193,10 +189,6 @@ notes = "This is a pinned version of the upstream code, pending update of the cr audit-as-crates-io = true notes = "This is the upstream code plus a backported fix." -[policy.ohttp] -audit-as-crates-io = true -notes = "This is upstream code in between released versions." - [policy.osclientcerts] audit-as-crates-io = false notes = "This is a first-party crate that happens to have been pushed to crates.io a very long time ago but was yanked." diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock @@ -44,8 +44,8 @@ user-login = "jschanck" user-name = "John Schanck" [[publisher.bhttp]] -version = "0.6.1" -when = "2025-06-19" +version = "0.7.2" +when = "2025-12-11" user-id = 128763 user-login = "martinthomson" user-name = "Martin Thomson" @@ -300,7 +300,7 @@ version = "5.0.1" when = "2024-09-13" user-id = 158511 user-login = "lotas" -user-name = "Yarik" +user-name = "Yaraslau Kurmyza" [[publisher.headers]] version = "0.3.9" @@ -457,8 +457,8 @@ user-login = "seanmonstar" user-name = "Sean McArthur" [[publisher.ohttp]] -version = "0.6.1" -when = "2025-06-19" +version = "0.7.2" +when = "2025-12-11" user-id = 128763 user-login = "martinthomson" user-name = "Martin Thomson" diff --git a/third_party/rust/bhttp/.cargo-checksum.json b/third_party/rust/bhttp/.cargo-checksum.json @@ -1 +1 @@ -{"files":{"Cargo.toml":"f67579f75f89317e8e2137cbfd59a2656692313921744089fda61c698743dc25","README.md":"30c2c4a78875fd78a6b007954ed0aceed4004983e5ff62a38261bbcd1a9c36a3","src/err.rs":"f380bc2c62cc0acc7d2a11f988b8cbd94014ae1665cdf5271165a511e7be0800","src/lib.rs":"619c1508a57222561bc4c193dafbcf88bf7685a743e99a90c44cf26874abaea2","src/parse.rs":"c759c0446c1016b412c4d294686713f296888dfa9713d9c6fa7382d4e4b5540a","src/rw.rs":"df7a0efd46e3ac0561428929bd447aef97ce380c83d2855996a0bf3a71cee0a7","tests/test.rs":"37c93b4f3e74ab474004bdc2d6688e970443ee006c4ea7101fcfcf17e8f7f880"},"package":null} -\ No newline at end of file +{"files":{"Cargo.lock":"abacac088dc60284ffd2560189c70b35ada294c82194e0421c92d75d8604719f","Cargo.toml":"8d63f765cdf3dc2e8f77668f4f438676bfe7128dc48cf6a16a99d28cd96056f2","README.md":"30c2c4a78875fd78a6b007954ed0aceed4004983e5ff62a38261bbcd1a9c36a3","src/err.rs":"adf8299c67f6eb7955716d0e587e787abf4240a4f7a08ac2d9206588e8b3c477","src/lib.rs":"040572fedab9111c48d1a1d310881a8e645620794cf4f8fe720e9ff9dbf3af48","src/parse.rs":"925f656ddfb201151432f24b8a93c1274635ea843783d7de3ce341ce82385ffc","src/rw.rs":"1a05eeaed114e102f2c4e2c76626898e86c19216cbcf6c5683995e9f49e0632a","src/stream/int.rs":"866522fcb3d75d4cbefeb7ba73e4a63b7a4879b289260b4906e5580cfe2d1bee","src/stream/mod.rs":"9ec0000243f93e674d9c775bfcff96c9ff7a900da0bbed2925462b629e4aa4e4","src/stream/vec.rs":"0730ada5482bcd70548ba5a39043562cec46bef0db43afedc14b07e283ac8813","tests/test.rs":"54cc29b5c9395f11c519bce7aebc85d7cfd65585eadfccbdad8cb4df3a56339a"},"package":"9d305a54bcb99974213b4c78a486c34091e83c5d6d6572f7f4331c904ea9d127"} +\ No newline at end of file diff --git a/third_party/rust/bhttp/Cargo.lock b/third_party/rust/bhttp/Cargo.lock @@ -0,0 +1,521 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bhttp" +version = "0.7.2" +dependencies = [ + "futures", + "hex", + "pin-project", + "thiserror", + "url", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/third_party/rust/bhttp/Cargo.toml b/third_party/rust/bhttp/Cargo.toml @@ -11,9 +11,9 @@ [package] edition = "2021" -rust-version = "1.82.0" +rust-version = "1.83.0" name = "bhttp" -version = "0.6.1" +version = "0.7.2" authors = ["Martin Thomson <mt@lowentropy.net>"] build = false autolib = false @@ -23,7 +23,7 @@ autotests = false autobenches = false description = "Binary HTTP messages (RFC 9292)" homepage = "https://github.com/martinthomson/ohttp" -readme = "../README.md" +readme = "README.md" keywords = [ "ohttp", "http", @@ -39,19 +39,12 @@ license = "MIT OR Apache-2.0" repository = "https://github.com/martinthomson/ohttp" [features] -bhttp = [ - "read-bhttp", - "write-bhttp", +default = [] +http = ["dep:url"] +stream = [ + "dep:futures", + "dep:pin-project", ] -default = ["bhttp"] -http = [ - "read-http", - "write-http", -] -read-bhttp = [] -read-http = ["url"] -write-bhttp = [] -write-http = [] [lib] name = "bhttp" @@ -61,12 +54,20 @@ path = "src/lib.rs" name = "test" path = "tests/test.rs" -[dependencies] -thiserror = "1" +[dependencies.futures] +version = "0.3" +optional = true + +[dependencies.pin-project] +version = "1.1" +optional = true + +[dependencies.thiserror] +version = "2" [dependencies.url] version = "2" optional = true -[dev-dependencies] -hex = "0.4" +[dev-dependencies.hex] +version = "0.4" diff --git a/third_party/rust/bhttp/src/err.rs b/third_party/rust/bhttp/src/err.rs @@ -1,6 +1,4 @@ -use thiserror::Error; - -#[derive(Error, Debug)] +#[derive(thiserror::Error, Debug)] pub enum Error { #[error("a request used the CONNECT method")] ConnectUnsupported, @@ -16,8 +14,14 @@ pub enum Error { InvalidMode, #[error("the status code of a response needs to be in 100..=599")] InvalidStatus, + #[cfg(feature = "stream")] + #[error("a method was called when the message was in the wrong state")] + InvalidState, #[error("IO error {0}")] Io(#[from] std::io::Error), + #[cfg(feature = "stream")] + #[error("the size of a vector exceeded the limit that was set")] + LimitExceeded, #[error("a field or line was missing a necessary character 0x{0:x}")] Missing(u8), #[error("a URL was missing a key component")] @@ -31,14 +35,8 @@ pub enum Error { #[error("a message included the Upgrade field")] UpgradeUnsupported, #[error("a URL could not be parsed into components: {0}")] - #[cfg(feature = "read-http")] + #[cfg(feature = "http")] UrlParse(#[from] url::ParseError), } -#[cfg(any( - feature = "read-http", - feature = "write-http", - feature = "read-bhttp", - feature = "write-bhttp" -))] pub type Res<T> = Result<T, Error>; diff --git a/third_party/rust/bhttp/src/lib.rs b/third_party/rust/bhttp/src/lib.rs @@ -1,51 +1,36 @@ #![deny(warnings, clippy::pedantic)] #![allow(clippy::missing_errors_doc)] // Too lazy to document these. -#[cfg(feature = "read-bhttp")] -use std::convert::TryFrom; -#[cfg(any( - feature = "read-http", - feature = "write-http", - feature = "read-bhttp", - feature = "write-bhttp" -))] -use std::io; - -#[cfg(feature = "read-http")] +use std::{ + borrow::BorrowMut, + io, + ops::{Deref, DerefMut}, +}; + +#[cfg(feature = "http")] use url::Url; mod err; mod parse; -#[cfg(any(feature = "read-bhttp", feature = "write-bhttp"))] mod rw; - -#[cfg(any(feature = "read-http", feature = "read-bhttp",))] -use std::borrow::BorrowMut; +#[cfg(feature = "stream")] +pub mod stream; pub use err::Error; -#[cfg(any( - feature = "read-http", - feature = "write-http", - feature = "read-bhttp", - feature = "write-bhttp" -))] use err::Res; -#[cfg(feature = "read-http")] +#[cfg(feature = "http")] use parse::{downcase, is_ows, read_line, split_at, COLON, SEMICOLON, SLASH, SP}; use parse::{index_of, trim_ows, COMMA}; -#[cfg(feature = "read-bhttp")] -use rw::{read_varint, read_vec}; -#[cfg(feature = "write-bhttp")] -use rw::{write_len, write_varint, write_vec}; +use rw::{read_varint, read_vec, write_len, write_varint, write_vec}; -#[cfg(feature = "read-http")] +#[cfg(feature = "http")] const CONTENT_LENGTH: &[u8] = b"content-length"; -#[cfg(feature = "read-bhttp")] const COOKIE: &[u8] = b"cookie"; const TRANSFER_ENCODING: &[u8] = b"transfer-encoding"; const CHUNKED: &[u8] = b"chunked"; -#[derive(Clone, Copy, PartialEq, Eq)] +/// An HTTP status code. +#[derive(Clone, Copy, Debug)] pub struct StatusCode(u16); impl StatusCode { @@ -88,17 +73,49 @@ impl From<StatusCode> for u16 { } } +#[cfg(test)] +impl<T> PartialEq<T> for StatusCode +where + Self: TryFrom<T>, + T: Copy, +{ + fn eq(&self, other: &T) -> bool { + StatusCode::try_from(*other).is_ok_and(|o| o.0 == self.0) + } +} + +#[cfg(not(test))] +impl PartialEq<StatusCode> for StatusCode { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Eq for StatusCode {} + pub trait ReadSeek: io::BufRead + io::Seek {} impl<T> ReadSeek for io::Cursor<T> where T: AsRef<[u8]> {} impl<T> ReadSeek for io::BufReader<T> where T: io::Read + io::Seek {} +/// The encoding mode of a binary HTTP message. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[cfg(any(feature = "read-bhttp", feature = "write-bhttp"))] pub enum Mode { KnownLength, IndeterminateLength, } +impl TryFrom<u64> for Mode { + type Error = Error; + fn try_from(t: u64) -> Result<Self, Self::Error> { + match t { + 0 | 1 => Ok(Self::KnownLength), + 2 | 3 => Ok(Self::IndeterminateLength), + _ => Err(Error::InvalidMode), + } + } +} + +/// A single HTTP field. pub struct Field { name: Vec<u8>, value: Vec<u8>, @@ -120,7 +137,7 @@ impl Field { &self.value } - #[cfg(feature = "write-http")] + #[cfg(feature = "http")] pub fn write_http(&self, w: &mut impl io::Write) -> Res<()> { w.write_all(&self.name)?; w.write_all(b": ")?; @@ -129,37 +146,64 @@ impl Field { Ok(()) } - #[cfg(feature = "write-bhttp")] pub fn write_bhttp(&self, w: &mut impl io::Write) -> Res<()> { write_vec(&self.name, w)?; write_vec(&self.value, w)?; Ok(()) } - #[cfg(feature = "read-http")] + #[cfg(feature = "http")] pub fn obs_fold(&mut self, extra: &[u8]) { self.value.push(SP); self.value.extend(trim_ows(extra)); } } +#[cfg(test)] +impl std::fmt::Debug for Field { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!( + f, + "{n}: {v}", + n = String::from_utf8_lossy(&self.name), + v = String::from_utf8_lossy(&self.value), + ) + } +} + +/// A field section (headers or trailers). #[derive(Default)] pub struct FieldSection(Vec<Field>); + impl FieldSection { #[must_use] pub fn is_empty(&self) -> bool { self.0.is_empty() } + #[must_use] + pub fn len(&self) -> usize { + self.0.len() + } + /// Gets the value from the first instance of the field. #[must_use] pub fn get(&self, n: &[u8]) -> Option<&[u8]> { - for f in &self.0 { + self.get_all(n).next() + } + + /// Gets all of the values of the named field. + pub fn get_all<'a, 'b>(&'a self, n: &'b [u8]) -> impl Iterator<Item = &'a [u8]> + 'b + where + 'a: 'b, + { + self.0.iter().filter_map(move |f| { if &f.name[..] == n { - return Some(&f.value); + Some(&f.value[..]) + } else { + None } - } - None + }) } pub fn put(&mut self, name: impl Into<Vec<u8>>, value: impl Into<Vec<u8>>) { @@ -192,7 +236,7 @@ impl FieldSection { /// As required by the HTTP specification, remove the Connection header /// field, everything it refers to, and a few extra fields. - #[cfg(feature = "read-http")] + #[cfg(feature = "http")] fn strip_connection_headers(&mut self) { const CONNECTION: &[u8] = b"connection"; const PROXY_CONNECTION: &[u8] = b"proxy-connection"; @@ -232,7 +276,7 @@ impl FieldSection { }); } - #[cfg(feature = "read-http")] + #[cfg(feature = "http")] fn parse_line(fields: &mut Vec<Field>, line: Vec<u8>) -> Res<()> { // obs-fold is helpful in specs, so support it here too let f = if is_ows(line[0]) { @@ -251,7 +295,7 @@ impl FieldSection { Ok(()) } - #[cfg(feature = "read-http")] + #[cfg(feature = "http")] pub fn read_http<T, R>(r: &mut T) -> Res<Self> where T: BorrowMut<R> + ?Sized, @@ -267,7 +311,6 @@ impl FieldSection { } } - #[cfg(feature = "read-bhttp")] fn read_bhttp_fields<T, R>(terminator: bool, r: &mut T) -> Res<Vec<Field>> where T: BorrowMut<R> + ?Sized, @@ -302,7 +345,6 @@ impl FieldSection { } } - #[cfg(feature = "read-bhttp")] pub fn read_bhttp<T, R>(mode: Mode, r: &mut T) -> Res<Self> where T: BorrowMut<R> + ?Sized, @@ -320,7 +362,6 @@ impl FieldSection { Ok(Self(fields)) } - #[cfg(feature = "write-bhttp")] fn write_bhttp_headers(&self, w: &mut impl io::Write) -> Res<()> { for f in &self.0 { f.write_bhttp(w)?; @@ -328,7 +369,6 @@ impl FieldSection { Ok(()) } - #[cfg(feature = "write-bhttp")] pub fn write_bhttp(&self, mode: Mode, w: &mut impl io::Write) -> Res<()> { if mode == Mode::KnownLength { let mut buf = Vec::new(); @@ -341,7 +381,7 @@ impl FieldSection { Ok(()) } - #[cfg(feature = "write-http")] + #[cfg(feature = "http")] pub fn write_http(&self, w: &mut impl io::Write) -> Res<()> { for f in &self.0 { f.write_http(w)?; @@ -351,6 +391,17 @@ impl FieldSection { } } +#[cfg(test)] +impl std::fmt::Debug for FieldSection { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + for fv in self.fields() { + fv.fmt(f)?; + } + Ok(()) + } +} + +/// Control data for an HTTP message, either request or response. pub enum ControlData { Request { method: Vec<u8>, @@ -420,7 +471,7 @@ impl ControlData { } } - #[cfg(feature = "read-http")] + #[cfg(feature = "http")] pub fn read_http(line: Vec<u8>) -> Res<Self> { // request-line = method SP request-target SP HTTP-version // status-line = HTTP-version SP status-code SP [reason-phrase] @@ -467,7 +518,6 @@ impl ControlData { } } - #[cfg(feature = "read-bhttp")] pub fn read_bhttp<T, R>(request: bool, r: &mut T) -> Res<Self> where T: BorrowMut<R> + ?Sized, @@ -493,7 +543,6 @@ impl ControlData { } /// If this is an informational response. - #[cfg(any(feature = "read-bhttp", feature = "read-http"))] #[must_use] fn informational(&self) -> Option<StatusCode> { match self { @@ -502,7 +551,6 @@ impl ControlData { } } - #[cfg(feature = "write-bhttp")] #[must_use] fn code(&self, mode: Mode) -> u64 { match (self, mode) { @@ -513,7 +561,6 @@ impl ControlData { } } - #[cfg(feature = "write-bhttp")] pub fn write_bhttp(&self, w: &mut impl io::Write) -> Res<()> { match self { Self::Request { @@ -532,7 +579,7 @@ impl ControlData { Ok(()) } - #[cfg(feature = "write-http")] + #[cfg(feature = "http")] pub fn write_http(&self, w: &mut impl io::Write) -> Res<()> { match self { Self::Request { @@ -560,6 +607,69 @@ impl ControlData { } } +#[cfg(test)] +impl<M, S, A, P> PartialEq<(M, S, A, P)> for ControlData +where + M: AsRef<[u8]>, + S: AsRef<[u8]>, + A: AsRef<[u8]>, + P: AsRef<[u8]>, +{ + fn eq(&self, other: &(M, S, A, P)) -> bool { + match self { + Self::Request { + method, + scheme, + authority, + path, + } => { + method == other.0.as_ref() + && scheme == other.1.as_ref() + && authority == other.2.as_ref() + && path == other.3.as_ref() + } + Self::Response(_) => false, + } + } +} + +#[cfg(test)] +impl<T> PartialEq<T> for ControlData +where + StatusCode: TryFrom<T>, + T: Copy, +{ + fn eq(&self, other: &T) -> bool { + match self { + Self::Request { .. } => false, + Self::Response(code) => code == other, + } + } +} + +#[cfg(test)] +impl std::fmt::Debug for ControlData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + match self { + Self::Request { + method, + scheme, + authority, + path, + } => write!( + f, + "{m} {s}://{a}{p}", + m = String::from_utf8_lossy(method), + s = String::from_utf8_lossy(scheme), + a = String::from_utf8_lossy(authority), + p = String::from_utf8_lossy(path), + ), + Self::Response(code) => write!(f, "{code:?}"), + } + } +} + +/// An informational status code and the associated header fields. pub struct InformationalResponse { status: StatusCode, fields: FieldSection, @@ -581,7 +691,6 @@ impl InformationalResponse { &self.fields } - #[cfg(feature = "write-bhttp")] fn write_bhttp(&self, mode: Mode, w: &mut impl io::Write) -> Res<()> { write_varint(self.status.code(), w)?; self.fields.write_bhttp(mode, w)?; @@ -589,80 +698,152 @@ impl InformationalResponse { } } +impl Deref for InformationalResponse { + type Target = FieldSection; + + fn deref(&self) -> &Self::Target { + &self.fields + } +} + +impl DerefMut for InformationalResponse { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.fields + } +} + +/// A header block, including control data and headers. +pub struct Header { + control: ControlData, + fields: FieldSection, +} + +impl Header { + #[must_use] + pub fn control(&self) -> &ControlData { + &self.control + } +} + +impl From<ControlData> for Header { + fn from(control: ControlData) -> Self { + Self { + control, + fields: FieldSection::default(), + } + } +} + +impl From<(ControlData, FieldSection)> for Header { + fn from((control, fields): (ControlData, FieldSection)) -> Self { + Self { control, fields } + } +} + +impl std::ops::Deref for Header { + type Target = FieldSection; + fn deref(&self) -> &Self::Target { + &self.fields + } +} + +impl std::ops::DerefMut for Header { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.fields + } +} + +#[cfg(test)] +impl std::fmt::Debug for Header { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + self.control.fmt(f)?; + self.fields.fmt(f) + } +} + +/// An HTTP message, either request or response, +/// including any optional informational responses on a response. pub struct Message { informational: Vec<InformationalResponse>, - control: ControlData, - header: FieldSection, + header: Header, content: Vec<u8>, trailer: FieldSection, } impl Message { + /// Construct a minimal request message. #[must_use] pub fn request(method: Vec<u8>, scheme: Vec<u8>, authority: Vec<u8>, path: Vec<u8>) -> Self { Self { informational: Vec::new(), - control: ControlData::Request { + header: Header::from(ControlData::Request { method, scheme, authority, path, - }, - header: FieldSection::default(), + }), content: Vec::new(), trailer: FieldSection::default(), } } + /// Construct a minimal response message. #[must_use] pub fn response(status: StatusCode) -> Self { Self { informational: Vec::new(), - control: ControlData::Response(status), - header: FieldSection::default(), + header: Header::from(ControlData::Response(status)), content: Vec::new(), trailer: FieldSection::default(), } } + /// Set a header field value. pub fn put_header(&mut self, name: impl Into<Vec<u8>>, value: impl Into<Vec<u8>>) { self.header.put(name, value); } + /// Set a trailer field value. pub fn put_trailer(&mut self, name: impl Into<Vec<u8>>, value: impl Into<Vec<u8>>) { self.trailer.put(name, value); } + /// Extend the content of the message with the given bytes. pub fn write_content(&mut self, d: impl AsRef<[u8]>) { self.content.extend_from_slice(d.as_ref()); } + /// Access informational status responses. #[must_use] pub fn informational(&self) -> &[InformationalResponse] { &self.informational } + /// Access control data. #[must_use] pub fn control(&self) -> &ControlData { - &self.control + self.header.control() } + /// Get the header. #[must_use] - pub fn header(&self) -> &FieldSection { + pub fn header(&self) -> &Header { &self.header } + /// Get the content of the message. #[must_use] pub fn content(&self) -> &[u8] { &self.content } + /// Get the trailer fields. #[must_use] pub fn trailer(&self) -> &FieldSection { &self.trailer } - #[cfg(feature = "read-http")] + #[cfg(feature = "http")] fn read_chunked<T, R>(r: &mut T) -> Res<Vec<u8>> where T: BorrowMut<R> + ?Sized, @@ -686,7 +867,8 @@ impl Message { } } - #[cfg(feature = "read-http")] + /// Read an HTTP/1.1 message. + #[cfg(feature = "http")] #[allow(clippy::read_zero_byte_vec)] // https://github.com/rust-lang/rust-clippy/issues/9274 pub fn read_http<T, R>(r: &mut T) -> Res<Self> where @@ -703,20 +885,20 @@ impl Message { control = ControlData::read_http(line)?; } - let mut header = FieldSection::read_http(r)?; + let mut hfields = FieldSection::read_http(r)?; let (content, trailer) = if matches!(control.status().map(StatusCode::code), Some(204 | 304)) { // 204 and 304 have no body, no matter what Content-Length says. // Unfortunately, we can't do the same for responses to HEAD. (Vec::new(), FieldSection::default()) - } else if header.is_chunked() { + } else if hfields.is_chunked() { let content = Self::read_chunked(r)?; let trailer = FieldSection::read_http(r)?; (content, trailer) } else { let mut content = Vec::new(); - if let Some(cl) = header.get(CONTENT_LENGTH) { + if let Some(cl) = hfields.get(CONTENT_LENGTH) { let cl_str = String::from_utf8(Vec::from(cl))?; let cl_int = cl_str.parse::<usize>()?; if cl_int > 0 { @@ -731,23 +913,23 @@ impl Message { (content, FieldSection::default()) }; - header.strip_connection_headers(); + hfields.strip_connection_headers(); Ok(Self { informational, - control, - header, + header: Header::from((control, hfields)), content, trailer, }) } - #[cfg(feature = "write-http")] + /// Write out an HTTP/1.1 message. + #[cfg(feature = "http")] pub fn write_http(&self, w: &mut impl io::Write) -> Res<()> { for info in &self.informational { ControlData::Response(info.status()).write_http(w)?; info.fields().write_http(w)?; } - self.control.write_http(w)?; + self.header.control.write_http(w)?; if !self.content.is_empty() { if self.trailer.is_empty() { write!(w, "Content-Length: {}\r\n", self.content.len())?; @@ -770,7 +952,6 @@ impl Message { } /// Read a BHTTP message. - #[cfg(feature = "read-bhttp")] pub fn read_bhttp<T, R>(r: &mut T) -> Res<Self> where T: BorrowMut<R> + ?Sized, @@ -778,11 +959,7 @@ impl Message { { let t = read_varint(r)?.ok_or(Error::Truncated)?; let request = t == 0 || t == 2; - let mode = match t { - 0 | 1 => Mode::KnownLength, - 2 | 3 => Mode::IndeterminateLength, - _ => return Err(Error::InvalidMode), - }; + let mode = Mode::try_from(t)?; let mut control = ControlData::read_bhttp(request, r)?; let mut informational = Vec::new(); @@ -791,7 +968,7 @@ impl Message { informational.push(InformationalResponse::new(status, fields)); control = ControlData::read_bhttp(request, r)?; } - let header = FieldSection::read_bhttp(mode, r)?; + let hfields = FieldSection::read_bhttp(mode, r)?; let mut content = read_vec(r)?.unwrap_or_default(); if mode == Mode::IndeterminateLength && !content.is_empty() { @@ -808,20 +985,19 @@ impl Message { Ok(Self { informational, - control, - header, + header: Header::from((control, hfields)), content, trailer, }) } - #[cfg(feature = "write-bhttp")] + /// Write a BHTTP message. pub fn write_bhttp(&self, mode: Mode, w: &mut impl io::Write) -> Res<()> { - write_varint(self.control.code(mode), w)?; + write_varint(self.header.control.code(mode), w)?; for info in &self.informational { info.write_bhttp(mode, w)?; } - self.control.write_bhttp(w)?; + self.header.control.write_bhttp(w)?; self.header.write_bhttp(mode, w)?; write_vec(&self.content, w)?; @@ -833,7 +1009,7 @@ impl Message { } } -#[cfg(feature = "write-http")] +#[cfg(feature = "http")] impl std::fmt::Debug for Message { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { let mut buf = Vec::new(); diff --git a/third_party/rust/bhttp/src/parse.rs b/third_party/rust/bhttp/src/parse.rs @@ -1,20 +1,21 @@ -#[cfg(feature = "read-http")] -use crate::{Error, ReadSeek, Res}; -#[cfg(feature = "read-http")] +#[cfg(feature = "http")] use std::borrow::BorrowMut; +#[cfg(feature = "http")] +use crate::{Error, ReadSeek, Res}; + pub const HTAB: u8 = 0x09; -#[cfg(feature = "read-http")] +#[cfg(feature = "http")] pub const NL: u8 = 0x0a; -#[cfg(feature = "read-http")] +#[cfg(feature = "http")] pub const CR: u8 = 0x0d; pub const SP: u8 = 0x20; pub const COMMA: u8 = 0x2c; -#[cfg(feature = "read-http")] +#[cfg(feature = "http")] pub const SLASH: u8 = 0x2f; -#[cfg(feature = "read-http")] +#[cfg(feature = "http")] pub const COLON: u8 = 0x3a; -#[cfg(feature = "read-http")] +#[cfg(feature = "http")] pub const SEMICOLON: u8 = 0x3b; pub fn is_ows(x: u8) -> bool { @@ -34,7 +35,7 @@ pub fn trim_ows(v: &[u8]) -> &[u8] { &v[..0] } -#[cfg(feature = "read-http")] +#[cfg(feature = "http")] pub fn downcase(n: &mut [u8]) { for i in n { if *i >= 0x41 && *i <= 0x5a { @@ -52,7 +53,7 @@ pub fn index_of(v: u8, line: &[u8]) -> Option<usize> { None } -#[cfg(feature = "read-http")] +#[cfg(feature = "http")] pub fn split_at(v: u8, mut line: Vec<u8>) -> Option<(Vec<u8>, Vec<u8>)> { index_of(v, &line).map(|i| { let tail = line.split_off(i + 1); @@ -61,7 +62,7 @@ pub fn split_at(v: u8, mut line: Vec<u8>) -> Option<(Vec<u8>, Vec<u8>)> { }) } -#[cfg(feature = "read-http")] +#[cfg(feature = "http")] pub fn read_line<T, R>(r: &mut T) -> Res<Vec<u8>> where T: BorrowMut<R> + ?Sized, diff --git a/third_party/rust/bhttp/src/rw.rs b/third_party/rust/bhttp/src/rw.rs @@ -1,12 +1,10 @@ -#[cfg(feature = "read-bhttp")] -use std::borrow::BorrowMut; -use std::{convert::TryFrom, io}; +use std::{borrow::BorrowMut, convert::TryFrom, io}; -use crate::err::Res; -#[cfg(feature = "read-bhttp")] -use crate::{err::Error, ReadSeek}; +use crate::{ + err::{Error, Res}, + ReadSeek, +}; -#[cfg(feature = "write-bhttp")] #[allow(clippy::cast_possible_truncation)] pub(crate) fn write_uint<const N: usize>(v: impl Into<u64>, w: &mut impl io::Write) -> Res<()> { let v = v.into().to_be_bytes(); @@ -15,7 +13,6 @@ pub(crate) fn write_uint<const N: usize>(v: impl Into<u64>, w: &mut impl io::Wri Ok(()) } -#[cfg(feature = "write-bhttp")] pub fn write_varint(v: impl Into<u64>, w: &mut impl io::Write) -> Res<()> { let v = v.into(); match () { @@ -27,51 +24,43 @@ pub fn write_varint(v: impl Into<u64>, w: &mut impl io::Write) -> Res<()> { } } -#[cfg(feature = "write-bhttp")] pub fn write_len(len: usize, w: &mut impl io::Write) -> Res<()> { write_varint(u64::try_from(len).unwrap(), w) } -#[cfg(feature = "write-bhttp")] pub fn write_vec(v: &[u8], w: &mut impl io::Write) -> Res<()> { write_len(v.len(), w)?; w.write_all(v)?; Ok(()) } -#[cfg(feature = "read-bhttp")] -fn read_uint<T, R>(n: usize, r: &mut T) -> Res<Option<u64>> +fn read_uint<T, R, const N: usize>(r: &mut T) -> Res<Option<u64>> where T: BorrowMut<R> + ?Sized, R: ReadSeek + ?Sized, { - let mut buf = [0; 7]; - let count = r.borrow_mut().read(&mut buf[..n])?; + let mut buf = [0; 8]; + let count = r.borrow_mut().read(&mut buf[(8 - N)..])?; if count == 0 { Ok(None) - } else if count < n { + } else if count < N { Err(Error::Truncated) } else { - let mut v = 0; - for i in &buf[..n] { - v = (v << 8) | u64::from(*i); - } - Ok(Some(v)) + Ok(Some(u64::from_be_bytes(buf))) } } -#[cfg(feature = "read-bhttp")] pub fn read_varint<T, R>(r: &mut T) -> Res<Option<u64>> where T: BorrowMut<R> + ?Sized, R: ReadSeek + ?Sized, { - if let Some(b1) = read_uint(1, r)? { + if let Some(b1) = read_uint::<_, _, 1>(r)? { Ok(Some(match b1 >> 6 { 0 => b1 & 0x3f, - 1 => ((b1 & 0x3f) << 8) | read_uint(1, r)?.ok_or(Error::Truncated)?, - 2 => ((b1 & 0x3f) << 24) | read_uint(3, r)?.ok_or(Error::Truncated)?, - 3 => ((b1 & 0x3f) << 56) | read_uint(7, r)?.ok_or(Error::Truncated)?, + 1 => ((b1 & 0x3f) << 8) | read_uint::<_, _, 1>(r)?.ok_or(Error::Truncated)?, + 2 => ((b1 & 0x3f) << 24) | read_uint::<_, _, 3>(r)?.ok_or(Error::Truncated)?, + 3 => ((b1 & 0x3f) << 56) | read_uint::<_, _, 7>(r)?.ok_or(Error::Truncated)?, _ => unreachable!(), })) } else { @@ -79,7 +68,6 @@ where } } -#[cfg(feature = "read-bhttp")] pub fn read_vec<T, R>(r: &mut T) -> Res<Option<Vec<u8>>> where T: BorrowMut<R> + ?Sized, diff --git a/third_party/rust/bhttp/src/stream/int.rs b/third_party/rust/bhttp/src/stream/int.rs @@ -0,0 +1,258 @@ +use std::{ + future::Future, + pin::{pin, Pin}, + task::{Context, Poll}, +}; + +use futures::io::AsyncRead; + +use crate::{Error, Res}; + +/// A reader for a network-byte-order integer of predetermined size. +#[pin_project::pin_project] +pub struct ReadUint<S, const N: usize> { + /// The source of data. + src: S, + /// A buffer that holds the bytes that have been read so far. + v: [u8; 8], + /// A counter of the number of bytes that are already in place. + /// This starts out at `8-N`. + read: usize, +} + +impl<S, const N: usize> ReadUint<S, N> { + pub fn stream(self) -> S { + self.src + } +} + +impl<S: AsyncRead + Unpin, const N: usize> Future for ReadUint<S, N> { + type Output = Res<u64>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { + let this = self.project(); + match pin!(this.src).poll_read(cx, &mut this.v[*this.read..]) { + Poll::Pending => Poll::Pending, + Poll::Ready(Ok(count)) => { + if count == 0 { + return Poll::Ready(Err(Error::Truncated)); + } + *this.read += count; + if *this.read == 8 { + Poll::Ready(Ok(u64::from_be_bytes(*this.v))) + } else { + Poll::Pending + } + } + Poll::Ready(Err(e)) => Poll::Ready(Err(Error::from(e))), + } + } +} + +#[cfg(test)] +fn read_uint<S, const N: usize>(src: S) -> ReadUint<S, N> { + ReadUint { + src, + v: [0; 8], + read: 8 - N, + } +} + +/// A reader for a [QUIC variable-length integer](https://datatracker.ietf.org/doc/html/rfc9000#section-16). +#[pin_project::pin_project(project = ReadVarintProj)] +pub enum ReadVarint<S> { + // Invariant: this Option always contains Some. + First(Option<S>), + Extra1(#[pin] ReadUint<S, 8>), + Extra3(#[pin] ReadUint<S, 8>), + Extra7(#[pin] ReadUint<S, 8>), +} + +impl<S> ReadVarint<S> { + pub fn stream(self) -> S { + match self { + Self::Extra1(s) | Self::Extra3(s) | Self::Extra7(s) => s.stream(), + Self::First(mut s) => s.take().unwrap(), + } + } +} + +impl<S: AsyncRead + Unpin> Future for ReadVarint<S> { + type Output = Res<Option<u64>>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { + let this = self.as_mut(); + if let Self::First(ref mut src) = this.get_mut() { + let mut buf = [0; 1]; + let src_ref = src.as_mut().unwrap(); + if let Poll::Ready(res) = pin!(src_ref).poll_read(cx, &mut buf[..]) { + match res { + Ok(0) => return Poll::Ready(Ok(None)), + Ok(_) => (), + Err(e) => return Poll::Ready(Err(Error::from(e))), + } + + let b1 = buf[0]; + let mut v = [0; 8]; + let next = match b1 >> 6 { + 0 => return Poll::Ready(Ok(Some(u64::from(b1)))), + 1 => { + let src = src.take().unwrap(); + v[6] = b1 & 0x3f; + Self::Extra1(ReadUint { src, v, read: 7 }) + } + 2 => { + let src = src.take().unwrap(); + v[4] = b1 & 0x3f; + Self::Extra3(ReadUint { src, v, read: 5 }) + } + 3 => { + let src = src.take().unwrap(); + v[0] = b1 & 0x3f; + Self::Extra7(ReadUint { src, v, read: 1 }) + } + _ => unreachable!(), + }; + + self.set(next); + } + } + let extra = match self.project() { + ReadVarintProj::Extra1(s) | ReadVarintProj::Extra3(s) | ReadVarintProj::Extra7(s) => { + s.poll(cx) + } + ReadVarintProj::First(_) => return Poll::Pending, + }; + if let Poll::Ready(v) = extra { + Poll::Ready(v.map(Some)) + } else { + Poll::Pending + } + } +} + +/// Read a [QUIC variable-length integer](https://datatracker.ietf.org/doc/html/rfc9000#section-16). +pub fn read_varint<S>(src: S) -> ReadVarint<S> { + ReadVarint::First(Some(src)) +} + +#[cfg(test)] +mod test { + use sync_async::SyncResolve; + + use crate::{ + err::Error, + rw::{write_uint as sync_write_uint, write_varint as sync_write_varint}, + stream::int::{read_uint, read_varint}, + }; + + const VARINTS: &[u64] = &[ + 0, + 1, + 63, + 64, + (1 << 14) - 1, + 1 << 14, + (1 << 30) - 1, + 1 << 30, + (1 << 62) - 1, + ]; + + #[test] + fn read_uint_values() { + macro_rules! validate_uint_range { + (@ $n:expr) => { + let m = u64::MAX >> (64 - 8 * $n); + for v in [0, 1, m] { + println!("{n} byte encoding of 0x{v:x}", n = $n); + let mut buf = Vec::with_capacity($n); + sync_write_uint::<$n>(v, &mut buf).unwrap(); + let mut buf_ref = &buf[..]; + let mut fut = read_uint::<_, $n>(&mut buf_ref); + assert_eq!(v, fut.sync_resolve().unwrap()); + let s = fut.stream(); + assert!(s.is_empty()); + } + }; + ($($n:expr),+ $(,)?) => { + $( + validate_uint_range!(@ $n); + )+ + } + } + validate_uint_range!(1, 2, 3, 4, 5, 6, 7, 8); + } + + #[test] + fn read_uint_truncated() { + macro_rules! validate_uint_truncated { + (@ $n:expr) => { + let m = u64::MAX >> (64 - 8 * $n); + for v in [0, 1, m] { + println!("{n} byte encoding of 0x{v:x}", n = $n); + let mut buf = Vec::with_capacity($n); + sync_write_uint::<$n>(v, &mut buf).unwrap(); + for i in 1..buf.len() { + let err = read_uint::<_, $n>(&mut &buf[..i]).sync_resolve().unwrap_err(); + assert!(matches!(err, Error::Truncated)); + } + } + }; + ($($n:expr),+ $(,)?) => { + $( + validate_uint_truncated!(@ $n); + )+ + } + } + validate_uint_truncated!(1, 2, 3, 4, 5, 6, 7, 8); + } + + #[test] + fn read_varint_values() { + for &v in VARINTS { + let mut buf = Vec::new(); + sync_write_varint(v, &mut buf).unwrap(); + let mut buf_ref = &buf[..]; + let mut fut = read_varint(&mut buf_ref); + assert_eq!(Some(v), fut.sync_resolve().unwrap()); + let s = fut.stream(); + assert!(s.is_empty()); + } + } + + #[test] + fn read_varint_none() { + assert!(read_varint(&mut &[][..]).sync_resolve().unwrap().is_none()); + } + + #[test] + fn read_varint_truncated() { + for &v in VARINTS { + let mut buf = Vec::new(); + sync_write_varint(v, &mut buf).unwrap(); + for i in 1..buf.len() { + let err = { + let mut buf: &[u8] = &buf[..i]; + read_varint(&mut buf).sync_resolve() + } + .unwrap_err(); + assert!(matches!(err, Error::Truncated)); + } + } + } + + #[test] + fn read_varint_extra() { + const EXTRA: &[u8] = &[161, 2, 49]; + for &v in VARINTS { + let mut buf = Vec::new(); + sync_write_varint(v, &mut buf).unwrap(); + buf.extend_from_slice(EXTRA); + let mut buf_ref = &buf[..]; + let mut fut = read_varint(&mut buf_ref); + assert_eq!(Some(v), fut.sync_resolve().unwrap()); + let s = fut.stream(); + assert_eq!(&s[..], EXTRA); + } + } +} diff --git a/third_party/rust/bhttp/src/stream/mod.rs b/third_party/rust/bhttp/src/stream/mod.rs @@ -0,0 +1,589 @@ +#![allow(dead_code)] +#![allow(clippy::incompatible_msrv)] // This module uses features from rust 1.82 + +use std::{ + cmp::min, + io::{Cursor, Error as IoError, Result as IoResult}, + mem, + pin::{pin, Pin}, + task::{Context, Poll}, +}; + +use futures::{stream::unfold, AsyncRead, Stream, TryStreamExt}; + +use crate::{ + err::Res, + stream::{int::read_varint, vec::read_vec}, + ControlData, Error, Field, FieldSection, Header, InformationalResponse, Message, Mode, COOKIE, +}; +mod int; +mod vec; + +trait AsyncReadControlData: Sized { + async fn async_read<S: AsyncRead + Unpin>(request: bool, src: S) -> Res<Self>; +} + +impl AsyncReadControlData for ControlData { + async fn async_read<S: AsyncRead + Unpin>(request: bool, mut src: S) -> Res<Self> { + let v = if request { + let method = read_vec(&mut src).await?.ok_or(Error::Truncated)?; + let scheme = read_vec(&mut src).await?.ok_or(Error::Truncated)?; + let authority = read_vec(&mut src).await?.ok_or(Error::Truncated)?; + let path = read_vec(&mut src).await?.ok_or(Error::Truncated)?; + Self::Request { + method, + scheme, + authority, + path, + } + } else { + let code = read_varint(&mut src).await?.ok_or(Error::Truncated)?; + Self::Response(crate::StatusCode::try_from(code)?) + }; + Ok(v) + } +} + +trait AsyncReadFieldSection: Sized { + async fn async_read<S: AsyncRead + Unpin>(mode: Mode, src: S) -> Res<Self>; +} + +impl AsyncReadFieldSection for FieldSection { + async fn async_read<S: AsyncRead + Unpin>(mode: Mode, mut src: S) -> Res<Self> { + let fields = if mode == Mode::KnownLength { + // Known-length fields can just be read into a buffer. + if let Some(buf) = read_vec(&mut src).await? { + Self::read_bhttp_fields(false, &mut Cursor::new(&buf[..]))? + } else { + Vec::new() + } + } else { + // The async version needs to be implemented directly. + let mut fields: Vec<Field> = Vec::new(); + let mut cookie_index: Option<usize> = None; + loop { + if let Some(n) = read_vec(&mut src).await? { + if n.is_empty() { + break fields; + } + let mut v = read_vec(&mut src).await?.ok_or(Error::Truncated)?; + if n == COOKIE { + if let Some(i) = &cookie_index { + fields[*i].value.extend_from_slice(b"; "); + fields[*i].value.append(&mut v); + continue; + } + cookie_index = Some(fields.len()); + } + fields.push(Field::new(n, v)); + } else if fields.is_empty() { + break fields; + } else { + return Err(Error::Truncated); + } + } + }; + Ok(Self(fields)) + } +} + +#[derive(Default)] +enum BodyState { + // The starting state. + #[default] + Init, + // When reading the length, use this. + ReadLength { + buf: [u8; 8], + read: usize, + }, + // When reading the data, track how much is left. + ReadData { + remaining: usize, + }, +} + +impl BodyState { + fn read_len() -> Self { + Self::ReadLength { + buf: [0; 8], + read: 0, + } + } +} + +pub struct Body<'b, S> { + msg: &'b mut AsyncMessage<S>, +} + +impl<S> Body<'_, S> {} + +impl<S: AsyncRead + Unpin> AsyncRead for Body<'_, S> { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll<IoResult<usize>> { + self.msg.read_body(cx, buf).map_err(IoError::other) + } +} + +/// A helper function for the more complex body-reading code. +fn poll_error(e: Error) -> Poll<IoResult<usize>> { + Poll::Ready(Err(IoError::other(e))) +} + +enum AsyncMessageState { + Init, + // Processing Informational responses (or before that). + Informational(bool), + // Having obtained the control data for the header, this is it. + Header(ControlData), + // Processing the Body. + Body(BodyState), + // Processing the trailer. + Trailer, + // All done. + Done, +} + +pub struct AsyncMessage<S> { + // Whether this is a request and which mode. + mode: Option<Mode>, + state: AsyncMessageState, + src: S, +} + +unsafe impl<S: Send> Send for AsyncMessage<S> {} + +impl<S: AsyncRead + Unpin> AsyncMessage<S> { + async fn next_info(&mut self) -> Res<Option<InformationalResponse>> { + let request = if matches!(self.state, AsyncMessageState::Init) { + // Read control data ... + let t = read_varint(&mut self.src).await?.ok_or(Error::Truncated)?; + let request = t == 0 || t == 2; + self.mode = Some(Mode::try_from(t)?); + self.state = AsyncMessageState::Informational(request); + request + } else { + // ... or recover it. + let AsyncMessageState::Informational(request) = self.state else { + return Err(Error::InvalidState); + }; + request + }; + + let control = ControlData::async_read(request, &mut self.src).await?; + if let Some(status) = control.informational() { + let mode = self.mode.unwrap(); + let fields = FieldSection::async_read(mode, &mut self.src).await?; + Ok(Some(InformationalResponse::new(status, fields))) + } else { + self.state = AsyncMessageState::Header(control); + Ok(None) + } + } + + /// Produces a stream of informational responses from a fresh message. + /// Returns an empty stream if passed a request (or if there are no informational responses). + /// Error values on the stream indicate failures. + /// + /// There is no need to call this method to read a request, though + /// doing so is harmless. + /// + /// You can discard the stream that this function returns + /// without affecting the message. You can then either call this + /// method again to get any additional informational responses or + /// call `header()` to get the message header. + pub fn informational(&mut self) -> impl Stream<Item = Res<InformationalResponse>> + '_ { + unfold(self, |this| async move { + this.next_info().await.transpose().map(|info| (info, this)) + }) + } + + /// This reads the header. If you have not called `informational` + /// and drained the resulting stream, this will do that for you. + /// # Panics + /// Never. + pub async fn header(&mut self) -> Res<Header> { + if matches!( + self.state, + AsyncMessageState::Init | AsyncMessageState::Informational(_) + ) { + // Need to scrub for errors, + // so that this can abort properly if there is one. + // The `try_any` usage is there to ensure that the stream is fully drained. + _ = self.informational().try_any(|_| async { false }).await?; + } + + if matches!(self.state, AsyncMessageState::Header(_)) { + let mode = self.mode.unwrap(); + let hfields = FieldSection::async_read(mode, &mut self.src).await?; + + let AsyncMessageState::Header(control) = mem::replace( + &mut self.state, + AsyncMessageState::Body(BodyState::default()), + ) else { + unreachable!(); + }; + Ok(Header::from((control, hfields))) + } else { + Err(Error::InvalidState) + } + } + + fn body_state(&mut self, s: BodyState) { + self.state = AsyncMessageState::Body(s); + } + + fn body_done(&mut self) { + self.state = AsyncMessageState::Trailer; + } + + /// Read the length of a body chunk. + /// This updates the values of `read` and `buf` to track the portion of the length + /// that was successfully read. + /// Returns `Some` with the error code that should be used if the reading + /// resulted in a conclusive outcome. + fn read_body_len( + cx: &mut Context<'_>, + src: &mut S, + first: bool, + read: &mut usize, + buf: &mut [u8; 8], + ) -> Option<Poll<Result<usize, IoError>>> { + let mut src = pin!(src); + if *read == 0 { + let mut b = [0; 1]; + match src.as_mut().poll_read(cx, &mut b[..]) { + Poll::Pending => return Some(Poll::Pending), + Poll::Ready(Ok(0)) => { + return if first { + // It's OK for the first length to be absent. + // Just skip to the end. + *read = 8; + None + } else { + // ...it's not OK to drop length when continuing. + Some(poll_error(Error::Truncated)) + }; + } + Poll::Ready(Ok(1)) => match b[0] >> 6 { + 0 => { + buf[7] = b[0] & 0x3f; + *read = 8; + } + 1 => { + buf[6] = b[0] & 0x3f; + *read = 7; + } + 2 => { + buf[4] = b[0] & 0x3f; + *read = 5; + } + 3 => { + buf[0] = b[0] & 0x3f; + *read = 1; + } + _ => unreachable!(), + }, + Poll::Ready(Ok(_)) => unreachable!(), + Poll::Ready(Err(e)) => return Some(Poll::Ready(Err(e))), + } + } + if *read < 8 { + match src.as_mut().poll_read(cx, &mut buf[*read..]) { + Poll::Pending => return Some(Poll::Pending), + Poll::Ready(Ok(0)) => return Some(poll_error(Error::Truncated)), + Poll::Ready(Ok(len)) => { + *read += len; + } + Poll::Ready(Err(e)) => return Some(Poll::Ready(Err(e))), + } + } + None + } + + fn read_body(&mut self, cx: &mut Context<'_>, buf: &mut [u8]) -> Poll<IoResult<usize>> { + // The length that precedes the first chunk can be absent. + // Only allow that for the first chunk (if indeterminate length). + let first = if let AsyncMessageState::Body(BodyState::Init) = &self.state { + self.body_state(BodyState::read_len()); + true + } else { + false + }; + + // Read the length. This uses `read_body_len` to track the state of this reading. + // This doesn't use `ReadVarint` or any convenience functions because we + // need to track the state and we don't want the borrow checker to flip out. + if let AsyncMessageState::Body(BodyState::ReadLength { buf, read }) = &mut self.state { + if let Some(res) = Self::read_body_len(cx, &mut self.src, first, read, buf) { + return res; + } + if *read == 8 { + match usize::try_from(u64::from_be_bytes(*buf)) { + Ok(0) => { + self.body_done(); + return Poll::Ready(Ok(0)); + } + Ok(remaining) => { + self.body_state(BodyState::ReadData { remaining }); + } + Err(e) => return poll_error(Error::IntRange(e)), + } + } + } + + match &mut self.state { + AsyncMessageState::Body(BodyState::ReadData { remaining }) => { + let amount = min(*remaining, buf.len()); + let res = pin!(&mut self.src).poll_read(cx, &mut buf[..amount]); + match res { + Poll::Pending => Poll::Pending, + Poll::Ready(Ok(0)) => poll_error(Error::Truncated), + Poll::Ready(Ok(len)) => { + *remaining -= len; + if *remaining == 0 { + let mode = self.mode.unwrap(); + if mode == Mode::IndeterminateLength { + self.body_state(BodyState::read_len()); + } else { + self.body_done(); + } + } + Poll::Ready(Ok(len)) + } + Poll::Ready(Err(e)) => Poll::Ready(Err(e)), + } + } + AsyncMessageState::Trailer => Poll::Ready(Ok(0)), + _ => Poll::Pending, + } + } + + /// Read the body. + /// This produces an implementation of `AsyncRead` that filters out + /// the framing from the message body. + /// # Errors + /// This errors when the header has not been read. + /// Any IO errors are generated by the returned `Body` instance. + pub fn body(&mut self) -> Res<Body<'_, S>> { + match self.state { + AsyncMessageState::Body(_) => Ok(Body { msg: self }), + _ => Err(Error::InvalidState), + } + } + + /// Read any trailer. + /// This might be empty. + /// # Errors + /// This errors when the body has not been read. + /// # Panics + /// Never. + pub async fn trailer(&mut self) -> Res<FieldSection> { + if matches!(self.state, AsyncMessageState::Trailer) { + let trailer = FieldSection::async_read(self.mode.unwrap(), &mut self.src).await?; + self.state = AsyncMessageState::Done; + Ok(trailer) + } else { + Err(Error::InvalidState) + } + } +} + +/// Asynchronous reading for a [`Message`]. +pub trait AsyncReadMessage: Sized { + fn async_read<S: AsyncRead + Unpin>(src: S) -> AsyncMessage<S>; +} + +impl AsyncReadMessage for Message { + fn async_read<S: AsyncRead + Unpin>(src: S) -> AsyncMessage<S> { + AsyncMessage { + mode: None, + state: AsyncMessageState::Init, + src, + } + } +} + +#[cfg(test)] +mod test { + use std::pin::pin; + + use futures::TryStreamExt; + use sync_async::{Dribble, SyncRead, SyncResolve, SyncTryCollect}; + + use crate::{stream::AsyncReadMessage, Error, Message}; + + // Example from Section 5.1 of RFC 9292. + const REQUEST1: &[u8] = &[ + 0x00, 0x03, 0x47, 0x45, 0x54, 0x05, 0x68, 0x74, 0x74, 0x70, 0x73, 0x00, 0x0a, 0x2f, 0x68, + 0x65, 0x6c, 0x6c, 0x6f, 0x2e, 0x74, 0x78, 0x74, 0x40, 0x6c, 0x0a, 0x75, 0x73, 0x65, 0x72, + 0x2d, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x34, 0x63, 0x75, 0x72, 0x6c, 0x2f, 0x37, 0x2e, 0x31, + 0x36, 0x2e, 0x33, 0x20, 0x6c, 0x69, 0x62, 0x63, 0x75, 0x72, 0x6c, 0x2f, 0x37, 0x2e, 0x31, + 0x36, 0x2e, 0x33, 0x20, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x53, 0x4c, 0x2f, 0x30, 0x2e, 0x39, + 0x2e, 0x37, 0x6c, 0x20, 0x7a, 0x6c, 0x69, 0x62, 0x2f, 0x31, 0x2e, 0x32, 0x2e, 0x33, 0x04, + 0x68, 0x6f, 0x73, 0x74, 0x0f, 0x77, 0x77, 0x77, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, + 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x0f, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x2d, 0x6c, 0x61, + 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x06, 0x65, 0x6e, 0x2c, 0x20, 0x6d, 0x69, 0x00, 0x00, + ]; + const REQUEST2: &[u8] = &[ + 0x02, 0x03, 0x47, 0x45, 0x54, 0x05, 0x68, 0x74, 0x74, 0x70, 0x73, 0x00, 0x0a, 0x2f, 0x68, + 0x65, 0x6c, 0x6c, 0x6f, 0x2e, 0x74, 0x78, 0x74, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x2d, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x34, 0x63, 0x75, 0x72, 0x6c, 0x2f, 0x37, 0x2e, 0x31, 0x36, 0x2e, + 0x33, 0x20, 0x6c, 0x69, 0x62, 0x63, 0x75, 0x72, 0x6c, 0x2f, 0x37, 0x2e, 0x31, 0x36, 0x2e, + 0x33, 0x20, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x53, 0x4c, 0x2f, 0x30, 0x2e, 0x39, 0x2e, 0x37, + 0x6c, 0x20, 0x7a, 0x6c, 0x69, 0x62, 0x2f, 0x31, 0x2e, 0x32, 0x2e, 0x33, 0x04, 0x68, 0x6f, + 0x73, 0x74, 0x0f, 0x77, 0x77, 0x77, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, + 0x63, 0x6f, 0x6d, 0x0f, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x2d, 0x6c, 0x61, 0x6e, 0x67, + 0x75, 0x61, 0x67, 0x65, 0x06, 0x65, 0x6e, 0x2c, 0x20, 0x6d, 0x69, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + + #[test] + fn informational() { + const INFO: &[u8] = &[1, 64, 100, 0, 64, 200, 0]; + let mut buf_alias = INFO; + let mut msg = Message::async_read(&mut buf_alias); + let info = msg.informational().sync_collect::<Vec<_>>().unwrap(); + assert_eq!(info.len(), 1); + let err = msg.informational().sync_collect::<Vec<_>>(); + assert!(matches!(err, Err(Error::InvalidState))); + let hdr = pin!(msg.header()).sync_resolve().unwrap(); + assert_eq!(hdr.control().status().unwrap().code(), 200); + assert!(hdr.is_empty()); + } + + #[test] + fn sample_requests() { + fn validate_sample_request(mut buf: &[u8]) { + let mut msg = Message::async_read(&mut buf); + let info = msg.informational().sync_collect::<Vec<_>>().unwrap(); + assert!(info.is_empty()); + + let hdr = pin!(msg.header()).sync_resolve().unwrap(); + assert_eq!(hdr.control(), &(b"GET", b"https", b"", b"/hello.txt")); + assert_eq!( + hdr.get(b"user-agent"), + Some(&b"curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3"[..]), + ); + assert_eq!(hdr.get(b"host"), Some(&b"www.example.com"[..])); + assert_eq!(hdr.get(b"accept-language"), Some(&b"en, mi"[..])); + assert_eq!(hdr.len(), 3); + + let body = pin!(msg.body().unwrap()).sync_read_to_end(); + assert!(body.is_empty()); + + let trailer = pin!(msg.trailer()).sync_resolve().unwrap(); + assert!(trailer.is_empty()); + } + + validate_sample_request(REQUEST1); + validate_sample_request(REQUEST2); + validate_sample_request(&REQUEST2[..REQUEST2.len() - 12]); + } + + #[test] + fn truncated_header() { + // The indefinite-length request example includes 10 bytes of padding. + // The three additional zero values at the end represent: + // 1. The terminating zero for the header field section. + // 2. The terminating zero for the (empty) body. + // 3. The terminating zero for the (absent) trailer field section. + // The latter two (body and trailer) can be cut and the message will still work. + // The first is not optional; dropping it means that the message is truncated. + let mut buf = &mut &REQUEST2[..REQUEST2.len() - 13]; + let mut msg = Message::async_read(&mut buf); + // Use this test to test skipping a few things. + let err = pin!(msg.header()).sync_resolve().unwrap_err(); + assert!(matches!(err, Error::Truncated)); + } + + /// This test is crazy. It reads a byte at a time and checks the state constantly. + #[test] + fn sample_response() { + const RESPONSE: &[u8] = &[ + 0x03, 0x40, 0x66, 0x07, 0x72, 0x75, 0x6e, 0x6e, 0x69, 0x6e, 0x67, 0x0a, 0x22, 0x73, + 0x6c, 0x65, 0x65, 0x70, 0x20, 0x31, 0x35, 0x22, 0x00, 0x40, 0x67, 0x04, 0x6c, 0x69, + 0x6e, 0x6b, 0x23, 0x3c, 0x2f, 0x73, 0x74, 0x79, 0x6c, 0x65, 0x2e, 0x63, 0x73, 0x73, + 0x3e, 0x3b, 0x20, 0x72, 0x65, 0x6c, 0x3d, 0x70, 0x72, 0x65, 0x6c, 0x6f, 0x61, 0x64, + 0x3b, 0x20, 0x61, 0x73, 0x3d, 0x73, 0x74, 0x79, 0x6c, 0x65, 0x04, 0x6c, 0x69, 0x6e, + 0x6b, 0x24, 0x3c, 0x2f, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x2e, 0x6a, 0x73, 0x3e, + 0x3b, 0x20, 0x72, 0x65, 0x6c, 0x3d, 0x70, 0x72, 0x65, 0x6c, 0x6f, 0x61, 0x64, 0x3b, + 0x20, 0x61, 0x73, 0x3d, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x00, 0x40, 0xc8, 0x04, + 0x64, 0x61, 0x74, 0x65, 0x1d, 0x4d, 0x6f, 0x6e, 0x2c, 0x20, 0x32, 0x37, 0x20, 0x4a, + 0x75, 0x6c, 0x20, 0x32, 0x30, 0x30, 0x39, 0x20, 0x31, 0x32, 0x3a, 0x32, 0x38, 0x3a, + 0x35, 0x33, 0x20, 0x47, 0x4d, 0x54, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x06, + 0x41, 0x70, 0x61, 0x63, 0x68, 0x65, 0x0d, 0x6c, 0x61, 0x73, 0x74, 0x2d, 0x6d, 0x6f, + 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x1d, 0x57, 0x65, 0x64, 0x2c, 0x20, 0x32, 0x32, + 0x20, 0x4a, 0x75, 0x6c, 0x20, 0x32, 0x30, 0x30, 0x39, 0x20, 0x31, 0x39, 0x3a, 0x31, + 0x35, 0x3a, 0x35, 0x36, 0x20, 0x47, 0x4d, 0x54, 0x04, 0x65, 0x74, 0x61, 0x67, 0x14, + 0x22, 0x33, 0x34, 0x61, 0x61, 0x33, 0x38, 0x37, 0x2d, 0x64, 0x2d, 0x31, 0x35, 0x36, + 0x38, 0x65, 0x62, 0x30, 0x30, 0x22, 0x0d, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x2d, + 0x72, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x05, 0x62, 0x79, 0x74, 0x65, 0x73, 0x0e, 0x63, + 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x02, + 0x35, 0x31, 0x04, 0x76, 0x61, 0x72, 0x79, 0x0f, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, + 0x2d, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x0c, 0x63, 0x6f, 0x6e, 0x74, + 0x65, 0x6e, 0x74, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x0a, 0x74, 0x65, 0x78, 0x74, 0x2f, + 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x00, 0x33, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, + 0x6f, 0x72, 0x6c, 0x64, 0x21, 0x20, 0x4d, 0x79, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x65, + 0x6e, 0x74, 0x20, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x73, 0x20, 0x61, 0x20, + 0x74, 0x72, 0x61, 0x69, 0x6c, 0x69, 0x6e, 0x67, 0x20, 0x43, 0x52, 0x4c, 0x46, 0x2e, + 0x0d, 0x0a, 0x00, 0x00, + ]; + + let mut buf = RESPONSE; + let mut msg = Message::async_read(Dribble::new(&mut buf)); + + { + // Need to scope access to `info` or it will hold the reference to `msg`. + let mut info = pin!(msg.informational()); + + let info1 = info.try_next().sync_resolve().unwrap().unwrap(); + assert_eq!(info1.status(), 102_u16); + assert_eq!(info1.len(), 1); + assert_eq!(info1.get(b"running"), Some(&b"\"sleep 15\""[..])); + + let info2 = info.try_next().sync_resolve().unwrap().unwrap(); + assert_eq!(info2.status(), 103_u16); + assert_eq!(info2.len(), 2); + let links = info2.get_all(b"link").collect::<Vec<_>>(); + assert_eq!( + &links, + &[ + &b"</style.css>; rel=preload; as=style"[..], + &b"</script.js>; rel=preload; as=script"[..], + ] + ); + + assert!(info.try_next().sync_resolve().unwrap().is_none()); + } + + let hdr = pin!(msg.header()).sync_resolve().unwrap(); + assert_eq!(hdr.control(), &200_u16); + assert_eq!(hdr.len(), 8); + assert_eq!(hdr.get(b"vary"), Some(&b"Accept-Encoding"[..])); + assert_eq!(hdr.get(b"etag"), Some(&b"\"34aa387-d-1568eb00\""[..])); + + { + let mut body = pin!(msg.body().unwrap()); + assert_eq!(body.sync_read_exact(12), b"Hello World!"); + } + // Attempting to read the trailer before finishing the body should fail. + assert!(matches!( + pin!(msg.trailer()).sync_resolve(), + Err(Error::InvalidState) + )); + { + // Picking up the body again should work fine. + let mut body = pin!(msg.body().unwrap()); + assert_eq!( + body.sync_read_to_end(), + b" My content includes a trailing CRLF.\r\n" + ); + } + let trailer = pin!(msg.trailer()).sync_resolve().unwrap(); + assert!(trailer.is_empty()); + } +} diff --git a/third_party/rust/bhttp/src/stream/vec.rs b/third_party/rust/bhttp/src/stream/vec.rs @@ -0,0 +1,221 @@ +use std::{ + future::Future, + mem, + pin::{pin, Pin}, + task::{Context, Poll}, +}; + +use futures::{io::AsyncRead, FutureExt}; + +use super::int::{read_varint, ReadVarint}; +use crate::{Error, Res}; + +/// A reader for a varint-length-prefixed buffer. +#[pin_project::pin_project(project = ReadVecProj)] +#[allow(clippy::module_name_repetitions)] +pub enum ReadVec<S> { + // Invariant: This Option is always Some. + ReadLen { + src: Option<ReadVarint<S>>, + cap: u64, + }, + ReadBody { + src: S, + buf: Vec<u8>, + remaining: usize, + }, +} + +impl<S> ReadVec<S> { + /// # Panics + /// If `limit` is more than `usize::MAX` or + /// if this is called after the length is read. + pub fn limit(&mut self, limit: u64) { + usize::try_from(limit).expect("cannot set a limit larger than usize::MAX"); + if let Self::ReadLen { ref mut cap, .. } = self { + *cap = limit; + } else { + panic!("cannot set a limit once the size has been read"); + } + } + + pub fn stream(self) -> S { + match self { + Self::ReadLen { mut src, .. } => src.take().unwrap().stream(), + Self::ReadBody { src, .. } => src, + } + } +} + +impl<S: AsyncRead + Unpin> Future for ReadVec<S> { + type Output = Res<Option<Vec<u8>>>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { + let this = self.as_mut(); + if let Self::ReadLen { src, cap } = this.get_mut() { + match src.as_mut().unwrap().poll_unpin(cx) { + Poll::Ready(Ok(None)) => return Poll::Ready(Ok(None)), + Poll::Ready(Ok(Some(0))) => return Poll::Ready(Ok(Some(Vec::new()))), + Poll::Ready(Ok(Some(sz))) => { + if sz > *cap { + return Poll::Ready(Err(Error::LimitExceeded)); + } + // `cap` cannot exceed min(usize::MAX, u64::MAX). + let sz = usize::try_from(sz).unwrap(); + let body = Self::ReadBody { + src: src.take().unwrap().stream(), + buf: vec![0; sz], + remaining: sz, + }; + self.set(body); + } + Poll::Ready(Err(e)) => return Poll::Ready(Err(e)), + Poll::Pending => return Poll::Pending, + } + } + + let ReadVecProj::ReadBody { + src, + buf, + remaining, + } = self.project() + else { + return Poll::Pending; + }; + + let offset = buf.len() - *remaining; + match pin!(src).poll_read(cx, &mut buf[offset..]) { + Poll::Pending => Poll::Pending, + Poll::Ready(Err(e)) => Poll::Ready(Err(Error::from(e))), + Poll::Ready(Ok(0)) => Poll::Ready(Err(Error::Truncated)), + Poll::Ready(Ok(c)) => { + *remaining -= c; + if *remaining > 0 { + Poll::Pending + } else { + Poll::Ready(Ok(Some(mem::take(buf)))) + } + } + } + } +} + +#[allow(clippy::module_name_repetitions)] +pub fn read_vec<S>(src: S) -> ReadVec<S> { + ReadVec::ReadLen { + src: Some(read_varint(src)), + cap: u64::try_from(usize::MAX).unwrap_or(u64::MAX), + } +} + +#[cfg(test)] +mod test { + + use std::{ + cmp, + fmt::Debug, + io::Result, + pin::Pin, + task::{Context, Poll}, + }; + + use futures::AsyncRead; + use sync_async::SyncResolve; + + use crate::{rw::write_varint as sync_write_varint, stream::vec::read_vec, Error}; + + const FILL_VALUE: u8 = 90; + + fn fill<T>(len: T) -> Vec<u8> + where + u64: TryFrom<T>, + <u64 as TryFrom<T>>::Error: Debug, + usize: TryFrom<T>, + <usize as TryFrom<T>>::Error: Debug, + T: Debug + Copy, + { + let mut buf = Vec::new(); + sync_write_varint(u64::try_from(len).unwrap(), &mut buf).unwrap(); + buf.resize(buf.len() + usize::try_from(len).unwrap(), FILL_VALUE); + buf + } + + #[test] + fn read_vecs() { + for len in [0, 1, 2, 3, 64] { + let buf = fill(len); + let mut buf_ref = &buf[..]; + let mut fut = read_vec(&mut buf_ref); + if let Ok(Some(out)) = fut.sync_resolve() { + assert_eq!(len, out.len()); + assert!(out.iter().all(|&v| v == FILL_VALUE)); + + assert!(fut.stream().is_empty()); + } + } + } + + #[test] + fn exceed_cap() { + const LEN: u64 = 20; + let buf = fill(LEN); + let mut buf_ref = &buf[..]; + let mut fut = read_vec(&mut buf_ref); + fut.limit(LEN - 1); + assert!(matches!(fut.sync_resolve(), Err(Error::LimitExceeded))); + } + + /// This class implements `AsyncRead`, but + /// always blocks after returning a fixed value. + #[derive(Default)] + struct IncompleteRead<'a> { + data: &'a [u8], + consumed: usize, + } + + impl<'a> IncompleteRead<'a> { + fn new(data: &'a [u8]) -> Self { + Self { data, consumed: 0 } + } + } + + impl AsyncRead for IncompleteRead<'_> { + fn poll_read( + mut self: Pin<&mut Self>, + _cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll<Result<usize>> { + let remaining = &self.data[self.consumed..]; + if remaining.is_empty() { + Poll::Pending + } else { + let copied = cmp::min(buf.len(), remaining.len()); + buf[..copied].copy_from_slice(&remaining[..copied]); + self.as_mut().consumed += copied; + Poll::Ready(std::io::Result::Ok(copied)) + } + } + } + + #[test] + #[should_panic(expected = "cannot set a limit once the size has been read")] + fn late_cap() { + let mut buf = IncompleteRead::new(&[2, 1]); + _ = read_vec(&mut buf).sync_resolve_with(|mut f| { + println!("pending"); + f.as_mut().limit(100); + }); + } + + #[test] + #[cfg(any(target_pointer_width = "32", target_pointer_width = "16"))] + #[should_panic(expected = "cannot set a limit larger than usize::MAX")] + fn too_large_cap() { + const LEN: u64 = 20; + let buf = fill(LEN); + + let mut buf_ref = &buf[..]; + let mut fut = read_vec(&mut buf_ref); + fut.limit(u64::try_from(usize::MAX).unwrap() + 1); + } +} diff --git a/third_party/rust/bhttp/tests/test.rs b/third_party/rust/bhttp/tests/test.rs @@ -1,5 +1,5 @@ // Rather than grapple with #[cfg(...)] for every variable and import. -#![cfg(all(feature = "http", feature = "bhttp"))] +#![cfg(feature = "http")] use std::{io::Cursor, mem::drop}; diff --git a/third_party/rust/ohttp/.cargo-checksum.json b/third_party/rust/ohttp/.cargo-checksum.json @@ -1 +1 @@ -{"files":{"Cargo.toml":"74baeadbe2810a83b2e49bab9ea7069bc033873b83a9e8cee16adee94bba73d6","README.md":"ca2de60828b7ec7d80729fd3e064cd8e08e3a72a94a50f29b7f4bd23b044fdbd","bindings/bindings.toml":"a016870127b63151e760c964d687934a4883ee165bdd9718341c8dd50be5a3f2","bindings/nspr_err.h":"2d5205d017b536c2d838bcf9bc4ec79f96dd50e7bb9b73892328781f1ee6629d","bindings/nspr_error.h":"e41c03c77b8c22046f8618832c9569fbcc7b26d8b9bbc35eea7168f35e346889","bindings/nss_init.h":"cd4dffd0c629ece5786736dd6d26db8a96f56fd56ef95b150c623c41080c2f9e","bindings/nss_p11.h":"a16f60d0210d5823f2d92d0c04988a0bb1da85901388490cb3e755a62cc7d5dd","bindings/nss_secerr.h":"713e8368bdae5159af7893cfa517dabfe5103cede051dee9c9557c850a2defc6","build.rs":"4a107baae67552e72f4db35a6dfdcef2101ff38350130f33ff37c882f542b934","src/config.rs":"bea6c3af58f3d03e9c06fa008523c57b6ea71a3c37a575823719418ea50e2072","src/err.rs":"f94da3b7d148e491471d5b6b0d450597b5e3ea05126576352b2879b446ff1430","src/hpke.rs":"d09e673a967f76e59f0b9c9d4c8ad25a9cf1f580dc7ebbdcbefc03f30e0642e7","src/lib.rs":"bfc972343aa3fa6707b0aaea04c25f1b6fa2ff3f9e9761ae6a09f9be52fce214","src/nss/aead.rs":"4bab67456c774f557035c01f5fb10be61f76c95ce4d7773d599268dba4527ec2","src/nss/err.rs":"d367116bf9840ebe06d358066a1755f7d9936438dbc2e4e71a6b345d05e4bcd5","src/nss/hkdf.rs":"fe66619f75affd4b812494c1433411b382a49b4bfbad5c0ac4deea89fea8065b","src/nss/hpke.rs":"e9dc855fa5c91a57d86c32859ab2f5e0c6f51bb2e97ac5710a5ce5152e1f2082","src/nss/mod.rs":"0308592fbc48ecf7cd13a92f2b0cea29fbcd9b8082586cee21d36329bf71042a","src/nss/p11.rs":"f6e68fe5a81271db63fd11e601bc83e5dca3b589064b528bc50403eaccf8c627","src/rand.rs":"be3a82fb6090b5cb833c2b8ba6e72690b9f44f7f91477e2c5b70b75623174b87","src/rh/aead.rs":"88dd43ffec20a8870a424d27dbda0a9db74f4b3d1b03097220854a304ab091cd","src/rh/hkdf.rs":"4c6e59ca24498939e9b8da2da6336ae2f62396101f1b9b25f1ae587ab21c1a9d","src/rh/hpke.rs":"0de3fd7a12ff551308a5ce6ad04cf077296571c265ddd3083d62b2bf7e51f70d","src/rh/mod.rs":"850772462778a74fd7402b4bb765ad57011b02729e6aa1bb3236afd490e5c3a4"},"package":null} -\ No newline at end of file +{"files":{"Cargo.lock":"cd7c572463c057533f2febfc63c2829a3892b4c3bb8482e194023fff9feeb91a","Cargo.toml":"faae934b1f4029fdf838d0ab6aa75bfd4290b02a32892cccd6c962a5cf02897f","README.md":"ca2de60828b7ec7d80729fd3e064cd8e08e3a72a94a50f29b7f4bd23b044fdbd","bindings/bindings.toml":"a016870127b63151e760c964d687934a4883ee165bdd9718341c8dd50be5a3f2","bindings/nspr_err.h":"2d5205d017b536c2d838bcf9bc4ec79f96dd50e7bb9b73892328781f1ee6629d","bindings/nspr_error.h":"e41c03c77b8c22046f8618832c9569fbcc7b26d8b9bbc35eea7168f35e346889","bindings/nss_init.h":"cd4dffd0c629ece5786736dd6d26db8a96f56fd56ef95b150c623c41080c2f9e","bindings/nss_p11.h":"a16f60d0210d5823f2d92d0c04988a0bb1da85901388490cb3e755a62cc7d5dd","bindings/nss_secerr.h":"713e8368bdae5159af7893cfa517dabfe5103cede051dee9c9557c850a2defc6","build.rs":"80ac37bb2c8617e09e9abd8d4326951871d0ead8bf53355f1fd0c0ab39e53d06","src/config.rs":"9e576ab0999ec61cef05314e1cf3974ffa04db1541b6374e9b15fbb02d2ca468","src/crypto.rs":"c3723bdaf0a20837464b8a82aa44b863f27b581915ec3b5c5c7d2d67ab57e7a8","src/err.rs":"f622dabee3541b7fe62163d9b690f0333812834f835558970ca4b1d9b4079e2f","src/hpke.rs":"d09e673a967f76e59f0b9c9d4c8ad25a9cf1f580dc7ebbdcbefc03f30e0642e7","src/lib.rs":"dc098efc3ec3ec8038cbf0f8efb6e31cda475ddc367f475a7be6dbdd924fbc30","src/nss/aead.rs":"f4ffffb47e9719678925e53efd5f57d6e0a4f752caf571af48544d63d7a61f79","src/nss/err.rs":"726ec69ab8060b837322e8152d8437f4cf6a246f9bbdb3e2111af435a883747a","src/nss/hkdf.rs":"7e55655215d0304c3ed5a0b1de2485babfd6e999f2b0515b1b250dc842f55623","src/nss/hpke.rs":"41934afb3e4f81d212d4ea8cb217cdb9abfe61b18d28291eca063eccc037400d","src/nss/mod.rs":"7dd53ba284439d99ba34f7af40f9760d8e480ad3ad373e9c518f9f68c579a68d","src/nss/p11.rs":"bd6921761e21134fe8147c2dc934ed4b6b3e9a0a5fd15c49aad110889d930f54","src/rand.rs":"90242c85555ae56ae22611de8e16713450dfc08e84a3c8c7e1d2641d36a33026","src/rh/aead.rs":"6d4cdaa102e33453b2e89129e17a547e877c12fcbeab73bf5961e565fe8e52b3","src/rh/hkdf.rs":"8a7108947d623cfc67a0fe9bb5916415ca3f175db307c01f0ca4ae855597c3c7","src/rh/hpke.rs":"35b3d8b47165936d0730736ec12f8bdcc8e8c45759e1d8c1dd95c55143c3b07a","src/rh/mod.rs":"850772462778a74fd7402b4bb765ad57011b02729e6aa1bb3236afd490e5c3a4","src/stream.rs":"cfbb17391633106482f15a1d578638467881b6edc0915d1e319c9b2f3ca5d92b"},"package":"41a03aaaf57495c75ce66aee6a7c3b21abf046c9d4cca3d45b22cdbf0de1bfba"} +\ No newline at end of file diff --git a/third_party/rust/ohttp/Cargo.lock b/third_party/rust/ohttp/Cargo.lock @@ -0,0 +1,961 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "hpke" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65d16b699dd1a1fa2d851c970b0c971b388eeeb40f744252b8de48860980c8f" +dependencies = [ + "aead", + "aes-gcm", + "chacha20poly1305", + "digest", + "generic-array", + "hkdf", + "hmac", + "rand_core 0.9.3", + "sha2", + "subtle", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "indexmap" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mozbuild" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "903970ae2f248d7275214cf8f387f8ba0c4ea7e3d87a320e85493db60ce28616" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "ohttp" +version = "0.7.2" +dependencies = [ + "aead", + "aes-gcm", + "bindgen", + "byteorder", + "chacha20poly1305", + "env_logger", + "futures", + "hex", + "hkdf", + "hpke", + "log", + "mozbuild", + "pin-project", + "rand", + "regex", + "serde", + "serde_derive", + "sha2", + "thiserror", + "toml", +] + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "regex" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", +] + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/third_party/rust/ohttp/Cargo.toml b/third_party/rust/ohttp/Cargo.toml @@ -11,9 +11,9 @@ [package] edition = "2021" -rust-version = "1.82.0" +rust-version = "1.83.0" name = "ohttp" -version = "0.6.1" +version = "0.7.2" authors = ["Martin Thomson <mt@lowentropy.net>"] build = "build.rs" autolib = false @@ -23,7 +23,7 @@ autotests = false autobenches = false description = "Oblivious HTTP" homepage = "https://github.com/martinthomson/ohttp" -readme = "../README.md" +readme = "README.md" keywords = [ "ohttp", "http", @@ -45,53 +45,63 @@ default = [ "client", "server", "rust-hpke", + "stream", ] external-sqlite = [] gecko = [ "nss", - "mozbuild", + "dep:mozbuild", ] -nss = ["bindgen"] +nss = ["dep:bindgen"] rust-hpke = [ - "rand", - "aead", - "aes-gcm", - "chacha20poly1305", - "hkdf", - "sha2", - "hpke", + "dep:rand", + "dep:aead", + "dep:aes-gcm", + "dep:chacha20poly1305", + "dep:hkdf", + "dep:sha2", + "dep:hpke", ] server = [] +stream = [ + "dep:futures", + "dep:pin-project", +] unsafe-print-secrets = [] [lib] name = "ohttp" path = "src/lib.rs" -[dependencies] -byteorder = "1.4" -hex = "0.4" -thiserror = "1" - [dependencies.aead] -version = "0.4" +version = "0.5" features = ["std"] optional = true [dependencies.aes-gcm] -version = "0.9" +version = "0.10" optional = true +[dependencies.byteorder] +version = "1.4" + [dependencies.chacha20poly1305] -version = "0.8" +version = "0.10" +optional = true + +[dependencies.futures] +version = "0.3" optional = true +[dependencies.hex] +version = "0.4" + [dependencies.hkdf] -version = "0.11" +version = "0.12" optional = true [dependencies.hpke] -version = "0.11.0" +version = "0.13" features = [ "std", "x25519", @@ -103,8 +113,12 @@ default-features = false version = "0.4" default-features = false +[dependencies.pin-project] +version = "1.1" +optional = true + [dependencies.rand] -version = "0.8" +version = "0.9" optional = true [dependencies.regex] @@ -112,18 +126,16 @@ version = "~1.11" optional = true [dependencies.sha2] -version = "0.9" +version = "0.10" optional = true +[dependencies.thiserror] +version = "2" + [dev-dependencies.env_logger] version = "0.10" default-features = false -[build-dependencies] -serde = "1.0" -serde_derive = "1.0" -toml = "0.5" - [build-dependencies.bindgen] version = "0.72" features = ["runtime"] @@ -133,3 +145,12 @@ default-features = false [build-dependencies.mozbuild] version = "0.1" optional = true + +[build-dependencies.serde] +version = "1.0" + +[build-dependencies.serde_derive] +version = "1.0" + +[build-dependencies.toml] +version = ">=0.5,<=0.9" diff --git a/third_party/rust/ohttp/build.rs b/third_party/rust/ohttp/build.rs @@ -168,8 +168,8 @@ mod nss { "armv8_c_lib", "gcm-aes-arm32-neon_c_lib", "gcm-aes-aarch64_c_lib", - // NOTE: The intel-gcm-* libraries are already automatically - // included in freebl_static as source files. + "intel-gcm-s_lib", + "intel-gcm-wrap_c_lib", ]; // Build rules are complex, so simply check the lib directory to see if @@ -191,6 +191,9 @@ mod nss { } fn static_link(nsslibdir: &Path, use_static_softoken: bool, use_static_nspr: bool) { + let target_os = + env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS must be set by Cargo"); + // The ordering of these libraries is critical for the linker. let mut static_libs = vec!["cryptohi", "nss_static"]; let mut dynamic_libs = vec![]; @@ -211,15 +214,22 @@ mod nss { dynamic_libs.append(&mut nspr_libs()); } - if cfg!(not(feature = "external-sqlite")) && env::consts::OS != "macos" { + if cfg!(not(feature = "external-sqlite")) && target_os != "macos" { static_libs.push("sqlite"); } // Dynamic libs that aren't transitively included by NSS libs. - if env::consts::OS != "windows" { - dynamic_libs.extend_from_slice(&["pthread", "dl", "c", "z"]); + match target_os.as_str() { + // Windows doesn't need these + "windows" => {} + // Android has pthread built into libc (bionic), don't link it separately + // pthread is not available as a separate library on Android + "android" => dynamic_libs.extend_from_slice(&["dl", "c", "z"]), + // Other Unix-like systems (Linux, macOS, etc.) + _ => dynamic_libs.extend_from_slice(&["pthread", "dl", "c", "z"]), } - if cfg!(not(feature = "external-sqlite")) && env::consts::OS == "macos" { + + if cfg!(not(feature = "external-sqlite")) && target_os == "macos" { dynamic_libs.push("sqlite3"); } @@ -344,7 +354,7 @@ mod nss { assert_eq!( v.next(), Some("3"), - "NSS version 3.62 or higher is needed (or set $NSS_DIR)" + " version 3.62 or higher is needed (or set $NSS_DIR)" ); if let Some(minor) = v.next() { let minor = minor diff --git a/third_party/rust/ohttp/src/config.rs b/third_party/rust/ohttp/src/config.rs @@ -1,24 +1,24 @@ -use crate::{ - err::{Error, Res}, - hpke::{Aead as AeadId, Kdf, Kem}, - KeyId, -}; -use byteorder::{NetworkEndian, ReadBytesExt, WriteBytesExt}; use std::{ convert::TryFrom, io::{BufRead, BufReader, Cursor, Read}, }; +use byteorder::{NetworkEndian, ReadBytesExt, WriteBytesExt}; + #[cfg(feature = "nss")] use crate::nss::{ hpke::{generate_key_pair, Config as HpkeConfig, HpkeR}, PrivateKey, PublicKey, }; - #[cfg(feature = "rust-hpke")] use crate::rh::hpke::{ derive_key_pair, generate_key_pair, Config as HpkeConfig, HpkeR, PrivateKey, PublicKey, }; +use crate::{ + err::{Error, Res}, + hpke::{Aead as AeadId, Kdf, Kem}, + KeyId, +}; /// A tuple of KDF and AEAD identifiers. #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -95,18 +95,15 @@ impl KeyConfig { Self::strip_unsupported(&mut symmetric, kem); assert!(!symmetric.is_empty()); let (sk, pk) = derive_key_pair(kem, ikm)?; - Ok(Self { + return Ok(Self { key_id, kem, symmetric, sk: Some(sk), pk, - }) - } - #[cfg(not(feature = "rust-hpke"))] - { - Err(Error::Unsupported) + }); } + Err(Error::Unsupported) } /// Encode a list of key configurations. @@ -260,6 +257,22 @@ impl KeyConfig { Err(Error::Unsupported) } } + + #[allow(clippy::similar_names)] // for kem_id and key_id + pub(crate) fn decode_hpke_config(&self, r: &mut Cursor<&[u8]>) -> Res<HpkeConfig> { + let key_id = r.read_u8()?; + if key_id != self.key_id { + return Err(Error::KeyId); + } + let kem_id = Kem::try_from(r.read_u16::<NetworkEndian>()?)?; + if kem_id != self.kem { + return Err(Error::InvalidKem); + } + let kdf_id = Kdf::try_from(r.read_u16::<NetworkEndian>()?)?; + let aead_id = AeadId::try_from(r.read_u16::<NetworkEndian>()?)?; + let hpke_config = HpkeConfig::new(self.kem, kdf_id, aead_id); + Ok(hpke_config) + } } impl AsRef<Self> for KeyConfig { @@ -270,11 +283,12 @@ impl AsRef<Self> for KeyConfig { #[cfg(test)] mod test { + use std::iter::zip; + use crate::{ hpke::{Aead, Kdf, Kem}, init, Error, KeyConfig, KeyId, SymmetricSuite, }; - use std::iter::zip; const KEY_ID: KeyId = 1; const KEM: Kem = Kem::X25519Sha256; diff --git a/third_party/rust/ohttp/src/crypto.rs b/third_party/rust/ohttp/src/crypto.rs @@ -0,0 +1,13 @@ +use crate::{err::Res, AeadId}; + +pub trait Decrypt { + fn open(&mut self, aad: &[u8], ct: &[u8]) -> Res<Vec<u8>>; + #[allow(dead_code)] // Used by stream feature. + fn alg(&self) -> AeadId; +} + +pub trait Encrypt { + fn seal(&mut self, aad: &[u8], ct: &[u8]) -> Res<Vec<u8>>; + #[allow(dead_code)] // Used by stream feature. + fn alg(&self) -> AeadId; +} diff --git a/third_party/rust/ohttp/src/err.rs b/third_party/rust/ohttp/src/err.rs @@ -5,9 +5,15 @@ pub enum Error { #[cfg(feature = "rust-hpke")] #[error("a problem occurred with the AEAD")] Aead(#[from] aead::Error), + #[cfg(feature = "stream")] + #[error("a stream chunk was larger than the maximum allowed size")] + ChunkTooLarge, #[cfg(feature = "nss")] #[error("a problem occurred during cryptographic processing: {0}")] Crypto(#[from] crate::nss::Error), + #[cfg(feature = "stream")] + #[error("a stream contained data after the last chunk")] + ExtraData, #[error("an error was found in the format")] Format, #[cfg(feature = "rust-hpke")] @@ -23,12 +29,18 @@ pub enum Error { Io(#[from] std::io::Error), #[error("the key ID was invalid")] KeyId, + #[cfg(feature = "stream")] + #[error("the object was not ready")] + NotReady, + #[error("the configuration contained too many symmetric suites")] + TooManySymmetricSuites, #[error("a field was truncated")] Truncated, #[error("the configuration was not supported")] Unsupported, - #[error("the configuration contained too many symmetric suites")] - TooManySymmetricSuites, + #[cfg(feature = "stream")] + #[error("writes are not supported after closing")] + WriteAfterClose, } impl From<std::num::TryFromIntError> for Error { diff --git a/third_party/rust/ohttp/src/lib.rs b/third_party/rust/ohttp/src/lib.rs @@ -4,8 +4,11 @@ not(all(feature = "client", feature = "server")), allow(dead_code, unused_imports) )] +#[cfg(all(feature = "nss", feature = "rust-hpke"))] +compile_error!("features \"nss\" and \"rust-hpke\" are mutually incompatible"); mod config; +mod crypto; mod err; pub mod hpke; #[cfg(feature = "nss")] @@ -14,48 +17,48 @@ mod nss; mod rand; #[cfg(feature = "rust-hpke")] mod rh; +#[cfg(feature = "stream")] +mod stream; -pub use crate::{ - config::{KeyConfig, SymmetricSuite}, - err::Error, -}; - -use crate::{ - err::Res, - hpke::{Aead as AeadId, Kdf, Kem}, -}; -use byteorder::{NetworkEndian, ReadBytesExt, WriteBytesExt}; -use log::trace; use std::{ cmp::max, convert::TryFrom, - io::{BufReader, Read}, + io::{Cursor, Read}, mem::size_of, }; -#[cfg(feature = "nss")] -use crate::nss::random; +use byteorder::{NetworkEndian, WriteBytesExt}; +use crypto::{Decrypt, Encrypt}; +use log::trace; + #[cfg(feature = "nss")] use crate::nss::{ aead::{Aead, Mode, NONCE_LEN}, hkdf::{Hkdf, KeyMechanism}, hpke::{Config as HpkeConfig, Exporter, HpkeR, HpkeS}, + random, PublicKey, SymKey, }; - -#[cfg(feature = "rust-hpke")] -use crate::rand::random; +#[cfg(feature = "stream")] +use crate::stream::{ClientRequest as StreamClient, ServerRequest as ServerRequestStream}; +pub use crate::{ + config::{KeyConfig, SymmetricSuite}, + err::Error, +}; +use crate::{err::Res, hpke::Aead as AeadId}; #[cfg(feature = "rust-hpke")] -use crate::rh::{ - aead::{Aead, Mode, NONCE_LEN}, - hkdf::{Hkdf, KeyMechanism}, - hpke::{Config as HpkeConfig, Exporter, HpkeR, HpkeS}, +use crate::{ + rand::random, + rh::{ + aead::{Aead, Mode, NONCE_LEN}, + hkdf::{Hkdf, KeyMechanism}, + hpke::{Config as HpkeConfig, Exporter, HpkeR, HpkeS, PublicKey}, + SymKey, + }, }; /// The request header is a `KeyId` and 2 each for KEM, KDF, and AEAD identifiers const REQUEST_HEADER_LEN: usize = size_of::<KeyId>() + 6; const INFO_REQUEST: &[u8] = b"message/bhttp request"; -/// The info used for HPKE export is `INFO_REQUEST`, a zero byte, and the header. -const INFO_LEN: usize = INFO_REQUEST.len() + 1 + REQUEST_HEADER_LEN; const LABEL_RESPONSE: &[u8] = b"message/bhttp response"; const INFO_KEY: &[u8] = b"key"; const INFO_NONCE: &[u8] = b"nonce"; @@ -69,9 +72,9 @@ pub fn init() { } /// Construct the info parameter we use to initialize an `HpkeS` instance. -fn build_info(key_id: KeyId, config: HpkeConfig) -> Res<Vec<u8>> { - let mut info = Vec::with_capacity(INFO_LEN); - info.extend_from_slice(INFO_REQUEST); +fn build_info(label: &[u8], key_id: KeyId, config: HpkeConfig) -> Res<Vec<u8>> { + let mut info = Vec::with_capacity(label.len() + 1 + REQUEST_HEADER_LEN); + info.extend_from_slice(label); info.push(0); info.write_u8(key_id)?; info.write_u16::<NetworkEndian>(u16::from(config.kem()))?; @@ -85,8 +88,9 @@ fn build_info(key_id: KeyId, config: HpkeConfig) -> Res<Vec<u8>> { /// This might not be necessary if we agree on a format. #[cfg(feature = "client")] pub struct ClientRequest { - hpke: HpkeS, - header: Vec<u8>, + key_id: KeyId, + config: HpkeConfig, + pk: PublicKey, } #[cfg(feature = "client")] @@ -95,14 +99,11 @@ impl ClientRequest { pub fn from_config(config: &mut KeyConfig) -> Res<Self> { // TODO(mt) choose the best config, not just the first. let selected = config.select(config.symmetric[0])?; - - // Build the info, which contains the message header. - let info = build_info(config.key_id, selected)?; - let hpke = HpkeS::new(selected, &mut config.pk, &info)?; - - let header = Vec::from(&info[INFO_REQUEST.len() + 1..]); - debug_assert_eq!(header.len(), REQUEST_HEADER_LEN); - Ok(Self { hpke, header }) + Ok(Self { + key_id: config.key_id, + config: selected, + pk: config.pk.clone(), + }) } /// Reads an encoded configuration and constructs a single use client sender. @@ -126,22 +127,33 @@ impl ClientRequest { /// Encapsulate a request. This consumes this object. /// This produces a response handler and the bytes of an encapsulated request. - pub fn encapsulate(mut self, request: &[u8]) -> Res<(Vec<u8>, ClientResponse)> { - let extra = - self.hpke.config().kem().n_enc() + self.hpke.config().aead().n_t() + request.len(); - let expected_len = self.header.len() + extra; + pub fn encapsulate(self, request: &[u8]) -> Res<(Vec<u8>, ClientResponse)> { + // Build the info, which contains the message header. + let info = build_info(INFO_REQUEST, self.key_id, self.config)?; + let mut hpke = HpkeS::new(self.config, &self.pk, &info)?; - let mut enc_request = self.header; + let header = Vec::from(&info[INFO_REQUEST.len() + 1..]); + debug_assert_eq!(header.len(), REQUEST_HEADER_LEN); + + let extra = hpke.config().kem().n_enc() + hpke.config().aead().n_t() + request.len(); + let expected_len = header.len() + extra; + + let mut enc_request = header; enc_request.reserve_exact(extra); - let enc = self.hpke.enc()?; + let enc = hpke.enc()?; enc_request.extend_from_slice(&enc); - let mut ct = self.hpke.seal(&[], request)?; + let mut ct = hpke.seal(&[], request)?; enc_request.append(&mut ct); debug_assert_eq!(expected_len, enc_request.len()); - Ok((enc_request, ClientResponse::new(self.hpke, enc))) + Ok((enc_request, ClientResponse::new(hpke, enc))) + } + + #[cfg(feature = "stream")] + pub fn encapsulate_stream<S>(self, dst: S) -> Res<StreamClient<S>> { + StreamClient::start(dst, self.config, self.key_id, &self.pk) } } @@ -170,48 +182,45 @@ impl Server { &self.config } - /// Remove encapsulation on a message. + fn decode_request_header(&self, r: &mut Cursor<&[u8]>, label: &[u8]) -> Res<(HpkeR, Vec<u8>)> { + let hpke_config = self.config.decode_hpke_config(r)?; + let sym = SymmetricSuite::new(hpke_config.kdf(), hpke_config.aead()); + let config = self.config.select(sym)?; + let info = build_info(label, self.config.key_id, hpke_config)?; + + let mut enc = vec![0; config.kem().n_enc()]; + r.read_exact(&mut enc)?; + + Ok(( + HpkeR::new( + config, + &self.config.pk, + self.config.sk.as_ref().unwrap(), + &enc, + &info, + )?, + enc, + )) + } + + /// Remove encapsulation on a request. /// # Panics /// Not as a consequence of this code, but Rust won't know that for sure. - #[allow(clippy::similar_names)] // for kem_id and key_id pub fn decapsulate(&self, enc_request: &[u8]) -> Res<(Vec<u8>, ServerResponse)> { - if enc_request.len() < REQUEST_HEADER_LEN { + if enc_request.len() <= REQUEST_HEADER_LEN { return Err(Error::Truncated); } - let mut r = BufReader::new(enc_request); - let key_id = r.read_u8()?; - if key_id != self.config.key_id { - return Err(Error::KeyId); - } - let kem_id = Kem::try_from(r.read_u16::<NetworkEndian>()?)?; - if kem_id != self.config.kem { - return Err(Error::InvalidKem); - } - let kdf_id = Kdf::try_from(r.read_u16::<NetworkEndian>()?)?; - let aead_id = AeadId::try_from(r.read_u16::<NetworkEndian>()?)?; - let sym = SymmetricSuite::new(kdf_id, aead_id); - - let info = build_info( - key_id, - HpkeConfig::new(self.config.kem, sym.kdf(), sym.aead()), - )?; - - let cfg = self.config.select(sym)?; - let mut enc = vec![0; cfg.kem().n_enc()]; - r.read_exact(&mut enc)?; - let mut hpke = HpkeR::new( - cfg, - &self.config.pk, - self.config.sk.as_ref().unwrap(), - &enc, - &info, - )?; + let mut r = Cursor::new(enc_request); + let (mut hpke, enc) = self.decode_request_header(&mut r, INFO_REQUEST)?; - let mut ct = Vec::new(); - r.read_to_end(&mut ct)?; + let request = hpke.open(&[], &enc_request[usize::try_from(r.position())?..])?; + Ok((request, ServerResponse::new(&hpke, &enc)?)) + } - let request = hpke.open(&[], &ct)?; - Ok((request, ServerResponse::new(&hpke, enc)?)) + /// Remove encapsulation on a streamed request. + #[cfg(feature = "stream")] + pub fn decapsulate_stream<S>(self, src: S) -> ServerRequestStream<S> { + ServerRequestStream::new(self.config, src) } } @@ -219,19 +228,16 @@ fn entropy(config: HpkeConfig) -> usize { max(config.aead().n_n(), config.aead().n_k()) } -fn make_aead( - mode: Mode, - cfg: HpkeConfig, - exp: &impl Exporter, - enc: Vec<u8>, - response_nonce: &[u8], -) -> Res<Aead> { - let secret = exp.export(LABEL_RESPONSE, entropy(cfg))?; - let mut salt = enc; - salt.extend_from_slice(response_nonce); +fn export_secret<E: Exporter>(exp: &E, label: &[u8], cfg: HpkeConfig) -> Res<SymKey> { + exp.export(label, entropy(cfg)) +} + +fn make_aead(mode: Mode, cfg: HpkeConfig, secret: &SymKey, enc: &[u8], nonce: &[u8]) -> Res<Aead> { + let mut salt = enc.to_vec(); + salt.extend_from_slice(nonce); let hkdf = Hkdf::new(cfg.kdf()); - let prk = hkdf.extract(&salt, &secret)?; + let prk = hkdf.extract(&salt, secret)?; let key = hkdf.expand_key(&prk, INFO_KEY, KeyMechanism::Aead(cfg.aead()))?; let iv = hkdf.expand_data(&prk, INFO_NONCE, cfg.aead().n_n())?; @@ -250,9 +256,15 @@ pub struct ServerResponse { #[cfg(feature = "server")] impl ServerResponse { - fn new(hpke: &HpkeR, enc: Vec<u8>) -> Res<Self> { + fn new(hpke: &HpkeR, enc: &[u8]) -> Res<Self> { let response_nonce = random(entropy(hpke.config())); - let aead = make_aead(Mode::Encrypt, hpke.config(), hpke, enc, &response_nonce)?; + let aead = make_aead( + Mode::Encrypt, + hpke.config(), + &export_secret(hpke, LABEL_RESPONSE, hpke.config())?, + enc, + &response_nonce, + )?; Ok(Self { response_nonce, aead, @@ -302,48 +314,54 @@ impl ClientResponse { let mut aead = make_aead( Mode::Decrypt, self.hpke.config(), - &self.hpke, - self.enc, + &export_secret(&self.hpke, LABEL_RESPONSE, self.hpke.config())?, + &self.enc, response_nonce, )?; - aead.open(&[], 0, ct) // 0 is the sequence number + aead.open(&[], ct) // 0 is the sequence number } } #[cfg(all(test, feature = "client", feature = "server"))] mod test { + use std::{fmt::Debug, io::ErrorKind}; + + use log::trace; + use crate::{ config::SymmetricSuite, err::Res, hpke::{Aead, Kdf, Kem}, ClientRequest, Error, KeyConfig, KeyId, Server, }; - use log::trace; - use std::{fmt::Debug, io::ErrorKind}; - const KEY_ID: KeyId = 1; - const KEM: Kem = Kem::X25519Sha256; - const SYMMETRIC: &[SymmetricSuite] = &[ + pub const KEY_ID: KeyId = 1; + pub const KEM: Kem = Kem::X25519Sha256; + pub const SYMMETRIC: &[SymmetricSuite] = &[ SymmetricSuite::new(Kdf::HkdfSha256, Aead::Aes128Gcm), SymmetricSuite::new(Kdf::HkdfSha256, Aead::ChaCha20Poly1305), ]; - const REQUEST: &[u8] = &[ + pub const REQUEST: &[u8] = &[ 0x00, 0x03, 0x47, 0x45, 0x54, 0x05, 0x68, 0x74, 0x74, 0x70, 0x73, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x01, 0x2f, ]; - const RESPONSE: &[u8] = &[0x01, 0x40, 0xc8]; + pub const RESPONSE: &[u8] = &[0x01, 0x40, 0xc8]; - fn init() { + pub fn init() { crate::init(); _ = env_logger::try_init(); // ignore errors here } + pub fn make_config() -> KeyConfig { + KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).unwrap() + } + #[test] fn request_response() { init(); - let server_config = KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).unwrap(); + let server_config = make_config(); let server = Server::new(server_config).unwrap(); let encoded_config = server.config().encode().unwrap(); trace!("Config: {}", hex::encode(&encoded_config)); @@ -368,7 +386,7 @@ mod test { fn two_requests() { init(); - let server_config = KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).unwrap(); + let server_config = make_config(); let server = Server::new(server_config).unwrap(); let encoded_config = server.config().encode().unwrap(); @@ -408,7 +426,7 @@ mod test { fn request_truncated(cut: usize) { init(); - let server_config = KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).unwrap(); + let server_config = make_config(); let server = Server::new(server_config).unwrap(); let encoded_config = server.config().encode().unwrap(); @@ -439,7 +457,7 @@ mod test { fn response_truncated(cut: usize) { init(); - let server_config = KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).unwrap(); + let server_config = make_config(); let server = Server::new(server_config).unwrap(); let encoded_config = server.config().encode().unwrap(); @@ -498,7 +516,7 @@ mod test { fn request_from_config_list() { init(); - let server_config = KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).unwrap(); + let server_config = make_config(); let server = Server::new(server_config).unwrap(); let encoded_config = server.config().encode().unwrap(); diff --git a/third_party/rust/ohttp/src/nss/aead.rs b/third_party/rust/ohttp/src/nss/aead.rs @@ -18,6 +18,7 @@ use super::{ }, }; use crate::{ + crypto::{Decrypt, Encrypt}, err::{Error, Res}, hpke::Aead as AeadId, }; @@ -69,8 +70,11 @@ impl Mode { /// This is an AEAD instance that uses the pub struct Aead { mode: Mode, + #[allow(dead_code)] + algorithm: AeadId, ctx: Context, nonce_base: [u8; NONCE_LEN], + decrypt_counter: SequenceNumber, } impl Aead { @@ -87,7 +91,7 @@ impl Aead { let slot = super::p11::Slot::internal()?; let ptr = unsafe { sys::PK11_ImportSymKey( - *slot, + slot.ptr(), Self::mech(algorithm), sys::PK11Origin::PK11_OriginUnwrap, sys::CK_ATTRIBUTE_TYPE::from(sys::CKA_ENCRYPT | sys::CKA_DECRYPT), @@ -114,21 +118,78 @@ impl Aead { PK11_CreateContextBySymKey( Self::mech(algorithm), mode.p11mode(), - **key, + key.ptr(), &Item::wrap(&nonce_base[..]), ) }; Ok(Self { mode, + algorithm, ctx: Context::from_ptr(ptr)?, nonce_base, + decrypt_counter: 0, }) } - pub fn seal(&mut self, aad: &[u8], pt: &[u8]) -> Res<Vec<u8>> { + fn do_open(ctx: &Context, aad: &[u8], ct: &[u8], nonce: &mut [u8], mech: u32) -> Res<Vec<u8>> { + let mut pt = vec![0; ct.len()]; // NSS needs more space than it uses for plaintext. + let mut pt_len: c_int = 0; + let pt_expected = ct.len().checked_sub(TAG_LEN).ok_or(Error::Truncated)?; + secstatus_to_res(unsafe { + PK11_AEADOp( + ctx.ptr(), + CK_GENERATOR_FUNCTION::from(mech), + c_int_len(NONCE_LEN - COUNTER_LEN), // Fixed portion of the nonce. + nonce.as_mut_ptr(), + c_int_len(nonce.len()), + aad.as_ptr(), + c_int_len(aad.len()), + pt.as_mut_ptr(), + &raw mut pt_len, + c_int_len(pt.len()), // signed :( + ct.as_ptr().add(pt_expected).cast_mut(), // const cast :( + c_int_len(TAG_LEN), + ct.as_ptr(), + c_int_len(pt_expected), + ) + })?; + let len = usize::try_from(pt_len).unwrap(); + debug_assert_eq!(len, pt_expected); + pt.truncate(len); + Ok(pt) + } + + pub fn open_seq(&mut self, aad: &[u8], seq: SequenceNumber, ct: &[u8]) -> Res<Vec<u8>> { + assert_eq!(self.mode, Mode::Decrypt); + let mut nonce = self.nonce_base; + for (i, n) in nonce.iter_mut().rev().take(COUNTER_LEN).enumerate() { + *n ^= u8::try_from((seq >> (8 * i)) & 0xff).unwrap(); + } + + Self::do_open(&self.ctx, aad, ct, &mut nonce, CKG_NO_GENERATE) + } +} + +impl Decrypt for Aead { + fn open(&mut self, aad: &[u8], ct: &[u8]) -> Res<Vec<u8>> { + assert_eq!(self.mode, Mode::Decrypt); + // Note: NSS doesn't do any nonce generation for decryption, which is CRAZY. + let counter = self.decrypt_counter; + self.decrypt_counter += 1; + self.open_seq(aad, counter, ct) + } + + fn alg(&self) -> AeadId { + self.algorithm + } +} + +impl Encrypt for Aead { + fn seal(&mut self, aad: &[u8], pt: &[u8]) -> Res<Vec<u8>> { assert_eq!(self.mode, Mode::Encrypt); // A copy for the nonce generator to write into. But we don't use the value. let mut nonce = self.nonce_base; + // Ciphertext with enough space for the tag. // Even though we give the operation a separate buffer for the tag, // reserve the capacity on allocation. @@ -137,7 +198,7 @@ impl Aead { let mut tag = vec![0; TAG_LEN]; secstatus_to_res(unsafe { PK11_AEADOp( - *self.ctx, + self.ctx.ptr(), CK_GENERATOR_FUNCTION::from(CKG_GENERATE_COUNTER_XOR), c_int_len(NONCE_LEN - COUNTER_LEN), // Fixed portion of the nonce. nonce.as_mut_ptr(), @@ -159,37 +220,8 @@ impl Aead { Ok(ct) } - pub fn open(&mut self, aad: &[u8], seq: SequenceNumber, ct: &[u8]) -> Res<Vec<u8>> { - assert_eq!(self.mode, Mode::Decrypt); - let mut nonce = self.nonce_base; - for (i, n) in nonce.iter_mut().rev().take(COUNTER_LEN).enumerate() { - *n ^= u8::try_from((seq >> (8 * i)) & 0xff).unwrap(); - } - let mut pt = vec![0; ct.len()]; // NSS needs more space than it uses for plaintext. - let mut pt_len: c_int = 0; - let pt_expected = ct.len().checked_sub(TAG_LEN).ok_or(Error::Truncated)?; - secstatus_to_res(unsafe { - PK11_AEADOp( - *self.ctx, - CK_GENERATOR_FUNCTION::from(CKG_NO_GENERATE), - c_int_len(NONCE_LEN - COUNTER_LEN), // Fixed portion of the nonce. - nonce.as_mut_ptr(), - c_int_len(nonce.len()), - aad.as_ptr(), - c_int_len(aad.len()), - pt.as_mut_ptr(), - &raw mut pt_len, - c_int_len(pt.len()), // signed :( - ct.as_ptr().add(pt_expected).cast_mut(), // const cast :( - c_int_len(TAG_LEN), - ct.as_ptr(), - c_int_len(pt_expected), - ) - })?; - let len = usize::try_from(pt_len).unwrap(); - debug_assert_eq!(len, pt_expected); - pt.truncate(len); - Ok(pt) + fn alg(&self) -> AeadId { + self.algorithm } } @@ -199,6 +231,7 @@ mod test { super::{super::hpke::Aead as AeadId, init}, Aead, Mode, SequenceNumber, NONCE_LEN, }; + use crate::crypto::{Decrypt, Encrypt}; /// Check that the first invocation of encryption matches expected values. /// Also check decryption of the same. @@ -218,7 +251,7 @@ mod test { assert_eq!(&ciphertext[..], ct); let mut dec = Aead::new(Mode::Decrypt, algorithm, &k, *nonce).unwrap(); - let plaintext = dec.open(aad, 0, ct).unwrap(); + let plaintext = dec.open(aad, ct).unwrap(); assert_eq!(&plaintext[..], pt); } @@ -233,7 +266,11 @@ mod test { ) { let k = Aead::import_key(algorithm, key).unwrap(); let mut dec = Aead::new(Mode::Decrypt, algorithm, &k, *nonce).unwrap(); - let plaintext = dec.open(aad, seq, ct).unwrap(); + let plaintext = if seq == 0 { + dec.open(aad, ct).unwrap() + } else { + dec.open_seq(aad, seq, ct).unwrap() + }; assert_eq!(&plaintext[..], pt); } @@ -330,4 +367,26 @@ mod test { // Now use the real nonce and sequence number from the example. decrypt(ALG, KEY, NONCE_BASE, 654_360_564, AAD, PT, CT); } + + #[test] + fn seal_open_many() { + const PT: &[u8] = b"abc"; + const ALG: AeadId = AeadId::Aes128Gcm; + const KEY: &[u8] = &[0; 16]; + const NONCE: [u8; NONCE_LEN] = [0; NONCE_LEN]; + + init(); + let k = Aead::import_key(ALG, KEY).unwrap(); + + let mut e = Aead::new(Mode::Encrypt, ALG, &k, NONCE).unwrap(); + let mut d = Aead::new(Mode::Decrypt, ALG, &k, NONCE).unwrap(); + + for i in 1..5 { + let ct = e.seal(&[], PT).unwrap(); + println!("ct{i}: {}", hex::encode(&ct)); + + let pt = d.open(&[], &ct).unwrap(); + assert_eq!(pt, PT); + } + } } diff --git a/third_party/rust/ohttp/src/nss/err.rs b/third_party/rust/ohttp/src/nss/err.rs @@ -10,9 +10,10 @@ clippy::module_name_repetitions )] +use std::os::raw::c_char; + use super::{SECStatus, SECSuccess}; use crate::err::Res; -use std::os::raw::c_char; include!(concat!(env!("OUT_DIR"), "/nspr_error.rs")); mod codes { @@ -62,12 +63,12 @@ impl std::fmt::Display for Error { } } -use std::ffi::CStr; - fn wrap_str_fn<F>(f: F, dflt: &str) -> String where F: FnOnce() -> *const c_char, { + use std::ffi::CStr; + unsafe { let p = f(); if p.is_null() { diff --git a/third_party/rust/ohttp/src/nss/hkdf.rs b/third_party/rust/ohttp/src/nss/hkdf.rs @@ -1,3 +1,7 @@ +use std::{convert::TryFrom, os::raw::c_int, ptr::null_mut}; + +use log::trace; + use super::{ super::hpke::{Aead, Kdf}, p11::{ @@ -10,8 +14,6 @@ use super::{ }, }; use crate::err::Res; -use log::trace; -use std::{convert::TryFrom, os::raw::c_int, ptr::null_mut}; #[derive(Clone, Copy)] pub enum KeyMechanism { @@ -51,7 +53,7 @@ impl Hkdf { let slot = super::p11::Slot::internal()?; let ptr = unsafe { sys::PK11_ImportSymKey( - *slot, + slot.ptr(), CK_MECHANISM_TYPE::from(sys::CKM_HKDF_KEY_GEN), sys::PK11Origin::PK11_OriginUnwrap, sys::CK_ATTRIBUTE_TYPE::from(sys::CKA_SIGN), @@ -89,7 +91,7 @@ impl Hkdf { let mut params_item = ParamItem::new(&mut params); let ptr = unsafe { sys::PK11_Derive( - **ikm, + ikm.ptr(), CK_MECHANISM_TYPE::from(CKM_HKDF_DERIVE), params_item.ptr(), CK_MECHANISM_TYPE::from(CKM_HKDF_DERIVE), @@ -128,7 +130,7 @@ impl Hkdf { let mut params_item = ParamItem::new(&mut params); let ptr = unsafe { sys::PK11_Derive( - **prk, + prk.ptr(), CK_MECHANISM_TYPE::from(CKM_HKDF_DERIVE), params_item.ptr(), key_mech.mech(), @@ -151,7 +153,7 @@ impl Hkdf { let mut params_item = ParamItem::new(&mut params); let ptr = unsafe { sys::PK11_Derive( - **prk, + prk.ptr(), CK_MECHANISM_TYPE::from(CKM_HKDF_DATA), params_item.ptr(), CK_MECHANISM_TYPE::from(CKM_HKDF_DERIVE), diff --git a/third_party/rust/ohttp/src/nss/hpke.rs b/third_party/rust/ohttp/src/nss/hpke.rs @@ -13,7 +13,10 @@ use super::{ err::{sec::SEC_ERROR_INVALID_ARGS, secstatus_to_res, Error}, p11::{sys, Item, PrivateKey, PublicKey, Slot, SymKey}, }; -use crate::err::Res; +use crate::{ + crypto::{Decrypt, Encrypt}, + err::Res, +}; /// Configuration for `Hpke`. #[derive(Clone, Copy)] @@ -113,11 +116,17 @@ pub struct HpkeS { impl HpkeS { /// Create a new context that uses the KEM mode for sending. #[allow(clippy::similar_names)] - pub fn new(config: Config, pk_r: &mut PublicKey, info: &[u8]) -> Res<Self> { + pub fn new(config: Config, pk_r: &PublicKey, info: &[u8]) -> Res<Self> { let (sk_e, pk_e) = generate_key_pair(config.kem)?; let context = HpkeContext::new(config)?; secstatus_to_res(unsafe { - sys::PK11_HPKE_SetupS(*context, *pk_e, *sk_e, **pk_r, &Item::wrap(info)) + sys::PK11_HPKE_SetupS( + context.ptr(), + pk_e.ptr(), + sk_e.ptr(), + pk_r.ptr(), + &Item::wrap(info), + ) })?; Ok(Self { context, config }) } @@ -128,19 +137,21 @@ impl HpkeS { /// Get the encapsulated KEM secret. pub fn enc(&self) -> Res<Vec<u8>> { - let v = unsafe { sys::PK11_HPKE_GetEncapPubKey(*self.context) }; + let v = unsafe { sys::PK11_HPKE_GetEncapPubKey(self.context.ptr()) }; let r = unsafe { v.as_ref() }.ok_or_else(|| Error::from(SEC_ERROR_INVALID_ARGS))?; // This is just an alias, so we can't use `Item`. let len = usize::try_from(r.len).unwrap(); let slc = unsafe { std::slice::from_raw_parts(r.data, len) }; Ok(Vec::from(slc)) } +} - pub fn seal(&mut self, aad: &[u8], pt: &[u8]) -> Res<Vec<u8>> { +impl Encrypt for HpkeS { + fn seal(&mut self, aad: &[u8], pt: &[u8]) -> Res<Vec<u8>> { let mut out: *mut sys::SECItem = null_mut(); secstatus_to_res(unsafe { sys::PK11_HPKE_Seal( - *self.context, + self.context.ptr(), &Item::wrap(aad), &Item::wrap(pt), &raw mut out, @@ -149,6 +160,10 @@ impl HpkeS { let v = Item::from_ptr(out)?; Ok(unsafe { v.into_vec() }) } + + fn alg(&self) -> Aead { + self.config.aead() + } } impl Exporter for HpkeS { @@ -183,9 +198,9 @@ impl HpkeR { let context = HpkeContext::new(config)?; secstatus_to_res(unsafe { sys::PK11_HPKE_SetupR( - *context, - **pk_r, - **sk_r, + context.ptr(), + pk_r.ptr(), + sk_r.ptr(), &Item::wrap(enc), &Item::wrap(info), ) @@ -206,7 +221,7 @@ impl HpkeR { let mut ptr: *mut sys::SECKEYPublicKey = null_mut(); secstatus_to_res(unsafe { sys::PK11_HPKE_Deserialize( - *context, + context.ptr(), k.as_ptr(), c_uint::try_from(k.len()).unwrap(), &raw mut ptr, @@ -214,12 +229,14 @@ impl HpkeR { })?; PublicKey::from_ptr(ptr) } +} - pub fn open(&mut self, aad: &[u8], ct: &[u8]) -> Res<Vec<u8>> { +impl Decrypt for HpkeR { + fn open(&mut self, aad: &[u8], ct: &[u8]) -> Res<Vec<u8>> { let mut out: *mut sys::SECItem = null_mut(); secstatus_to_res(unsafe { sys::PK11_HPKE_Open( - *self.context, + self.context.ptr(), &Item::wrap(aad), &Item::wrap(ct), &raw mut out, @@ -228,6 +245,10 @@ impl HpkeR { let v = Item::from_ptr(out)?; Ok(unsafe { v.into_vec() }) } + + fn alg(&self) -> Aead { + self.config.aead() + } } impl Exporter for HpkeR { @@ -264,7 +285,7 @@ pub fn generate_key_pair(kem: Kem) -> Res<(PrivateKey, PublicKey)> { let insensitive_secret_ptr = if log_enabled!(log::Level::Trace) { unsafe { sys::PK11_GenerateKeyPairWithOpFlags( - *slot, + slot.ptr(), sys::CK_MECHANISM_TYPE::from(sys::CKM_EC_KEY_PAIR_GEN), addr_of_mut!(wrapped).cast(), &raw mut public_ptr, @@ -281,7 +302,7 @@ pub fn generate_key_pair(kem: Kem) -> Res<(PrivateKey, PublicKey)> { let secret_ptr = if insensitive_secret_ptr.is_null() { unsafe { sys::PK11_GenerateKeyPairWithOpFlags( - *slot, + slot.ptr(), sys::CK_MECHANISM_TYPE::from(sys::CKM_EC_KEY_PAIR_GEN), addr_of_mut!(wrapped).cast(), &raw mut public_ptr, @@ -304,7 +325,11 @@ pub fn generate_key_pair(kem: Kem) -> Res<(PrivateKey, PublicKey)> { #[cfg(test)] mod test { use super::{generate_key_pair, Config, HpkeContext, HpkeR, HpkeS}; - use crate::{hpke::Aead, init}; + use crate::{ + crypto::{Decrypt, Encrypt}, + hpke::Aead, + init, + }; const INFO: &[u8] = b"info"; const AAD: &[u8] = b"aad"; @@ -315,8 +340,8 @@ mod test { fn make() { init(); let cfg = Config::default(); - let (sk_r, mut pk_r) = generate_key_pair(cfg.kem()).unwrap(); - let hpke_s = HpkeS::new(cfg, &mut pk_r, INFO).unwrap(); + let (sk_r, pk_r) = generate_key_pair(cfg.kem()).unwrap(); + let hpke_s = HpkeS::new(cfg, &pk_r, INFO).unwrap(); let _hpke_r = HpkeR::new(cfg, &pk_r, &sk_r, &hpke_s.enc().unwrap(), INFO).unwrap(); } @@ -329,10 +354,10 @@ mod test { ..Config::default() }; assert!(cfg.supported()); - let (sk_r, mut pk_r) = generate_key_pair(cfg.kem()).unwrap(); + let (sk_r, pk_r) = generate_key_pair(cfg.kem()).unwrap(); // Send - let mut hpke_s = HpkeS::new(cfg, &mut pk_r, INFO).unwrap(); + let mut hpke_s = HpkeS::new(cfg, &pk_r, INFO).unwrap(); let enc = hpke_s.enc().unwrap(); let ct = hpke_s.seal(AAD, PT).unwrap(); diff --git a/third_party/rust/ohttp/src/nss/mod.rs b/third_party/rust/ohttp/src/nss/mod.rs @@ -1,8 +1,4 @@ -// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or -// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license -// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. +#![allow(clippy::incompatible_msrv)] // This feature needs 1.70 mod err; #[macro_use] @@ -11,10 +7,12 @@ pub mod aead; pub mod hkdf; pub mod hpke; -pub use self::p11::{random, PrivateKey, PublicKey}; +use std::{ptr::null, sync::OnceLock}; + use err::secstatus_to_res; pub use err::Error; -use std::{ptr::null, sync::OnceLock}; + +pub use self::p11::{random, PrivateKey, PublicKey, SymKey}; #[allow(clippy::pedantic, non_upper_case_globals, clippy::upper_case_acronyms)] mod nss_init { diff --git a/third_party/rust/ohttp/src/nss/p11.rs b/third_party/rust/ohttp/src/nss/p11.rs @@ -12,8 +12,10 @@ use std::{ ptr::{self, null_mut}, }; -use super::err::{secstatus_to_res, Error}; -use crate::err::Res; +use crate::{ + err::{Error, Res}, + nss::err::{secstatus_to_res, Error as NssError}, +}; #[allow( clippy::pedantic, @@ -37,7 +39,7 @@ use sys::{ }; macro_rules! scoped_ptr { - ($scoped:ident, $target:ty, $dtor:path) => { + ($scoped:ident, $target:ty, $dtor:path, noptr) => { pub struct $scoped { ptr: *mut $target, } @@ -52,31 +54,30 @@ macro_rules! scoped_ptr { } } - impl std::ops::Deref for $scoped { - type Target = *mut $target; - fn deref(&self) -> &*mut $target { - &self.ptr - } - } - - impl std::ops::DerefMut for $scoped { - fn deref_mut(&mut self) -> &mut *mut $target { - &mut self.ptr - } - } - impl Drop for $scoped { fn drop(&mut self) { unsafe { $dtor(self.ptr) }; } } }; + ($scoped:ident, $target:ty, $dtor:path) => { + scoped_ptr!($scoped, $target, $dtor, noptr); + impl $scoped { + pub(crate) fn ptr(&self) -> *mut $target { + self.ptr + } + } + }; } scoped_ptr!(PrivateKey, SECKEYPrivateKey, SECKEY_DestroyPrivateKey); impl PrivateKey { - pub fn key_data(&self) -> Res<Vec<u8>> { + fn key_data(&self) -> Res<Vec<u8>> { + if !cfg!(feature = "unsafe-print-secrets") { + return Err(Error::from(NssError::internal())); + } + let mut key_item = SECItem { type_: SECItemType::siBuffer, data: null_mut(), @@ -85,7 +86,7 @@ impl PrivateKey { secstatus_to_res(unsafe { PK11_ReadRawAttribute( PK11ObjectType::PK11_TypePrivKey, - (**self).cast(), + self.ptr().cast(), CK_ATTRIBUTE_TYPE::from(CKA_VALUE), &raw mut key_item, ) @@ -115,12 +116,11 @@ impl Clone for PrivateKey { impl std::fmt::Debug for PrivateKey { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - if cfg!(feature = "unsafe-print-secrets") { - if let Ok(b) = self.key_data() { - return write!(f, "PrivateKey {}", hex::encode(b)); - } + if let Ok(b) = self.key_data() { + write!(f, "PrivateKey {}", hex::encode(b)) + } else { + write!(f, "Opaque PrivateKey") } - write!(f, "Opaque PrivateKey") } } @@ -133,7 +133,7 @@ impl PublicKey { let mut len: c_uint = 0; secstatus_to_res(unsafe { sys::PK11_HPKE_Serialize( - **self, + self.ptr(), buf.as_mut_ptr(), &raw mut len, c_uint::try_from(buf.len()).unwrap(), @@ -187,7 +187,7 @@ impl SymKey { let key_item = unsafe { PK11_GetKeyData(self.ptr) }; // This is accessing a value attached to the key, so we can treat this as a borrow. match unsafe { key_item.as_mut() } { - None => Err(Error::last()), + None => Err(NssError::last()), Some(key) => Ok(unsafe { std::slice::from_raw_parts(key.data, key.len as usize) }), } } @@ -244,14 +244,14 @@ impl<'a, T: Sized + 'a> ParamItem<'a, T> { } pub fn ptr(&mut self) -> *mut SECItem { - std::ptr::addr_of_mut!(self.item) + ptr::addr_of_mut!(self.item) } } unsafe fn destroy_secitem(item: *mut SECItem) { SECITEM_FreeItem(item, PRBool::from(true)); } -scoped_ptr!(Item, SECItem, destroy_secitem); +scoped_ptr!(Item, SECItem, destroy_secitem, noptr); impl Item { /// Create a wrapper for a slice of this object. diff --git a/third_party/rust/ohttp/src/rand.rs b/third_party/rust/ohttp/src/rand.rs @@ -4,11 +4,11 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. -use ::rand::{thread_rng, RngCore}; +use ::rand::{rng, RngCore}; #[must_use] pub fn random(size: usize) -> Vec<u8> { - let mut rng = thread_rng(); + let mut rng = rng(); let mut buf = vec![0; size]; rng.fill_bytes(&mut buf); buf diff --git a/third_party/rust/ohttp/src/rh/aead.rs b/third_party/rust/ohttp/src/rh/aead.rs @@ -1,16 +1,19 @@ -#![allow(dead_code)] // TODO: remove +use std::convert::TryFrom; -use super::SymKey; -use crate::{err::Res, hpke::Aead as AeadId}; -use aead::{AeadMut, Key, NewAead, Nonce, Payload}; +use aead::{AeadMut, Key, KeyInit, Nonce, Payload}; use aes_gcm::{Aes128Gcm, Aes256Gcm}; use chacha20poly1305::ChaCha20Poly1305; -use std::convert::TryFrom; + +use super::SymKey; +use crate::{ + crypto::{Decrypt, Encrypt}, + err::Res, + hpke::Aead as AeadId, +}; /// All the nonces are the same length. Exploit that. pub const NONCE_LEN: usize = 12; const COUNTER_LEN: usize = 8; -const TAG_LEN: usize = 16; type SequenceNumber = u64; @@ -54,6 +57,8 @@ impl AeadEngine { /// A switch-hitting AEAD that uses a selected primitive. pub struct Aead { mode: Mode, + #[allow(dead_code, reason = "Used by stream feature")] + algorithm: AeadId, engine: AeadEngine, nonce_base: [u8; NONCE_LEN], seq: SequenceNumber, @@ -80,6 +85,7 @@ impl Aead { }; Ok(Self { mode, + algorithm, engine: aead, nonce_base, seq: 0, @@ -100,7 +106,28 @@ impl Aead { nonce } - pub fn seal(&mut self, aad: &[u8], pt: &[u8]) -> Res<Vec<u8>> { + pub fn open_seq(&mut self, aad: &[u8], seq: SequenceNumber, ct: &[u8]) -> Res<Vec<u8>> { + assert_eq!(self.mode, Mode::Decrypt); + let nonce = self.nonce(seq); + let pt = self.engine.decrypt(&nonce, Payload { msg: ct, aad })?; + Ok(pt) + } +} + +impl Decrypt for Aead { + fn open(&mut self, aad: &[u8], ct: &[u8]) -> Res<Vec<u8>> { + let res = self.open_seq(aad, self.seq, ct); + self.seq += 1; + res + } + + fn alg(&self) -> AeadId { + self.algorithm + } +} + +impl Encrypt for Aead { + fn seal(&mut self, aad: &[u8], pt: &[u8]) -> Res<Vec<u8>> { assert_eq!(self.mode, Mode::Encrypt); // A copy for the nonce generator to write into. But we don't use the value. let nonce = self.nonce(self.seq); @@ -109,19 +136,18 @@ impl Aead { Ok(ct) } - pub fn open(&mut self, aad: &[u8], seq: SequenceNumber, ct: &[u8]) -> Res<Vec<u8>> { - assert_eq!(self.mode, Mode::Decrypt); - let nonce = self.nonce(seq); - let pt = self.engine.decrypt(&nonce, Payload { msg: ct, aad })?; - Ok(pt) + fn alg(&self) -> AeadId { + self.algorithm } } #[cfg(test)] mod test { - use super::{ - super::super::{hpke::Aead as AeadId, init}, - Aead, Mode, SequenceNumber, NONCE_LEN, + use super::SequenceNumber; + use crate::{ + crypto::{Decrypt, Encrypt}, + hpke::Aead as AeadId, + init, Aead, Mode, NONCE_LEN, }; /// Check that the first invocation of encryption matches expected values. @@ -142,7 +168,7 @@ mod test { assert_eq!(&ciphertext[..], ct); let mut dec = Aead::new(Mode::Decrypt, algorithm, &k, *nonce).unwrap(); - let plaintext = dec.open(aad, 0, ct).unwrap(); + let plaintext = dec.open(aad, ct).unwrap(); assert_eq!(&plaintext[..], pt); } @@ -157,7 +183,7 @@ mod test { ) { let k = Aead::import_key(algorithm, key).unwrap(); let mut dec = Aead::new(Mode::Decrypt, algorithm, &k, *nonce).unwrap(); - let plaintext = dec.open(aad, seq, ct).unwrap(); + let plaintext = dec.open_seq(aad, seq, ct).unwrap(); assert_eq!(&plaintext[..], pt); } @@ -254,4 +280,26 @@ mod test { // Now use the real nonce and sequence number from the example. decrypt(ALG, KEY, NONCE_BASE, 654_360_564, AAD, PT, CT); } + + #[test] + fn seal_open_many() { + const PT: &[u8] = b"abc"; + const ALG: AeadId = AeadId::Aes128Gcm; + const KEY: &[u8] = &[0; 16]; + const NONCE: [u8; NONCE_LEN] = [0; NONCE_LEN]; + + init(); + let k = Aead::import_key(ALG, KEY).unwrap(); + + let mut e = Aead::new(Mode::Encrypt, ALG, &k, NONCE).unwrap(); + let mut d = Aead::new(Mode::Decrypt, ALG, &k, NONCE).unwrap(); + + for i in 1..5 { + let ct = e.seal(&[], PT).unwrap(); + println!("ct{i}: {}", hex::encode(&ct)); + + let pt = d.open(&[], &ct).unwrap(); + assert_eq!(pt, PT); + } + } } diff --git a/third_party/rust/ohttp/src/rh/hkdf.rs b/third_party/rust/ohttp/src/rh/hkdf.rs @@ -1,13 +1,14 @@ #![allow(dead_code)] // TODO: remove +use hkdf::Hkdf as HkdfImpl; +use log::trace; +use sha2::{Sha256, Sha384, Sha512}; + use super::SymKey; use crate::{ err::{Error, Res}, hpke::{Aead, Kdf}, }; -use hkdf::Hkdf as HkdfImpl; -use log::trace; -use sha2::{Sha256, Sha384, Sha512}; #[derive(Clone, Copy)] pub enum KeyMechanism { diff --git a/third_party/rust/ohttp/src/rh/hpke.rs b/third_party/rust/ohttp/src/rh/hpke.rs @@ -1,7 +1,7 @@ use std::ops::Deref; use ::hpke as rust_hpke; -use ::rand::thread_rng; +use ::rand::rng; use log::trace; use rust_hpke::{ aead::{AeadCtxR, AeadCtxS, AeadTag, AesGcm128, ChaCha20Poly1305}, @@ -12,6 +12,7 @@ use rust_hpke::{ use super::SymKey; use crate::{ + crypto::{Decrypt, Encrypt}, hpke::{Aead, Kdf, Kem}, Error, Res, }; @@ -171,8 +172,8 @@ pub struct HpkeS { impl HpkeS { /// Create a new context that uses the KEM mode for sending. - pub fn new(config: Config, pk_r: &mut PublicKey, info: &[u8]) -> Res<Self> { - let mut csprng = thread_rng(); + pub fn new(config: Config, pk_r: &PublicKey, info: &[u8]) -> Res<Self> { + let mut csprng = rng(); macro_rules! dispatch_hpkes_new { { @@ -248,13 +249,19 @@ impl HpkeS { pub fn enc(&self) -> Res<Vec<u8>> { Ok(self.enc.clone()) } +} - pub fn seal(&mut self, aad: &[u8], pt: &[u8]) -> Res<Vec<u8>> { +impl Encrypt for HpkeS { + fn seal(&mut self, aad: &[u8], pt: &[u8]) -> Res<Vec<u8>> { let mut buf = pt.to_owned(); let mut tag = self.context.seal(&mut buf, aad)?; buf.append(&mut tag); Ok(buf) } + + fn alg(&self) -> Aead { + self.config.aead() + } } impl Exporter for HpkeS { @@ -417,13 +424,19 @@ impl HpkeR { } }) } +} - pub fn open(&mut self, aad: &[u8], ct: &[u8]) -> Res<Vec<u8>> { +impl Decrypt for HpkeR { + fn open(&mut self, aad: &[u8], ct: &[u8]) -> Res<Vec<u8>> { let mut buf = ct.to_owned(); let pt_len = self.context.open(&mut buf, aad)?.len(); buf.truncate(pt_len); Ok(buf) } + + fn alg(&self) -> Aead { + self.config.aead() + } } impl Exporter for HpkeR { @@ -444,7 +457,7 @@ impl Deref for HpkeR { /// Generate a key pair for the identified KEM. #[allow(clippy::unnecessary_wraps)] pub fn generate_key_pair(kem: Kem) -> Res<(PrivateKey, PublicKey)> { - let mut csprng = thread_rng(); + let mut csprng = rng(); let (sk, pk) = match kem { Kem::X25519Sha256 => { let (sk, pk) = X25519HkdfSha256::gen_keypair(&mut csprng); @@ -471,6 +484,7 @@ pub fn derive_key_pair(kem: Kem, ikm: &[u8]) -> Res<(PrivateKey, PublicKey)> { mod test { use super::{generate_key_pair, Config, HpkeR, HpkeS}; use crate::{ + crypto::{Decrypt, Encrypt}, hpke::{Aead, Kem}, init, }; @@ -484,8 +498,8 @@ mod test { fn make() { init(); let cfg = Config::default(); - let (sk_r, mut pk_r) = generate_key_pair(cfg.kem()).unwrap(); - let hpke_s = HpkeS::new(cfg, &mut pk_r, INFO).unwrap(); + let (sk_r, pk_r) = generate_key_pair(cfg.kem()).unwrap(); + let hpke_s = HpkeS::new(cfg, &pk_r, INFO).unwrap(); let _hpke_r = HpkeR::new(cfg, &pk_r, &sk_r, &hpke_s.enc().unwrap(), INFO).unwrap(); } @@ -499,10 +513,10 @@ mod test { ..Config::default() }; assert!(cfg.supported()); - let (sk_r, mut pk_r) = generate_key_pair(cfg.kem()).unwrap(); + let (sk_r, pk_r) = generate_key_pair(cfg.kem()).unwrap(); // Send - let mut hpke_s = HpkeS::new(cfg, &mut pk_r, INFO).unwrap(); + let mut hpke_s = HpkeS::new(cfg, &pk_r, INFO).unwrap(); let enc = hpke_s.enc().unwrap(); let ct = hpke_s.seal(AAD, PT).unwrap(); diff --git a/third_party/rust/ohttp/src/stream.rs b/third_party/rust/ohttp/src/stream.rs @@ -0,0 +1,1109 @@ +#![allow(clippy::incompatible_msrv)] // Until I can make MSRV conditional on feature choice. + +use std::{ + cmp::min, + io::{Cursor, Error as IoError, Result as IoResult}, + mem, + pin::Pin, + task::{Context, Poll}, +}; + +use futures::{AsyncRead, AsyncWrite}; +use pin_project::pin_project; + +use crate::{ + build_info, + crypto::{Decrypt, Encrypt}, + entropy, + err::Res, + export_secret, make_aead, random, Aead, Error, HpkeConfig, HpkeR, HpkeS, KeyConfig, KeyId, + Mode, PublicKey, SymKey, REQUEST_HEADER_LEN, +}; + +/// The info string for a chunked request. +pub(crate) const INFO_REQUEST: &[u8] = b"message/bhttp chunked request"; +/// The exporter label for a chunked response. +pub(crate) const LABEL_RESPONSE: &[u8] = b"message/bhttp chunked response"; +/// The length of the plaintext of the largest chunk that is permitted. +const MAX_CHUNK_PLAINTEXT: usize = 1 << 14; +const CHUNK_AAD: &[u8] = b""; +const FINAL_CHUNK_AAD: &[u8] = b"final"; + +#[allow(clippy::unnecessary_wraps)] +fn ioerror<T, E>(e: E) -> Poll<IoResult<T>> +where + Error: From<E>, +{ + Poll::Ready(Err(IoError::other(Error::from(e)))) +} + +#[pin_project(project = ChunkWriterProjection)] +struct ChunkWriter<D, E> { + #[pin] + dst: D, + cipher: E, + buf: Vec<u8>, + closed: bool, +} + +impl<D, E> ChunkWriter<D, E> { + fn write_len(w: &mut [u8], len: usize) -> &[u8] { + let v: u64 = len.try_into().unwrap(); + let (v, len) = match () { + () if v < (1 << 6) => (v, 1), + () if v < (1 << 14) => (v | (1 << 14), 2), + () if v < (1 << 30) => (v | (2 << 30), 4), + () if v < (1 << 62) => (v | (3 << 62), 8), + () => panic!("varint value too large"), + }; + w[..len].copy_from_slice(&v.to_be_bytes()[(8 - len)..]); + &w[..len] + } +} + +impl<D: AsyncWrite, C: Encrypt> ChunkWriter<D, C> { + /// Flush our buffer. + /// Returns `Poll::Pending` when blocked, + /// `Poll::Ready(Ok(()))` when flushed, + /// and `Poll::Ready(Err(..))` when it encounters an error. + fn flush( + this: &mut ChunkWriterProjection<'_, D, C>, + cx: &mut Context<'_>, + ) -> Poll<Result<(), IoError>> { + if this.buf.is_empty() { + return Poll::Ready(Ok(())); + } + loop { + match this.dst.as_mut().poll_write(cx, &this.buf[..]) { + Poll::Pending => return Poll::Pending, + Poll::Ready(Ok(len)) => { + if len < this.buf.len() { + // We've written something to the underlying writer, + // which is probably blocked. + // We could return `Poll::Pending`, + // but that would mean taking responsibility + // for calling `cx.waker().wake()` + // when more space comes available. + // + // So, rather than do that, loop. + // If the underlying writer is truly blocked, + // it assumes responsibility for waking the task. + *this.buf = this.buf.split_off(len); + } else { + this.buf.clear(); + return Poll::Ready(Ok(())); + } + } + Poll::Ready(Err(e)) => return Poll::Ready(Err(e)), + } + } + } + + fn write_chunk( + this: &mut ChunkWriterProjection<'_, D, C>, + cx: &mut Context<'_>, + input: &[u8], + last: bool, + ) -> IoResult<usize> { + let aad = if last { FINAL_CHUNK_AAD } else { CHUNK_AAD }; + let mut ct = this.cipher.seal(aad, input).map_err(IoError::other)?; + let (len, written) = if last { + (0, 0) + } else { + (ct.len(), input.len()) + }; + + let mut len_buf = [0; 8]; + let len = Self::write_len(&mut len_buf[..], len); + let w = match this.dst.as_mut().poll_write(cx, len) { + Poll::Pending => 0, + Poll::Ready(Ok(w)) => w, + Poll::Ready(e @ Err(_)) => return e, + }; + + if w < len.len() { + this.buf.extend_from_slice(&len[w..]); + this.buf.append(&mut ct); + } else { + match this.dst.as_mut().poll_write(cx, &ct[..]) { + Poll::Pending => { + *this.buf = ct; + } + Poll::Ready(Ok(w)) => { + *this.buf = ct.split_off(w); + } + Poll::Ready(e @ Err(_)) => return e, + } + } + Ok(written) + } +} + +impl<D: AsyncWrite, C: Encrypt> AsyncWrite for ChunkWriter<D, C> { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + input: &[u8], + ) -> Poll<IoResult<usize>> { + let mut this = self.project(); + if *this.closed { + return ioerror(Error::WriteAfterClose); + } + + // We have buffered data, so dump it into the output directly. + let flushed = Self::flush(&mut this, cx); + if matches!(flushed, Poll::Pending | Poll::Ready(Err(_))) { + return flushed.map(|_| unreachable!()); + } + + // Now encipher a chunk. + let len = min(input.len(), MAX_CHUNK_PLAINTEXT); + Poll::Ready(Self::write_chunk(&mut this, cx, &input[..len], false)) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> { + let mut this = self.project(); + let flushed = Self::flush(&mut this, cx); + if matches!(flushed, Poll::Pending | Poll::Ready(Err(_))) { + flushed.map(|_| unreachable!()) + } else { + this.dst.as_mut().poll_flush(cx) + } + } + + fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> { + let mut this = self.project(); + let flushed = Self::flush(&mut this, cx); + if matches!(flushed, Poll::Pending | Poll::Ready(Err(_))) { + return flushed; + } + + if !*this.closed { + *this.closed = true; + if let Err(e) = Self::write_chunk(&mut this, cx, &[], true) { + return Poll::Ready(Err(e)); + } + // `write_chunk` might have buffered some data after being blocked. + // We have to try to write that out here (see `flush()` for details). + let flushed = Self::flush(&mut this, cx); + if matches!(flushed, Poll::Pending | Poll::Ready(Err(_))) { + return flushed; + } + } + this.dst.as_mut().poll_close(cx) + } +} + +#[pin_project(project = ClientProjection)] +pub struct ClientRequest<D> { + #[pin] + writer: ChunkWriter<D, HpkeS>, +} + +impl<D> ClientRequest<D> { + /// Start the processing of a stream. + pub fn start(dst: D, config: HpkeConfig, key_id: KeyId, pk: &PublicKey) -> Res<Self> { + let info = build_info(INFO_REQUEST, key_id, config)?; + let hpke = HpkeS::new(config, pk, &info)?; + + let mut header = Vec::from(&info[INFO_REQUEST.len() + 1..]); + debug_assert_eq!(header.len(), REQUEST_HEADER_LEN); + + let mut e = hpke.enc()?; + header.append(&mut e); + + Ok(Self { + writer: ChunkWriter { + dst, + cipher: hpke, + buf: header, + closed: false, + }, + }) + } + + /// Get an object that can be used to process the response. + /// + /// While this can be used while sending the request, + /// doing so creates a risk of revealing unwanted information to the gateway. + /// That includes the round trip time between client and gateway, + /// which might reveal information about the location of the client. + pub fn response<R>(&self, src: R) -> Res<ClientResponse<R>> { + let enc = self.writer.cipher.enc()?; + let secret = export_secret( + &self.writer.cipher, + LABEL_RESPONSE, + self.writer.cipher.config(), + )?; + Ok(ClientResponse { + src, + config: self.writer.cipher.config(), + state: ClientResponseState::Header { + enc, + secret, + nonce: [0; 16], + read: 0, + }, + }) + } +} + +impl<D: AsyncWrite> AsyncWrite for ClientRequest<D> { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + input: &[u8], + ) -> Poll<IoResult<usize>> { + self.project().writer.as_mut().poll_write(cx, input) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> { + self.project().writer.as_mut().poll_flush(cx) + } + + fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> { + self.project().writer.as_mut().poll_close(cx) + } +} + +enum ChunkReader { + Length { + len: [u8; 8], + offset: usize, + }, + EncryptedData { + buf: Vec<u8>, + offset: usize, + length: usize, + }, + CleartextData { + buf: Vec<u8>, + offset: usize, + last: bool, + }, + Done, +} + +impl ChunkReader { + fn length() -> Self { + Self::Length { + len: [0; 8], + offset: 0, + } + } + + fn data(length: usize) -> Self { + // Avoid use `with_capacity` here. Only allocate when necessary. + // We might be able to into the buffer we're given instead, to save an allocation. + Self::EncryptedData { + buf: Vec::new(), + // Note that because we're allocating the full chunk, + // we need to track what has been used. + offset: 0, + length, + } + } + + fn read_fixed<S: AsyncRead>( + mut src: Pin<&mut S>, + cx: &mut Context<'_>, + buf: &mut [u8], + offset: &mut usize, + ) -> Option<Poll<IoResult<usize>>> { + while *offset < buf.len() { + // Read any remaining bytes of the length. + match src.as_mut().poll_read(cx, &mut buf[*offset..]) { + Poll::Pending => return Some(Poll::Pending), + Poll::Ready(Ok(0)) => { + return Some(ioerror(Error::Truncated)); + } + Poll::Ready(Ok(r)) => { + *offset += r; + } + e @ Poll::Ready(Err(_)) => return Some(e), + } + } + None + } + + fn read_length0<S: AsyncRead>( + &mut self, + mut src: Pin<&mut S>, + cx: &mut Context<'_>, + ) -> Option<Poll<IoResult<usize>>> { + let Self::Length { len, offset } = self else { + return None; + }; + + let res = Self::read_fixed(src.as_mut(), cx, &mut len[..1], offset); + if res.is_some() { + return res; + } + + let form = len[0] >> 6; + if form == 0 { + *self = Self::data(usize::from(len[0])); + } else { + let v = mem::replace(&mut len[0], 0) & 0x3f; + let i = match form { + 1 => 6, + 2 => 4, + 3 => 0, + _ => unreachable!(), + }; + len[i] = v; + *offset = i + 1; + } + None + } + + fn read_length<S: AsyncRead, C: Decrypt>( + &mut self, + mut src: Pin<&mut S>, + cx: &mut Context<'_>, + aead: &mut C, + ) -> Option<Poll<IoResult<usize>>> { + // Read the first byte. + let res = self.read_length0(src.as_mut(), cx); + if res.is_some() { + return res; + } + + let Self::Length { len, offset } = self else { + return None; + }; + + let res = Self::read_fixed(src.as_mut(), cx, &mut len[..], offset); + if res.is_some() { + return res; + } + + let remaining = match usize::try_from(u64::from_be_bytes(*len)) { + Ok(remaining) => remaining, + Err(e) => return Some(ioerror(e)), + }; + if remaining > MAX_CHUNK_PLAINTEXT + aead.alg().n_t() { + return Some(ioerror(Error::ChunkTooLarge)); + } + + *self = Self::data(remaining); + None + } + + /// Optional optimization that reads a single chunk into the output buffer. + fn read_into_output<S: AsyncRead, C: Decrypt>( + &mut self, + mut src: Pin<&mut S>, + cx: &mut Context<'_>, + aead: &mut C, + output: &mut [u8], + ) -> Option<Poll<IoResult<usize>>> { + let Self::EncryptedData { + buf, + offset, + length, + } = self + else { + return None; + }; + if *length == 0 || *offset > 0 || output.len() < *length { + // We need to pull in a complete chunk in one go for this to be worthwhile. + return None; + } + + match src.as_mut().poll_read(cx, &mut output[..*length]) { + Poll::Pending => Some(Poll::Pending), + Poll::Ready(Ok(0)) => Some(ioerror(Error::Truncated)), + Poll::Ready(Ok(r)) => { + if r == *length { + let pt = match aead.open(CHUNK_AAD, &output[..r]) { + Ok(pt) => pt, + Err(e) => return Some(ioerror(e)), + }; + output[..pt.len()].copy_from_slice(&pt); + *self = Self::length(); + Some(Poll::Ready(Ok(pt.len()))) + } else { + buf.reserve_exact(*length); + buf.extend_from_slice(&output[..r]); + buf.resize(*length, 0); + *offset += r; + None + } + } + e @ Poll::Ready(Err(_)) => Some(e), + } + } + + /// Provide any decrypted cleartext that we were unable to deliver + /// on previous calls to `read()`. + fn deliver_cleartext(&mut self, output: &mut [u8]) -> Option<usize> { + let Self::CleartextData { buf, offset, last } = self else { + return None; + }; + + if *offset + output.len() < buf.len() { + // `output` is too small for the chunk, fill it and update the offset. + output.copy_from_slice(&buf[*offset..*offset + output.len()]); + *offset += output.len(); + Some(output.len()) + } else { + // Deliver what we have remaining. + // + // Note that this could, by using a different return status, + // allow `read()` function to continue reading. + // However, that complicates the code more than is really worth it. + let len = buf.len() - *offset; + output[..len].copy_from_slice(&buf[*offset..]); + if *last { + *self = Self::Done; + } else { + *self = Self::length(); + } + Some(len) + } + } + + fn read<S: AsyncRead, C: Decrypt>( + &mut self, + mut src: Pin<&mut S>, + cx: &mut Context<'_>, + cipher: &mut C, + output: &mut [u8], + ) -> Poll<IoResult<usize>> { + if let Some(delivered) = self.deliver_cleartext(output) { + return Poll::Ready(Ok(delivered)); + } + + while !matches!(self, Self::Done) { + if let Some(res) = self.read_length(src.as_mut(), cx, cipher) { + return res; + } + + // Read data. + if let Some(res) = self.read_into_output(src.as_mut(), cx, cipher, output) { + return res; + } + + let Self::EncryptedData { + buf, + offset, + length, + } = self + else { + unreachable!(); + }; + + // Allocate now as needed. + let last = *length == 0; + if buf.is_empty() { + let sz = if last { + MAX_CHUNK_PLAINTEXT + cipher.alg().n_t() + } else { + *length + }; + buf.resize(sz, 0); + } + + match src.as_mut().poll_read(cx, &mut buf[*offset..]) { + Poll::Pending => return Poll::Pending, + Poll::Ready(Ok(0)) => { + if last { + buf.truncate(*offset); + } else { + return ioerror(Error::Truncated); + } + } + Poll::Ready(Ok(r)) => { + *offset += r; + if last || *offset < *length { + continue; // Keep reading + } + } + e @ Poll::Ready(Err(_)) => return e, + } + + let aad = if last { FINAL_CHUNK_AAD } else { CHUNK_AAD }; + let pt = cipher.open(aad, buf).map_err(IoError::other)?; + + let delivered = if pt.len() > output.len() { + output.copy_from_slice(&pt[..output.len()]); + // Buffer any undelivered cleartext data. + *self = Self::CleartextData { + buf: pt[output.len()..].to_vec(), + offset: 0, + last, + }; + output.len() + } else { + output[..pt.len()].copy_from_slice(&pt); + if last { + *self = Self::Done; + } else { + *self = Self::length(); + if pt.is_empty() { + // We can't return zero length, as that means "end of stream". + // So read the next chunk if this one was empty. + continue; + } + } + pt.len() + }; + return Poll::Ready(Ok(delivered)); + } + + Poll::Ready(Ok(0)) + } +} + +enum ServerRequestState { + HpkeConfig { + buf: [u8; 7], + read: usize, + }, + Enc { + config: HpkeConfig, + info: Vec<u8>, + read: usize, + }, + Body { + hpke: HpkeR, + state: ChunkReader, + }, +} + +#[pin_project(project = ServerRequestProjection)] +pub struct ServerRequest<S> { + #[pin] + src: S, + key_config: KeyConfig, + enc: Vec<u8>, + state: ServerRequestState, +} + +impl<S> ServerRequest<S> { + pub fn new(key_config: KeyConfig, src: S) -> Self { + Self { + src, + key_config, + enc: Vec::new(), + state: ServerRequestState::HpkeConfig { + buf: [0; 7], + read: 0, + }, + } + } + + /// Get a response that wraps the given async write instance. + /// This fails with an error if the request header hasn't been processed. + /// This condition is not exposed through a future anywhere, + /// but you can wait for the first byte of data. + pub fn response<D>(&self, dst: D) -> Res<ServerResponse<D>> { + let ServerRequestState::Body { hpke, state: _ } = &self.state else { + return Err(Error::NotReady); + }; + + let response_nonce = random(entropy(hpke.config())); + let aead = make_aead( + Mode::Encrypt, + hpke.config(), + &export_secret(hpke, LABEL_RESPONSE, hpke.config())?, + &self.enc, + &response_nonce, + )?; + Ok(ServerResponse { + writer: ChunkWriter { + dst, + cipher: aead, + buf: response_nonce, + closed: false, + }, + }) + } +} + +impl<S: AsyncRead> ServerRequest<S> { + fn read_config( + this: &mut ServerRequestProjection<'_, S>, + cx: &mut Context<'_>, + ) -> Option<Poll<IoResult<usize>>> { + let ServerRequestState::HpkeConfig { buf, read } = this.state else { + return None; + }; + + let res = ChunkReader::read_fixed(this.src.as_mut(), cx, &mut buf[..], read); + if res.is_some() { + return res; + } + + let config = match this + .key_config + .decode_hpke_config(&mut Cursor::new(&buf[..])) + { + Ok(cfg) => cfg, + Err(e) => return Some(ioerror(e)), + }; + let info = match build_info(INFO_REQUEST, this.key_config.key_id, config) { + Ok(info) => info, + Err(e) => return Some(ioerror(e)), + }; + this.enc.resize(config.kem().n_enc(), 0); + + *this.state = ServerRequestState::Enc { + config, + info, + read: 0, + }; + None + } + + fn read_enc( + this: &mut ServerRequestProjection<'_, S>, + cx: &mut Context<'_>, + ) -> Option<Poll<IoResult<usize>>> { + let ServerRequestState::Enc { config, info, read } = this.state else { + return None; + }; + + let res = ChunkReader::read_fixed(this.src.as_mut(), cx, &mut this.enc[..], read); + if res.is_some() { + return res; + } + + let hpke = match HpkeR::new( + *config, + &this.key_config.pk, + this.key_config.sk.as_ref().unwrap(), + this.enc, + info, + ) { + Ok(hpke) => hpke, + Err(e) => return Some(ioerror(e)), + }; + + *this.state = ServerRequestState::Body { + hpke, + state: ChunkReader::length(), + }; + None + } +} + +impl<S: AsyncRead> AsyncRead for ServerRequest<S> { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + output: &mut [u8], + ) -> Poll<IoResult<usize>> { + let mut this = self.project(); + if let Some(res) = Self::read_config(&mut this, cx) { + return res; + } + + if let Some(res) = Self::read_enc(&mut this, cx) { + return res; + } + + if let ServerRequestState::Body { hpke, state } = this.state { + state.read(this.src, cx, hpke, output) + } else { + Poll::Ready(Ok(0)) + } + } +} + +#[pin_project(project = ServerResponseProjection)] +pub struct ServerResponse<D> { + #[pin] + writer: ChunkWriter<D, Aead>, +} + +impl<D: AsyncWrite> AsyncWrite for ServerResponse<D> { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + input: &[u8], + ) -> Poll<IoResult<usize>> { + self.project().writer.as_mut().poll_write(cx, input) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> { + self.project().writer.as_mut().poll_flush(cx) + } + + fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> { + self.project().writer.as_mut().poll_close(cx) + } +} + +enum ClientResponseState { + Header { + enc: Vec<u8>, + secret: SymKey, + nonce: [u8; 16], + read: usize, + }, + Body { + aead: Aead, + state: ChunkReader, + }, +} + +#[pin_project(project = ClientResponseProjection)] +pub struct ClientResponse<S> { + #[pin] + src: S, + config: HpkeConfig, + state: ClientResponseState, +} + +impl<S: AsyncRead> ClientResponse<S> { + fn read_nonce( + this: &mut ClientResponseProjection<'_, S>, + cx: &mut Context<'_>, + ) -> Option<Poll<IoResult<usize>>> { + let ClientResponseState::Header { + enc, + secret, + nonce, + read, + } = this.state + else { + return None; + }; + + let nonce = &mut nonce[..entropy(*this.config)]; + let res = ChunkReader::read_fixed(this.src.as_mut(), cx, nonce, read); + if res.is_some() { + return res; + } + + let aead = match make_aead(Mode::Decrypt, *this.config, secret, enc, nonce) { + Ok(aead) => aead, + Err(e) => return Some(ioerror(e)), + }; + + *this.state = ClientResponseState::Body { + aead, + state: ChunkReader::length(), + }; + None + } +} + +impl<S: AsyncRead> AsyncRead for ClientResponse<S> { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + output: &mut [u8], + ) -> Poll<IoResult<usize>> { + let mut this = self.project(); + if let Some(res) = Self::read_nonce(&mut this, cx) { + return res; + } + + if let ClientResponseState::Body { aead, state } = this.state { + state.read(this.src, cx, aead, output) + } else { + Poll::Ready(Ok(0)) + } + } +} + +#[cfg(test)] +mod test { + use std::{ + io::Result as IoResult, + pin::Pin, + task::{Context, Poll}, + }; + + use futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + use log::trace; + use pin_project::pin_project; + use sync_async::{Dribble, Pipe, SplitAt, Stutter, SyncRead, SyncResolve, Unadapt}; + + use crate::{ + test::{init, make_config, REQUEST, RESPONSE}, + ClientRequest, Server, + }; + + #[test] + fn request_response() { + init(); + + let server_config = make_config(); + let server = Server::new(server_config).unwrap(); + let encoded_config = server.config().encode().unwrap(); + trace!("Config: {}", hex::encode(&encoded_config)); + + // The client sends a request. + let client = ClientRequest::from_encoded_config(&encoded_config).unwrap(); + let (mut request_read, request_write) = Pipe::new(); + let mut client_request = client.encapsulate_stream(request_write).unwrap(); + client_request.write_all(REQUEST).sync_resolve().unwrap(); + client_request.close().sync_resolve().unwrap(); + + trace!("Request: {}", hex::encode(REQUEST)); + let enc_request = request_read.sync_read_to_end(); + trace!("Encapsulated Request: {}", hex::encode(&enc_request)); + + // The server receives a request. + let mut server_request = server.decapsulate_stream(&enc_request[..]); + assert_eq!(server_request.sync_read_to_end(), REQUEST); + + // The server sends a response. + let (mut response_read, response_write) = Pipe::new(); + let mut server_response = server_request.response(response_write).unwrap(); + server_response.write_all(RESPONSE).sync_resolve().unwrap(); + server_response.close().sync_resolve().unwrap(); + + let enc_response = response_read.sync_read_to_end(); + trace!("Encapsulated Response: {}", hex::encode(&enc_response)); + + // The client receives a response. + let mut client_response = client_request.response(&enc_response[..]).unwrap(); + let response_buf = client_response.sync_read_to_end(); + assert_eq!(response_buf, RESPONSE); + trace!("Response: {}", hex::encode(response_buf)); + } + + /// Run the `request_response` test, but do it with streams that are one byte apiece + /// on the output side. + #[test] + fn dribble_out() { + init(); + + let server_config = make_config(); + let server = Server::new(server_config).unwrap(); + let encoded_config = server.config().encode().unwrap(); + trace!("Config: {}", hex::encode(&encoded_config)); + + // The client sends a request. + let client = ClientRequest::from_encoded_config(&encoded_config).unwrap(); + let (mut request_read, request_write) = Pipe::new(); + let request_write = Stutter::new(Dribble::new(request_write)); + let mut client_request = client.encapsulate_stream(request_write).unwrap(); + client_request.write_all(REQUEST).sync_resolve().unwrap(); + client_request.close().sync_resolve().unwrap(); + + trace!("Request: {}", hex::encode(REQUEST)); + let enc_request = request_read.sync_read_to_end(); + trace!("Encapsulated Request: {}", hex::encode(&enc_request)); + + // The server receives a request. + let enc_req_stream = Stutter::new(Dribble::new(&enc_request[..])); + let mut server_request = server.decapsulate_stream(enc_req_stream); + assert_eq!(server_request.sync_read_to_end(), REQUEST); + + // The server sends a response. + let (mut response_read, response_write) = Pipe::new(); + let response_write = Stutter::new(Dribble::new(response_write)); + let mut server_response = server_request.response(response_write).unwrap(); + server_response.write_all(RESPONSE).sync_resolve().unwrap(); + server_response.close().sync_resolve().unwrap(); + + let enc_response = response_read.sync_read_to_end(); + trace!("Encapsulated Response: {}", hex::encode(&enc_response)); + + // The client receives a response. + let enc_resp_stream = Stutter::new(Dribble::new(&enc_response[..])); + let mut client_response = client_request.response(enc_resp_stream).unwrap(); + let response_buf = client_response.sync_read_to_end(); + assert_eq!(response_buf, RESPONSE); + trace!("Response: {}", hex::encode(response_buf)); + } + + fn write_wrapped<S, W, T>(s: S, w: W, data: &[u8]) -> S + where + S: AsyncWrite + AsyncWriteExt + Unpin, + W: FnOnce(S) -> T, + T: AsyncWrite + AsyncWriteExt + Unpin + Unadapt<S = S>, + { + let mut s = w(s); + s.write_all(data).sync_resolve().unwrap(); + s.close().sync_resolve().unwrap(); + s.unadapt() + } + + fn read_wrapped<S, W, T>(s: S, w: W) -> (Vec<u8>, S) + where + S: AsyncRead + AsyncReadExt + Unpin, + W: FnOnce(S) -> T, + T: AsyncRead + AsyncReadExt + Unpin + Unadapt<S = S>, + { + let mut s = w(s); + (s.sync_read_to_end(), s.unadapt()) + } + + /// With each on its own, Stutter and Dribble don't cause the code to do anything differently. + /// You need both in order to effect a change in the way that the streaming code operates. + #[pin_project] + struct StutterDribble<S> { + #[pin] + s: Stutter<Dribble<S>>, + } + + impl<S> StutterDribble<S> { + fn new(s: S) -> Self { + Self { + s: Stutter::new(Dribble::new(s)), + } + } + } + + impl<S> Unadapt for StutterDribble<S> { + type S = S; + fn unadapt(self) -> Self::S { + self.s.unadapt().unadapt() + } + } + + impl<S: AsyncRead + Unpin> AsyncRead for StutterDribble<S> { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll<IoResult<usize>> { + let this = self.project(); + this.s.poll_read(cx, buf) + } + } + + impl<S: AsyncWrite + Unpin> AsyncWrite for StutterDribble<S> { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll<IoResult<usize>> { + self.project().s.poll_write(cx, buf) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> { + self.project().s.poll_flush(cx) + } + + fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> { + self.project().s.poll_close(cx) + } + } + + /// Run the `request_response` test, but do it with streams that are one byte apiece + /// on the input side. This is the one that produces the most output. + #[test] + fn dribble_in() { + init(); + + let server_config = make_config(); + let server = Server::new(server_config).unwrap(); + let encoded_config = server.config().encode().unwrap(); + trace!("Config: {}", hex::encode(&encoded_config)); + + // The client sends a request. + let client = ClientRequest::from_encoded_config(&encoded_config).unwrap(); + let (mut request_read, request_write) = Pipe::new(); + let client_request = client.encapsulate_stream(request_write).unwrap(); + let client_request = write_wrapped(client_request, StutterDribble::new, REQUEST); + + trace!("Request: {}", hex::encode(REQUEST)); + let enc_request = request_read.sync_read_to_end(); + trace!("Encapsulated Request: {}", hex::encode(&enc_request)); + + // The server receives a request. + let enc_req_stream = &enc_request[..]; + let server_request = server.decapsulate_stream(enc_req_stream); + let (request_data, server_request) = read_wrapped(server_request, StutterDribble::new); + assert_eq!(request_data, REQUEST); + + // The server sends a response. + let (mut response_read, response_write) = Pipe::new(); + let server_response = server_request.response(response_write).unwrap(); + _ = write_wrapped(server_response, StutterDribble::new, RESPONSE); + + let enc_response = response_read.sync_read_to_end(); + trace!("Encapsulated Response: {}", hex::encode(&enc_response)); + + // The client receives a response. + let client_response = client_request.response(&enc_response[..]).unwrap(); + let (response_data, _) = read_wrapped(client_response, StutterDribble::new); + assert_eq!(response_data, RESPONSE); + trace!("Response: {}", hex::encode(response_data)); + } + + /// Run the `request_response` test, but do it with streams that are one byte apiece + /// on the input side. This is the one that produces the most output. + #[test] + fn split_in() { + init(); + + let server_config = make_config(); + let server = Server::new(server_config).unwrap(); + let encoded_config = server.config().encode().unwrap(); + trace!("Config: {}", hex::encode(&encoded_config)); + + // The client sends a request. + let client = ClientRequest::from_encoded_config(&encoded_config).unwrap(); + let (mut request_read, request_write) = Pipe::new(); + let client_request = client.encapsulate_stream(request_write).unwrap(); + let client_request = write_wrapped( + client_request, + |s| SplitAt::new(s, REQUEST.len() / 2), + REQUEST, + ); + + trace!("Request: {}", hex::encode(REQUEST)); + let enc_request = request_read.sync_read_to_end(); + trace!("Encapsulated Request: {}", hex::encode(&enc_request)); + + // The server receives a request. + let enc_req_stream = &enc_request[..]; + let server_request = server.decapsulate_stream(enc_req_stream); + let (request_data, server_request) = read_wrapped(server_request, Stutter::new); + assert_eq!(request_data, REQUEST); + + // The server sends a response. + let (mut response_read, response_write) = Pipe::new(); + let server_response = server_request.response(response_write).unwrap(); + _ = write_wrapped( + server_response, + |s| SplitAt::new(s, RESPONSE.len() / 2), + RESPONSE, + ); + + let enc_response = response_read.sync_read_to_end(); + trace!("Encapsulated Response: {}", hex::encode(&enc_response)); + + // The client receives a response. + let client_response = client_request.response(&enc_response[..]).unwrap(); + let (response_data, _) = read_wrapped(client_response, Stutter::new); + assert_eq!(response_data, RESPONSE); + trace!("Response: {}", hex::encode(response_data)); + } + + /// Check that a longer request can be read properly. + /// This checks that any cleartext that doesn't fit in the output buffer + /// is correctly buffered. + #[test] + fn long_request() { + /// A longer request. + const LONG_REQUEST: &[u8] = &[0u8; 1024]; + init(); + let server_config = make_config(); + let server = Server::new(server_config).unwrap(); + let encoded_config = server.config().encode().unwrap(); + trace!("Config: {}", hex::encode(&encoded_config)); // The client sends a request. + let client = ClientRequest::from_encoded_config(&encoded_config).unwrap(); + let (mut request_read, request_write) = Pipe::new(); + let mut client_request = client.encapsulate_stream(request_write).unwrap(); + client_request + .write_all(LONG_REQUEST) + .sync_resolve() + .unwrap(); + client_request.close().sync_resolve().unwrap(); + trace!("Request: {}", hex::encode(LONG_REQUEST)); + let enc_request = request_read.sync_read_to_end(); + trace!("Encapsulated Request: {}", hex::encode(&enc_request)); // The server receives a request. + let mut server_request = server.decapsulate_stream(&enc_request[..]); + assert_eq!(server_request.sync_read_to_end(), LONG_REQUEST); + } +} diff --git a/toolkit/components/glean/Cargo.toml b/toolkit/components/glean/Cargo.toml @@ -18,8 +18,8 @@ cstr = "0.2" viaduct = "0.1" url = "2.1" thin-vec = { version = "0.2.1", features = ["gecko-ffi"] } -ohttp = { version = "0.6", default-features = false, features = ["gecko", "nss", "client"] } -bhttp = "0.6" +ohttp = { version = "0.7.2", default-features = false, features = ["gecko", "nss", "client"] } +bhttp = "0.7.2" thiserror = "1.0" mozbuild = "0.1"