diff --git a/.env.template b/.env.template index f379fac6..84722c50 100644 --- a/.env.template +++ b/.env.template @@ -15,6 +15,14 @@ #################### ## Main data folder +## This can be a path to local folder or a path to an external location +## depending on features enabled at build time. Possible external locations: +## +## - AWS S3 Bucket (via `s3` feature): s3://bucket-name/path/to/folder +## +## When using an external location, make sure to set TMP_FOLDER, +## TEMPLATES_FOLDER, and DATABASE_URL to local paths and/or a remote database +## location. # DATA_FOLDER=data ## Individual folders, these override %DATA_FOLDER% @@ -22,10 +30,13 @@ # ICON_CACHE_FOLDER=data/icon_cache # ATTACHMENTS_FOLDER=data/attachments # SENDS_FOLDER=data/sends + +## Temporary folder used for storing temporary file uploads +## Must be a local path. # TMP_FOLDER=data/tmp -## Templates data folder, by default uses embedded templates -## Check source code to see the format +## HTML template overrides data folder +## Must be a local path. # TEMPLATES_FOLDER=data/templates ## Automatically reload the templates for every request, slow, use only for development # RELOAD_TEMPLATES=false @@ -39,7 +50,9 @@ ######################### ## Database URL -## When using SQLite, this is the path to the DB file, default to %DATA_FOLDER%/db.sqlite3 +## When using SQLite, this is the path to the DB file, and it defaults to +## %DATA_FOLDER%/db.sqlite3. If DATA_FOLDER is set to an external location, this +## must be set to a local sqlite3 file path. # DATABASE_URL=data/db.sqlite3 ## When using MySQL, specify an appropriate connection URI. ## Details: https://docs.diesel.rs/2.1.x/diesel/mysql/struct.MysqlConnection.html diff --git a/Cargo.lock b/Cargo.lock index 7d8ee10f..8a16dbbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,17 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.8.12" @@ -74,6 +85,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + [[package]] name = "argon2" version = "0.5.3" @@ -312,6 +329,364 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "aws-config" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02a18fd934af6ae7ca52410d4548b98eb895aab0f1ea417d168d85db1434a141" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "hex", + "http 1.3.1", + "ring", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687bc16bc431a8533fe0097c7f0182874767f920989d7260950172ae8e3c4465" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-lc-rs" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b756939cb2f8dc900aa6dcd505e6e2428e9cae7ff7b028c49e3946efa70878" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa9b6986f250236c27e5a204062434a773a13243d2ffc2955f37bdba4c5c6a1" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "aws-runtime" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c4063282c69991e57faab9e5cb21ae557e59f5b0fb285c196335243df8dc25c" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http-body 0.4.6", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.68.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd5f01ea61fed99b5fe4877abff6c56943342a56ff145e9e0c7e2494419008be" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.69.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27454e4c55aaa4ef65647e3a1cf095cb834ca6d54e959e2909f1fef96ad87860" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.69.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd6ef5d00c94215960fabcdf2d9fe7c090eed8be482d66d47b92d4aba1dd4aa" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 0.2.12", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3734aecf9ff79aa401a6ca099d076535ab465ff76b46440cf567c8e70b65dc13" +dependencies = [ + "aws-credential-types", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "form_urlencoded", + "hex", + "hmac", + "http 0.2.12", + "http 1.3.1", + "percent-encoding", + "sha2", + "time", + "tracing", +] + +[[package]] +name = "aws-smithy-async" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e190749ea56f8c42bf15dd76c65e14f8f765233e6df9b0506d9d934ebef867c" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-http" +version = "0.62.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99335bec6cdc50a346fda1437f9fefe33abf8c99060739a546a16457f2862ca9" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.3.1", + "http-body 0.4.6", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8aff1159006441d02e57204bf57a1b890ba68bedb6904ffd2873c1c4c11c546b" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2", + "http 1.3.1", + "hyper 1.6.0", + "hyper-rustls", + "hyper-util", + "pin-project-lite", + "rustls 0.23.27", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tower", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92144e45819cae7dc62af23eac5a038a58aa544432d2102609654376a900bd07" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9364d5989ac4dd918e5cc4c4bdcc61c9be17dcd2586ea7f69e348fc7c6cab393" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fbd61ceb3fe8a1cb7352e42689cec5335833cd9f94103a61e98f9bb61c64bb" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14302f06d1d5b7d333fd819943075b13d27c7700b414f574c3c35859bfb55d5e" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-http-client", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.3.1", + "http-body 0.4.6", + "http-body 1.0.1", + "pin-project-lite", + "pin-utils", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e5d9e3a80a18afa109391fb5ad09c3daf887b516c6fd805a157c6ea7994a57" +dependencies = [ + "aws-smithy-async", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.3.1", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-types" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40076bd09fadbc12d5e026ae080d0930defa606856186e31d83ccc6a255eeaf3" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "http 0.2.12", + "http 1.3.1", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a322fec39e4df22777ed3ad8ea868ac2f94cd15e1a55f6ee8d8d6305057689a" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "rustc_version", + "tracing", +] + +[[package]] +name = "backon" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49fef586913a57ff189f25c9b3d034356a5bf6b3fa9a7f067588fe1698ba1f5d" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + [[package]] name = "backtrace" version = "0.3.75" @@ -345,6 +720,16 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + [[package]] name = "base64ct" version = "1.7.3" @@ -370,6 +755,29 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn", + "which 4.4.2", +] + [[package]] name = "bitflags" version = "2.9.1" @@ -394,6 +802,15 @@ dependencies = [ "generic-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.1" @@ -452,6 +869,16 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + [[package]] name = "cached" version = "0.55.1" @@ -488,21 +915,47 @@ 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.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" dependencies = [ + "jobserver", + "libc", "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.41" @@ -511,8 +964,10 @@ checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link", ] @@ -547,6 +1002,36 @@ dependencies = [ "stacker", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + [[package]] name = "codemap" version = "0.1.3" @@ -562,6 +1047,32 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "tiny-keccak", +] + [[package]] name = "cookie" version = "0.18.1" @@ -601,6 +1112,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -616,6 +1137,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32c" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a47af21622d091a8f0fb295b88bc886ac74efcc613efc19f5d0b21de5c89e47" +dependencies = [ + "rustc_version", +] + [[package]] name = "crc32fast" version = "1.4.2" @@ -666,6 +1196,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + [[package]] name = "crypto-common" version = "0.1.6" @@ -737,6 +1273,17 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.4.0" @@ -916,6 +1463,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -931,6 +1479,15 @@ dependencies = [ "syn", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "document-features" version = "0.2.11" @@ -960,6 +1517,12 @@ dependencies = [ "syn", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.15.0" @@ -1133,6 +1696,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.31" @@ -1446,6 +2015,12 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hickory-proto" version = "0.25.2" @@ -1501,6 +2076,15 @@ dependencies = [ "digest", ] +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "hostname" version = "0.4.1" @@ -1642,10 +2226,12 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "rustls 0.23.27", + "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls 0.26.2", "tower-service", + "webpki-roots 1.0.0", ] [[package]] @@ -1838,6 +2424,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.2" @@ -1867,6 +2463,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1890,6 +2495,16 @@ dependencies = [ "uuid", ] +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -1938,6 +2553,15 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lettre" @@ -1976,6 +2600,16 @@ version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + [[package]] name = "libm" version = "0.2.15" @@ -2091,6 +2725,16 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.7.4" @@ -2220,7 +2864,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -2270,6 +2914,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -2296,6 +2957,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-modular" version = "0.6.1" @@ -2318,6 +2990,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2358,6 +3031,35 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "opendal" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ff9e9656d1cb3c58582ea18e6d9e71076a7ab2614207821d1242d7da2daed5" +dependencies = [ + "anyhow", + "async-trait", + "backon", + "base64 0.22.1", + "bytes", + "chrono", + "crc32c", + "futures", + "getrandom 0.2.16", + "http 1.3.1", + "http-body 1.0.1", + "log", + "md-5", + "percent-encoding", + "quick-xml", + "reqsign", + "reqwest", + "serde", + "serde_json", + "tokio", + "uuid", +] + [[package]] name = "openssl" version = "0.10.72" @@ -2412,6 +3114,22 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "overload" version = "0.1.1" @@ -2479,6 +3197,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3a8cb46bdc156b1c90460339ae6bfd45ba0394e5effbaa640badb4987fdc261" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "pear" version = "0.2.9" @@ -2512,6 +3240,15 @@ dependencies = [ "serde", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -2644,6 +3381,44 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs5" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" +dependencies = [ + "aes", + "cbc", + "der", + "pbkdf2", + "scrypt", + "sha2", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "pkcs5", + "rand_core 0.6.4", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -2705,6 +3480,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "prettyplease" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -2767,6 +3552,70 @@ dependencies = [ "winapi", ] +[[package]] +name = "quick-xml" +version = "0.37.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ce8c88de324ff838700f36fb6ab86c96df0e3c4ab6ef3a9b2044465cce1369" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quinn" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.1", + "rustls 0.23.27", + "socket2", + "thiserror 2.0.12", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcbafbbdbb0f638fe3f35f3c56739f77a8a1d070cb25603226c83339b391472b" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "rand 0.9.1", + "ring", + "rustc-hash 2.1.1", + "rustls 0.23.27", + "rustls-pki-types", + "slab", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "541d0f57c6ec747a90738a52741d3221f7960e8ac2f0ff4b1a63680e033b4ab5" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.40" @@ -2928,6 +3777,12 @@ dependencies = [ "regex-syntax 0.8.5", ] +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + [[package]] name = "regex-syntax" version = "0.6.29" @@ -2951,6 +3806,38 @@ dependencies = [ "signal-hook", ] +[[package]] +name = "reqsign" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9323c0afb30e54f793f4705b10c890395bccc87c6e6ea62c4e7e82d09a380dc6" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.22.1", + "chrono", + "form_urlencoded", + "getrandom 0.2.16", + "hex", + "hmac", + "home", + "http 1.3.1", + "jsonwebtoken", + "log", + "once_cell", + "percent-encoding", + "quick-xml", + "rand 0.8.5", + "reqwest", + "rsa", + "rust-ini", + "serde", + "serde_json", + "sha1", + "sha2", + "toml", +] + [[package]] name = "reqwest" version = "0.12.15" @@ -2982,7 +3869,10 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "quinn", + "rustls 0.23.27", "rustls-pemfile 2.2.0", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", @@ -2990,6 +3880,7 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", + "tokio-rustls 0.26.2", "tokio-socks", "tokio-util", "tower", @@ -2999,6 +3890,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", + "webpki-roots 0.26.8", "windows-registry", ] @@ -3149,6 +4041,27 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rsa" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "sha2", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rtoolbox" version = "0.0.3" @@ -3159,12 +4072,35 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rust-ini" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f" +dependencies = [ + "cfg-if", + "ordered-multimap", + "trim-in-place", +] + [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -3218,13 +4154,27 @@ version = "0.23.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" dependencies = [ + "aws-lc-rs", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki 0.103.3", "subtle", "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.2.0", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -3249,6 +4199,7 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ + "web-time", "zeroize", ] @@ -3268,6 +4219,7 @@ version = "0.103.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -3285,6 +4237,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[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" @@ -3324,6 +4285,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", +] + [[package]] name = "sct" version = "0.7.1" @@ -3341,7 +4313,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +dependencies = [ + "bitflags", + "core-foundation 0.10.0", "core-foundation-sys", "libc", "security-framework-sys", @@ -3482,6 +4467,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "simple_asn1" version = "0.6.3" @@ -3540,6 +4535,16 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable-pattern" version = "0.1.0" @@ -3639,7 +4644,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -3764,6 +4769,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.8.1" @@ -3891,6 +4905,7 @@ checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "pin-project-lite", "tokio", @@ -4038,6 +5053,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "trim-in-place" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" + [[package]] name = "try-lock" version = "0.2.5" @@ -4124,6 +5145,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf-8" version = "0.7.6" @@ -4144,6 +5171,7 @@ checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" dependencies = [ "getrandom 0.3.3", "js-sys", + "serde", "wasm-bindgen", ] @@ -4163,7 +5191,10 @@ checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" name = "vaultwarden" version = "1.0.0" dependencies = [ + "anyhow", "argon2", + "aws-config", + "aws-credential-types", "bigdecimal", "bytes", "cached", @@ -4198,12 +5229,14 @@ dependencies = [ "num-derive", "num-traits", "once_cell", + "opendal", "openssl", "pastey", "percent-encoding", "pico-args", "rand 0.9.1", "regex", + "reqsign", "reqwest", "ring", "rmpv", @@ -4217,12 +5250,13 @@ dependencies = [ "syslog", "time", "tokio", + "tokio-util", "totp-lite", "tracing", "url", "uuid", "webauthn-rs", - "which", + "which 7.0.3", "yubico_ng", ] @@ -4238,6 +5272,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "walkdir" version = "2.5.0" @@ -4395,6 +5435,36 @@ dependencies = [ "url", ] +[[package]] +name = "webpki-roots" +version = "0.26.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + [[package]] name = "which" version = "7.0.3" @@ -4845,6 +5915,12 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 2742f428..a871cc73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ enable_mimalloc = ["dep:mimalloc"] # You also need to set an env variable `QUERY_LOGGER=1` to fully activate this so you do not have to re-compile # if you want to turn off the logging for a specific run. query_logger = ["dep:diesel_logger"] +s3 = ["opendal/services-s3", "dep:aws-config", "dep:aws-credential-types", "dep:anyhow", "dep:reqsign"] # Enable unstable features, requires nightly # Currently only used to enable rusts official ip support @@ -73,6 +74,7 @@ dashmap = "6.1.0" # Async futures futures = "0.3.31" tokio = { version = "1.45.1", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] } +tokio-util = { version = "0.7.15", features = ["compat"]} # A generic serialization/deserialization framework serde = { version = "1.0.219", features = ["derive"] } @@ -176,6 +178,15 @@ rpassword = "7.4.0" # Loading a dynamic CSS Stylesheet grass_compiler = { version = "0.13.4", default-features = false } +# File are accessed through Apache OpenDAL +opendal = { version = "0.53.2", features = ["services-fs"] } + +# For retrieving AWS credentials, including temporary SSO credentials +anyhow = { version = "1.0.98", optional = true } +aws-config = { version = "1.6.3", features = ["behavior-version-latest"], optional = true } +aws-credential-types = { version = "1.2.3", optional = true } +reqsign = { version = "0.16.3", optional = true } + # Strip debuginfo from the release builds # The debug symbols are to provide better panic traces # Also enable fat LTO and use 1 codegen unit for optimizations diff --git a/build.rs b/build.rs index f66ce69d..1dbb1a0b 100644 --- a/build.rs +++ b/build.rs @@ -11,6 +11,8 @@ fn main() { println!("cargo:rustc-cfg=postgresql"); #[cfg(feature = "query_logger")] println!("cargo:rustc-cfg=query_logger"); + #[cfg(feature = "s3")] + println!("cargo:rustc-cfg=s3"); #[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgresql")))] compile_error!( @@ -23,6 +25,7 @@ fn main() { println!("cargo::rustc-check-cfg=cfg(mysql)"); println!("cargo::rustc-check-cfg=cfg(postgresql)"); println!("cargo::rustc-check-cfg=cfg(query_logger)"); + println!("cargo::rustc-check-cfg=cfg(s3)"); // Rerun when these paths are changed. // Someone could have checked-out a tag or specific commit, but no other files changed. diff --git a/src/api/admin.rs b/src/api/admin.rs index 0909ddd8..36a3174f 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -753,17 +753,17 @@ fn get_diagnostics_http(code: u16, _token: AdminToken) -> EmptyResult { } #[post("/config", format = "application/json", data = "")] -fn post_config(data: Json, _token: AdminToken) -> EmptyResult { +async fn post_config(data: Json, _token: AdminToken) -> EmptyResult { let data: ConfigBuilder = data.into_inner(); - if let Err(e) = CONFIG.update_config(data, true) { + if let Err(e) = CONFIG.update_config(data, true).await { err!(format!("Unable to save config: {e:?}")) } Ok(()) } #[post("/config/delete", format = "application/json")] -fn delete_config(_token: AdminToken) -> EmptyResult { - if let Err(e) = CONFIG.delete_user_config() { +async fn delete_config(_token: AdminToken) -> EmptyResult { + if let Err(e) = CONFIG.delete_user_config().await { err!(format!("Unable to delete config: {e:?}")) } Ok(()) diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 9e7dc045..aecbe28a 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -11,10 +11,11 @@ use rocket::{ use serde_json::Value; use crate::auth::ClientVersion; -use crate::util::NumberOrString; +use crate::util::{save_temp_file, NumberOrString}; use crate::{ api::{self, core::log_event, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType}, auth::Headers, + config::PathType, crypto, db::{models::*, DbConn, DbPool}, CONFIG, @@ -105,12 +106,7 @@ struct SyncData { } #[get("/sync?")] -async fn sync( - data: SyncData, - headers: Headers, - client_version: Option, - mut conn: DbConn, -) -> Json { +async fn sync(data: SyncData, headers: Headers, client_version: Option, mut conn: DbConn) -> JsonResult { let user_json = headers.user.to_json(&mut conn).await; // Get all ciphers which are visible by the user @@ -134,7 +130,7 @@ async fn sync( for c in ciphers { ciphers_json.push( c.to_json(&headers.host, &headers.user.uuid, Some(&cipher_sync_data), CipherSyncType::User, &mut conn) - .await, + .await?, ); } @@ -159,7 +155,7 @@ async fn sync( api::core::_get_eq_domains(headers, true).into_inner() }; - Json(json!({ + Ok(Json(json!({ "profile": user_json, "folders": folders_json, "collections": collections_json, @@ -168,11 +164,11 @@ async fn sync( "domains": domains_json, "sends": sends_json, "object": "sync" - })) + }))) } #[get("/ciphers")] -async fn get_ciphers(headers: Headers, mut conn: DbConn) -> Json { +async fn get_ciphers(headers: Headers, mut conn: DbConn) -> JsonResult { let ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &mut conn).await; let cipher_sync_data = CipherSyncData::new(&headers.user.uuid, CipherSyncType::User, &mut conn).await; @@ -180,15 +176,15 @@ async fn get_ciphers(headers: Headers, mut conn: DbConn) -> Json { for c in ciphers { ciphers_json.push( c.to_json(&headers.host, &headers.user.uuid, Some(&cipher_sync_data), CipherSyncType::User, &mut conn) - .await, + .await?, ); } - Json(json!({ + Ok(Json(json!({ "data": ciphers_json, "object": "list", "continuationToken": null - })) + }))) } #[get("/ciphers/")] @@ -201,7 +197,7 @@ async fn get_cipher(cipher_id: CipherId, headers: Headers, mut conn: DbConn) -> err!("Cipher is not owned by user") } - Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await)) + Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await?)) } #[get("/ciphers//admin")] @@ -339,7 +335,7 @@ async fn post_ciphers(data: Json, headers: Headers, mut conn: DbConn let mut cipher = Cipher::new(data.r#type, data.name.clone()); update_cipher_from_data(&mut cipher, data, &headers, None, &mut conn, &nt, UpdateType::SyncCipherCreate).await?; - Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await)) + Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await?)) } /// Enforces the personal ownership policy on user-owned ciphers, if applicable. @@ -676,7 +672,7 @@ async fn put_cipher( update_cipher_from_data(&mut cipher, data, &headers, None, &mut conn, &nt, UpdateType::SyncCipherUpdate).await?; - Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await)) + Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await?)) } #[post("/ciphers//partial", data = "")] @@ -714,7 +710,7 @@ async fn put_cipher_partial( // Update favorite cipher.set_favorite(Some(data.favorite), &headers.user.uuid, &mut conn).await?; - Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await)) + Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await?)) } #[derive(Deserialize)] @@ -825,7 +821,7 @@ async fn post_collections_update( ) .await; - Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await)) + Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await?)) } #[put("/ciphers//collections-admin", data = "")] @@ -1030,7 +1026,7 @@ async fn share_cipher_by_uuid( update_cipher_from_data(&mut cipher, data.cipher, headers, Some(shared_to_collections), conn, nt, ut).await?; - Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await)) + Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await?)) } /// v2 API for downloading an attachment. This just redirects the client to @@ -1055,7 +1051,7 @@ async fn get_attachment( } match Attachment::find_by_id(&attachment_id, &mut conn).await { - Some(attachment) if cipher_id == attachment.cipher_uuid => Ok(Json(attachment.to_json(&headers.host))), + Some(attachment) if cipher_id == attachment.cipher_uuid => Ok(Json(attachment.to_json(&headers.host).await?)), Some(_) => err!("Attachment doesn't belong to cipher"), None => err!("Attachment doesn't exist"), } @@ -1116,7 +1112,7 @@ async fn post_attachment_v2( "attachmentId": attachment_id, "url": url, "fileUploadType": FileUploadType::Direct as i32, - response_key: cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await, + response_key: cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await?, }))) } @@ -1142,7 +1138,7 @@ async fn save_attachment( mut conn: DbConn, nt: Notify<'_>, ) -> Result<(Cipher, DbConn), crate::error::Error> { - let mut data = data.into_inner(); + let data = data.into_inner(); let Some(size) = data.data.len().to_i64() else { err!("Attachment data size overflow"); @@ -1269,13 +1265,7 @@ async fn save_attachment( attachment.save(&mut conn).await.expect("Error saving attachment"); } - let folder_path = tokio::fs::canonicalize(&CONFIG.attachments_folder()).await?.join(cipher_id.as_ref()); - let file_path = folder_path.join(file_id.as_ref()); - tokio::fs::create_dir_all(&folder_path).await?; - - if let Err(_err) = data.data.persist_to(&file_path).await { - data.data.move_copy_to(file_path).await? - } + save_temp_file(PathType::Attachments, &format!("{cipher_id}/{file_id}"), data.data, true).await?; nt.send_cipher_update( UpdateType::SyncCipherUpdate, @@ -1342,7 +1332,7 @@ async fn post_attachment( let (cipher, mut conn) = save_attachment(attachment, cipher_id, data, &headers, conn, nt).await?; - Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await)) + Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await?)) } #[post("/ciphers//attachment-admin", format = "multipart/form-data", data = "")] @@ -1786,7 +1776,7 @@ async fn _restore_cipher_by_uuid( .await; } - Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await)) + Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await?)) } async fn _restore_multiple_ciphers( @@ -1859,7 +1849,7 @@ async fn _delete_cipher_attachment_by_id( ) .await; } - let cipher_json = cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await; + let cipher_json = cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await?; Ok(Json(json!({"cipher":cipher_json}))) } diff --git a/src/api/core/emergency_access.rs b/src/api/core/emergency_access.rs index 9d5ed7aa..2c9844d6 100644 --- a/src/api/core/emergency_access.rs +++ b/src/api/core/emergency_access.rs @@ -582,7 +582,7 @@ async fn view_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut CipherSyncType::User, &mut conn, ) - .await, + .await?, ); } diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 2c70bff0..5b912a36 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -917,21 +917,26 @@ async fn get_org_details(data: OrgIdData, headers: OrgMemberHeaders, mut conn: D } Ok(Json(json!({ - "data": _get_org_details(&data.organization_id, &headers.host, &headers.user.uuid, &mut conn).await, + "data": _get_org_details(&data.organization_id, &headers.host, &headers.user.uuid, &mut conn).await?, "object": "list", "continuationToken": null, }))) } -async fn _get_org_details(org_id: &OrganizationId, host: &str, user_id: &UserId, conn: &mut DbConn) -> Value { +async fn _get_org_details( + org_id: &OrganizationId, + host: &str, + user_id: &UserId, + conn: &mut DbConn, +) -> Result { let ciphers = Cipher::find_by_org(org_id, conn).await; let cipher_sync_data = CipherSyncData::new(user_id, CipherSyncType::Organization, conn).await; let mut ciphers_json = Vec::with_capacity(ciphers.len()); for c in ciphers { - ciphers_json.push(c.to_json(host, user_id, Some(&cipher_sync_data), CipherSyncType::Organization, conn).await); + ciphers_json.push(c.to_json(host, user_id, Some(&cipher_sync_data), CipherSyncType::Organization, conn).await?); } - json!(ciphers_json) + Ok(json!(ciphers_json)) } #[derive(FromForm)] @@ -3372,7 +3377,7 @@ async fn get_org_export(org_id: OrganizationId, headers: AdminHeaders, mut conn: Ok(Json(json!({ "collections": convert_json_key_lcase_first(_get_org_collections(&org_id, &mut conn).await), - "ciphers": convert_json_key_lcase_first(_get_org_details(&org_id, &headers.host, &headers.user.uuid, &mut conn).await), + "ciphers": convert_json_key_lcase_first(_get_org_details(&org_id, &headers.host, &headers.user.uuid, &mut conn).await?), }))) } diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs index e99face4..96bf71a0 100644 --- a/src/api/core/sends.rs +++ b/src/api/core/sends.rs @@ -1,4 +1,5 @@ use std::path::Path; +use std::time::Duration; use chrono::{DateTime, TimeDelta, Utc}; use num_traits::ToPrimitive; @@ -12,8 +13,9 @@ use serde_json::Value; use crate::{ api::{ApiResult, EmptyResult, JsonResult, Notify, UpdateType}, auth::{ClientIp, Headers, Host}, + config::PathType, db::{models::*, DbConn, DbPool}, - util::NumberOrString, + util::{save_temp_file, NumberOrString}, CONFIG, }; @@ -228,7 +230,7 @@ async fn post_send_file(data: Form>, headers: Headers, mut conn: let UploadData { model, - mut data, + data, } = data.into_inner(); let model = model.into_inner(); @@ -268,13 +270,8 @@ async fn post_send_file(data: Form>, headers: Headers, mut conn: } let file_id = crate::crypto::generate_send_file_id(); - let folder_path = tokio::fs::canonicalize(&CONFIG.sends_folder()).await?.join(&send.uuid); - let file_path = folder_path.join(&file_id); - tokio::fs::create_dir_all(&folder_path).await?; - if let Err(_err) = data.persist_to(&file_path).await { - data.move_copy_to(file_path).await? - } + save_temp_file(PathType::Sends, &format!("{}/{file_id}", send.uuid), data, true).await?; let mut data_value: Value = serde_json::from_str(&send.data)?; if let Some(o) = data_value.as_object_mut() { @@ -381,7 +378,7 @@ async fn post_send_file_v2_data( ) -> EmptyResult { enforce_disable_send_policy(&headers, &mut conn).await?; - let mut data = data.into_inner(); + let data = data.into_inner(); let Some(send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &mut conn).await else { err!("Send not found. Unable to save the file.", "Invalid send uuid or does not belong to user.") @@ -424,19 +421,9 @@ async fn post_send_file_v2_data( err!("Send file size does not match.", format!("Expected a file size of {} got {size}", send_data.size)); } - let folder_path = tokio::fs::canonicalize(&CONFIG.sends_folder()).await?.join(send_id); - let file_path = folder_path.join(file_id); - - // Check if the file already exists, if that is the case do not overwrite it - if tokio::fs::metadata(&file_path).await.is_ok() { - err!("Send file has already been uploaded.", format!("File {file_path:?} already exists")) - } - - tokio::fs::create_dir_all(&folder_path).await?; + let file_path = format!("{send_id}/{file_id}"); - if let Err(_err) = data.data.persist_to(&file_path).await { - data.data.move_copy_to(file_path).await? - } + save_temp_file(PathType::Sends, &file_path, data.data, false).await?; nt.send_send_update( UpdateType::SyncSendCreate, @@ -569,15 +556,26 @@ async fn post_access_file( ) .await; - let token_claims = crate::auth::generate_send_claims(&send_id, &file_id); - let token = crate::auth::encode_jwt(&token_claims); Ok(Json(json!({ "object": "send-fileDownload", "id": file_id, - "url": format!("{}/api/sends/{send_id}/{file_id}?t={token}", &host.host) + "url": download_url(&host, &send_id, &file_id).await?, }))) } +async fn download_url(host: &Host, send_id: &SendId, file_id: &SendFileId) -> Result { + let operator = CONFIG.opendal_operator_for_path_type(PathType::Sends)?; + + if operator.info().scheme() == opendal::Scheme::Fs { + let token_claims = crate::auth::generate_send_claims(send_id, file_id); + let token = crate::auth::encode_jwt(&token_claims); + + Ok(format!("{}/api/sends/{send_id}/{file_id}?t={token}", &host.host)) + } else { + Ok(operator.presign_read(&format!("{send_id}/{file_id}"), Duration::from_secs(5 * 60)).await?.uri().to_string()) + } +} + #[get("/sends//?")] async fn download_send(send_id: SendId, file_id: SendFileId, t: &str) -> Option { if let Ok(claims) = crate::auth::decode_send(t) { diff --git a/src/api/core/two_factor/duo.rs b/src/api/core/two_factor/duo.rs index 6600bfde..d5914608 100644 --- a/src/api/core/two_factor/duo.rs +++ b/src/api/core/two_factor/duo.rs @@ -261,7 +261,7 @@ pub(crate) async fn get_duo_keys_email(email: &str, conn: &mut DbConn) -> ApiRes } .map_res("Can't fetch Duo Keys")?; - Ok((data.ik, data.sk, CONFIG.get_duo_akey(), data.host)) + Ok((data.ik, data.sk, CONFIG.get_duo_akey().await, data.host)) } pub async fn generate_duo_signature(email: &str, conn: &mut DbConn) -> ApiResult<(String, String)> { diff --git a/src/api/icons.rs b/src/api/icons.rs index 3cccb351..acf3943e 100644 --- a/src/api/icons.rs +++ b/src/api/icons.rs @@ -14,14 +14,11 @@ use reqwest::{ Client, Response, }; use rocket::{http::ContentType, response::Redirect, Route}; -use tokio::{ - fs::{create_dir_all, remove_file, symlink_metadata, File}, - io::{AsyncReadExt, AsyncWriteExt}, -}; use html5gum::{Emitter, HtmlString, Readable, StringReader, Tokenizer}; use crate::{ + config::PathType, error::Error, http_client::{get_reqwest_client_builder, should_block_address, CustomHttpClientError}, util::Cached, @@ -158,7 +155,7 @@ fn is_valid_domain(domain: &str) -> bool { } async fn get_icon(domain: &str) -> Option<(Vec, String)> { - let path = format!("{}/{domain}.png", CONFIG.icon_cache_folder()); + let path = format!("{domain}.png"); // Check for expiration of negatively cached copy if icon_is_negcached(&path).await { @@ -177,7 +174,7 @@ async fn get_icon(domain: &str) -> Option<(Vec, String)> { // Get the icon, or None in case of error match download_icon(domain).await { Ok((icon, icon_type)) => { - save_icon(&path, &icon).await; + save_icon(&path, icon.to_vec()).await; Some((icon.to_vec(), icon_type.unwrap_or("x-icon").to_string())) } Err(e) => { @@ -190,7 +187,7 @@ async fn get_icon(domain: &str) -> Option<(Vec, String)> { warn!("Unable to download icon: {e:?}"); let miss_indicator = path + ".miss"; - save_icon(&miss_indicator, &[]).await; + save_icon(&miss_indicator, vec![]).await; None } } @@ -203,11 +200,9 @@ async fn get_cached_icon(path: &str) -> Option> { } // Try to read the cached icon, and return it if it exists - if let Ok(mut f) = File::open(path).await { - let mut buffer = Vec::new(); - - if f.read_to_end(&mut buffer).await.is_ok() { - return Some(buffer); + if let Ok(operator) = CONFIG.opendal_operator_for_path_type(PathType::IconCache) { + if let Ok(buf) = operator.read(path).await { + return Some(buf.to_vec()); } } @@ -215,9 +210,11 @@ async fn get_cached_icon(path: &str) -> Option> { } async fn file_is_expired(path: &str, ttl: u64) -> Result { - let meta = symlink_metadata(path).await?; - let modified = meta.modified()?; - let age = SystemTime::now().duration_since(modified)?; + let operator = CONFIG.opendal_operator_for_path_type(PathType::IconCache)?; + let meta = operator.stat(path).await?; + let modified = + meta.last_modified().ok_or_else(|| std::io::Error::other(format!("No last modified time for `{path}`")))?; + let age = SystemTime::now().duration_since(modified.into())?; Ok(ttl > 0 && ttl <= age.as_secs()) } @@ -229,8 +226,13 @@ async fn icon_is_negcached(path: &str) -> bool { match expired { // No longer negatively cached, drop the marker Ok(true) => { - if let Err(e) = remove_file(&miss_indicator).await { - error!("Could not remove negative cache indicator for icon {path:?}: {e:?}"); + match CONFIG.opendal_operator_for_path_type(PathType::IconCache) { + Ok(operator) => { + if let Err(e) = operator.delete(&miss_indicator).await { + error!("Could not remove negative cache indicator for icon {path:?}: {e:?}"); + } + } + Err(e) => error!("Could not remove negative cache indicator for icon {path:?}: {e:?}"), } false } @@ -564,17 +566,17 @@ async fn download_icon(domain: &str) -> Result<(Bytes, Option<&str>), Error> { Ok((buffer, icon_type)) } -async fn save_icon(path: &str, icon: &[u8]) { - match File::create(path).await { - Ok(mut f) => { - f.write_all(icon).await.expect("Error writing icon file"); - } - Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => { - create_dir_all(&CONFIG.icon_cache_folder()).await.expect("Error creating icon cache folder"); - } +async fn save_icon(path: &str, icon: Vec) { + let operator = match CONFIG.opendal_operator_for_path_type(PathType::IconCache) { + Ok(operator) => operator, Err(e) => { - warn!("Unable to save icon: {e:?}"); + warn!("Failed to get OpenDAL operator while saving icon: {e}"); + return; } + }; + + if let Err(e) = operator.write(path, icon).await { + warn!("Unable to save icon: {e:?}"); } } diff --git a/src/auth.rs b/src/auth.rs index c86c0a41..2d51d539 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -7,16 +7,14 @@ use once_cell::sync::{Lazy, OnceCell}; use openssl::rsa::Rsa; use serde::de::DeserializeOwned; use serde::ser::Serialize; -use std::{ - env, - fs::File, - io::{Read, Write}, - net::IpAddr, -}; - -use crate::db::models::{ - AttachmentId, CipherId, CollectionId, DeviceId, EmergencyAccessId, MembershipId, OrgApiKeyId, OrganizationId, - SendFileId, SendId, UserId, +use std::{env, net::IpAddr}; + +use crate::{ + config::PathType, + db::models::{ + AttachmentId, CipherId, CollectionId, DeviceId, EmergencyAccessId, MembershipId, OrgApiKeyId, OrganizationId, + SendFileId, SendId, UserId, + }, }; use crate::{error::Error, CONFIG}; @@ -40,37 +38,33 @@ static JWT_REGISTER_VERIFY_ISSUER: Lazy = Lazy::new(|| format!("{}|regis static PRIVATE_RSA_KEY: OnceCell = OnceCell::new(); static PUBLIC_RSA_KEY: OnceCell = OnceCell::new(); -pub fn initialize_keys() -> Result<(), Error> { - fn read_key(create_if_missing: bool) -> Result<(Rsa, Vec), Error> { - let mut priv_key_buffer = Vec::with_capacity(2048); - - let mut priv_key_file = File::options() - .create(create_if_missing) - .truncate(false) - .read(true) - .write(create_if_missing) - .open(CONFIG.private_rsa_key())?; - - #[allow(clippy::verbose_file_reads)] - let bytes_read = priv_key_file.read_to_end(&mut priv_key_buffer)?; - - let rsa_key = if bytes_read > 0 { - Rsa::private_key_from_pem(&priv_key_buffer[..bytes_read])? - } else if create_if_missing { - // Only create the key if the file doesn't exist or is empty - let rsa_key = Rsa::generate(2048)?; - priv_key_buffer = rsa_key.private_key_to_pem()?; - priv_key_file.write_all(&priv_key_buffer)?; - info!("Private key '{}' created correctly", CONFIG.private_rsa_key()); - rsa_key - } else { - err!("Private key does not exist or invalid format", CONFIG.private_rsa_key()); - }; - - Ok((rsa_key, priv_key_buffer)) - } - - let (priv_key, priv_key_buffer) = read_key(true).or_else(|_| read_key(false))?; +pub async fn initialize_keys() -> Result<(), Error> { + use std::io::Error; + + 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)?; + + let priv_key_buffer = match operator.read(&rsa_key_filename).await { + Ok(buffer) => Some(buffer), + Err(e) if e.kind() == opendal::ErrorKind::NotFound => None, + Err(e) => return Err(e.into()), + }; + + let (priv_key, priv_key_buffer) = if let Some(priv_key_buffer) = priv_key_buffer { + (Rsa::private_key_from_pem(priv_key_buffer.to_vec().as_slice())?, priv_key_buffer.to_vec()) + } else { + let rsa_key = Rsa::generate(2048)?; + let priv_key_buffer = rsa_key.private_key_to_pem()?; + operator.write(&rsa_key_filename, priv_key_buffer.clone()).await?; + info!("Private key '{}' created correctly", CONFIG.private_rsa_key()); + (rsa_key, priv_key_buffer) + }; let pub_key_buffer = priv_key.public_key_to_pem()?; let enc = EncodingKey::from_rsa_pem(&priv_key_buffer)?; diff --git a/src/config.rs b/src/config.rs index 068735ff..5b995c6d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,7 +3,7 @@ use std::{ process::exit, sync::{ atomic::{AtomicBool, Ordering}, - RwLock, + LazyLock, RwLock, }, }; @@ -22,10 +22,32 @@ static CONFIG_FILE: Lazy = Lazy::new(|| { get_env("CONFIG_FILE").unwrap_or_else(|| format!("{data_folder}/config.json")) }); +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(|| { + 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); pub static CONFIG: Lazy = Lazy::new(|| { - Config::load().unwrap_or_else(|e| { + std::thread::spawn(|| { + let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap_or_else(|e| { + println!("Error loading config:\n {e:?}\n"); + exit(12) + }); + + rt.block_on(Config::load()).unwrap_or_else(|e| { + println!("Error loading config:\n {e:?}\n"); + exit(12) + }) + }) + .join() + .unwrap_or_else(|e| { println!("Error loading config:\n {e:?}\n"); exit(12) }) @@ -110,10 +132,11 @@ macro_rules! make_config { builder } - fn from_file(path: &str) -> Result { - let config_str = std::fs::read_to_string(path)?; - println!("[INFO] Using saved config from `{path}` for configuration.\n"); - serde_json::from_str(&config_str).map_err(Into::into) + async fn from_file() -> Result { + 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) } fn clear_non_editable(&mut self) { @@ -1138,11 +1161,93 @@ 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 { + // 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< + aws_config::default_provider::credentials::DefaultCredentialsChain, + > = OnceCell::const_new(); + + let chain = DEFAULT_CREDENTIAL_CHAIN + .get_or_init(|| aws_config::default_provider::credentials::DefaultCredentialsChain::builder().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, + Attachments, + Sends, + RsaKey, +} + impl Config { - pub fn load() -> Result { + pub async fn load() -> Result { // Loading from env and file let _env = ConfigBuilder::from_env(); - let _usr = ConfigBuilder::from_file(&CONFIG_FILE).unwrap_or_default(); + let _usr = ConfigBuilder::from_file().await.unwrap_or_default(); // Create merged config, config file overwrites env let mut _overrides = Vec::new(); @@ -1166,7 +1271,7 @@ impl Config { }) } - pub fn update_config(&self, other: ConfigBuilder, ignore_non_editable: bool) -> Result<(), Error> { + pub async fn update_config(&self, other: ConfigBuilder, ignore_non_editable: bool) -> Result<(), Error> { // Remove default values //let builder = other.remove(&self.inner.read().unwrap()._env); @@ -1198,20 +1303,19 @@ impl Config { } //Save to file - use std::{fs::File, io::Write}; - let mut file = File::create(&*CONFIG_FILE)?; - file.write_all(config_str.as_bytes())?; + let operator = opendal_operator_for_path(&CONFIG_FILE_PARENT_DIR)?; + operator.write(&CONFIG_FILENAME, config_str).await?; Ok(()) } - fn update_config_partial(&self, other: ConfigBuilder) -> Result<(), Error> { + async fn update_config_partial(&self, other: ConfigBuilder) -> Result<(), Error> { let builder = { let usr = &self.inner.read().unwrap()._usr; let mut _overrides = Vec::new(); usr.merge(&other, false, &mut _overrides) }; - self.update_config(builder, false) + self.update_config(builder, false).await } /// Tests whether an email's domain is allowed. A domain is allowed if it @@ -1253,8 +1357,9 @@ impl Config { } } - pub fn delete_user_config(&self) -> Result<(), Error> { - std::fs::remove_file(&*CONFIG_FILE)?; + pub async fn delete_user_config(&self) -> Result<(), Error> { + let operator = opendal_operator_for_path(&CONFIG_FILE_PARENT_DIR)?; + operator.delete(&CONFIG_FILENAME).await?; // Empty user config let usr = ConfigBuilder::default(); @@ -1284,7 +1389,7 @@ impl Config { inner._enable_smtp && (inner.smtp_host.is_some() || inner.use_sendmail) } - pub fn get_duo_akey(&self) -> String { + pub async fn get_duo_akey(&self) -> String { if let Some(akey) = self._duo_akey() { akey } else { @@ -1295,7 +1400,7 @@ impl Config { _duo_akey: Some(akey_s.clone()), ..Default::default() }; - self.update_config_partial(builder).ok(); + self.update_config_partial(builder).await.ok(); akey_s } @@ -1308,6 +1413,23 @@ impl Config { token.is_some() && !token.unwrap().trim().is_empty() } + pub fn opendal_operator_for_path_type(&self, path_type: PathType) -> Result { + let path = match path_type { + PathType::Data => self.data_folder(), + PathType::IconCache => self.icon_cache_folder(), + PathType::Attachments => self.attachments_folder(), + PathType::Sends => self.sends_folder(), + 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(), + }; + + opendal_operator_for_path(&path) + } + pub fn render_template(&self, name: &str, data: &T) -> Result { if self.reload_templates() { warn!("RELOADING TEMPLATES"); diff --git a/src/db/models/attachment.rs b/src/db/models/attachment.rs index e8da38a5..aafb8766 100644 --- a/src/db/models/attachment.rs +++ b/src/db/models/attachment.rs @@ -1,11 +1,11 @@ -use std::io::ErrorKind; +use std::time::Duration; use bigdecimal::{BigDecimal, ToPrimitive}; use derive_more::{AsRef, Deref, Display}; use serde_json::Value; use super::{CipherId, OrganizationId, UserId}; -use crate::CONFIG; +use crate::{config::PathType, CONFIG}; use macros::IdFromParam; db_object! { @@ -41,24 +41,30 @@ impl Attachment { } pub fn get_file_path(&self) -> String { - format!("{}/{}/{}", CONFIG.attachments_folder(), self.cipher_uuid, self.id) + format!("{}/{}", self.cipher_uuid, self.id) } - pub fn get_url(&self, host: &str) -> String { - let token = encode_jwt(&generate_file_download_claims(self.cipher_uuid.clone(), self.id.clone())); - format!("{host}/attachments/{}/{}?token={token}", self.cipher_uuid, self.id) + pub async fn get_url(&self, host: &str) -> Result { + let operator = CONFIG.opendal_operator_for_path_type(PathType::Attachments)?; + + if operator.info().scheme() == 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 { + Ok(operator.presign_read(&self.get_file_path(), Duration::from_secs(5 * 60)).await?.uri().to_string()) + } } - pub fn to_json(&self, host: &str) -> Value { - json!({ + pub async fn to_json(&self, host: &str) -> Result { + Ok(json!({ "id": self.id, - "url": self.get_url(host), + "url": self.get_url(host).await?, "fileName": self.file_name, "size": self.file_size.to_string(), "sizeName": crate::util::get_display_size(self.file_size), "key": self.akey, "object": "attachment" - }) + })) } } @@ -104,26 +110,26 @@ impl Attachment { pub async fn delete(&self, conn: &mut DbConn) -> EmptyResult { db_run! { conn: { - let _: () = crate::util::retry( + crate::util::retry( || diesel::delete(attachments::table.filter(attachments::id.eq(&self.id))).execute(conn), 10, ) - .map_res("Error deleting attachment")?; - - let file_path = &self.get_file_path(); - - match std::fs::remove_file(file_path) { - // Ignore "file not found" errors. This can happen when the - // upstream caller has already cleaned up the file as part of - // its own error handling. - Err(e) if e.kind() == ErrorKind::NotFound => { - debug!("File '{file_path}' already deleted."); - Ok(()) - } - Err(e) => Err(e.into()), - _ => Ok(()), + .map(|_| ()) + .map_res("Error deleting attachment") + }}?; + + let operator = CONFIG.opendal_operator_for_path_type(PathType::Attachments)?; + let file_path = self.get_file_path(); + + if let Err(e) = operator.delete(&file_path).await { + if e.kind() == opendal::ErrorKind::NotFound { + debug!("File '{file_path}' already deleted."); + } else { + return Err(e.into()); } - }} + } + + Ok(()) } pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &mut DbConn) -> EmptyResult { diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index 4a1b9789..f9b4b2cf 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -141,18 +141,28 @@ impl Cipher { cipher_sync_data: Option<&CipherSyncData>, sync_type: CipherSyncType, conn: &mut DbConn, - ) -> Value { + ) -> Result { use crate::util::{format_date, validate_and_format_date}; let mut attachments_json: Value = Value::Null; if let Some(cipher_sync_data) = cipher_sync_data { if let Some(attachments) = cipher_sync_data.cipher_attachments.get(&self.uuid) { - attachments_json = attachments.iter().map(|c| c.to_json(host)).collect(); + if !attachments.is_empty() { + let mut attachments_json_vec = vec![]; + for attachment in attachments { + attachments_json_vec.push(attachment.to_json(host).await?); + } + attachments_json = Value::Array(attachments_json_vec); + } } } else { let attachments = Attachment::find_by_cipher(&self.uuid, conn).await; if !attachments.is_empty() { - attachments_json = attachments.iter().map(|c| c.to_json(host)).collect() + let mut attachments_json_vec = vec![]; + for attachment in attachments { + attachments_json_vec.push(attachment.to_json(host).await?); + } + attachments_json = Value::Array(attachments_json_vec); } } @@ -384,7 +394,7 @@ impl Cipher { }; json_object[key] = type_data_json; - json_object + Ok(json_object) } pub async fn update_users_revision(&self, conn: &mut DbConn) -> Vec { diff --git a/src/db/models/send.rs b/src/db/models/send.rs index c0bb0b33..bf82c181 100644 --- a/src/db/models/send.rs +++ b/src/db/models/send.rs @@ -1,7 +1,7 @@ use chrono::{NaiveDateTime, Utc}; use serde_json::Value; -use crate::util::LowerCase; +use crate::{config::PathType, util::LowerCase, CONFIG}; use super::{OrganizationId, User, UserId}; use id::SendId; @@ -226,7 +226,8 @@ impl Send { self.update_users_revision(conn).await; if self.atype == SendType::File as i32 { - std::fs::remove_dir_all(std::path::Path::new(&crate::CONFIG.sends_folder()).join(&self.uuid)).ok(); + let operator = CONFIG.opendal_operator_for_path_type(PathType::Sends)?; + operator.remove_all(&self.uuid).await.ok(); } db_run! { conn: { diff --git a/src/error.rs b/src/error.rs index a7a8ab07..04de98a4 100644 --- a/src/error.rs +++ b/src/error.rs @@ -46,6 +46,7 @@ use jsonwebtoken::errors::Error as JwtErr; use lettre::address::AddressError as AddrErr; use lettre::error::Error as LettreErr; use lettre::transport::smtp::Error as SmtpErr; +use opendal::Error as OpenDALErr; use openssl::error::ErrorStack as SSLErr; use regex::Error as RegexErr; use reqwest::Error as ReqErr; @@ -98,6 +99,8 @@ make_error! { DieselCon(DieselConErr): _has_source, _api_error, Webauthn(WebauthnErr): _has_source, _api_error, + + OpenDAL(OpenDALErr): _has_source, _api_error, } impl std::fmt::Debug for Error { diff --git a/src/main.rs b/src/main.rs index ccb64914..fc104997 100644 --- a/src/main.rs +++ b/src/main.rs @@ -61,7 +61,7 @@ mod util; use crate::api::core::two_factor::duo_oidc::purge_duo_contexts; use crate::api::purge_auth_requests; use crate::api::{WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS}; -pub use config::CONFIG; +pub use config::{PathType, CONFIG}; pub use error::{Error, MapResult}; use rocket::data::{Limits, ToByteUnit}; use std::sync::{atomic::Ordering, Arc}; @@ -75,16 +75,13 @@ async fn main() -> Result<(), Error> { let level = init_logging()?; check_data_folder().await; - auth::initialize_keys().unwrap_or_else(|e| { + auth::initialize_keys().await.unwrap_or_else(|e| { error!("Error creating private key '{}'\n{e:?}\nExiting Vaultwarden!", CONFIG.private_rsa_key()); exit(1); }); check_web_vault(); - create_dir(&CONFIG.icon_cache_folder(), "icon cache"); create_dir(&CONFIG.tmp_folder(), "tmp folder"); - create_dir(&CONFIG.sends_folder(), "sends folder"); - create_dir(&CONFIG.attachments_folder(), "attachments folder"); let pool = create_db_pool().await; schedule_jobs(pool.clone()); @@ -464,6 +461,24 @@ fn create_dir(path: &str, description: &str) { async fn check_data_folder() { let data_folder = &CONFIG.data_folder(); + + if data_folder.starts_with("s3://") { + if let Err(e) = CONFIG + .opendal_operator_for_path_type(PathType::Data) + .unwrap_or_else(|e| { + error!("Failed to create S3 operator for data folder '{data_folder}': {e:?}"); + exit(1); + }) + .check() + .await + { + error!("Could not access S3 data folder '{data_folder}': {e:?}"); + exit(1); + } + + return; + } + let path = Path::new(data_folder); if !path.exists() { error!("Data folder '{data_folder}' doesn't exist."); diff --git a/src/util.rs b/src/util.rs index 2d6dc3a5..c8a86d43 100644 --- a/src/util.rs +++ b/src/util.rs @@ -16,7 +16,7 @@ use tokio::{ time::{sleep, Duration}, }; -use crate::CONFIG; +use crate::{config::PathType, CONFIG}; pub struct AppHeaders(); @@ -827,6 +827,26 @@ pub fn is_global(ip: std::net::IpAddr) -> bool { ip.is_global() } +/// Saves a Rocket temporary file to the OpenDAL Operator at the given path. +pub async fn save_temp_file( + path_type: PathType, + path: &str, + temp_file: rocket::fs::TempFile<'_>, + overwrite: bool, +) -> Result<(), crate::Error> { + use futures::AsyncWriteExt as _; + use tokio_util::compat::TokioAsyncReadCompatExt as _; + + let operator = CONFIG.opendal_operator_for_path_type(path_type)?; + + let mut read_stream = temp_file.open().await?.compat(); + let mut writer = operator.writer_with(path).if_not_exists(!overwrite).await?.into_futures_async_write(); + futures::io::copy(&mut read_stream, &mut writer).await?; + writer.close().await?; + + Ok(()) +} + /// These are some tests to check that the implementations match /// The IPv4 can be all checked in 30 seconds or so and they are correct as of nightly 2023-07-17 /// The IPV6 can't be checked in a reasonable time, so we check over a hundred billion random ones, so far correct