Vendor dependencies

This commit is contained in:
2026-08-01 16:11:49 +03:00
parent 7f139a0241
commit 6b5e7f0f8b
29706 changed files with 9575646 additions and 0 deletions
@@ -0,0 +1 @@
{"$comment":"This file only protects against accidental modifications. It is not a security mechanism and does not protect against malicious changes.","files":{".cargo_vcs_info.json":"7c517dc579bc6ad8c5bd2b7f490c1be26a5b79bc140209e660ccf108a5f347de","Cargo.lock":"1872ecc9d38150cb5c12514282eae3a254e3dec3127afd7819677295f23fe28e","Cargo.toml":"98192d2d00a81ee101cf98f58be6398e65be4d79321a06d1496077e48b0595aa","Cargo.toml.orig":"dfc6e122ca746f1cad6dcb370b0bae1ec65b380ec646c70774feb416be560e1b","LICENSE":"c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4","README.md":"3e324eb643559c7094181292f1ee1459b9d6e676e713327369b9c212934c1841","src/api.rs":"d68e471dd96abfb54936230d44994718a5f5933e58e9c5f3cf5247515a16488e","src/context.rs":"8e71455a1d353508626be8802a1e97314b439e031fe5ac0ee744c31434e82946","src/error.rs":"d3c77a40db6958a1aaf753ce15abcae6ef26f0c13d17ad91dd0f66a5884a2bb9","src/futures_util.rs":"430de33030c03f71fa659270723e1b820d439d1ef6b58191ac2442675de96a48","src/hash.rs":"efd4941fb80fd78d7a91ba3e6ed1f027f4ac5423930ef8ea2ad0c8600d97b19a","src/jwt.rs":"dfe84a218018cd8f3e1c28816eff5d18d31bf5623d7e2a48a6afd3627e4c8e92","src/lib.rs":"41c362c79a71e29c2e545175ef32bc9192171fcbff038422e7328567f7ffb9cd","src/request.rs":"8c6769dd71d5716276024d981d087524eec49f8408f342415226e28419fbafc5","src/signer.rs":"24587397c64be9bf4abd540997c9609312885a0dd8a176870cda0234be643e97","src/time.rs":"0e485de49b1861b23bafa2ac16d91ecf8898e8caf8b5f4217872f9e2e49a7855","src/utils.rs":"19c4aa2bbe54c20103fb877388a7fced430f27e2a6e3136721a7123d1daac9c2"},"package":"7e38b44697c60a823705ccef85cb04d8e0527c9d16ed7c58bf1c6395bdd24ceb"}
@@ -0,0 +1,6 @@
{
"git": {
"sha1": "b1f72c17caa20ead87a20ffb2d2ba92266fe2f7c"
},
"path_in_vcs": "core"
}
+967
View File
@@ -0,0 +1,967 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures 0.2.17",
]
[[package]]
name = "anyhow"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[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 = "block-buffer"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
dependencies = [
"hybrid-array",
]
[[package]]
name = "block-padding"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
dependencies = [
"generic-array",
]
[[package]]
name = "bytes"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cbc"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
dependencies = [
"cipher",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common 0.1.7",
"inout",
]
[[package]]
name = "cmov"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "const-oid"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "crypto-common"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
dependencies = [
"hybrid-array",
]
[[package]]
name = "ctutils"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
dependencies = [
"cmov",
]
[[package]]
name = "defmt"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
dependencies = [
"bitflags",
"defmt-macros",
]
[[package]]
name = "defmt-macros"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
dependencies = [
"defmt-parser",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "defmt-parser"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
dependencies = [
"thiserror",
]
[[package]]
name = "der"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
"const-oid 0.9.6",
"pem-rfc7468",
"zeroize",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer 0.10.4",
"const-oid 0.9.6",
"crypto-common 0.1.7",
"subtle",
]
[[package]]
name = "digest"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
dependencies = [
"block-buffer 0.12.1",
"const-oid 0.10.2",
"crypto-common 0.2.2",
"ctutils",
]
[[package]]
name = "futures"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
[[package]]
name = "futures-executor"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a"
[[package]]
name = "futures-macro"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "futures-sink"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307"
[[package]]
name = "futures-task"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109"
[[package]]
name = "futures-util"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"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.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest 0.10.7",
]
[[package]]
name = "hmac"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f"
dependencies = [
"digest 0.11.3",
]
[[package]]
name = "http"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
dependencies = [
"bytes",
"itoa",
]
[[package]]
name = "hybrid-array"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c"
dependencies = [
"typenum",
]
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"block-padding",
"generic-array",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "jiff"
version = "0.2.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc"
dependencies = [
"defmt",
"jiff-core",
"jiff-static",
"jiff-tzdb-platform",
"log",
"portable-atomic",
"portable-atomic-util",
"serde_core",
"windows-link",
]
[[package]]
name = "jiff-core"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09"
dependencies = [
"defmt",
]
[[package]]
name = "jiff-static"
version = "0.2.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204"
dependencies = [
"jiff-core",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "jiff-tzdb"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e"
[[package]]
name = "jiff-tzdb-platform"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
dependencies = [
"jiff-tzdb",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [
"spin",
]
[[package]]
name = "libc"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "libm"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "log"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "num-bigint-dig"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
dependencies = [
"lazy_static",
"libm",
"num-integer",
"num-iter",
"num-traits",
"rand",
"smallvec",
"zeroize",
]
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
"libm",
]
[[package]]
name = "pbkdf2"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [
"digest 0.10.7",
"hmac 0.12.1",
]
[[package]]
name = "pem-rfc7468"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
dependencies = [
"base64ct",
]
[[package]]
name = "percent-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pkcs1"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
dependencies = [
"der",
"pkcs8",
"spki",
]
[[package]]
name = "pkcs5"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6"
dependencies = [
"aes",
"cbc",
"der",
"pbkdf2",
"scrypt",
"sha2 0.10.9",
"spki",
]
[[package]]
name = "pkcs8"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"pkcs5",
"rand_core",
"spki",
]
[[package]]
name = "portable-atomic"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3"
[[package]]
name = "portable-atomic-util"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
dependencies = [
"portable-atomic",
]
[[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.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a"
dependencies = [
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "reqsign-core"
version = "3.2.0"
dependencies = [
"anyhow",
"base64",
"bytes",
"futures",
"hex",
"hmac 0.13.0",
"http",
"jiff",
"log",
"percent-encoding",
"rsa",
"serde",
"serde_json",
"sha1",
"sha2 0.11.0",
"windows-sys",
]
[[package]]
name = "rsa"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
dependencies = [
"const-oid 0.9.6",
"digest 0.10.7",
"num-bigint-dig",
"num-integer",
"num-traits",
"pkcs1",
"pkcs8",
"rand_core",
"sha2 0.10.9",
"signature",
"spki",
"subtle",
"zeroize",
]
[[package]]
name = "salsa20"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
dependencies = [
"cipher",
]
[[package]]
name = "scrypt"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
dependencies = [
"pbkdf2",
"salsa20",
"sha2 0.10.9",
]
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.3",
]
[[package]]
name = "serde_json"
version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "sha1"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"digest 0.11.3",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"digest 0.10.7",
]
[[package]]
name = "sha2"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"digest 0.11.3",
]
[[package]]
name = "signature"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"digest 0.10.7",
"rand_core",
]
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
[[package]]
name = "spin"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e"
[[package]]
name = "spki"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"base64ct",
"der",
]
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.3",
]
[[package]]
name = "typenum"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "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 = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "zerocopy"
version = "0.8.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "zeroize"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+105
View File
@@ -0,0 +1,105 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2024"
rust-version = "1.85.0"
name = "reqsign-core"
version = "3.2.0"
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Signing API requests without effort."
readme = "README.md"
categories = [
"command-line-utilities",
"web-programming",
]
license = "Apache-2.0"
repository = "https://github.com/apache/opendal-reqsign"
resolver = "2"
[features]
default = []
jwt = [
"dep:rsa",
"dep:serde",
"dep:serde_json",
]
[lib]
name = "reqsign_core"
path = "src/lib.rs"
[dependencies.anyhow]
version = "1"
[dependencies.base64]
version = "0.22"
[dependencies.bytes]
version = "1"
[dependencies.futures]
version = "0.3"
[dependencies.hex]
version = "0.4"
[dependencies.hmac]
version = "0.13"
[dependencies.http]
version = "1"
[dependencies.jiff]
version = "0.2"
[dependencies.log]
version = "0.4"
[dependencies.percent-encoding]
version = "2"
[dependencies.rsa]
version = "0.9.2"
features = [
"pkcs5",
"sha2",
]
optional = true
[dependencies.serde]
version = "1"
features = ["derive"]
optional = true
[dependencies.serde_json]
version = "1"
optional = true
[dependencies.sha1]
version = "0.11"
[dependencies.sha2]
version = "0.11"
features = ["oid"]
[target.'cfg(target_os = "windows")'.dependencies.windows-sys]
version = "0.61.0"
features = [
"Win32_Foundation",
"Win32_UI_Shell",
"Win32_System_Com",
]
+58
View File
@@ -0,0 +1,58 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
[package]
name = "reqsign-core"
version = "3.2.0"
categories = ["command-line-utilities", "web-programming"]
description = "Signing API requests without effort."
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[features]
default = []
jwt = ["dep:rsa", "dep:serde", "dep:serde_json"]
[dependencies]
anyhow = { workspace = true }
base64 = { workspace = true }
bytes = { workspace = true }
futures = { workspace = true }
hex = { workspace = true }
hmac = { workspace = true }
http = { workspace = true }
jiff = { workspace = true }
log = { workspace = true }
percent-encoding = { workspace = true }
sha1 = { workspace = true }
sha2 = { workspace = true }
# Optional dependencies
rsa = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
serde_json = { workspace = true, optional = true }
[target.'cfg(target_os = "windows")'.dependencies]
windows-sys = { version = "0.61.0", features = [
"Win32_Foundation",
"Win32_UI_Shell",
"Win32_System_Com",
] }
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+75
View File
@@ -0,0 +1,75 @@
# reqsign-core
Core components for signing API requests.
---
This crate provides the foundational types and traits for the reqsign ecosystem. It defines the core abstractions that enable flexible and extensible request signing.
## Quick Start
```rust
use reqsign_core::{Context, Signer, ProvideCredential, SignRequest};
// Create a context with your implementations
let ctx = Context::default();
// Create a signer with credential loader and request builder
let signer = Signer::new(ctx, credential_loader, request_builder);
// Sign your requests
let mut parts = /* your request parts */;
signer.sign(&mut parts, None).await?;
```
## Features
- **Flexible Architecture**: Define your own credential types and signing logic
- **Async Support**: Built with async/await for modern Rust applications
- **Environment Integration**: Access environment variables through the Context
- **Type Safety**: Strong typing ensures compile-time correctness
## Core Concepts
### Context
The `Context` struct serves as a container for runtime dependencies:
- File system access via `FileRead` trait
- HTTP client via `HttpSend` trait
- Environment variables via `Env` trait
### Traits
- **`ProvideCredential`**: Load credentials from various sources
- **`SignRequest`**: Build service-specific signing requests
- **`SigningCredential`**: Validate credential validity
- **`FileRead`**: Async file reading operations
- **`HttpSend`**: HTTP request execution
- **`Env`**: Environment variable access
### Signer
The `Signer` orchestrates the signing process by:
1. Loading credentials using the provided loader
2. Building signing requests with the builder
3. Applying signatures to HTTP requests
## Examples
Check out the [custom_signer example](examples/custom_signer.rs) to see how to implement your own signing logic.
```bash
cargo run --example custom_signer
```
## Integration
This crate is typically used with service-specific implementations:
- `reqsign-aws-v4` for AWS services
- `reqsign-aliyun-oss` for Aliyun OSS
- `reqsign-azure-storage` for Azure Storage
- And more...
## License
Licensed under [Apache License, Version 2.0](./LICENSE).
+420
View File
@@ -0,0 +1,420 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use crate::time::Timestamp;
use crate::{BoxedFuture, Context, MaybeSend, Result};
use std::fmt::Debug;
use std::future::Future;
use std::ops::Deref;
use std::time::Duration;
/// A credential that can distinguish cache freshness from exact usability.
///
/// Both checks must reject credentials that lack fields required for authentication.
pub trait SigningCredential: Clone + Debug + Send + Sync + Unpin + 'static {
/// Return whether a cached credential can be reused without refreshing it.
///
/// Implementations may include a proactive refresh window in this check.
fn is_valid(&self) -> bool;
/// Return whether the credential is usable at this exact timestamp.
///
/// Implementations with an expiration time should not add a refresh or
/// operation-specific buffer here. The default preserves the behavior of
/// implementations that only provide [`SigningCredential::is_valid`].
fn is_valid_at(&self, _ts: Timestamp) -> bool {
self.is_valid()
}
}
impl<T: SigningCredential> SigningCredential for Option<T> {
fn is_valid(&self) -> bool {
let Some(ctx) = self else {
return false;
};
ctx.is_valid()
}
fn is_valid_at(&self, ts: Timestamp) -> bool {
let Some(ctx) = self else {
return false;
};
ctx.is_valid_at(ts)
}
}
/// ProvideCredential is the trait used by signer to load the credential from the environment.
///`
/// Service may require different credential to sign the request, for example, AWS require
/// access key and secret key, while Google Cloud Storage require token.
pub trait ProvideCredential: Debug + Send + Sync + Unpin + 'static {
/// Credential returned by this loader.
///
/// Typically, it will be a credential.
type Credential: Send + Sync + Unpin + 'static;
/// Load signing credential from current env.
fn provide_credential(
&self,
ctx: &Context,
) -> impl Future<Output = Result<Option<Self::Credential>>> + MaybeSend;
}
/// ProvideCredentialDyn is the dyn version of [`ProvideCredential`].
pub trait ProvideCredentialDyn: Debug + Send + Sync + Unpin + 'static {
/// Credential returned by this loader.
type Credential: Send + Sync + Unpin + 'static;
/// Dyn version of [`ProvideCredential::provide_credential`].
fn provide_credential_dyn<'a>(
&'a self,
ctx: &'a Context,
) -> BoxedFuture<'a, Result<Option<Self::Credential>>>;
}
impl<T> ProvideCredentialDyn for T
where
T: ProvideCredential + ?Sized,
{
type Credential = T::Credential;
fn provide_credential_dyn<'a>(
&'a self,
ctx: &'a Context,
) -> BoxedFuture<'a, Result<Option<Self::Credential>>> {
Box::pin(self.provide_credential(ctx))
}
}
impl<T> ProvideCredential for std::sync::Arc<T>
where
T: ProvideCredentialDyn + ?Sized,
{
type Credential = T::Credential;
async fn provide_credential(&self, ctx: &Context) -> Result<Option<Self::Credential>> {
self.deref().provide_credential_dyn(ctx).await
}
}
/// Service-specific request signing.
///
/// Implementations receive a request URI that is already percent-encoded and ready
/// for transport. They must derive canonical paths, queries, and headers as local
/// views without normalizing or rebuilding the existing wire URI.
///
/// Header authentication must preserve the URI. Query authentication must preserve
/// the existing URI representation and append only protocol-encoded authentication
/// fields. In particular, existing percent escapes, parameter order, duplicate keys,
/// empty values, and literal `+` characters are caller-owned wire data.
pub trait SignRequest: Debug + Send + Sync + Unpin + 'static {
/// Credential used by this builder.
///
/// Typically, it will be a credential.
type Credential: Send + Sync + Unpin + 'static;
/// Return the timestamp through which the credential must remain usable
/// for the requested signing operation.
///
/// Implementations own the signing clock, the service-specific meaning of
/// `expires_in`, and any transport, RPC, or artifact-lifetime headroom. This
/// method must not perform I/O or mutate state. When a deadline depends on an
/// artifact's signing time, [`SignRequest::sign_request`] must derive both the
/// deadline check and the artifact from the same captured timestamp.
fn required_valid_until(
&self,
_credential: &Self::Credential,
expires_in: Option<Duration>,
) -> Timestamp {
Timestamp::now() + expires_in.unwrap_or_default()
}
/// Sign a request head.
///
/// On `Err`, an implementation must leave the entire request head unchanged. On
/// `Ok`, it may change only `req.uri` and `req.headers`; the method, version, and
/// extensions remain caller-owned. [`crate::Signer`] enforces this commit boundary
/// when it invokes the implementation, but implementations must also uphold it for
/// callers that invoke this method directly.
///
/// ## Credential
///
/// The `credential` parameter is the credential required by the signer to sign the request.
/// Implementations with expiring credentials must validate it against
/// [`SignRequest::required_valid_until`] before mutating the request or performing
/// external signing calls. [`crate::Signer`] performs the same validation before
/// invoking this method, while direct callers rely on the implementation.
///
/// ## Expires In
///
/// The `expires_in` parameter requests a validity duration when the service supports
/// one. It is not a universal header-versus-query mode selector. Each service and
/// credential type defines whether the value selects presigning, configures an
/// expiration, is ignored, or is rejected.
fn sign_request<'a>(
&'a self,
ctx: &'a Context,
req: &'a mut http::request::Parts,
credential: Option<&'a Self::Credential>,
expires_in: Option<Duration>,
) -> impl Future<Output = Result<()>> + MaybeSend + 'a;
}
/// SignRequestDyn is the dyn version of [`SignRequest`].
pub trait SignRequestDyn: Debug + Send + Sync + Unpin + 'static {
/// Credential used by this builder.
type Credential: Send + Sync + Unpin + 'static;
/// Dyn version of [`SignRequest::required_valid_until`].
fn required_valid_until_dyn(
&self,
_credential: &Self::Credential,
expires_in: Option<Duration>,
) -> Timestamp {
Timestamp::now() + expires_in.unwrap_or_default()
}
/// Dyn version of [`SignRequest::sign_request`].
fn sign_request_dyn<'a>(
&'a self,
ctx: &'a Context,
req: &'a mut http::request::Parts,
credential: Option<&'a Self::Credential>,
expires_in: Option<Duration>,
) -> BoxedFuture<'a, Result<()>>;
}
impl<T> SignRequestDyn for T
where
T: SignRequest + ?Sized,
{
type Credential = T::Credential;
fn required_valid_until_dyn(
&self,
credential: &Self::Credential,
expires_in: Option<Duration>,
) -> Timestamp {
self.required_valid_until(credential, expires_in)
}
fn sign_request_dyn<'a>(
&'a self,
ctx: &'a Context,
req: &'a mut http::request::Parts,
credential: Option<&'a Self::Credential>,
expires_in: Option<Duration>,
) -> BoxedFuture<'a, Result<()>> {
Box::pin(self.sign_request(ctx, req, credential, expires_in))
}
}
impl<T> SignRequest for std::sync::Arc<T>
where
T: SignRequestDyn + ?Sized,
{
type Credential = T::Credential;
fn required_valid_until(
&self,
credential: &Self::Credential,
expires_in: Option<Duration>,
) -> Timestamp {
self.deref()
.required_valid_until_dyn(credential, expires_in)
}
async fn sign_request(
&self,
ctx: &Context,
req: &mut http::request::Parts,
credential: Option<&Self::Credential>,
expires_in: Option<Duration>,
) -> Result<()> {
self.deref()
.sign_request_dyn(ctx, req, credential, expires_in)
.await
}
}
/// A chain of credential providers that will be tried in order.
///
/// This is a generic implementation that can be used by any service to chain multiple
/// credential providers together. The chain will try each provider in order until one
/// returns credentials or all providers have been exhausted.
///
/// # Example
///
/// ```no_run
/// use reqsign_core::{ProvideCredentialChain, Context, ProvideCredential, Result};
///
/// #[derive(Debug)]
/// struct MyCredential {
/// token: String,
/// }
///
/// #[derive(Debug)]
/// struct EnvironmentProvider;
///
/// impl ProvideCredential for EnvironmentProvider {
/// type Credential = MyCredential;
///
/// async fn provide_credential(&self, ctx: &Context) -> Result<Option<Self::Credential>> {
/// // Implementation
/// Ok(None)
/// }
/// }
///
/// # async fn example(ctx: Context) {
/// let chain = ProvideCredentialChain::new()
/// .push(EnvironmentProvider);
///
/// let credentials = chain.provide_credential(&ctx).await;
/// # }
/// ```
pub struct ProvideCredentialChain<C> {
providers: Vec<Box<dyn ProvideCredentialDyn<Credential = C>>>,
}
impl<C> ProvideCredentialChain<C>
where
C: Send + Sync + Unpin + 'static,
{
/// Create a new empty credential provider chain.
pub fn new() -> Self {
Self {
providers: Vec::new(),
}
}
/// Add a credential provider to the chain.
pub fn push(mut self, provider: impl ProvideCredential<Credential = C> + 'static) -> Self {
self.providers.push(Box::new(provider));
self
}
/// Add a credential provider to the front of the chain.
///
/// This provider will be tried first before all existing providers.
pub fn push_front(
mut self,
provider: impl ProvideCredential<Credential = C> + 'static,
) -> Self {
self.providers.insert(0, Box::new(provider));
self
}
/// Create a credential provider chain from a vector of providers.
pub fn from_vec(providers: Vec<Box<dyn ProvideCredentialDyn<Credential = C>>>) -> Self {
Self { providers }
}
/// Get the number of providers in the chain.
pub fn len(&self) -> usize {
self.providers.len()
}
/// Check if the chain is empty.
pub fn is_empty(&self) -> bool {
self.providers.is_empty()
}
}
impl<C> Default for ProvideCredentialChain<C>
where
C: Send + Sync + Unpin + 'static,
{
fn default() -> Self {
Self::new()
}
}
impl<C> Debug for ProvideCredentialChain<C>
where
C: Send + Sync + Unpin + 'static,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ProvideCredentialChain")
.field("providers_count", &self.providers.len())
.finish()
}
}
impl<C> ProvideCredential for ProvideCredentialChain<C>
where
C: Send + Sync + Unpin + 'static,
{
type Credential = C;
async fn provide_credential(&self, ctx: &Context) -> Result<Option<Self::Credential>> {
for provider in &self.providers {
log::debug!("Trying credential provider: {provider:?}");
match provider.provide_credential_dyn(ctx).await {
Ok(Some(cred)) => {
log::debug!("Successfully loaded credential from provider: {provider:?}");
return Ok(Some(cred));
}
Ok(None) => {
log::debug!("No credential found in provider: {provider:?}");
continue;
}
Err(e) => {
log::warn!("Error loading credential from provider {provider:?}: {e:?}");
// Continue to next provider on error
continue;
}
}
}
Ok(None)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Clone, Debug)]
struct ExactCredential {
valid_at: Timestamp,
}
impl SigningCredential for ExactCredential {
fn is_valid(&self) -> bool {
false
}
fn is_valid_at(&self, timestamp: Timestamp) -> bool {
self.valid_at == timestamp
}
}
#[test]
fn option_forwards_exact_validity_check() {
let timestamp = Timestamp::from_second(42).expect("timestamp must be valid");
let credential = Some(ExactCredential {
valid_at: timestamp,
});
assert!(credential.is_valid_at(timestamp));
assert!(!credential.is_valid_at(timestamp + Duration::from_secs(1)));
assert!(!None::<ExactCredential>.is_valid_at(timestamp));
}
}
+526
View File
@@ -0,0 +1,526 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use crate::{BoxedFuture, Error, MaybeSend, Result};
use bytes::Bytes;
use std::collections::HashMap;
use std::fmt::Debug;
use std::future::Future;
use std::ops::Deref;
use std::path::PathBuf;
use std::sync::Arc;
/// Context provides the context for the request signing.
///
/// ## Important
///
/// reqsign provides NO default implementations. Users MAY configure components they need.
/// Any unconfigured component will use a no-op implementation that returns errors or empty values when called.
///
/// ## Example
///
/// ```
/// use reqsign_core::{Context, OsEnv};
///
/// // Create a context with explicit implementations
/// let ctx = Context::new()
/// .with_env(OsEnv); // Optionally configure environment implementation
/// ```
#[derive(Clone)]
pub struct Context {
fs: Arc<dyn FileReadDyn>,
http: Arc<dyn HttpSendDyn>,
env: Arc<dyn Env>,
cmd: Arc<dyn CommandExecuteDyn>,
}
impl Debug for Context {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Context")
.field("fs", &self.fs)
.field("http", &self.http)
.field("env", &self.env)
.field("cmd", &self.cmd)
.finish()
}
}
impl Default for Context {
fn default() -> Self {
Self::new()
}
}
impl Context {
/// Create a new Context with no-op implementations.
///
/// All components use no-op implementations by default.
/// Use the `with_*` methods to configure the components you need.
///
/// ```
/// use reqsign_core::Context;
///
/// let ctx = Context::new();
/// // All components use no-op implementations by default
/// // You can configure specific components as needed:
/// // ctx.with_file_read(my_file_reader)
/// // .with_http_send(my_http_client)
/// // .with_env(my_env_provider);
/// ```
pub fn new() -> Self {
Self {
fs: Arc::new(NoopFileRead),
http: Arc::new(NoopHttpSend),
env: Arc::new(NoopEnv),
cmd: Arc::new(NoopCommandExecute),
}
}
/// Replace the file reader implementation.
pub fn with_file_read(mut self, fs: impl FileRead) -> Self {
self.fs = Arc::new(fs);
self
}
/// Replace the HTTP client implementation.
pub fn with_http_send(mut self, http: impl HttpSend) -> Self {
self.http = Arc::new(http);
self
}
/// Replace the environment implementation.
pub fn with_env(mut self, env: impl Env) -> Self {
self.env = Arc::new(env);
self
}
/// Replace the command executor implementation.
pub fn with_command_execute(mut self, cmd: impl CommandExecute) -> Self {
self.cmd = Arc::new(cmd);
self
}
/// Read the file content entirely in `Vec<u8>`.
#[inline]
pub async fn file_read(&self, path: &str) -> Result<Vec<u8>> {
self.fs.file_read_dyn(path).await
}
/// Read the file content entirely in `String`.
pub async fn file_read_as_string(&self, path: &str) -> Result<String> {
let bytes = self.file_read(path).await?;
Ok(String::from_utf8_lossy(&bytes).to_string())
}
/// Send http request and return the response.
#[inline]
pub async fn http_send(&self, req: http::Request<Bytes>) -> Result<http::Response<Bytes>> {
self.http.http_send_dyn(req).await
}
/// Send http request and return the response as string.
pub async fn http_send_as_string(
&self,
req: http::Request<Bytes>,
) -> Result<http::Response<String>> {
let (parts, body) = self.http.http_send_dyn(req).await?.into_parts();
let body = String::from_utf8_lossy(&body).to_string();
Ok(http::Response::from_parts(parts, body))
}
/// Get the home directory of the current user.
#[inline]
pub fn home_dir(&self) -> Option<PathBuf> {
self.env.home_dir()
}
/// Expand `~` in input path.
///
/// - If path not starts with `~/` or `~\\`, returns `Some(path)` directly.
/// - Otherwise, replace `~` with home dir instead.
/// - If home_dir is not found, returns `None`.
pub fn expand_home_dir(&self, path: &str) -> Option<String> {
if !path.starts_with("~/") && !path.starts_with("~\\") {
Some(path.to_string())
} else {
self.home_dir()
.map(|home| path.replace('~', &home.to_string_lossy()))
}
}
/// Get the environment variable.
///
/// - Returns `Some(v)` if the environment variable is found and is valid utf-8.
/// - Returns `None` if the environment variable is not found or value is invalid.
#[inline]
pub fn env_var(&self, key: &str) -> Option<String> {
self.env.var(key)
}
/// Returns a hashmap of (variable, value) pairs of strings, for all the
/// environment variables of the current process.
#[inline]
pub fn env_vars(&self) -> HashMap<String, String> {
self.env.vars()
}
/// Execute an external command with the given program and arguments.
///
/// Returns the command output including exit status, stdout, and stderr.
pub async fn command_execute(&self, program: &str, args: &[&str]) -> Result<CommandOutput> {
self.cmd.command_execute_dyn(program, args).await
}
}
/// FileRead is used to read the file content entirely in `Vec<u8>`.
///
/// This could be used by `Load` to load the credential from the file.
pub trait FileRead: Debug + Send + Sync + 'static {
/// Read the file content entirely in `Vec<u8>`.
fn file_read(&self, path: &str) -> impl Future<Output = Result<Vec<u8>>> + MaybeSend;
}
/// FileReadDyn is the dyn version of [`FileRead`].
pub trait FileReadDyn: Debug + Send + Sync + 'static {
/// Dyn version of [`FileRead::file_read`].
fn file_read_dyn<'a>(&'a self, path: &'a str) -> BoxedFuture<'a, Result<Vec<u8>>>;
}
impl<T: FileRead + ?Sized> FileReadDyn for T {
fn file_read_dyn<'a>(&'a self, path: &'a str) -> BoxedFuture<'a, Result<Vec<u8>>> {
Box::pin(self.file_read(path))
}
}
impl<T: FileReadDyn + ?Sized> FileRead for Arc<T> {
async fn file_read(&self, path: &str) -> Result<Vec<u8>> {
self.deref().file_read_dyn(path).await
}
}
/// HttpSend is used to send http request during the signing process.
///
/// For example, fetch IMDS token from AWS or OAuth2 refresh token. This trait is designed
/// especially for the signer, please don't use it as a general http client.
pub trait HttpSend: Debug + Send + Sync + 'static {
/// Send http request and return the response.
fn http_send(
&self,
req: http::Request<Bytes>,
) -> impl Future<Output = Result<http::Response<Bytes>>> + MaybeSend;
}
/// HttpSendDyn is the dyn version of [`HttpSend`].
pub trait HttpSendDyn: Debug + Send + Sync + 'static {
/// Dyn version of [`HttpSend::http_send`].
fn http_send_dyn(
&self,
req: http::Request<Bytes>,
) -> BoxedFuture<'_, Result<http::Response<Bytes>>>;
}
impl<T: HttpSend + ?Sized> HttpSendDyn for T {
fn http_send_dyn(
&self,
req: http::Request<Bytes>,
) -> BoxedFuture<'_, Result<http::Response<Bytes>>> {
Box::pin(self.http_send(req))
}
}
impl<T: HttpSendDyn + ?Sized> HttpSend for Arc<T> {
async fn http_send(&self, req: http::Request<Bytes>) -> Result<http::Response<Bytes>> {
self.deref().http_send_dyn(req).await
}
}
/// Permits parameterizing the home functions via the _from variants
pub trait Env: Debug + Send + Sync + 'static {
/// Get an environment variable.
///
/// - Returns `Some(v)` if the environment variable is found and is valid utf-8.
/// - Returns `None` if the environment variable is not found or value is invalid.
fn var(&self, key: &str) -> Option<String>;
/// Returns a hashmap of (variable, value) pairs of strings, for all the
/// environment variables of the current process.
fn vars(&self) -> HashMap<String, String>;
/// Return the path to the users home dir, returns `None` if any error occurs.
fn home_dir(&self) -> Option<PathBuf>;
}
/// Implements Env for the OS context, both Unix style and Windows.
#[derive(Debug, Copy, Clone)]
pub struct OsEnv;
impl Env for OsEnv {
fn var(&self, key: &str) -> Option<String> {
std::env::var_os(key)?.into_string().ok()
}
fn vars(&self) -> HashMap<String, String> {
std::env::vars().collect()
}
#[cfg(any(unix, target_os = "redox"))]
fn home_dir(&self) -> Option<PathBuf> {
#[allow(deprecated)]
std::env::home_dir()
}
#[cfg(windows)]
fn home_dir(&self) -> Option<PathBuf> {
windows::home_dir_inner()
}
#[cfg(target_arch = "wasm32")]
fn home_dir(&self) -> Option<PathBuf> {
None
}
}
/// StaticEnv provides a static env environment.
///
/// This is useful for testing or for providing a fixed environment.
#[derive(Debug, Clone, Default)]
pub struct StaticEnv {
/// The home directory to use.
pub home_dir: Option<PathBuf>,
/// The environment variables to use.
pub envs: HashMap<String, String>,
}
impl Env for StaticEnv {
fn var(&self, key: &str) -> Option<String> {
self.envs.get(key).cloned()
}
fn vars(&self) -> HashMap<String, String> {
self.envs.clone()
}
fn home_dir(&self) -> Option<PathBuf> {
self.home_dir.clone()
}
}
/// CommandOutput represents the output of a command execution.
#[derive(Debug, Clone)]
pub struct CommandOutput {
/// Exit status code (0 for success)
pub status: i32,
/// Standard output as bytes
pub stdout: Vec<u8>,
/// Standard error as bytes
pub stderr: Vec<u8>,
}
impl CommandOutput {
/// Check if the command exited successfully.
pub fn success(&self) -> bool {
self.status == 0
}
}
/// CommandExecute is used to execute external commands for credential retrieval.
///
/// This trait abstracts command execution to support different runtime environments:
/// - Tokio-based async execution
/// - Blocking execution for non-async contexts
/// - WebAssembly environments (returning errors)
/// - Mock implementations for testing
pub trait CommandExecute: Debug + Send + Sync + 'static {
/// Execute a command with the given program and arguments.
fn command_execute<'a>(
&'a self,
program: &'a str,
args: &'a [&'a str],
) -> impl Future<Output = Result<CommandOutput>> + MaybeSend + 'a;
}
/// CommandExecuteDyn is the dyn version of [`CommandExecute`].
pub trait CommandExecuteDyn: Debug + Send + Sync + 'static {
/// Dyn version of [`CommandExecute::command_execute`].
fn command_execute_dyn<'a>(
&'a self,
program: &'a str,
args: &'a [&'a str],
) -> BoxedFuture<'a, Result<CommandOutput>>;
}
impl<T: CommandExecute + ?Sized> CommandExecuteDyn for T {
fn command_execute_dyn<'a>(
&'a self,
program: &'a str,
args: &'a [&'a str],
) -> BoxedFuture<'a, Result<CommandOutput>> {
Box::pin(self.command_execute(program, args))
}
}
impl<T: CommandExecuteDyn + ?Sized> CommandExecute for Arc<T> {
async fn command_execute(&self, program: &str, args: &[&str]) -> Result<CommandOutput> {
self.deref().command_execute_dyn(program, args).await
}
}
/// NoopFileRead is a no-op implementation that always returns an error.
///
/// This is used when no file reader is configured.
#[derive(Debug, Clone, Copy, Default)]
pub struct NoopFileRead;
impl FileRead for NoopFileRead {
async fn file_read(&self, _path: &str) -> Result<Vec<u8>> {
Err(Error::unexpected(
"file reading not supported: no file reader configured",
))
}
}
/// NoopHttpSend is a no-op implementation that always returns an error.
///
/// This is used when no HTTP client is configured.
#[derive(Debug, Clone, Copy, Default)]
pub struct NoopHttpSend;
impl HttpSend for NoopHttpSend {
async fn http_send(&self, _req: http::Request<Bytes>) -> Result<http::Response<Bytes>> {
Err(Error::unexpected(
"HTTP sending not supported: no HTTP client configured",
))
}
}
/// NoopEnv is a no-op implementation that always returns None/empty.
///
/// This is used when no environment is configured.
#[derive(Debug, Clone, Copy, Default)]
pub struct NoopEnv;
impl Env for NoopEnv {
fn var(&self, _key: &str) -> Option<String> {
None
}
fn vars(&self) -> HashMap<String, String> {
HashMap::new()
}
fn home_dir(&self) -> Option<PathBuf> {
None
}
}
/// NoopCommandExecute is a no-op implementation that always returns an error.
///
/// This is used when no command executor is configured.
#[derive(Debug, Clone, Copy, Default)]
pub struct NoopCommandExecute;
impl CommandExecute for NoopCommandExecute {
async fn command_execute(&self, _program: &str, _args: &[&str]) -> Result<CommandOutput> {
Err(Error::unexpected(
"command execution not supported: no command executor configured",
))
}
}
#[cfg(target_os = "windows")]
mod windows {
use std::env;
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;
use std::path::PathBuf;
use std::ptr;
use std::slice;
use windows_sys::Win32::Foundation::S_OK;
use windows_sys::Win32::System::Com::CoTaskMemFree;
use windows_sys::Win32::UI::Shell::{
FOLDERID_Profile, KF_FLAG_DONT_VERIFY, SHGetKnownFolderPath,
};
pub fn home_dir_inner() -> Option<PathBuf> {
env::var_os("USERPROFILE")
.filter(|s| !s.is_empty())
.map(PathBuf::from)
.or_else(home_dir_crt)
}
#[cfg(not(target_vendor = "uwp"))]
fn home_dir_crt() -> Option<PathBuf> {
unsafe {
let mut path = ptr::null_mut();
match SHGetKnownFolderPath(
&FOLDERID_Profile,
KF_FLAG_DONT_VERIFY as u32,
std::ptr::null_mut(),
&mut path,
) {
S_OK => {
let path_slice = slice::from_raw_parts(path, wcslen(path));
let s = OsString::from_wide(&path_slice);
CoTaskMemFree(path.cast());
Some(PathBuf::from(s))
}
_ => {
// Free any allocated memory even on failure. A null ptr is a no-op for `CoTaskMemFree`.
CoTaskMemFree(path.cast());
None
}
}
}
}
#[cfg(target_vendor = "uwp")]
fn home_dir_crt() -> Option<PathBuf> {
None
}
unsafe extern "C" {
unsafe fn wcslen(buf: *const u16) -> usize;
}
#[cfg(not(target_vendor = "uwp"))]
#[cfg(test)]
mod tests {
use super::home_dir_inner;
use std::env;
use std::ops::Deref;
use std::path::{Path, PathBuf};
#[test]
fn test_with_without() {
let olduserprofile = env::var_os("USERPROFILE").unwrap();
unsafe {
env::remove_var("HOME");
env::remove_var("USERPROFILE");
}
assert_eq!(home_dir_inner(), Some(PathBuf::from(olduserprofile)));
let home = Path::new(r"C:\Users\foo tar baz");
unsafe {
env::set_var("HOME", home.as_os_str());
env::set_var("USERPROFILE", home.as_os_str());
}
assert_ne!(home_dir_inner().as_ref().map(Deref::deref), Some(home));
assert_eq!(home_dir_inner().as_ref().map(Deref::deref), Some(home));
}
}
}
+327
View File
@@ -0,0 +1,327 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use std::fmt;
/// The error type for reqsign operations
pub struct Error {
/// The category of error that occurred
kind: ErrorKind,
/// Human-readable error message
message: String,
/// The underlying error source
source: Option<anyhow::Error>,
/// Additional context information for debugging
context: Vec<String>,
/// Whether this error is retryable
retryable: bool,
}
/// The kind of error that occurred
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorKind {
/// Credentials are invalid, expired, or malformed
/// User action: Check credential format, refresh if expired
CredentialInvalid,
/// Permission denied when accessing credentials or resources
/// User action: Check IAM policies, role trust relationships
PermissionDenied,
/// Required configuration is missing or invalid
/// User action: Check configuration files, environment variables
ConfigInvalid,
/// Request cannot be signed or is malformed
/// User action: Check request parameters, headers
RequestInvalid,
/// Rate limit exceeded
/// User action: Implement backoff, check quotas
RateLimited,
/// Unexpected error that doesn't fit other categories
/// User action: Check logs, report bug if persistent
Unexpected,
}
impl Error {
/// Create a new error with the given kind and message
pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
Self {
kind,
message: message.into(),
source: None,
context: Vec::new(),
retryable: kind.default_retryable(),
}
}
/// Add a source error
pub fn with_source(mut self, source: impl Into<anyhow::Error>) -> Self {
self.source = Some(source.into());
self
}
/// Add context information for debugging
pub fn with_context(mut self, context: impl fmt::Display) -> Self {
self.context.push(context.to_string());
self
}
/// Override the retryable status
pub fn set_retryable(mut self, retryable: bool) -> Self {
self.retryable = retryable;
self
}
/// Get the error kind
pub fn kind(&self) -> ErrorKind {
self.kind
}
/// Check if this error is retryable
pub fn is_retryable(&self) -> bool {
self.retryable
}
/// Get the context information
pub fn context(&self) -> &[String] {
&self.context
}
}
impl ErrorKind {
/// Default retryable status for each error kind
fn default_retryable(&self) -> bool {
matches!(self, ErrorKind::RateLimited)
}
}
// Convenience constructors
impl Error {
/// Create a credential invalid error
pub fn credential_invalid(message: impl Into<String>) -> Self {
Self::new(ErrorKind::CredentialInvalid, message)
}
/// Create a permission denied error
pub fn permission_denied(message: impl Into<String>) -> Self {
Self::new(ErrorKind::PermissionDenied, message)
}
/// Create a config invalid error
pub fn config_invalid(message: impl Into<String>) -> Self {
Self::new(ErrorKind::ConfigInvalid, message)
}
/// Create a request invalid error
pub fn request_invalid(message: impl Into<String>) -> Self {
Self::new(ErrorKind::RequestInvalid, message)
}
/// Create a rate limited error
pub fn rate_limited(message: impl Into<String>) -> Self {
Self::new(ErrorKind::RateLimited, message)
}
/// Create an unexpected error
pub fn unexpected(message: impl Into<String>) -> Self {
Self::new(ErrorKind::Unexpected, message)
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
// Custom Debug implementation for better error display
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut debug = f.debug_struct("Error");
debug.field("kind", &self.kind);
debug.field("message", &self.message);
if !self.context.is_empty() {
debug.field("context", &self.context);
}
if let Some(source) = &self.source {
debug.field("source", source);
}
debug.field("retryable", &self.retryable);
debug.finish()
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source.as_ref().map(|e| e.as_ref())
}
}
impl fmt::Display for ErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ErrorKind::CredentialInvalid => write!(f, "invalid credentials"),
ErrorKind::PermissionDenied => write!(f, "permission denied"),
ErrorKind::ConfigInvalid => write!(f, "invalid configuration"),
ErrorKind::RequestInvalid => write!(f, "invalid request"),
ErrorKind::RateLimited => write!(f, "rate limited"),
ErrorKind::Unexpected => write!(f, "unexpected error"),
}
}
}
/// Convenience type alias for Results
pub type Result<T> = std::result::Result<T, Error>;
// Common From implementations for better ergonomics
impl From<anyhow::Error> for Error {
fn from(err: anyhow::Error) -> Self {
Self::unexpected(err.to_string()).with_source(err)
}
}
impl From<fmt::Error> for Error {
fn from(err: fmt::Error) -> Self {
Self::unexpected(err.to_string()).with_source(err)
}
}
impl From<http::Error> for Error {
fn from(err: http::Error) -> Self {
Self::request_invalid(err.to_string()).with_source(err)
}
}
impl From<http::header::InvalidHeaderValue> for Error {
fn from(err: http::header::InvalidHeaderValue) -> Self {
Self::request_invalid(err.to_string()).with_source(err)
}
}
impl From<http::uri::InvalidUri> for Error {
fn from(err: http::uri::InvalidUri) -> Self {
Self::request_invalid(err.to_string()).with_source(err)
}
}
impl From<http::uri::InvalidUriParts> for Error {
fn from(err: http::uri::InvalidUriParts) -> Self {
Self::request_invalid(err.to_string()).with_source(err)
}
}
impl From<std::string::FromUtf8Error> for Error {
fn from(err: std::string::FromUtf8Error) -> Self {
Self::unexpected(err.to_string()).with_source(err)
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
use std::io::ErrorKind;
let kind = err.kind();
let message = err.to_string();
let source = anyhow::Error::from(err);
match kind {
ErrorKind::NotFound => Self::config_invalid(message).with_source(source),
ErrorKind::PermissionDenied => Self::permission_denied(message).with_source(source),
_ => Self::unexpected(message)
.with_source(source)
.set_retryable(matches!(
kind,
ErrorKind::TimedOut | ErrorKind::Interrupted | ErrorKind::ConnectionRefused
)),
}
}
}
impl From<http::header::InvalidHeaderName> for Error {
fn from(err: http::header::InvalidHeaderName) -> Self {
Self::request_invalid(err.to_string()).with_source(err)
}
}
impl From<http::header::ToStrError> for Error {
fn from(err: http::header::ToStrError) -> Self {
Self::request_invalid(err.to_string()).with_source(err)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_creation() {
let err = Error::credential_invalid("token expired");
assert_eq!(err.kind(), ErrorKind::CredentialInvalid);
assert!(!err.is_retryable());
}
#[test]
fn test_error_with_context() {
let err = Error::permission_denied("access denied")
.with_context("role: arn:aws:iam::123456789012:role/MyRole")
.with_context("operation: AssumeRole");
assert_eq!(err.context().len(), 2);
assert_eq!(
err.context()[0],
"role: arn:aws:iam::123456789012:role/MyRole"
);
assert_eq!(err.context()[1], "operation: AssumeRole");
}
#[test]
fn test_rate_limited_default_retryable() {
let err = Error::rate_limited("too many requests");
assert!(err.is_retryable());
}
#[test]
fn test_override_retryable() {
let err = Error::unexpected("network timeout").set_retryable(true);
assert!(err.is_retryable());
let err = Error::rate_limited("quota exceeded").set_retryable(false);
assert!(!err.is_retryable());
}
#[test]
fn test_error_debug_format() {
let err = Error::config_invalid("missing region")
.with_context("file: ~/.aws/config")
.with_context("profile: default");
let debug_str = format!("{:?}", err);
assert!(debug_str.contains("ConfigInvalid"));
assert!(debug_str.contains("missing region"));
assert!(debug_str.contains("~/.aws/config"));
}
}
+42
View File
@@ -0,0 +1,42 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/// BoxedFuture is the type alias of [`futures::future::BoxFuture`].
#[cfg(not(target_arch = "wasm32"))]
pub type BoxedFuture<'a, T> = futures::future::BoxFuture<'a, T>;
/// BoxedFuture is the type alias of [`futures::future::LocalBoxFuture`].
#[cfg(target_arch = "wasm32")]
pub type BoxedFuture<'a, T> = futures::future::LocalBoxFuture<'a, T>;
/// MaybeSend is a marker to determine whether a type is `Send` or not.
///
/// [`MaybeSend`] is equivalent to `Send` on non-wasm32 target.
#[cfg(not(target_arch = "wasm32"))]
pub trait MaybeSend: Send {}
/// MaybeSend is a marker to determine whether a type is `Send` or not.
///
/// [`MaybeSend`] is empty on wasm32 target.
#[cfg(target_arch = "wasm32")]
pub trait MaybeSend {}
#[cfg(not(target_arch = "wasm32"))]
impl<T: Send> MaybeSend for T {}
#[cfg(target_arch = "wasm32")]
impl<T> MaybeSend for T {}
+107
View File
@@ -0,0 +1,107 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! Hash related utils.
use crate::Error;
use base64::Engine;
use base64::prelude::BASE64_STANDARD;
use hmac::Hmac;
use hmac::KeyInit;
use hmac::Mac;
use sha1::Sha1;
use sha2::Digest;
use sha2::Sha256;
/// Base64 encode
pub fn base64_encode(content: &[u8]) -> String {
BASE64_STANDARD.encode(content)
}
/// Base64 decode
pub fn base64_decode(content: &str) -> crate::Result<Vec<u8>> {
BASE64_STANDARD
.decode(content)
.map_err(|e| Error::unexpected("base64 decode failed").with_source(e))
}
/// Hex encoded SHA1 hash.
///
/// Use this function instead of `hex::encode(sha1(content))` can reduce
/// extra copy.
pub fn hex_sha1(content: &[u8]) -> String {
hex::encode(Sha1::digest(content))
}
/// Hex encoded SHA256 hash.
///
/// Use this function instead of `hex::encode(sha256(content))` can reduce
/// extra copy.
pub fn hex_sha256(content: &[u8]) -> String {
hex::encode(Sha256::digest(content))
}
/// HMAC with SHA256 hash.
pub fn hmac_sha256(key: &[u8], content: &[u8]) -> Vec<u8> {
// SAFETY: HMAC's new_from_slice always returns Ok - it handles any key length
let mut h = Hmac::<Sha256>::new_from_slice(key).unwrap();
h.update(content);
h.finalize().into_bytes().to_vec()
}
/// Base64 encoded HMAC with SHA256 hash.
pub fn base64_hmac_sha256(key: &[u8], content: &[u8]) -> String {
// SAFETY: HMAC's new_from_slice always returns Ok - it handles any key length
let mut h = Hmac::<Sha256>::new_from_slice(key).unwrap();
h.update(content);
base64_encode(&h.finalize().into_bytes())
}
/// Hex encoded HMAC with SHA1 hash.
///
/// Use this function instead of `hex::encode(hmac_sha1(key, content))` can
/// reduce extra copy.
pub fn hex_hmac_sha1(key: &[u8], content: &[u8]) -> String {
// SAFETY: HMAC's new_from_slice always returns Ok - it handles any key length
let mut h = Hmac::<Sha1>::new_from_slice(key).unwrap();
h.update(content);
hex::encode(h.finalize().into_bytes())
}
/// Hex encoded HMAC with SHA256 hash.
///
/// Use this function instead of `hex::encode(hmac_sha256(key, content))` can
/// reduce extra copy.
pub fn hex_hmac_sha256(key: &[u8], content: &[u8]) -> String {
// SAFETY: HMAC's new_from_slice always returns Ok - it handles any key length
let mut h = Hmac::<Sha256>::new_from_slice(key).unwrap();
h.update(content);
hex::encode(h.finalize().into_bytes())
}
/// Base64 encoded HMAC with SHA1 hash.
pub fn base64_hmac_sha1(key: &[u8], content: &[u8]) -> String {
// SAFETY: HMAC's new_from_slice always returns Ok - it handles any key length
let mut h = Hmac::<Sha1>::new_from_slice(key).unwrap();
h.update(content);
base64_encode(&h.finalize().into_bytes())
}
+153
View File
@@ -0,0 +1,153 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! JWT encoding helpers.
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use rsa::RsaPrivateKey;
use rsa::pkcs1v15::SigningKey;
use rsa::pkcs8::DecodePrivateKey;
use rsa::rand_core::OsRng;
use rsa::sha2::Sha256;
use rsa::signature::{RandomizedSigner, SignatureEncoding};
use serde::Serialize;
use crate::{Error, Result};
/// Encode a JWS compact JWT using the RS256 algorithm.
///
/// RS256 is RSA PKCS#1 v1.5 with SHA-256. The caller owns the JWT header and
/// claims shape so service-specific fields such as `x5t` stay service-local.
pub fn encode_rs256<H, C>(header: &H, claims: &C, private_key: &RsaPrivateKey) -> Result<String>
where
H: Serialize,
C: Serialize,
{
let encoded_header = encode_json(header)?;
let encoded_claims = encode_json(claims)?;
let signing_input = format!("{encoded_header}.{encoded_claims}");
let mut rng = OsRng;
let signing_key = SigningKey::<Sha256>::new(private_key.clone());
let signature = signing_key.sign_with_rng(&mut rng, signing_input.as_bytes());
let encoded_signature = URL_SAFE_NO_PAD.encode(signature.to_bytes());
Ok(format!("{signing_input}.{encoded_signature}"))
}
/// Encode a JWS compact JWT using an RSA private key in PKCS#8 PEM format.
pub fn encode_rs256_pem<H, C>(header: &H, claims: &C, private_key_pem: &[u8]) -> Result<String>
where
H: Serialize,
C: Serialize,
{
let private_key_pem = std::str::from_utf8(private_key_pem).map_err(|e| {
Error::credential_invalid("RSA private key PEM is not valid UTF-8").with_source(e)
})?;
let private_key = RsaPrivateKey::from_pkcs8_pem(private_key_pem).map_err(|e| {
Error::credential_invalid("failed to parse PKCS#8 RSA private key PEM").with_source(e)
})?;
encode_rs256(header, claims, &private_key)
}
fn encode_json<T>(value: &T) -> Result<String>
where
T: Serialize,
{
let json = serde_json::to_vec(value)
.map_err(|e| Error::unexpected("failed to serialize JWT JSON").with_source(e))?;
Ok(URL_SAFE_NO_PAD.encode(json))
}
#[cfg(test)]
mod tests {
use super::*;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use rsa::pkcs1v15::{Signature, VerifyingKey};
use rsa::rand_core::OsRng;
use rsa::signature::Verifier;
use serde_json::json;
#[derive(Serialize)]
struct Header<'a> {
alg: &'a str,
typ: &'a str,
x5t: &'a str,
}
#[derive(Serialize)]
struct Claims<'a> {
iss: &'a str,
sub: &'a str,
aud: &'a str,
exp: u64,
}
#[test]
fn encode_rs256_keeps_jws_shape_and_verifiable_signature() -> Result<()> {
let mut rng = OsRng;
let private_key = RsaPrivateKey::new(&mut rng, 2048)
.map_err(|e| Error::unexpected("failed to generate test RSA key").with_source(e))?;
let jwt = encode_rs256(
&Header {
alg: "RS256",
typ: "JWT",
x5t: "thumbprint",
},
&Claims {
iss: "client",
sub: "client",
aud: "https://example.com/token",
exp: 1,
},
&private_key,
)?;
let parts = jwt.split('.').collect::<Vec<_>>();
assert_eq!(parts.len(), 3);
let header: serde_json::Value = serde_json::from_slice(
&URL_SAFE_NO_PAD
.decode(parts[0])
.map_err(|e| Error::unexpected("failed to decode JWT header").with_source(e))?,
)
.map_err(|e| Error::unexpected("failed to parse JWT header").with_source(e))?;
assert_eq!(
header,
json!({
"alg": "RS256",
"typ": "JWT",
"x5t": "thumbprint"
})
);
let signature = URL_SAFE_NO_PAD
.decode(parts[2])
.map_err(|e| Error::unexpected("failed to decode JWT signature").with_source(e))?;
let signature = Signature::try_from(signature.as_slice())
.map_err(|e| Error::unexpected("failed to parse JWT signature").with_source(e))?;
let verifying_key = VerifyingKey::<Sha256>::new(private_key.to_public_key());
verifying_key
.verify(format!("{}.{}", parts[0], parts[1]).as_bytes(), &signature)
.map_err(|e| Error::unexpected("failed to verify JWT signature").with_source(e))?;
Ok(())
}
}
+220
View File
@@ -0,0 +1,220 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! Core components for signing API requests.
//!
//! This crate provides the foundational types and traits for the reqsign ecosystem.
//! It defines the core abstractions that enable flexible and extensible request signing.
//!
//! ## Overview
//!
//! The crate is built around several key concepts:
//!
//! - **Context**: A container that holds implementations for file reading, HTTP sending, and environment access
//! - **Traits**: Abstract interfaces for credential loading (`ProvideCredential`) and request signing (`SignRequest`)
//! - **Signer**: The main orchestrator that coordinates credential loading and request signing
//!
//! ## Request URI contract
//!
//! Built-in request signers expect the request URI to be a valid, wire-ready URI
//! with an authority. Callers must construct the intended path and query structure
//! and percent-encode data components exactly once before signing. Structural URI
//! delimiters remain literal, while delimiter bytes that belong to data must already
//! be encoded, such as `%2F` for a slash inside one path segment.
//!
//! Existing path and query representations are authoritative. Canonicalization is a
//! service-specific, read-only view: header authentication preserves the URI, while
//! query authentication appends protocol-encoded authentication fields without
//! decoding, sorting, or rebuilding the existing URI.
//!
//! [`Signer::sign`] runs the service signer against a private candidate request head.
//! On error, the caller's method, URI, version, headers, and extensions remain
//! unchanged. On success, only the URI and headers are committed; the caller retains
//! ownership of the method, version, and extensions.
//!
//! `expires_in` is a service-specific validity input, not a universal selector between
//! header and query authentication. The service and credential type determine the
//! authentication mode.
//!
//! [`SigningCredential::is_valid`] controls whether a cached credential can be reused
//! without refresh. [`SigningCredential::is_valid_at`] checks exact usability at the
//! timestamp returned by [`SignRequest::required_valid_until`]. A refreshed credential
//! only needs to satisfy the exact operation requirement; provider errors are returned
//! without retrying internally or falling back to the old cached credential.
//!
//! ## Example
//!
//! ```no_run
//! use reqsign_core::{Context, OsEnv, ProvideCredential, Result, SignRequest, Signer, SigningCredential};
//! use http::request::Parts;
//! use std::time::Duration;
//!
//! // Define your credential type
//! #[derive(Clone, Debug)]
//! struct MyCredential {
//! key: String,
//! secret: String,
//! }
//!
//! impl SigningCredential for MyCredential {
//! fn is_valid(&self) -> bool {
//! !self.key.is_empty() && !self.secret.is_empty()
//! }
//! }
//!
//! // Implement credential loader
//! #[derive(Debug)]
//! struct MyLoader;
//!
//! impl ProvideCredential for MyLoader {
//! type Credential = MyCredential;
//!
//! async fn provide_credential(&self, _: &Context) -> Result<Option<Self::Credential>> {
//! Ok(Some(MyCredential {
//! key: "my-access-key".to_string(),
//! secret: "my-secret-key".to_string(),
//! }))
//! }
//! }
//!
//! // Implement request builder
//! #[derive(Debug)]
//! struct MyBuilder;
//!
//! impl SignRequest for MyBuilder {
//! type Credential = MyCredential;
//!
//! async fn sign_request(
//! &self,
//! _ctx: &Context,
//! req: &mut Parts,
//! _cred: Option<&Self::Credential>,
//! _expires_in: Option<Duration>,
//! ) -> Result<()> {
//! // Add example header
//! req.headers.insert("x-custom-auth", "signed".parse()?);
//! Ok(())
//! }
//! }
//!
//! # async fn example() -> Result<()> {
//! # use reqsign_core::{FileRead, HttpSend};
//! # use bytes::Bytes;
//! #
//! # // Mock implementations for the example
//! # #[derive(Debug, Clone)]
//! # struct MockFileRead;
//! # impl FileRead for MockFileRead {
//! # async fn file_read(&self, _path: &str) -> Result<Vec<u8>> {
//! # Ok(vec![])
//! # }
//! # }
//! #
//! # #[derive(Debug, Clone)]
//! # struct MockHttpSend;
//! # impl HttpSend for MockHttpSend {
//! # async fn http_send(&self, _req: http::Request<Bytes>) -> Result<http::Response<Bytes>> {
//! # Ok(http::Response::builder().status(200).body(Bytes::new())?)
//! # }
//! # }
//! #
//! // Create a context with your implementations
//! let ctx = Context::new()
//! .with_file_read(MockFileRead)
//! .with_http_send(MockHttpSend)
//! .with_env(OsEnv);
//!
//! // Create a signer
//! let signer = Signer::new(ctx, MyLoader, MyBuilder);
//!
//! // Sign your requests
//! let mut parts = http::Request::builder()
//! .method("GET")
//! .uri("https://example.com")
//! .body(())
//! .unwrap()
//! .into_parts()
//! .0;
//!
//! signer.sign(&mut parts, None).await?;
//! # Ok(())
//! # }
//! ```
//!
//! ## Traits
//!
//! This crate defines several important traits:
//!
//! - [`FileRead`]: For asynchronous file reading
//! - [`HttpSend`]: For sending HTTP requests
//! - [`Env`]: For environment variable access
//! - [`ProvideCredential`]: For loading credentials from various sources
//! - [`SignRequest`]: For building service-specific signing requests
//! - [`SigningCredential`]: For validating credentials
//!
//! ## Utilities
//!
//! The crate also provides utility modules:
//!
//! - [`hash`]: Cryptographic hashing utilities
//! - [`time`]: Time manipulation utilities
//! - [`utils`]: General utilities including data redaction
// Make sure all our public APIs have docs.
#![warn(missing_docs)]
/// Error types for reqsign operations
pub mod error;
mod futures_util;
pub mod hash;
#[cfg(all(not(target_arch = "wasm32"), feature = "jwt"))]
pub mod jwt;
pub mod time;
pub mod utils;
pub use error::{Error, ErrorKind, Result};
pub use futures_util::BoxedFuture;
pub use futures_util::MaybeSend;
mod context;
pub use context::CommandExecute;
pub use context::CommandExecuteDyn;
pub use context::CommandOutput;
pub use context::Context;
pub use context::Env;
pub use context::FileRead;
pub use context::FileReadDyn;
pub use context::HttpSend;
pub use context::HttpSendDyn;
pub use context::NoopCommandExecute;
pub use context::NoopEnv;
pub use context::NoopFileRead;
pub use context::NoopHttpSend;
pub use context::OsEnv;
pub use context::StaticEnv;
mod api;
pub use api::ProvideCredential;
pub use api::ProvideCredentialChain;
pub use api::ProvideCredentialDyn;
pub use api::SignRequest;
pub use api::SignRequestDyn;
pub use api::SigningCredential;
mod request;
pub use request::{SigningMethod, SigningRequest};
mod signer;
pub use signer::Signer;
+495
View File
@@ -0,0 +1,495 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use std::borrow::Cow;
use std::time::Duration;
use crate::{Error, Result};
use http::HeaderMap;
use http::HeaderValue;
use http::Method;
use http::header::HeaderName;
use http::uri::Authority;
use http::uri::PathAndQuery;
use http::uri::Scheme;
fn parse_query(query: &str) -> Vec<(String, String)> {
query
.split('&')
.filter(|pair| !pair.is_empty())
.map(|pair| {
let (key, value) = pair.split_once('=').unwrap_or((pair, ""));
(
percent_encoding::percent_decode_str(key)
.decode_utf8_lossy()
.into_owned(),
percent_encoding::percent_decode_str(value)
.decode_utf8_lossy()
.into_owned(),
)
})
.collect()
}
/// A service-local canonicalization view and signed-header staging area.
///
/// The method and URI-derived fields are read-only working values. They do not own
/// the wire URI and must not be mutated to express signing output. The URI supplied
/// to [`Self::build`] is already the final caller-provided representation; services
/// derive canonical values locally and construct query authentication from the
/// original URI.
#[derive(Debug)]
pub struct SigningRequest {
/// Read-only HTTP method used for canonicalization.
pub method: Method,
/// Read-only HTTP scheme used for canonicalization.
pub scheme: Scheme,
/// Read-only HTTP authority used for canonicalization.
pub authority: Authority,
/// Read-only, percent-encoded wire path.
///
/// Services may derive a canonical path from this value, but must not decode it
/// and write the result back to the request URI.
pub path: String,
/// HTTP query parameters decoded once from the wire query for canonicalization.
///
/// Percent escapes are decoded once, literal `+` remains `+`, and duplicate order
/// is retained. This working view does not own or rebuild the wire URI.
pub query: Vec<(String, String)>,
/// Staged HTTP headers committed by [`Self::apply`].
pub headers: HeaderMap,
}
impl SigningRequest {
/// Build a read-only request-target working view from http::request::Parts.
///
/// The URI path and query must already be percent-encoded for transport, and the
/// URI must contain an authority. This method clones the URI-derived values and
/// headers; the input request head remains unchanged on success or error.
pub fn build(parts: &mut http::request::Parts) -> Result<Self> {
let uri = parts.uri.clone().into_parts();
let paq = uri
.path_and_query
.unwrap_or_else(|| PathAndQuery::from_static("/"));
Ok(SigningRequest {
method: parts.method.clone(),
scheme: uri.scheme.unwrap_or(Scheme::HTTP),
authority: uri.authority.ok_or_else(|| {
Error::request_invalid("request without authority is invalid for signing")
})?,
path: paq.path().to_string(),
query: paq.query().map(parse_query).unwrap_or_default(),
headers: parts.headers.clone(),
})
}
/// Commit staged headers back to http::request::Parts.
///
/// In debug builds, this method verifies that the method, scheme, authority, path,
/// and decoded query working view still match `parts`. A mismatch returns an error
/// without changing `parts`. Release builds omit this implementation check.
///
/// On success, only headers are committed. This method never writes the method or
/// URI. Query signers must construct their final URI from the original wire URI and
/// assign it separately after all fallible signing work succeeds.
pub fn apply(self, parts: &mut http::request::Parts) -> Result<()> {
#[cfg(debug_assertions)]
self.validate_request_view(parts)?;
parts.headers = self.headers;
Ok(())
}
#[cfg(debug_assertions)]
fn validate_request_view(&self, parts: &http::request::Parts) -> Result<()> {
let uri = parts.uri.clone().into_parts();
let paq = uri
.path_and_query
.unwrap_or_else(|| PathAndQuery::from_static("/"));
let scheme = uri.scheme.unwrap_or(Scheme::HTTP);
let authority = uri.authority.ok_or_else(|| {
Error::request_invalid("request without authority is invalid for signing")
})?;
let query = paq.query().map(parse_query).unwrap_or_default();
if self.method != parts.method
|| self.scheme != scheme
|| self.authority != authority
|| self.path != paq.path()
|| self.query != query
{
return Err(Error::request_invalid(
"signing request method or URI working view was modified",
));
}
Ok(())
}
/// Return the entire working path percent-decoded.
///
/// This is a canonicalization helper, not a wire URI builder. Decoding the entire
/// path turns encoded slashes such as `%2F` into `/`; services where encoded slash
/// is data must decode path segments separately.
pub fn path_percent_decoded(&self) -> Cow<'_, str> {
percent_encoding::percent_decode_str(&self.path).decode_utf8_lossy()
}
/// Return the combined key and value length of the decoded working query.
///
/// This is not the byte length of the wire query.
#[inline]
pub fn query_size(&self) -> usize {
self.query
.iter()
.map(|(k, v)| k.len() + v.len())
.sum::<usize>()
}
/// Push a new query pair into the working query view.
///
/// This does not modify the wire URI. [`Self::apply`] rejects a modified
/// request-target view in debug builds and ignores it in release builds. Query
/// signers must construct their final URI from the original wire URI instead.
#[inline]
pub fn query_push(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.query.push((key.into(), value.into()));
}
/// Push a query string into the working query view.
///
/// This does not modify the wire URI; see [`Self::query_push`].
#[inline]
pub fn query_append(&mut self, query: &str) {
self.query.push((query.to_string(), "".to_string()));
}
/// Clone working query pairs whose keys match a canonicalization filter.
pub fn query_to_vec_with_filter(&self, filter: impl Fn(&str) -> bool) -> Vec<(String, String)> {
self.query
.iter()
// Filter all queries
.filter(|(k, _)| filter(k))
// Clone all queries
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
/// Convert sorted query pairs to a canonical string.
///
/// This helper does not produce or modify a wire URI.
///
/// ```shell
/// [(a, b), (c, d)] => "a:b\nc:d"
/// ```
pub fn query_to_string(mut query: Vec<(String, String)>, sep: &str, join: &str) -> String {
let mut s = String::with_capacity(16);
// Sort via header name.
query.sort();
for (idx, (k, v)) in query.into_iter().enumerate() {
if idx != 0 {
s.push_str(join);
}
s.push_str(&k);
if !v.is_empty() {
s.push_str(sep);
s.push_str(&v);
}
}
s
}
/// Convert sorted query pairs to a string after percent-decoding each value.
///
/// Values from [`Self::query`] have already been decoded once. Passing them to
/// this helper performs an additional decode and is only correct when a service
/// protocol explicitly requires it. This helper does not produce a wire URI.
///
/// ```shell
/// [(a, b), (c, d)] => "a:b\nc:d"
/// ```
pub fn query_to_percent_decoded_string(
mut query: Vec<(String, String)>,
sep: &str,
join: &str,
) -> String {
let mut s = String::with_capacity(16);
// Sort via header name.
query.sort();
for (idx, (k, v)) in query.into_iter().enumerate() {
if idx != 0 {
s.push_str(join);
}
s.push_str(&k);
if !v.is_empty() {
s.push_str(sep);
s.push_str(&percent_encoding::percent_decode_str(&v).decode_utf8_lossy());
}
}
s
}
/// Get header value by name.
///
/// Returns empty string if header not found.
#[inline]
pub fn header_get_or_default(&self, key: &HeaderName) -> Result<&str> {
match self.headers.get(key) {
Some(v) => v
.to_str()
.map_err(|e| Error::request_invalid("invalid header value").with_source(e)),
None => Ok(""),
}
}
/// Normalize a header value for canonicalization.
///
/// Normalize a clone when the protocol does not require changing the wire header;
/// mutating a value inside [`Self::headers`] changes the header committed by
/// [`Self::apply`].
pub fn header_value_normalize(v: &mut HeaderValue) {
let bs = v.as_bytes();
let starting_index = bs.iter().position(|b| *b != b' ').unwrap_or(0);
let ending_offset = bs.iter().rev().position(|b| *b != b' ').unwrap_or(0);
let ending_index = bs.len() - ending_offset;
// This can't fail because we started with a valid HeaderValue and then only trimmed spaces
*v = HeaderValue::from_bytes(&bs[starting_index..ending_index])
.expect("invalid header value")
}
/// Get header names as sorted vector.
pub fn header_name_to_vec_sorted(&self) -> Vec<&str> {
let mut h = self
.headers
.keys()
.map(|k| k.as_str())
.collect::<Vec<&str>>();
h.sort_unstable();
h
}
/// Get header names with given prefix.
pub fn header_to_vec_with_prefix(&self, prefix: &str) -> Vec<(String, String)> {
self.headers
.iter()
// Filter all header that starts with prefix
.filter(|(k, _)| k.as_str().starts_with(prefix))
// Convert all header name to lowercase
.map(|(k, v)| {
(
k.as_str().to_lowercase(),
v.to_str().expect("must be valid header").to_string(),
)
})
.collect()
}
/// Convert sorted headers to string.
///
/// ```shell
/// [(a, b), (c, d)] => "a:b\nc:d"
/// ```
pub fn header_to_string(mut headers: Vec<(String, String)>, sep: &str, join: &str) -> String {
let mut s = String::with_capacity(16);
// Sort via header name.
headers.sort();
for (idx, (k, v)) in headers.into_iter().enumerate() {
if idx != 0 {
s.push_str(join);
}
s.push_str(&k);
s.push_str(sep);
s.push_str(&v);
}
s
}
}
/// A service-selected authentication placement.
///
/// This type does not define a universal mapping from `expires_in` to query
/// authentication. Services and credential types decide which placement applies.
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum SigningMethod {
/// Signing with header.
Header,
/// Signing with query.
Query(Duration),
}
#[cfg(test)]
mod tests {
use super::*;
use http::{HeaderValue, Request};
const RAW_QUERY: &str = "slash=%2F&hash=%23&amp=%26&equals=%3D&space=%20&encoded-plus=%2B&literal-plus=+&double=%252F&dup=first&dup=second&=empty-key&empty=&flag&flag=&";
fn request_parts() -> http::request::Parts {
Request::get(format!("https://example.com/object%2Fname?{RAW_QUERY}"))
.header("x-original", " value ")
.body(())
.expect("request must build")
.into_parts()
.0
}
#[test]
fn build_is_read_only_and_parses_wire_query_once() {
let mut parts = request_parts();
let original = parts.clone();
let signing = SigningRequest::build(&mut parts).expect("signing request must build");
assert_eq!(parts.method, original.method);
assert_eq!(parts.uri, original.uri);
assert_eq!(parts.version, original.version);
assert_eq!(parts.headers, original.headers);
assert_eq!(signing.path, "/object%2Fname");
assert_eq!(
signing.query,
vec![
("slash".to_string(), "/".to_string()),
("hash".to_string(), "#".to_string()),
("amp".to_string(), "&".to_string()),
("equals".to_string(), "=".to_string()),
("space".to_string(), " ".to_string()),
("encoded-plus".to_string(), "+".to_string()),
("literal-plus".to_string(), "+".to_string()),
("double".to_string(), "%2F".to_string()),
("dup".to_string(), "first".to_string()),
("dup".to_string(), "second".to_string()),
(String::new(), "empty-key".to_string()),
("empty".to_string(), String::new()),
("flag".to_string(), String::new()),
("flag".to_string(), String::new()),
]
);
}
#[test]
fn build_error_leaves_request_unchanged() {
let mut parts = Request::get("/relative")
.header("x-original", "value")
.body(())
.expect("request must build")
.into_parts()
.0;
let original = parts.clone();
assert!(SigningRequest::build(&mut parts).is_err());
assert_eq!(parts.method, original.method);
assert_eq!(parts.uri, original.uri);
assert_eq!(parts.version, original.version);
assert_eq!(parts.headers, original.headers);
}
#[test]
fn apply_commits_only_headers() {
let mut parts = request_parts();
let original = parts.clone();
let mut signing =
SigningRequest::build(&mut parts).expect("signing request must build successfully");
signing
.headers
.insert("authorization", HeaderValue::from_static("signed"));
signing.apply(&mut parts).expect("apply must succeed");
assert_eq!(parts.method, original.method);
assert_eq!(parts.uri, original.uri);
assert_eq!(parts.version, original.version);
assert_eq!(
parts.headers.get("authorization"),
Some(&HeaderValue::from_static("signed"))
);
}
#[cfg(debug_assertions)]
#[test]
fn apply_rejects_modified_request_view_atomically() {
type ViewMutation = Box<dyn Fn(&mut SigningRequest)>;
let mutations: Vec<ViewMutation> = vec![
Box::new(|signing| signing.method = Method::POST),
Box::new(|signing| signing.scheme = Scheme::HTTP),
Box::new(|signing| signing.authority = "other.example.com".parse().unwrap()),
Box::new(|signing| signing.path.push_str("/changed")),
Box::new(|signing| signing.query_push("auth", "value")),
];
for mutate in mutations {
let mut parts = request_parts();
let original = parts.clone();
let mut signing =
SigningRequest::build(&mut parts).expect("signing request must build successfully");
signing
.headers
.insert("authorization", HeaderValue::from_static("signed"));
mutate(&mut signing);
assert!(signing.apply(&mut parts).is_err());
assert_eq!(parts.method, original.method);
assert_eq!(parts.uri, original.uri);
assert_eq!(parts.version, original.version);
assert_eq!(parts.headers, original.headers);
}
}
#[cfg(not(debug_assertions))]
#[test]
fn apply_omits_request_view_validation_in_release() {
let mut parts = request_parts();
let original = parts.clone();
let mut signing =
SigningRequest::build(&mut parts).expect("signing request must build successfully");
signing.method = Method::POST;
signing.scheme = Scheme::HTTP;
signing.authority = "other.example.com".parse().unwrap();
signing.path.push_str("/changed");
signing.query_push("auth", "value");
signing
.headers
.insert("authorization", HeaderValue::from_static("signed"));
signing.apply(&mut parts).expect("apply must succeed");
assert_eq!(parts.method, original.method);
assert_eq!(parts.uri, original.uri);
assert_eq!(parts.version, original.version);
assert_eq!(
parts.headers.get("authorization"),
Some(&HeaderValue::from_static("signed"))
);
}
}
+500
View File
@@ -0,0 +1,500 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use crate::Context;
use crate::Error;
use crate::ProvideCredential;
use crate::ProvideCredentialDyn;
use crate::Result;
use crate::SignRequest;
use crate::SignRequestDyn;
use crate::SigningCredential;
use std::any::type_name;
use std::sync::{Arc, Mutex};
use std::time::Duration;
/// Loads credentials and atomically signs request heads.
///
/// The service-specific [`SignRequest`] runs against a private candidate. Only the
/// candidate URI and headers are committed after successful signing.
#[derive(Clone, Debug)]
pub struct Signer<K: SigningCredential> {
ctx: Context,
loader: Arc<dyn ProvideCredentialDyn<Credential = K>>,
builder: Arc<dyn SignRequestDyn<Credential = K>>,
credential: Arc<Mutex<Option<K>>>,
}
impl<K: SigningCredential> Signer<K> {
/// Create a new signer.
pub fn new(
ctx: Context,
loader: impl ProvideCredential<Credential = K>,
builder: impl SignRequest<Credential = K>,
) -> Self {
Self {
ctx,
loader: Arc::new(loader),
builder: Arc::new(builder),
credential: Arc::new(Mutex::new(None)),
}
}
/// Replace the context while keeping credential provider and request signer.
pub fn with_context(mut self, ctx: Context) -> Self {
self.ctx = ctx;
self
}
/// Replace the credential provider while keeping context and request signer.
pub fn with_credential_provider(
mut self,
provider: impl ProvideCredential<Credential = K>,
) -> Self {
self.loader = Arc::new(provider);
self.credential = Arc::new(Mutex::new(None)); // Clear cached credential
self
}
/// Replace the request signer while keeping context and credential provider.
pub fn with_request_signer(mut self, signer: impl SignRequest<Credential = K>) -> Self {
self.builder = Arc::new(signer);
self
}
/// Sign a wire-ready request head.
///
/// The request URI must satisfy the input contract of the configured
/// [`SignRequest`]. Built-in signers require an authority and expect path and query
/// data to be percent-encoded exactly once before this call. Signing does not
/// perform general-purpose URI encoding for the caller.
///
/// If credential loading or request signing returns an error, `req` is unchanged.
/// On success, only `req.uri` and `req.headers` may change; the method, version, and
/// extensions retain their input values.
///
/// `expires_in` is a service-specific validity input and does not universally
/// select query authentication. The configured service signer and credential type
/// determine how it is interpreted.
///
/// Cached credentials must be fresh according to [`SigningCredential::is_valid`]
/// and usable through [`SignRequest::required_valid_until`]. A refreshed credential
/// only needs to satisfy the exact operation deadline. Provider errors are returned
/// without internal retry or fallback to the previous cached credential.
pub async fn sign(
&self,
req: &mut http::request::Parts,
expires_in: Option<Duration>,
) -> Result<()> {
let credential = self.credential.lock().expect("lock poisoned").clone();
let credential = match credential {
Some(credential)
if credential.is_valid()
&& credential.is_valid_at(
self.builder
.required_valid_until_dyn(&credential, expires_in),
) =>
{
credential
}
_ => {
let credential = self
.loader
.provide_credential_dyn(&self.ctx)
.await?
.ok_or_else(|| {
Error::credential_invalid("failed to load signing credential")
.with_context(format!("credential_type: {}", type_name::<K>()))
})?;
*self.credential.lock().expect("lock poisoned") = Some(credential.clone());
let required_until = self
.builder
.required_valid_until_dyn(&credential, expires_in);
if !credential.is_valid_at(required_until) {
return Err(Error::credential_invalid(
"refreshed signing credential expires before the requested operation deadline",
)
.with_context(format!("credential_type: {}", type_name::<K>()))
.with_context(format!("required_valid_until: {required_until}")));
}
credential
}
};
let mut candidate = req.clone();
self.builder
.sign_request_dyn(&self.ctx, &mut candidate, Some(&credential), expires_in)
.await?;
req.uri = candidate.uri;
req.headers = candidate.headers;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::time::Timestamp;
use crate::{ErrorKind, ProvideCredential, SignRequest};
use http::{HeaderValue, Method, Request, Version};
use std::collections::VecDeque;
use std::sync::atomic::{AtomicUsize, Ordering};
#[derive(Clone, Debug)]
struct TestCredential;
impl SigningCredential for TestCredential {
fn is_valid(&self) -> bool {
true
}
}
#[derive(Debug)]
struct StaticProvider;
impl ProvideCredential for StaticProvider {
type Credential = TestCredential;
async fn provide_credential(&self, _ctx: &Context) -> Result<Option<Self::Credential>> {
Ok(Some(TestCredential))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct Extension(&'static str);
#[derive(Debug)]
struct MutatingSigner {
fail: bool,
}
impl SignRequest for MutatingSigner {
type Credential = TestCredential;
async fn sign_request(
&self,
_ctx: &Context,
req: &mut http::request::Parts,
_credential: Option<&Self::Credential>,
_expires_in: Option<Duration>,
) -> Result<()> {
req.method = Method::POST;
req.uri = "https://signed.example.com/result?auth=1"
.parse()
.expect("URI must parse");
req.version = Version::HTTP_2;
req.headers.clear();
req.headers
.insert("authorization", HeaderValue::from_static("signed"));
req.extensions.insert(Extension("candidate"));
if self.fail {
Err(Error::unexpected("injected signing failure"))
} else {
Ok(())
}
}
}
#[derive(Clone, Debug)]
struct ExpiringCredential {
generation: u8,
fresh: bool,
expires_at: Timestamp,
required_until: Timestamp,
}
impl SigningCredential for ExpiringCredential {
fn is_valid(&self) -> bool {
self.fresh
}
fn is_valid_at(&self, timestamp: Timestamp) -> bool {
self.expires_at > timestamp
}
}
#[derive(Debug)]
struct SequenceProvider {
responses: Mutex<VecDeque<Result<Option<ExpiringCredential>>>>,
calls: Arc<AtomicUsize>,
}
impl SequenceProvider {
fn new(
responses: impl IntoIterator<Item = Result<Option<ExpiringCredential>>>,
) -> (Self, Arc<AtomicUsize>) {
let calls = Arc::new(AtomicUsize::new(0));
(
Self {
responses: Mutex::new(responses.into_iter().collect()),
calls: calls.clone(),
},
calls,
)
}
}
impl ProvideCredential for SequenceProvider {
type Credential = ExpiringCredential;
async fn provide_credential(&self, _ctx: &Context) -> Result<Option<Self::Credential>> {
self.calls.fetch_add(1, Ordering::SeqCst);
self.responses
.lock()
.expect("lock poisoned")
.pop_front()
.unwrap_or(Ok(None))
}
}
#[derive(Debug)]
struct OperationSigner;
impl SignRequest for OperationSigner {
type Credential = ExpiringCredential;
fn required_valid_until(
&self,
credential: &Self::Credential,
_expires_in: Option<Duration>,
) -> Timestamp {
credential.required_until
}
async fn sign_request(
&self,
_ctx: &Context,
req: &mut http::request::Parts,
credential: Option<&Self::Credential>,
expires_in: Option<Duration>,
) -> Result<()> {
let credential = credential.expect("credential must be present");
if !credential.is_valid_at(self.required_valid_until(credential, expires_in)) {
return Err(Error::credential_invalid(
"credential is not valid for operation",
));
}
req.headers.insert(
"x-credential-generation",
credential.generation.to_string().parse()?,
);
Ok(())
}
}
fn request_parts() -> http::request::Parts {
let mut parts = Request::get("https://example.com/original?x=%2F")
.version(Version::HTTP_11)
.header("x-original", "value")
.body(())
.expect("request must build")
.into_parts()
.0;
parts.extensions.insert(Extension("caller"));
parts
}
#[test]
fn failure_leaves_entire_request_head_unchanged() {
let signer = Signer::new(
Context::new(),
StaticProvider,
MutatingSigner { fail: true },
);
let mut parts = request_parts();
let original = parts.clone();
let result = futures::executor::block_on(signer.sign(&mut parts, None));
assert!(result.is_err());
assert_eq!(parts.method, original.method);
assert_eq!(parts.uri, original.uri);
assert_eq!(parts.version, original.version);
assert_eq!(parts.headers, original.headers);
assert_eq!(
parts.extensions.get::<Extension>(),
original.extensions.get::<Extension>()
);
}
#[test]
fn success_commits_only_uri_and_headers() {
let signer = Signer::new(
Context::new(),
StaticProvider,
MutatingSigner { fail: false },
);
let mut parts = request_parts();
let original = parts.clone();
futures::executor::block_on(signer.sign(&mut parts, None)).expect("signing must succeed");
assert_eq!(parts.method, original.method);
assert_eq!(parts.version, original.version);
assert_eq!(
parts.extensions.get::<Extension>(),
original.extensions.get::<Extension>()
);
assert_eq!(
parts.uri,
"https://signed.example.com/result?auth=1"
.parse::<http::Uri>()
.expect("URI must parse")
);
assert_eq!(
parts.headers.get("authorization"),
Some(&HeaderValue::from_static("signed"))
);
assert!(!parts.headers.contains_key("x-original"));
}
#[test]
fn refreshes_cached_credential_for_operation_requirement() {
let base = Timestamp::from_second(1_000).expect("timestamp must be valid");
let cached = ExpiringCredential {
generation: 1,
fresh: true,
expires_at: base + Duration::from_secs(20),
required_until: base + Duration::from_secs(30),
};
let refreshed = ExpiringCredential {
generation: 2,
fresh: true,
expires_at: base + Duration::from_secs(20),
required_until: base + Duration::from_secs(10),
};
let (provider, calls) = SequenceProvider::new([Ok(Some(refreshed))]);
let signer = Signer::new(Context::new(), provider, OperationSigner);
*signer.credential.lock().expect("lock poisoned") = Some(cached);
let mut parts = request_parts();
futures::executor::block_on(signer.sign(&mut parts, None))
.expect("refreshed credential must satisfy the recomputed requirement");
assert_eq!(calls.load(Ordering::SeqCst), 1);
assert_eq!(
parts.headers.get("x-credential-generation"),
Some(&HeaderValue::from_static("2"))
);
}
#[test]
fn uses_refreshed_credential_that_is_usable_but_not_fresh() {
let base = Timestamp::from_second(2_000).expect("timestamp must be valid");
let credential = ExpiringCredential {
generation: 1,
fresh: false,
expires_at: base + Duration::from_secs(30),
required_until: base + Duration::from_secs(10),
};
let (provider, calls) =
SequenceProvider::new([Ok(Some(credential.clone())), Ok(Some(credential))]);
let signer = Signer::new(Context::new(), provider, OperationSigner);
for _ in 0..2 {
let mut parts = request_parts();
futures::executor::block_on(signer.sign(&mut parts, None))
.expect("usable refreshed credential must be accepted");
}
assert_eq!(calls.load(Ordering::SeqCst), 2);
}
#[test]
fn refresh_error_does_not_fall_back_and_caller_can_retry() {
let base = Timestamp::from_second(3_000).expect("timestamp must be valid");
let cached = ExpiringCredential {
generation: 1,
fresh: false,
expires_at: base + Duration::from_secs(30),
required_until: base + Duration::from_secs(10),
};
let refreshed = ExpiringCredential {
generation: 2,
fresh: true,
expires_at: base + Duration::from_secs(30),
required_until: base + Duration::from_secs(10),
};
let (provider, calls) = SequenceProvider::new([
Err(Error::unexpected("injected refresh failure")),
Ok(Some(refreshed)),
]);
let signer = Signer::new(Context::new(), provider, OperationSigner);
*signer.credential.lock().expect("lock poisoned") = Some(cached);
let mut parts = request_parts();
let original = parts.clone();
let err = futures::executor::block_on(signer.sign(&mut parts, None))
.expect_err("refresh error must be returned");
assert_eq!(err.kind(), ErrorKind::Unexpected);
assert_eq!(parts.uri, original.uri);
assert_eq!(parts.headers, original.headers);
assert_eq!(calls.load(Ordering::SeqCst), 1);
futures::executor::block_on(signer.sign(&mut parts, None))
.expect("caller retry must attempt refresh again");
assert_eq!(calls.load(Ordering::SeqCst), 2);
assert_eq!(
parts.headers.get("x-credential-generation"),
Some(&HeaderValue::from_static("2"))
);
}
#[test]
fn missing_refresh_does_not_fall_back_and_caller_can_retry() {
let base = Timestamp::from_second(4_000).expect("timestamp must be valid");
let cached = ExpiringCredential {
generation: 1,
fresh: false,
expires_at: base + Duration::from_secs(30),
required_until: base + Duration::from_secs(10),
};
let refreshed = ExpiringCredential {
generation: 2,
fresh: true,
expires_at: base + Duration::from_secs(30),
required_until: base + Duration::from_secs(10),
};
let (provider, calls) = SequenceProvider::new([Ok(None), Ok(Some(refreshed))]);
let signer = Signer::new(Context::new(), provider, OperationSigner);
*signer.credential.lock().expect("lock poisoned") = Some(cached);
let mut parts = request_parts();
let original = parts.clone();
let err = futures::executor::block_on(signer.sign(&mut parts, None))
.expect_err("missing credential must fail");
assert_eq!(err.kind(), ErrorKind::CredentialInvalid);
assert_eq!(calls.load(Ordering::SeqCst), 1);
assert_eq!(parts.uri, original.uri);
assert_eq!(parts.headers, original.headers);
futures::executor::block_on(signer.sign(&mut parts, None))
.expect("caller retry must attempt refresh again");
assert_eq!(calls.load(Ordering::SeqCst), 2);
assert_eq!(
parts.headers.get("x-credential-generation"),
Some(&HeaderValue::from_static("2"))
);
}
}
+257
View File
@@ -0,0 +1,257 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! Time related utils.
use crate::Error;
use std::fmt;
use std::ops::{Add, AddAssign, Sub, SubAssign};
use std::str::FromStr;
use std::time::{Duration, SystemTime};
/// An instant in time represented as the number of nanoseconds since the Unix epoch.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Timestamp(jiff::Timestamp);
impl FromStr for Timestamp {
type Err = Error;
/// Parse a timestamp by the default [`DateTimeParser`].
///
/// All of them are valid time:
///
/// - `2022-03-13T07:20:04Z`
/// - `2022-03-01T08:12:34+00:00`
/// - `2022-03-01T08:12:34.00+00:00`
/// - `2022-07-08T02:14:07+02:00[Europe/Paris]`
///
/// [`DateTimeParser`]: jiff::fmt::temporal::DateTimeParser
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.parse() {
Ok(t) => Ok(Timestamp(t)),
Err(err) => Err(
Error::unexpected(format!("parse '{s}' into timestamp failed")).with_source(err),
),
}
}
}
impl fmt::Display for Timestamp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Timestamp {
/// Create the timestamp of now.
pub fn now() -> Self {
Self(jiff::Timestamp::now())
}
/// Format the timestamp into date: `20220301`
pub fn format_date(self) -> String {
self.0.strftime("%Y%m%d").to_string()
}
/// Format the timestamp into ISO8601: `20220313T072004Z`
pub fn format_iso8601(self) -> String {
self.0.strftime("%Y%m%dT%H%M%SZ").to_string()
}
/// Format the timestamp into http date: `Sun, 06 Nov 1994 08:49:37 GMT`
///
/// ## Note
///
/// HTTP date is slightly different from RFC2822.
///
/// - Timezone is fixed to GMT.
/// - Day must be 2 digit.
pub fn format_http_date(self) -> String {
self.0.strftime("%a, %d %b %Y %T GMT").to_string()
}
/// Format the timestamp into RFC3339 in Zulu: `2022-03-13T07:20:04Z`
pub fn format_rfc3339_zulu(self) -> String {
self.0.strftime("%FT%TZ").to_string()
}
/// Returns this timestamp as a number of seconds since the Unix epoch.
///
/// This only returns the number of whole seconds. That is, if there are
/// any fractional seconds in this timestamp, then they are truncated.
pub fn as_second(self) -> i64 {
self.0.as_second()
}
/// Returns the fractional second component of this timestamp in units of
/// nanoseconds.
///
/// It is guaranteed that this will never return a value that is greater
/// than 1 second (or less than -1 second).
pub fn subsec_nanosecond(self) -> i32 {
self.0.subsec_nanosecond()
}
/// Convert to `SystemTime`.
pub fn as_system_time(self) -> SystemTime {
SystemTime::from(self.0)
}
/// Creates a new instant in time from the number of milliseconds elapsed
/// since the Unix epoch.
///
/// When `millisecond` is negative, it corresponds to an instant in time
/// before the Unix epoch. A smaller number corresponds to an instant in
/// time further into the past.
pub fn from_millisecond(millis: i64) -> crate::Result<Self> {
match jiff::Timestamp::from_millisecond(millis) {
Ok(t) => Ok(Timestamp(t)),
Err(err) => Err(Error::unexpected(format!(
"convert '{millis}' milliseconds into timestamp failed"
))
.with_source(err)),
}
}
/// Creates a new instant in time from the number of seconds elapsed since
/// the Unix epoch.
///
/// When `second` is negative, it corresponds to an instant in time before
/// the Unix epoch. A smaller number corresponds to an instant in time
/// further into the past.
pub fn from_second(second: i64) -> crate::Result<Self> {
match jiff::Timestamp::from_second(second) {
Ok(t) => Ok(Timestamp(t)),
Err(err) => Err(Error::unexpected(format!(
"convert '{second}' seconds into timestamp failed"
))
.with_source(err)),
}
}
/// Parse a timestamp from RFC2822.
///
/// All of them are valid time:
///
/// - `Sat, 13 Jul 2024 15:09:59 -0400`
/// - `Mon, 15 Aug 2022 16:50:12 GMT`
pub fn parse_rfc2822(s: &str) -> crate::Result<Timestamp> {
match jiff::fmt::rfc2822::parse(s) {
Ok(zoned) => Ok(Timestamp(zoned.timestamp())),
Err(err) => {
Err(Error::unexpected(format!("parse '{s}' into rfc2822 failed")).with_source(err))
}
}
}
/// Parse the string format "2023-10-31 21:59:10.000000".
pub fn parse_datetime_utc(s: &str) -> crate::Result<Timestamp> {
let dt = s.parse::<jiff::civil::DateTime>().map_err(|err| {
Error::unexpected(format!("parse '{s}' into datetime failed")).with_source(err)
})?;
let ts = jiff::tz::TimeZone::UTC.to_timestamp(dt).map_err(|err| {
Error::unexpected(format!("convert '{s}' into timestamp failed")).with_source(err)
})?;
Ok(Timestamp(ts))
}
}
impl Add<Duration> for Timestamp {
type Output = Timestamp;
fn add(self, rhs: Duration) -> Timestamp {
let ts = self
.0
.checked_add(rhs)
.expect("adding unsigned duration to timestamp overflowed");
Timestamp(ts)
}
}
impl AddAssign<Duration> for Timestamp {
fn add_assign(&mut self, rhs: Duration) {
*self = *self + rhs
}
}
impl Sub<Duration> for Timestamp {
type Output = Timestamp;
fn sub(self, rhs: Duration) -> Timestamp {
let ts = self
.0
.checked_sub(rhs)
.expect("subtracting unsigned duration from timestamp overflowed");
Timestamp(ts)
}
}
impl SubAssign<Duration> for Timestamp {
fn sub_assign(&mut self, rhs: Duration) {
*self = *self - rhs
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_time() -> Timestamp {
Timestamp("2022-03-01T08:12:34Z".parse().unwrap())
}
#[test]
fn test_format_date() {
let t = test_time();
assert_eq!("20220301", t.format_date())
}
#[test]
fn test_format_ios8601() {
let t = test_time();
assert_eq!("20220301T081234Z", t.format_iso8601())
}
#[test]
fn test_format_http_date() {
let t = test_time();
assert_eq!("Tue, 01 Mar 2022 08:12:34 GMT", t.format_http_date())
}
#[test]
fn test_format_rfc3339() {
let t = test_time();
assert_eq!("2022-03-01T08:12:34Z", t.format_rfc3339_zulu())
}
#[test]
fn test_parse_rfc3339() {
let t = test_time();
for v in [
"2022-03-01T08:12:34Z",
"2022-03-01T08:12:34+00:00",
"2022-03-01T08:12:34.00+00:00",
] {
assert_eq!(t, v.parse().expect("must be valid time"));
}
}
}
+90
View File
@@ -0,0 +1,90 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! Utility functions and types.
use std::fmt::Debug;
/// Redacts a string by replacing all but the first and last three characters with asterisks.
///
/// - If the input string has fewer than 12 characters, it should be entirely redacted.
/// - If the input string has 12 or more characters, only the first three and the last three.
///
/// This design is to allow users to distinguish between different redacted strings but avoid
/// leaking sensitive information.
pub struct Redact<'a>(&'a str);
impl<'a> From<&'a str> for Redact<'a> {
fn from(value: &'a str) -> Self {
Redact(value)
}
}
impl<'a> From<&'a String> for Redact<'a> {
fn from(value: &'a String) -> Self {
Redact(value.as_str())
}
}
impl<'a> From<&'a Option<String>> for Redact<'a> {
fn from(value: &'a Option<String>) -> Self {
match value {
None => Redact(""),
Some(v) => Redact(v),
}
}
}
impl Debug for Redact<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let length = self.0.len();
if length == 0 {
f.write_str("EMPTY")
} else if length < 12 {
f.write_str("***")
} else {
f.write_str(&self.0[..3])?;
f.write_str("***")?;
f.write_str(&self.0[length - 3..])
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_redact() {
let cases = vec![
("Short", "***"),
("Hello World!", "Hel***ld!"),
("This is a longer string", "Thi***ing"),
("", "EMPTY"),
("HelloWorld", "***"),
];
for (input, expected) in cases {
assert_eq!(
format!("{:?}", Redact(input)),
expected,
"Failed on input: {}",
input
);
}
}
}