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:
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"