diff --git a/Cargo.lock b/Cargo.lock index 9a44e0b2..ac84b501 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 271d102d..e7fd5ade 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs index 45ead810..22abb396 100644 --- a/src/api/core/sends.rs +++ b/src/api/core/sends.rs @@ -568,7 +568,7 @@ async fn post_access_file( async fn download_url(host: &Host, send_id: &SendId, file_id: &SendFileId) -> Result { 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); diff --git a/src/api/identity.rs b/src/api/identity.rs index 9f64e560..569deaf9 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -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) diff --git a/src/auth.rs b/src/auth.rs index 06bd9c22..43184369 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -54,8 +54,12 @@ static PUBLIC_RSA_KEY: OnceLock = 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)?; diff --git a/src/config.rs b/src/config.rs index b6d0ce8a..ae995f69 100644 --- a/src/config.rs +++ b/src/config.rs @@ -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 = 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 = - LazyLock::new(|| storage::parent(&CONFIG_FILE).unwrap_or_else(|| "data".to_string())); +static CONFIG_FILE_PARENT_DIR: LazyLock = 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 = - LazyLock::new(|| storage::file_name(&CONFIG_FILE).unwrap_or_else(|| "config.json".to_string())); +static CONFIG_FILENAME: LazyLock = 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 { - 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, smtp_explicit_tls "starttls".to_string() } +fn opendal_operator_for_path(path: &str) -> Result { + // Cache of previously built operators by path + static OPERATORS_BY_PATH: LazyLock> = + 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 { + 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> { + use aws_credential_types::provider::ProvideCredentials as _; + use tokio::sync::OnceCell; + + static DEFAULT_CREDENTIAL_CHAIN: OnceCell = 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(&self, name: &str, data: &T) -> Result { diff --git a/src/db/models/attachment.rs b/src/db/models/attachment.rs index dad081bd..7611b927 100644 --- a/src/db/models/attachment.rs +++ b/src/db/models/attachment.rs @@ -46,7 +46,7 @@ impl Attachment { pub async fn get_url(&self, host: &str) -> Result { 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 { diff --git a/src/db/models/send.rs b/src/db/models/send.rs index 5b6611fa..84802c54 100644 --- a/src/db/models/send.rs +++ b/src/db/models/send.rs @@ -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: { diff --git a/src/main.rs b/src/main.rs index 4ffeacc1..60c5a593 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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!( "\ diff --git a/src/sso_client.rs b/src/sso_client.rs index 68e171c6..abff6bcb 100644 --- a/src/sso_client.rs +++ b/src/sso_client.rs @@ -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, Option); #[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 { - get_reqwest_client_builder().redirect(reqwest::redirect::Policy::none()).build().map(|client| Self { - client, - }) - } -} - -impl<'c> AsyncHttpClient<'c> for OidcHttpClient { - type Error = HttpClientError; - type Future = Pin> + 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 { @@ -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, }; diff --git a/src/storage.rs b/src/storage.rs deleted file mode 100644 index ada2a951..00000000 --- a/src/storage.rs +++ /dev/null @@ -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 { - #[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 { - #[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 { - // Cache of previously built operators by path - static OPERATORS_BY_PATH: LazyLock> = - 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 { - 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 { - 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 { - 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 { - 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> { - use aws_credential_types::provider::ProvideCredentials as _; - use reqsign_core::time::Timestamp; - use tokio::sync::OnceCell; - - static DEFAULT_CREDENTIAL_CHAIN: OnceCell = 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")); - } -}