Compare commits

..

No commits in common. "main" and "1.36.0" have entirely different histories.

11 changed files with 458 additions and 535 deletions

482
Cargo.lock generated
View file

@ -8,6 +8,17 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[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 = "ahash"
version = "0.8.12"
@ -659,6 +670,17 @@ dependencies = [
"tracing",
]
[[package]]
name = "backon"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef"
dependencies = [
"fastrand",
"gloo-timers",
"tokio",
]
[[package]]
name = "base16ct"
version = "0.2.0"
@ -756,6 +778,15 @@ 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 = "blocking"
version = "1.6.2"
@ -861,6 +892,15 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0"
[[package]]
name = "cbc"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
dependencies = [
"cipher",
]
[[package]]
name = "cc"
version = "1.2.61"
@ -879,6 +919,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chacha20"
version = "0.10.0"
@ -914,6 +960,16 @@ dependencies = [
"phf 0.12.1",
]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common 0.1.6",
"inout",
]
[[package]]
name = "cmov"
version = "0.5.3"
@ -2301,6 +2357,15 @@ dependencies = [
"digest 0.11.2",
]
[[package]]
name = "home"
version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "hostname"
version = "0.4.2"
@ -2451,9 +2516,11 @@ dependencies = [
"hyper 1.9.0",
"hyper-util",
"rustls 0.23.40",
"rustls-native-certs",
"tokio",
"tokio-rustls 0.26.4",
"tower-service",
"webpki-roots",
]
[[package]]
@ -2649,6 +2716,16 @@ version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
[[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 = "ipconfig"
version = "0.3.4"
@ -2721,12 +2798,10 @@ checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d"
dependencies = [
"jiff-static",
"jiff-tzdb-platform",
"js-sys",
"log",
"portable-atomic",
"portable-atomic-util",
"serde_core",
"wasm-bindgen",
"windows-sys 0.61.2",
]
@ -2838,6 +2913,21 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "jsonwebtoken"
version = "9.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
dependencies = [
"base64 0.22.1",
"js-sys",
"pem",
"ring",
"serde",
"serde_json",
"simple_asn1",
]
[[package]]
name = "jsonwebtoken"
version = "10.3.0"
@ -3008,6 +3098,12 @@ dependencies = [
"tracing-subscriber",
]
[[package]]
name = "lru-slab"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "macros"
version = "0.1.0"
@ -3035,15 +3131,6 @@ dependencies = [
"digest 0.10.7",
]
[[package]]
name = "mea"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6747f54621d156e1b47eb6b25f39a941b9fc347f98f67d25d8881ff99e8ed832"
dependencies = [
"slab",
]
[[package]]
name = "memchr"
version = "2.8.0"
@ -3321,6 +3408,7 @@ dependencies = [
"getrandom 0.2.17",
"http 1.4.0",
"rand 0.8.6",
"reqwest",
"serde",
"serde_json",
"serde_path_to_error",
@ -3350,76 +3438,31 @@ dependencies = [
[[package]]
name = "opendal"
version = "0.56.0"
version = "0.55.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97b31d3d8e99a85d83b73ec26647f5607b80578ed9375810b6e44ffa3590a236"
dependencies = [
"opendal-core",
"opendal-service-fs",
"opendal-service-s3",
]
[[package]]
name = "opendal-core"
version = "0.56.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1849dd2687e173e776d3af5fce1ba3ae47b9dd37a09d1c4deba850ef45fe00ca"
checksum = "d075ab8a203a6ab4bc1bce0a4b9fe486a72bf8b939037f4b78d95386384bc80a"
dependencies = [
"anyhow",
"backon",
"base64 0.22.1",
"bytes",
"crc32c",
"futures",
"getrandom 0.2.17",
"http 1.4.0",
"http-body 1.0.1",
"jiff",
"log",
"md-5",
"mea",
"percent-encoding",
"quick-xml 0.38.4",
"reqsign-core",
"reqsign",
"reqwest",
"serde",
"serde_json",
"tokio",
"url",
"uuid",
"web-time",
]
[[package]]
name = "opendal-service-fs"
version = "0.56.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf0be0417abeeb0053376d816b90fceb9ca98f20dfb54ebf1f2a282729f83663"
dependencies = [
"bytes",
"log",
"opendal-core",
"serde",
"tokio",
"xattr",
]
[[package]]
name = "opendal-service-s3"
version = "0.56.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dadddeb9bb50b0d30927dd914c298c4ddca47e4c1cfa7674d311f0cf9b051c8"
dependencies = [
"base64 0.22.1",
"bytes",
"crc32c",
"http 1.4.0",
"log",
"md-5",
"opendal-core",
"quick-xml 0.38.4",
"reqsign-aws-v4",
"reqsign-core",
"reqsign-file-read-tokio",
"serde",
"url",
]
[[package]]
@ -3608,6 +3651,16 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5a797f0e07bdf071d15742978fc3128ec6c22891c31a3a931513263904c982a"
[[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 = "pear"
version = "0.2.9"
@ -3799,6 +3852,21 @@ dependencies = [
"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"
@ -3806,6 +3874,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"pkcs5",
"rand_core 0.6.4",
"spki",
]
@ -3968,6 +4038,16 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quick-xml"
version = "0.37.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "quick-xml"
version = "0.38.4"
@ -3979,13 +4059,58 @@ dependencies = [
]
[[package]]
name = "quick-xml"
version = "0.39.4"
name = "quinn"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e"
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
dependencies = [
"memchr",
"serde",
"bytes",
"cfg_aliases",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls 0.23.40",
"socket2 0.6.3",
"thiserror 2.0.18",
"tokio",
"tracing",
"web-time",
]
[[package]]
name = "quinn-proto"
version = "0.11.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
dependencies = [
"bytes",
"getrandom 0.3.4",
"lru-slab",
"rand 0.9.4",
"ring",
"rustc-hash",
"rustls 0.23.40",
"rustls-pki-types",
"slab",
"thiserror 2.0.18",
"tinyvec",
"tracing",
"web-time",
]
[[package]]
name = "quinn-udp"
version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2 0.6.3",
"tracing",
"windows-sys 0.60.2",
]
[[package]]
@ -4187,64 +4312,43 @@ dependencies = [
]
[[package]]
name = "reqsign-aws-v4"
version = "3.0.0"
name = "reqsign"
version = "0.16.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44eaca382e94505a49f1a4849658d153aebf79d9c1a58e5dd3b10361511e9f43"
checksum = "43451dbf3590a7590684c25fb8d12ecdcc90ed3ac123433e500447c7d77ed701"
dependencies = [
"anyhow",
"bytes",
"async-trait",
"base64 0.22.1",
"chrono",
"form_urlencoded",
"getrandom 0.2.17",
"hex",
"hmac 0.12.1",
"home",
"http 1.4.0",
"jsonwebtoken 9.3.1",
"log",
"once_cell",
"percent-encoding",
"quick-xml 0.39.4",
"reqsign-core",
"quick-xml 0.37.5",
"rand 0.8.6",
"reqwest",
"rsa",
"rust-ini",
"serde",
"serde_json",
"serde_urlencoded",
"sha1",
]
[[package]]
name = "reqsign-core"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b10302cf0a7d7e7352ba211fc92c3c5bebf1286153e49cc5aa87348078a8e102"
dependencies = [
"anyhow",
"base64 0.22.1",
"bytes",
"form_urlencoded",
"futures",
"hex",
"hmac 0.12.1",
"http 1.4.0",
"jiff",
"log",
"percent-encoding",
"sha1",
"sha2 0.10.9",
"windows-sys 0.61.2",
]
[[package]]
name = "reqsign-file-read-tokio"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2d89295b3d17abea31851cc8de55d843d89c52132c864963c38d41920613dc5"
dependencies = [
"anyhow",
"reqsign-core",
"tokio",
"toml 0.8.23",
]
[[package]]
name = "reqwest"
version = "0.13.3"
version = "0.12.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0"
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64 0.22.1",
"bytes",
@ -4266,9 +4370,10 @@ dependencies = [
"mime",
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls 0.23.40",
"rustls-native-certs",
"rustls-pki-types",
"rustls-platform-verifier",
"serde",
"serde_json",
"serde_urlencoded",
@ -4284,6 +4389,7 @@ dependencies = [
"wasm-bindgen-futures",
"wasm-streams",
"web-sys",
"webpki-roots",
]
[[package]]
@ -4454,6 +4560,7 @@ dependencies = [
"pkcs1",
"pkcs8",
"rand_core 0.6.4",
"sha2 0.10.9",
"signature",
"spki",
"subtle",
@ -4490,6 +4597,12 @@ dependencies = [
"ordered-multimap",
]
[[package]]
name = "rustc-hash"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
[[package]]
name = "rustc_version"
version = "0.4.1"
@ -4575,36 +4688,10 @@ version = "1.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
dependencies = [
"web-time",
"zeroize",
]
[[package]]
name = "rustls-platform-verifier"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
dependencies = [
"core-foundation 0.10.1",
"core-foundation-sys",
"jni",
"log",
"once_cell",
"rustls 0.23.40",
"rustls-native-certs",
"rustls-platform-verifier-android",
"rustls-webpki 0.103.13",
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls-platform-verifier-android"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.101.7"
@ -4638,6 +4725,15 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "salsa20"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
dependencies = [
"cipher",
]
[[package]]
name = "same-file"
version = "1.0.6"
@ -4701,6 +4797,17 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[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 = "sct"
version = "0.7.1"
@ -5762,6 +5869,7 @@ checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0"
name = "vaultwarden"
version = "1.0.0"
dependencies = [
"anyhow",
"argon2",
"aws-config",
"aws-credential-types",
@ -5791,7 +5899,7 @@ dependencies = [
"html5gum",
"http 1.4.0",
"job_scheduler_ng",
"jsonwebtoken",
"jsonwebtoken 10.3.0",
"lettre",
"libsqlite3-sys",
"log",
@ -5808,15 +5916,13 @@ dependencies = [
"pico-args",
"rand 0.10.1",
"regex",
"reqsign-aws-v4",
"reqsign-core",
"reqsign",
"reqwest",
"ring",
"rmpv",
"rocket",
"rocket_ws",
"rpassword",
"rustls 0.23.40",
"semver",
"serde",
"serde_json",
@ -5977,9 +6083,9 @@ dependencies = [
[[package]]
name = "wasm-streams"
version = "0.5.0"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb"
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
dependencies = [
"futures-util",
"js-sys",
@ -6089,10 +6195,10 @@ dependencies = [
]
[[package]]
name = "webpki-root-certs"
name = "webpki-roots"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c"
checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d"
dependencies = [
"rustls-pki-types",
]
@ -6240,6 +6346,15 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.5",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
@ -6273,13 +6388,30 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm 0.53.1",
"windows_aarch64_msvc 0.53.1",
"windows_i686_gnu 0.53.1",
"windows_i686_gnullvm 0.53.1",
"windows_i686_msvc 0.53.1",
"windows_x86_64_gnu 0.53.1",
"windows_x86_64_gnullvm 0.53.1",
"windows_x86_64_msvc 0.53.1",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
@ -6292,6 +6424,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
@ -6304,6 +6442,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
@ -6316,12 +6460,24 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
@ -6334,6 +6490,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
@ -6346,6 +6508,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
@ -6358,6 +6526,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
@ -6370,6 +6544,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winnow"
version = "0.6.26"
@ -6511,16 +6691,6 @@ dependencies = [
"time",
]
[[package]]
name = "xattr"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
dependencies = [
"libc",
"rustix",
]
[[package]]
name = "xml"
version = "1.2.1"
@ -6567,9 +6737,9 @@ dependencies = [
[[package]]
name = "yubico_ng"
version = "0.15.0"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "228e2862e3c66f3224102d9a00d9d3646b271a05cc6c4819fea195fa8b5c00e0"
checksum = "929981f5b46b8fb8ee54b144de6b55c3a94fbe26635ee25b0e126e184250867c"
dependencies = [
"base64 0.22.1",
"form_urlencoded",

View file

@ -39,7 +39,7 @@ vendored_openssl = ["openssl/vendored"]
# Enable MiMalloc memory allocator to replace the default malloc
# This can improve performance for Alpine builds
enable_mimalloc = ["dep:mimalloc"]
s3 = ["opendal/services-s3", "dep:aws-config", "dep:aws-credential-types", "dep:aws-smithy-runtime-api", "dep:http", "dep:reqsign-aws-v4", "dep:reqsign-core"]
s3 = ["opendal/services-s3", "dep:aws-config", "dep:aws-credential-types", "dep:aws-smithy-runtime-api", "dep:anyhow", "dep:http", "dep:reqsign"]
# OIDC specific features
oidc-accept-rfc3339-timestamps = ["openidconnect/accept-rfc3339-timestamps"]
@ -102,7 +102,6 @@ libsqlite3-sys = { version = "0.37.0", optional = true }
# Crypto-related libraries
rand = "0.10.1"
ring = "0.17.14"
rustls = { version = "0.23.40", features = ["ring", "std"], default-features = false }
subtle = "2.6.1"
# UUID generation
@ -126,7 +125,7 @@ jsonwebtoken = { version = "10.3.0", features = ["use_pem", "rust_crypto"], defa
totp-lite = "2.0.1"
# Yubico Library
yubico = { package = "yubico_ng", version = "0.15.0", features = ["online-tokio"], default-features = false }
yubico = { package = "yubico_ng", version = "0.14.1", features = ["online-tokio"], default-features = false }
# WebAuthn libraries
# danger-allow-state-serialisation is needed to save the state in the db
@ -147,7 +146,7 @@ email_address = "0.2.9"
handlebars = { version = "6.4.0", features = ["dir_source"] }
# HTTP client (Used for favicons, version check, DUO and HIBP API)
reqwest = { version = "0.13.3", features = ["rustls-no-provider", "stream", "json", "form", "deflate", "gzip", "brotli", "zstd", "socks", "cookies", "charset", "http2", "system-proxy"], default-features = false}
reqwest = { version = "0.12.28", features = ["rustls-tls", "rustls-tls-native-roots", "stream", "json", "deflate", "gzip", "brotli", "zstd", "socks", "cookies", "charset", "http2", "system-proxy"], default-features = false}
hickory-resolver = "0.26.1"
# Favicon extraction libraries
@ -175,7 +174,7 @@ pastey = "0.2.2"
governor = "0.10.4"
# OIDC for SSO
openidconnect = { version = "4.0.1", default-features = false }
openidconnect = { version = "4.0.1", features = ["reqwest", "rustls-tls"] }
moka = { version = "0.12.15", features = ["future"] }
# Check client versions for specific features.
@ -197,15 +196,15 @@ rpassword = "7.5.1"
grass_compiler = { version = "0.13.4", default-features = false }
# File are accessed through Apache OpenDAL
opendal = { version = "0.56.0", features = ["services-fs"], default-features = false }
opendal = { version = "0.55.0", features = ["services-fs"], default-features = false }
# For retrieving AWS credentials, including temporary SSO credentials
anyhow = { version = "1.0.102", optional = true }
aws-config = { version = "1.8.16", features = ["behavior-version-latest", "rt-tokio", "credentials-process", "sso"], default-features = false, optional = true }
aws-credential-types = { version = "1.2.14", optional = true }
aws-smithy-runtime-api = { version = "1.12.0", optional = true }
http = { version = "1.4.0", optional = true }
reqsign-aws-v4 = { version = "3.0.0", optional = true }
reqsign-core = { version = "3.0.0", optional = true }
reqsign = { version = "0.16.5", optional = true }
# Strip debuginfo from the release builds
# The debug symbols are to provide better panic traces

View file

@ -568,7 +568,7 @@ async fn post_access_file(
async fn download_url(host: &Host, send_id: &SendId, file_id: &SendFileId) -> Result<String, crate::Error> {
let operator = CONFIG.opendal_operator_for_path_type(&PathType::Sends)?;
if crate::storage::is_fs_operator(&operator) {
if operator.info().scheme() == <&'static str>::from(opendal::Scheme::Fs) {
let token_claims = crate::auth::generate_send_claims(send_id, file_id);
let token = crate::auth::encode_jwt(&token_claims);

View file

@ -1222,8 +1222,7 @@ async fn _oidcsignin_redirect(
(Some(expected), Some(actual)) if crypto::ct_eq(expected, actual) => {}
_ => err!(format!("SSO session binding mismatch for {state}")),
}
cookies
.remove(Cookie::build(SSO_BINDING_COOKIE).path(format!("{}/identity/connect/", CONFIG.domain_path())).build());
cookies.remove(Cookie::build(SSO_BINDING_COOKIE).path("/identity/connect/").build());
sso_auth.code_response = Some(code_response);
sso_auth.updated_at = Utc::now().naive_utc();
@ -1295,7 +1294,7 @@ async fn authorize(data: AuthorizeData, cookies: &CookieJar<'_>, secure: Secure,
cookies.add(
Cookie::build((SSO_BINDING_COOKIE, binding_token))
.path(format!("{}/identity/connect/", CONFIG.domain_path()))
.path("/identity/connect/")
.max_age(time::Duration::seconds(sso::SSO_AUTH_EXPIRATION.num_seconds()))
.same_site(SameSite::Lax) // Lax is needed because the IdP runs on a different FQDN
.http_only(true)

View file

@ -54,8 +54,12 @@ static PUBLIC_RSA_KEY: OnceLock<DecodingKey> = OnceLock::new();
pub async fn initialize_keys() -> Result<(), Error> {
use std::io::Error;
let rsa_key_filename = crate::storage::file_name(&CONFIG.private_rsa_key())
.ok_or_else(|| Error::other("Private RSA key path missing filename"))?;
let rsa_key_filename = std::path::PathBuf::from(CONFIG.private_rsa_key())
.file_name()
.ok_or_else(|| Error::other("Private RSA key path missing filename"))?
.to_str()
.ok_or_else(|| Error::other("Private RSA key path filename is not valid UTF-8"))?
.to_string();
let operator = CONFIG.opendal_operator_for_path_type(&PathType::RsaKey).map_err(Error::other)?;

View file

@ -14,7 +14,6 @@ use serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor};
use crate::{
error::Error,
storage,
util::{
get_active_web_release, get_env, get_env_bool, is_valid_email, parse_experimental_client_feature_flags,
FeatureFlagFilter,
@ -23,14 +22,18 @@ use crate::{
static CONFIG_FILE: LazyLock<String> = LazyLock::new(|| {
let data_folder = get_env("DATA_FOLDER").unwrap_or_else(|| String::from("data"));
get_env("CONFIG_FILE").unwrap_or_else(|| storage::join_path(&data_folder, "config.json"))
get_env("CONFIG_FILE").unwrap_or_else(|| format!("{data_folder}/config.json"))
});
static CONFIG_FILE_PARENT_DIR: LazyLock<String> =
LazyLock::new(|| storage::parent(&CONFIG_FILE).unwrap_or_else(|| "data".to_string()));
static CONFIG_FILE_PARENT_DIR: LazyLock<String> = LazyLock::new(|| {
let path = std::path::PathBuf::from(&*CONFIG_FILE);
path.parent().unwrap_or(std::path::Path::new("data")).to_str().unwrap_or("data").to_string()
});
static CONFIG_FILENAME: LazyLock<String> =
LazyLock::new(|| storage::file_name(&CONFIG_FILE).unwrap_or_else(|| "config.json".to_string()));
static CONFIG_FILENAME: LazyLock<String> = LazyLock::new(|| {
let path = std::path::PathBuf::from(&*CONFIG_FILE);
path.file_name().unwrap_or(std::ffi::OsStr::new("config.json")).to_str().unwrap_or("config.json").to_string()
});
pub static SKIP_CONFIG_VALIDATION: AtomicBool = AtomicBool::new(false);
@ -260,7 +263,7 @@ macro_rules! make_config {
}
async fn from_file() -> Result<Self, Error> {
let operator = storage::operator_for_path(&CONFIG_FILE_PARENT_DIR)?;
let operator = opendal_operator_for_path(&CONFIG_FILE_PARENT_DIR)?;
let config_bytes = operator.read(&CONFIG_FILENAME).await?;
println!("[INFO] Using saved config from `{}` for configuration.\n", *CONFIG_FILE);
serde_json::from_slice(&config_bytes.to_vec()).map_err(Into::into)
@ -504,19 +507,19 @@ make_config! {
/// Data folder |> Main data folder
data_folder: String, false, def, "data".to_string();
/// Database URL
database_url: String, false, auto, |c| storage::join_path(&c.data_folder, "db.sqlite3");
database_url: String, false, auto, |c| format!("{}/db.sqlite3", c.data_folder);
/// Icon cache folder
icon_cache_folder: String, false, auto, |c| storage::join_path(&c.data_folder, "icon_cache");
icon_cache_folder: String, false, auto, |c| format!("{}/icon_cache", c.data_folder);
/// Attachments folder
attachments_folder: String, false, auto, |c| storage::join_path(&c.data_folder, "attachments");
attachments_folder: String, false, auto, |c| format!("{}/attachments", c.data_folder);
/// Sends folder
sends_folder: String, false, auto, |c| storage::join_path(&c.data_folder, "sends");
sends_folder: String, false, auto, |c| format!("{}/sends", c.data_folder);
/// Temp folder |> Used for storing temporary file uploads
tmp_folder: String, false, auto, |c| storage::join_path(&c.data_folder, "tmp");
tmp_folder: String, false, auto, |c| format!("{}/tmp", c.data_folder);
/// Templates folder
templates_folder: String, false, auto, |c| storage::join_path(&c.data_folder, "templates");
templates_folder: String, false, auto, |c| format!("{}/templates", c.data_folder);
/// Session JWT key
rsa_key_filename: String, false, auto, |c| storage::join_path(&c.data_folder, "rsa_key");
rsa_key_filename: String, false, auto, |c| format!("{}/rsa_key", c.data_folder);
/// Web vault folder
web_vault_folder: String, false, def, "web-vault/".to_string();
},
@ -1363,6 +1366,90 @@ fn smtp_convert_deprecated_ssl_options(smtp_ssl: Option<bool>, smtp_explicit_tls
"starttls".to_string()
}
fn opendal_operator_for_path(path: &str) -> Result<opendal::Operator, Error> {
// Cache of previously built operators by path
static OPERATORS_BY_PATH: LazyLock<dashmap::DashMap<String, opendal::Operator>> =
LazyLock::new(dashmap::DashMap::new);
if let Some(operator) = OPERATORS_BY_PATH.get(path) {
return Ok(operator.clone());
}
let operator = if path.starts_with("s3://") {
#[cfg(not(s3))]
return Err(opendal::Error::new(opendal::ErrorKind::ConfigInvalid, "S3 support is not enabled").into());
#[cfg(s3)]
opendal_s3_operator_for_path(path)?
} else {
let builder = opendal::services::Fs::default().root(path);
opendal::Operator::new(builder)?.finish()
};
OPERATORS_BY_PATH.insert(path.to_string(), operator.clone());
Ok(operator)
}
#[cfg(s3)]
fn opendal_s3_operator_for_path(path: &str) -> Result<opendal::Operator, Error> {
use crate::http_client::aws::AwsReqwestConnector;
use aws_config::{default_provider::credentials::DefaultCredentialsChain, provider_config::ProviderConfig};
// This is a custom AWS credential loader that uses the official AWS Rust
// SDK config crate to load credentials. This ensures maximum compatibility
// with AWS credential configurations. For example, OpenDAL doesn't support
// AWS SSO temporary credentials yet.
struct OpenDALS3CredentialLoader {}
#[async_trait]
impl reqsign::AwsCredentialLoad for OpenDALS3CredentialLoader {
async fn load_credential(&self, _client: reqwest::Client) -> anyhow::Result<Option<reqsign::AwsCredential>> {
use aws_credential_types::provider::ProvideCredentials as _;
use tokio::sync::OnceCell;
static DEFAULT_CREDENTIAL_CHAIN: OnceCell<DefaultCredentialsChain> = OnceCell::const_new();
let chain = DEFAULT_CREDENTIAL_CHAIN
.get_or_init(|| {
let reqwest_client = reqwest::Client::builder().build().unwrap();
let connector = AwsReqwestConnector {
client: reqwest_client,
};
let conf = ProviderConfig::default().with_http_client(connector);
DefaultCredentialsChain::builder().configure(conf).build()
})
.await;
let creds = chain.provide_credentials().await?;
Ok(Some(reqsign::AwsCredential {
access_key_id: creds.access_key_id().to_string(),
secret_access_key: creds.secret_access_key().to_string(),
session_token: creds.session_token().map(|s| s.to_string()),
expires_in: creds.expiry().map(|expiration| expiration.into()),
}))
}
}
const OPEN_DAL_S3_CREDENTIAL_LOADER: OpenDALS3CredentialLoader = OpenDALS3CredentialLoader {};
let url = Url::parse(path).map_err(|e| format!("Invalid path S3 URL path {path:?}: {e}"))?;
let bucket = url.host_str().ok_or_else(|| format!("Missing Bucket name in data folder S3 URL {path:?}"))?;
let builder = opendal::services::S3::default()
.customized_credential_load(Box::new(OPEN_DAL_S3_CREDENTIAL_LOADER))
.enable_virtual_host_style()
.bucket(bucket)
.root(url.path())
.default_storage_class("INTELLIGENT_TIERING");
Ok(opendal::Operator::new(builder)?.finish())
}
pub enum PathType {
Data,
IconCache,
@ -1460,7 +1547,7 @@ impl Config {
}
//Save to file
let operator = storage::operator_for_path(&CONFIG_FILE_PARENT_DIR)?;
let operator = opendal_operator_for_path(&CONFIG_FILE_PARENT_DIR)?;
operator.write(&CONFIG_FILENAME, config_str).await?;
Ok(())
@ -1525,7 +1612,7 @@ impl Config {
}
pub async fn delete_user_config(&self) -> Result<(), Error> {
let operator = storage::operator_for_path(&CONFIG_FILE_PARENT_DIR)?;
let operator = opendal_operator_for_path(&CONFIG_FILE_PARENT_DIR)?;
operator.delete(&CONFIG_FILENAME).await?;
// Empty user config
@ -1549,7 +1636,7 @@ impl Config {
}
pub fn private_rsa_key(&self) -> String {
storage::with_extension(&self.rsa_key_filename(), "pem")
format!("{}.pem", self.rsa_key_filename())
}
pub fn mail_enabled(&self) -> bool {
let inner = &self.inner.read().unwrap().config;
@ -1590,11 +1677,15 @@ impl Config {
PathType::IconCache => self.icon_cache_folder(),
PathType::Attachments => self.attachments_folder(),
PathType::Sends => self.sends_folder(),
PathType::RsaKey => storage::parent(&self.private_rsa_key())
.ok_or_else(|| std::io::Error::other("Failed to get directory of RSA key file"))?,
PathType::RsaKey => std::path::Path::new(&self.rsa_key_filename())
.parent()
.ok_or_else(|| std::io::Error::other("Failed to get directory of RSA key file"))?
.to_str()
.ok_or_else(|| std::io::Error::other("Failed to convert RSA key file directory to UTF-8 string"))?
.to_string(),
};
storage::operator_for_path(&path)
opendal_operator_for_path(&path)
}
pub fn render_template<T: serde::ser::Serialize>(&self, name: &str, data: &T) -> Result<String, Error> {

View file

@ -46,7 +46,7 @@ impl Attachment {
pub async fn get_url(&self, host: &str) -> Result<String, crate::Error> {
let operator = CONFIG.opendal_operator_for_path_type(&PathType::Attachments)?;
if crate::storage::is_fs_operator(&operator) {
if operator.info().scheme() == <&'static str>::from(opendal::Scheme::Fs) {
let token = encode_jwt(&generate_file_download_claims(self.cipher_uuid.clone(), self.id.clone()));
Ok(format!("{host}/attachments/{}/{}?token={token}", self.cipher_uuid, self.id))
} else {

View file

@ -237,7 +237,7 @@ impl Send {
if self.atype == SendType::File as i32 {
let operator = CONFIG.opendal_operator_for_path_type(&PathType::Sends)?;
operator.delete_with(&self.uuid).recursive(true).await.ok();
operator.remove_all(&self.uuid).await.ok();
}
db_run! { conn: {

View file

@ -57,7 +57,6 @@ mod mail;
mod ratelimit;
mod sso;
mod sso_client;
mod storage;
mod util;
use crate::api::core::two_factor::duo_oidc::purge_duo_contexts;
@ -71,7 +70,6 @@ pub use util::is_running_in_container;
#[rocket::main]
async fn main() -> Result<(), Error> {
install_rustls_crypto_provider();
parse_args();
launch_info();
@ -204,14 +202,6 @@ fn parse_args() {
}
}
fn install_rustls_crypto_provider() {
if rustls::crypto::CryptoProvider::get_default().is_none() {
rustls::crypto::ring::default_provider()
.install_default()
.expect("failed to install rustls ring crypto provider");
}
}
fn launch_info() {
println!(
"\

View file

@ -1,13 +1,12 @@
use std::{borrow::Cow, future::Future, pin::Pin, sync::LazyLock, time::Duration};
use std::{borrow::Cow, sync::LazyLock, time::Duration};
use openidconnect::{core::*, *};
use openidconnect::{core::*, reqwest, *};
use regex::Regex;
use url::Url;
use crate::{
api::{ApiResult, EmptyResult},
db::models::SsoAuth,
http_client::get_reqwest_client_builder,
sso::{OIDCCode, OIDCCodeChallenge, OIDCCodeVerifier, OIDCState},
CONFIG,
};
@ -47,42 +46,10 @@ pub type RefreshTokenResponse = (Option<String>, String, Option<Duration>);
#[derive(Clone)]
pub struct Client {
pub http_client: OidcHttpClient,
pub http_client: reqwest::Client,
pub core_client: CustomClient,
}
#[derive(Clone)]
pub struct OidcHttpClient {
client: reqwest::Client,
}
impl OidcHttpClient {
fn new() -> Result<Self, reqwest::Error> {
get_reqwest_client_builder().redirect(reqwest::redirect::Policy::none()).build().map(|client| Self {
client,
})
}
}
impl<'c> AsyncHttpClient<'c> for OidcHttpClient {
type Error = HttpClientError<reqwest::Error>;
type Future = Pin<Box<dyn Future<Output = Result<HttpResponse, Self::Error>> + Send + Sync + 'c>>;
fn call(&'c self, request: HttpRequest) -> Self::Future {
Box::pin(async move {
let response = self.client.execute(request.try_into().map_err(Box::new)?).await.map_err(Box::new)?;
let mut builder = http::Response::builder().status(response.status()).version(response.version());
for (name, value) in response.headers() {
builder = builder.header(name, value);
}
builder.body(response.bytes().await.map_err(Box::new)?.to_vec()).map_err(HttpClientError::Http)
})
}
}
impl Client {
// Call the OpenId discovery endpoint to retrieve configuration
async fn _get_client() -> ApiResult<Self> {
@ -91,7 +58,7 @@ impl Client {
let issuer_url = CONFIG.sso_issuer_url()?;
let http_client = match OidcHttpClient::new() {
let http_client = match reqwest::ClientBuilder::new().redirect(reqwest::redirect::Policy::none()).build() {
Err(err) => err!(format!("Failed to build http client: {err}")),
Ok(client) => client,
};

View file

@ -1,297 +0,0 @@
use std::sync::LazyLock;
pub(crate) fn join_path(base: &str, child: &str) -> String {
#[cfg(s3)]
if s3::is_uri(base) {
return s3::join_path(base, child);
}
let base = base.trim_end_matches('/');
let child = child.trim_start_matches('/');
if base.is_empty() {
child.to_string()
} else if child.is_empty() {
base.to_string()
} else {
format!("{base}/{child}")
}
}
pub(crate) fn with_extension(path: &str, extension: &str) -> String {
let extension = extension.trim_start_matches('.');
#[cfg(s3)]
if s3::is_uri(path) {
return s3::with_extension(path, extension);
}
format!("{path}.{extension}")
}
pub(crate) fn parent(path: &str) -> Option<String> {
#[cfg(s3)]
if s3::is_uri(path) {
return s3::parent(path);
}
std::path::Path::new(path).parent()?.to_str().map(ToString::to_string)
}
pub(crate) fn file_name(path: &str) -> Option<String> {
#[cfg(s3)]
if s3::is_uri(path) {
return s3::file_name(path);
}
std::path::Path::new(path).file_name()?.to_str().map(ToString::to_string)
}
pub(crate) fn is_fs_operator(operator: &opendal::Operator) -> bool {
operator.info().scheme() == opendal::services::FS_SCHEME
}
pub(crate) fn operator_for_path(path: &str) -> Result<opendal::Operator, crate::Error> {
// Cache of previously built operators by path
static OPERATORS_BY_PATH: LazyLock<dashmap::DashMap<String, opendal::Operator>> =
LazyLock::new(dashmap::DashMap::new);
if let Some(operator) = OPERATORS_BY_PATH.get(path) {
return Ok(operator.clone());
}
let operator = if path.starts_with("s3://") {
#[cfg(not(s3))]
return Err(opendal::Error::new(opendal::ErrorKind::ConfigInvalid, "S3 support is not enabled").into());
#[cfg(s3)]
s3::operator_for_path(path)?
} else {
let builder = opendal::services::Fs::default().root(path);
opendal::Operator::new(builder)?.finish()
};
OPERATORS_BY_PATH.insert(path.to_string(), operator.clone());
Ok(operator)
}
#[cfg(s3)]
mod s3 {
use reqwest::Url;
use crate::error::Error;
pub(super) fn is_uri(path: &str) -> bool {
path.starts_with("s3://")
}
pub(super) fn join_path(base: &str, child: &str) -> String {
if let Ok(mut url) = Url::parse(base) {
let mut segments = path_segments(&url);
segments.extend(child.split('/').filter(|segment| !segment.is_empty()).map(ToString::to_string));
set_path_segments(&mut url, &segments);
return url.to_string();
}
let base = base.trim_end_matches('/');
let child = child.trim_start_matches('/');
if base.is_empty() {
child.to_string()
} else if child.is_empty() {
base.to_string()
} else {
format!("{base}/{child}")
}
}
pub(super) fn with_extension(path: &str, extension: &str) -> String {
if let Ok(mut url) = Url::parse(path) {
let mut segments = path_segments(&url);
if let Some(file_name) = segments.last_mut() {
file_name.push('.');
file_name.push_str(extension);
set_path_segments(&mut url, &segments);
return url.to_string();
}
}
format!("{path}.{extension}")
}
pub(super) fn parent(path: &str) -> Option<String> {
if let Ok(mut url) = Url::parse(path) {
let mut segments = path_segments(&url);
segments.pop()?;
set_path_segments(&mut url, &segments);
return Some(url.to_string());
}
std::path::Path::new(path).parent()?.to_str().map(ToString::to_string)
}
pub(super) fn file_name(path: &str) -> Option<String> {
if let Ok(url) = Url::parse(path) {
return path_segments(&url).pop();
}
std::path::Path::new(path).file_name()?.to_str().map(ToString::to_string)
}
fn path_segments(url: &Url) -> Vec<String> {
url.path_segments()
.map(|segments| segments.filter(|segment| !segment.is_empty()).map(ToString::to_string).collect())
.unwrap_or_default()
}
fn set_path_segments(url: &mut Url, segments: &[String]) {
if segments.is_empty() {
url.set_path("");
} else {
url.set_path(&format!("/{}", segments.join("/")));
}
}
pub(super) fn operator_for_path(path: &str) -> Result<opendal::Operator, Error> {
use crate::http_client::aws::AwsReqwestConnector;
use aws_config::{default_provider::credentials::DefaultCredentialsChain, provider_config::ProviderConfig};
use opendal::Configurator;
use reqsign_aws_v4::Credential;
use reqsign_core::{Context, ProvideCredential, ProvideCredentialChain};
// This is a custom AWS credential loader that uses the official AWS Rust
// SDK config crate to load credentials. This ensures maximum compatibility
// with AWS credential configurations. For example, OpenDAL doesn't support
// AWS SSO temporary credentials yet.
#[derive(Debug)]
struct OpenDALS3CredentialProvider;
impl ProvideCredential for OpenDALS3CredentialProvider {
type Credential = Credential;
async fn provide_credential(&self, _ctx: &Context) -> reqsign_core::Result<Option<Self::Credential>> {
use aws_credential_types::provider::ProvideCredentials as _;
use reqsign_core::time::Timestamp;
use tokio::sync::OnceCell;
static DEFAULT_CREDENTIAL_CHAIN: OnceCell<DefaultCredentialsChain> = OnceCell::const_new();
let chain = DEFAULT_CREDENTIAL_CHAIN
.get_or_init(|| {
let reqwest_client = reqwest::Client::builder().build().unwrap();
let connector = AwsReqwestConnector {
client: reqwest_client,
};
let conf = ProviderConfig::default().with_http_client(connector);
DefaultCredentialsChain::builder().configure(conf).build()
})
.await;
let creds = chain.provide_credentials().await.map_err(|e| {
reqsign_core::Error::unexpected("failed to load AWS credentials via AWS SDK").with_source(e)
})?;
let expires_in = if let Some(expiration) = creds.expiry() {
let duration = expiration.duration_since(std::time::UNIX_EPOCH).map_err(|e| {
reqsign_core::Error::unexpected("AWS credential expiration is before the Unix epoch")
.with_source(e)
})?;
let seconds = i64::try_from(duration.as_secs()).map_err(|e| {
reqsign_core::Error::unexpected("AWS credential expiration is too large").with_source(e)
})?;
Some(Timestamp::from_second(seconds)?)
} else {
None
};
Ok(Some(Credential {
access_key_id: creds.access_key_id().to_string(),
secret_access_key: creds.secret_access_key().to_string(),
session_token: creds.session_token().map(|s| s.to_string()),
expires_in,
}))
}
}
let uri = opendal::OperatorUri::new(path, std::iter::empty::<(String, String)>())?;
let mut config = opendal::services::S3Config::from_uri(&uri)?;
if !uri_has_option(&uri, &["default_storage_class"]) {
config.default_storage_class = Some("INTELLIGENT_TIERING".to_string());
}
if !uri_has_option(
&uri,
&["enable_virtual_host_style", "aws_virtual_hosted_style_request", "virtual_hosted_style_request"],
) {
config.enable_virtual_host_style = true;
}
let use_aws_sdk_credentials = !uri_has_credential_options(&uri, &config);
let mut builder = config.into_builder();
if use_aws_sdk_credentials {
builder =
builder.credential_provider_chain(ProvideCredentialChain::new().push(OpenDALS3CredentialProvider));
}
Ok(opendal::Operator::new(builder)?.finish())
}
fn uri_has_option(uri: &opendal::OperatorUri, names: &[&str]) -> bool {
names.iter().any(|name| uri.options().contains_key(*name))
}
fn uri_has_credential_options(uri: &opendal::OperatorUri, config: &opendal::services::S3Config) -> bool {
config.access_key_id.is_some()
|| config.secret_access_key.is_some()
|| config.session_token.is_some()
|| config.role_arn.is_some()
|| config.external_id.is_some()
|| config.role_session_name.is_some()
|| uri_has_option(uri, &["allow_anonymous", "disable_config_load", "disable_ec2_metadata"])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn handles_local_paths() {
assert_eq!(join_path("data", "attachments"), "data/attachments");
assert_eq!(with_extension("data/rsa_key", "pem"), "data/rsa_key.pem");
assert_eq!(parent("data/rsa_key.pem").as_deref(), Some("data"));
assert_eq!(file_name("data/rsa_key.pem").as_deref(), Some("rsa_key.pem"));
}
}
#[cfg(all(test, s3))]
mod s3_tests {
use super::*;
#[test]
fn joins_s3_path_before_query_string() {
assert_eq!(
join_path("s3://bucket/base?region=us-west-2", "attachments"),
"s3://bucket/base/attachments?region=us-west-2"
);
}
#[test]
fn appends_extension_before_s3_query_string() {
assert_eq!(
with_extension("s3://bucket/base/rsa_key?region=us-west-2", "pem"),
"s3://bucket/base/rsa_key.pem?region=us-west-2"
);
}
#[test]
fn splits_s3_parent_and_file_name_without_query_string() {
let path = "s3://bucket/base/config.json?region=us-west-2";
assert_eq!(parent(path).as_deref(), Some("s3://bucket/base?region=us-west-2"));
assert_eq!(file_name(path).as_deref(), Some("config.json"));
}
}