Browse Source

Merge d416212de6 into fdc156b247

pull/5910/merge
Chase Douglas 6 days ago
committed by GitHub
parent
commit
f69c70c5ef
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 168
      Cargo.lock
  2. 5
      Cargo.toml
  3. 6
      build.rs
  4. 22
      src/aws.rs
  5. 13
      src/config.rs
  6. 86
      src/http_client.rs
  7. 47
      src/mail.rs
  8. 2
      src/main.rs
  9. 36
      src/storage.rs

168
Cargo.lock

@ -320,6 +320,16 @@ dependencies = [
"syn 3.0.3", "syn 3.0.3",
] ]
[[package]]
name = "asyncband"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94a214ba60d6231afd0e805e3c27c45a1626d9debaa5a5061c45a1ea1b2f1ed0"
dependencies = [
"hashbrown 0.17.1",
"slab",
]
[[package]] [[package]]
name = "atomic" name = "atomic"
version = "0.5.3" version = "0.5.3"
@ -359,11 +369,11 @@ dependencies = [
"aws-sdk-ssooidc", "aws-sdk-ssooidc",
"aws-sdk-sts", "aws-sdk-sts",
"aws-smithy-async", "aws-smithy-async",
"aws-smithy-http", "aws-smithy-http 0.64.0",
"aws-smithy-json", "aws-smithy-json 0.63.0",
"aws-smithy-runtime", "aws-smithy-runtime",
"aws-smithy-runtime-api", "aws-smithy-runtime-api",
"aws-smithy-schema", "aws-smithy-schema 0.2.0",
"aws-smithy-types", "aws-smithy-types",
"aws-types", "aws-types",
"bytes", "bytes",
@ -399,7 +409,7 @@ dependencies = [
"aws-credential-types", "aws-credential-types",
"aws-sigv4", "aws-sigv4",
"aws-smithy-async", "aws-smithy-async",
"aws-smithy-http", "aws-smithy-http 0.64.0",
"aws-smithy-runtime", "aws-smithy-runtime",
"aws-smithy-runtime-api", "aws-smithy-runtime-api",
"aws-smithy-types", "aws-smithy-types",
@ -415,6 +425,30 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "aws-sdk-sesv2"
version = "1.118.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8d0642857f4fe76cd9a3d8c4f2b393546f7561f7725052dd9f268005fda92b7"
dependencies = [
"aws-credential-types",
"aws-runtime",
"aws-smithy-async",
"aws-smithy-http 0.63.6",
"aws-smithy-json 0.62.7",
"aws-smithy-observability 0.2.6",
"aws-smithy-runtime",
"aws-smithy-runtime-api",
"aws-smithy-types",
"aws-types",
"bytes",
"fastrand",
"http 0.2.12",
"http 1.5.0",
"regex-lite",
"tracing",
]
[[package]] [[package]]
name = "aws-sdk-sso" name = "aws-sdk-sso"
version = "1.105.0" version = "1.105.0"
@ -425,12 +459,12 @@ dependencies = [
"aws-credential-types", "aws-credential-types",
"aws-runtime", "aws-runtime",
"aws-smithy-async", "aws-smithy-async",
"aws-smithy-http", "aws-smithy-http 0.64.0",
"aws-smithy-json", "aws-smithy-json 0.63.0",
"aws-smithy-observability", "aws-smithy-observability 0.3.0",
"aws-smithy-runtime", "aws-smithy-runtime",
"aws-smithy-runtime-api", "aws-smithy-runtime-api",
"aws-smithy-schema", "aws-smithy-schema 0.2.0",
"aws-smithy-types", "aws-smithy-types",
"aws-types", "aws-types",
"bytes", "bytes",
@ -451,12 +485,12 @@ dependencies = [
"aws-credential-types", "aws-credential-types",
"aws-runtime", "aws-runtime",
"aws-smithy-async", "aws-smithy-async",
"aws-smithy-http", "aws-smithy-http 0.64.0",
"aws-smithy-json", "aws-smithy-json 0.63.0",
"aws-smithy-observability", "aws-smithy-observability 0.3.0",
"aws-smithy-runtime", "aws-smithy-runtime",
"aws-smithy-runtime-api", "aws-smithy-runtime-api",
"aws-smithy-schema", "aws-smithy-schema 0.2.0",
"aws-smithy-types", "aws-smithy-types",
"aws-types", "aws-types",
"bytes", "bytes",
@ -477,13 +511,13 @@ dependencies = [
"aws-credential-types", "aws-credential-types",
"aws-runtime", "aws-runtime",
"aws-smithy-async", "aws-smithy-async",
"aws-smithy-http", "aws-smithy-http 0.64.0",
"aws-smithy-json", "aws-smithy-json 0.63.0",
"aws-smithy-observability", "aws-smithy-observability 0.3.0",
"aws-smithy-query", "aws-smithy-query",
"aws-smithy-runtime", "aws-smithy-runtime",
"aws-smithy-runtime-api", "aws-smithy-runtime-api",
"aws-smithy-schema", "aws-smithy-schema 0.2.0",
"aws-smithy-types", "aws-smithy-types",
"aws-smithy-xml", "aws-smithy-xml",
"aws-types", "aws-types",
@ -501,7 +535,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "723c2234ad7511ceef63eab016b7ba6ff7c55590fefb96fa8467af014a07309f" checksum = "723c2234ad7511ceef63eab016b7ba6ff7c55590fefb96fa8467af014a07309f"
dependencies = [ dependencies = [
"aws-credential-types", "aws-credential-types",
"aws-smithy-http", "aws-smithy-http 0.64.0",
"aws-smithy-runtime-api", "aws-smithy-runtime-api",
"aws-smithy-types", "aws-smithy-types",
"bytes", "bytes",
@ -527,6 +561,27 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "aws-smithy-http"
version = "0.63.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231"
dependencies = [
"aws-smithy-runtime-api",
"aws-smithy-types",
"bytes",
"bytes-utils",
"futures-core",
"futures-util",
"http 1.5.0",
"http-body 1.1.0",
"http-body-util",
"percent-encoding",
"pin-project-lite",
"pin-utils",
"tracing",
]
[[package]] [[package]]
name = "aws-smithy-http" name = "aws-smithy-http"
version = "0.64.0" version = "0.64.0"
@ -548,6 +603,17 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "aws-smithy-json"
version = "0.62.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "701a947f4797e52a911e114a898667c746c39feea467bbd1abd7b3721f702ffa"
dependencies = [
"aws-smithy-runtime-api",
"aws-smithy-schema 0.1.0",
"aws-smithy-types",
]
[[package]] [[package]]
name = "aws-smithy-json" name = "aws-smithy-json"
version = "0.63.0" version = "0.63.0"
@ -555,10 +621,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dc65a121adb4b33729919fcfa14fa36fb33c1555a8f06bb0e2188dbfdc1d9ef" checksum = "3dc65a121adb4b33729919fcfa14fa36fb33c1555a8f06bb0e2188dbfdc1d9ef"
dependencies = [ dependencies = [
"aws-smithy-runtime-api", "aws-smithy-runtime-api",
"aws-smithy-schema", "aws-smithy-schema 0.2.0",
"aws-smithy-types", "aws-smithy-types",
] ]
[[package]]
name = "aws-smithy-observability"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06c2315d173edbf1920da8ba3a7189695827002e4c0fc961973ab1c54abca9c"
dependencies = [
"aws-smithy-runtime-api",
]
[[package]] [[package]]
name = "aws-smithy-observability" name = "aws-smithy-observability"
version = "0.3.0" version = "0.3.0"
@ -575,7 +650,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512346c7212ab7436df2d77a16d976a468ae44a418835511d2a69269810aaf62" checksum = "512346c7212ab7436df2d77a16d976a468ae44a418835511d2a69269810aaf62"
dependencies = [ dependencies = [
"aws-smithy-runtime-api", "aws-smithy-runtime-api",
"aws-smithy-schema", "aws-smithy-schema 0.2.0",
"aws-smithy-types", "aws-smithy-types",
"aws-smithy-xml", "aws-smithy-xml",
"urlencoding", "urlencoding",
@ -588,10 +663,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07505b34e8f4b3591a4fa69e9792b52289b95488dbbc68c3c0075b7bedb245e1" checksum = "07505b34e8f4b3591a4fa69e9792b52289b95488dbbc68c3c0075b7bedb245e1"
dependencies = [ dependencies = [
"aws-smithy-async", "aws-smithy-async",
"aws-smithy-http", "aws-smithy-http 0.64.0",
"aws-smithy-observability", "aws-smithy-observability 0.3.0",
"aws-smithy-runtime-api", "aws-smithy-runtime-api",
"aws-smithy-schema", "aws-smithy-schema 0.2.0",
"aws-smithy-types", "aws-smithy-types",
"bytes", "bytes",
"fastrand", "fastrand",
@ -635,6 +710,17 @@ dependencies = [
"syn 2.0.119", "syn 2.0.119",
] ]
[[package]]
name = "aws-smithy-schema"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7442cb268338f0eb8278140a107c046756aa01093d8ef5e99628d34ae09c94f5"
dependencies = [
"aws-smithy-runtime-api",
"aws-smithy-types",
"http 1.5.0",
]
[[package]] [[package]]
name = "aws-smithy-schema" name = "aws-smithy-schema"
version = "0.2.0" version = "0.2.0"
@ -655,6 +741,7 @@ dependencies = [
"base64-simd", "base64-simd",
"bytes", "bytes",
"bytes-utils", "bytes-utils",
"futures-core",
"http 0.2.12", "http 0.2.12",
"http 1.5.0", "http 1.5.0",
"http-body 0.4.6", "http-body 0.4.6",
@ -667,6 +754,8 @@ dependencies = [
"ryu", "ryu",
"serde", "serde",
"time", "time",
"tokio",
"tokio-util",
] ]
[[package]] [[package]]
@ -676,7 +765,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce84f71c72fee2cbbadde6e7d082f5fb466e3a84733855295fa7aafd1b31b7d8" checksum = "ce84f71c72fee2cbbadde6e7d082f5fb466e3a84733855295fa7aafd1b31b7d8"
dependencies = [ dependencies = [
"aws-smithy-runtime-api", "aws-smithy-runtime-api",
"aws-smithy-schema", "aws-smithy-schema 0.2.0",
"aws-smithy-types", "aws-smithy-types",
"xmlparser", "xmlparser",
] ]
@ -690,7 +779,7 @@ dependencies = [
"aws-credential-types", "aws-credential-types",
"aws-smithy-async", "aws-smithy-async",
"aws-smithy-runtime-api", "aws-smithy-runtime-api",
"aws-smithy-schema", "aws-smithy-schema 0.2.0",
"aws-smithy-types", "aws-smithy-types",
"rustc_version", "rustc_version",
"tracing", "tracing",
@ -3100,15 +3189,6 @@ dependencies = [
"digest 0.11.3", "digest 0.11.3",
] ]
[[package]]
name = "mea"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31fc7d159de0085ab6dd7ff145a9819442cfd3d098f783263120503c3f3e58b0"
dependencies = [
"slab",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.8.3" version = "2.8.3"
@ -3437,11 +3517,12 @@ dependencies = [
[[package]] [[package]]
name = "opendal-core" name = "opendal-core"
version = "0.58.1" version = "0.58.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec75551ff4cf3e57da98979f6a937aaa9ddb3915bf68cc17d03df733be6646ed" checksum = "48dbcef97d3eb7591db2c18d5cae95c836bcce07359b98d98dd6f4e861eb77b7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"asyncband",
"base64 0.23.1", "base64 0.23.1",
"bytes", "bytes",
"futures", "futures",
@ -3449,7 +3530,6 @@ dependencies = [
"jiff", "jiff",
"log", "log",
"md-5", "md-5",
"mea",
"percent-encoding", "percent-encoding",
"quick-xml", "quick-xml",
"reqsign-core", "reqsign-core",
@ -3461,6 +3541,20 @@ dependencies = [
"web-time", "web-time",
] ]
[[package]]
name = "opendal-http-transport-reqwest"
version = "0.58.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85663452ea32bbc17e8f79ab29788c846d116ec7de31451be9c787e462dcb36c"
dependencies = [
"bytes",
"futures",
"http 1.5.0",
"http-body 1.1.0",
"opendal-core",
"reqwest",
]
[[package]] [[package]]
name = "opendal-service-fs" name = "opendal-service-fs"
version = "0.58.1" version = "0.58.1"
@ -5851,6 +5945,7 @@ dependencies = [
"argon2", "argon2",
"aws-config", "aws-config",
"aws-credential-types", "aws-credential-types",
"aws-sdk-sesv2",
"aws-smithy-runtime-api", "aws-smithy-runtime-api",
"bigdecimal", "bigdecimal",
"bytes", "bytes",
@ -5889,6 +5984,7 @@ dependencies = [
"num-derive", "num-derive",
"num-traits", "num-traits",
"opendal", "opendal",
"opendal-http-transport-reqwest",
"openidconnect", "openidconnect",
"openssl", "openssl",
"pastey 0.2.3", "pastey 0.2.3",

5
Cargo.toml

@ -39,8 +39,10 @@ vendored_openssl = ["openssl/vendored"]
# Enable MiMalloc memory allocator to replace the default malloc # Enable MiMalloc memory allocator to replace the default malloc
# This can improve performance for Alpine builds # This can improve performance for Alpine builds
enable_mimalloc = ["dep:mimalloc"] enable_mimalloc = ["dep:mimalloc"]
aws = ["s3", "ses"]
s3 = [ s3 = [
"opendal/services-s3", "opendal/services-s3",
"dep:opendal-http-transport-reqwest",
"dep:aws-config", "dep:aws-config",
"dep:aws-credential-types", "dep:aws-credential-types",
"dep:aws-smithy-runtime-api", "dep:aws-smithy-runtime-api",
@ -48,6 +50,7 @@ s3 = [
"dep:reqsign-aws-v4", "dep:reqsign-aws-v4",
"dep:reqsign-core", "dep:reqsign-core",
] ]
ses = ["dep:aws-config", "dep:aws-sdk-sesv2", "dep:aws-smithy-runtime-api"]
# OIDC specific features # OIDC specific features
oidc-accept-rfc3339-timestamps = ["openidconnect/accept-rfc3339-timestamps"] oidc-accept-rfc3339-timestamps = ["openidconnect/accept-rfc3339-timestamps"]
@ -258,6 +261,7 @@ grass_compiler = { version = "0.13.4", default-features = false }
# File are accessed through Apache OpenDAL # File are accessed through Apache OpenDAL
opendal = { version = "0.58.1", default-features = false, features = ["services-fs"] } opendal = { version = "0.58.1", default-features = false, features = ["services-fs"] }
opendal-http-transport-reqwest = { version = "0.58.1", default-features = false, features = ["rustls-no-provider"], optional = true }
# For retrieving AWS credentials, including temporary SSO credentials # For retrieving AWS credentials, including temporary SSO credentials
aws-config = { version = "1.10.1", optional = true, default-features = false, features = [ aws-config = { version = "1.10.1", optional = true, default-features = false, features = [
@ -267,6 +271,7 @@ aws-config = { version = "1.10.1", optional = true, default-features = false, fe
"sso", "sso",
] } ] }
aws-credential-types = { version = "1.3.0", optional = true } aws-credential-types = { version = "1.3.0", optional = true }
aws-sdk-sesv2 = { version = "1.118.0", features = ["behavior-version-latest", "rt-tokio"], default-features = false, optional = true }
aws-smithy-runtime-api = { version = "1.14.0", optional = true } aws-smithy-runtime-api = { version = "1.14.0", optional = true }
http = { version = "1.5.0", optional = true } http = { version = "1.5.0", optional = true }
reqsign-aws-v4 = { version = "3.1.0", optional = true } reqsign-aws-v4 = { version = "3.1.0", optional = true }

6
build.rs

@ -15,6 +15,10 @@ fn main() {
#[cfg(feature = "s3")] #[cfg(feature = "s3")]
println!("cargo:rustc-cfg=s3"); println!("cargo:rustc-cfg=s3");
#[cfg(feature = "ses")]
println!("cargo:rustc-cfg=ses");
#[cfg(feature = "aws")]
println!("cargo:rustc-cfg=aws");
// Use check-cfg to let cargo know which cfg's we define, // Use check-cfg to let cargo know which cfg's we define,
// and avoid warnings when they are used in the code. // and avoid warnings when they are used in the code.
@ -22,6 +26,8 @@ fn main() {
println!("cargo::rustc-check-cfg=cfg(mysql)"); println!("cargo::rustc-check-cfg=cfg(mysql)");
println!("cargo::rustc-check-cfg=cfg(postgresql)"); println!("cargo::rustc-check-cfg=cfg(postgresql)");
println!("cargo::rustc-check-cfg=cfg(s3)"); println!("cargo::rustc-check-cfg=cfg(s3)");
println!("cargo::rustc-check-cfg=cfg(ses)");
println!("cargo::rustc-check-cfg=cfg(aws)");
// Rerun when these paths are changed. // Rerun when these paths are changed.
// Someone could have checked-out a tag or specific commit, but no other files changed. // Someone could have checked-out a tag or specific commit, but no other files changed.

22
src/aws.rs

@ -0,0 +1,22 @@
use aws_config::{AppName, BehaviorVersion};
use tokio::sync::OnceCell;
use crate::http_client::aws::AwsReqwestConnector;
fn aws_reqwest_connector() -> AwsReqwestConnector {
AwsReqwestConnector::new()
}
pub(crate) async fn aws_sdk_config() -> &'static aws_config::SdkConfig {
static AWS_CONFIG: OnceCell<aws_config::SdkConfig> = OnceCell::const_new();
AWS_CONFIG
.get_or_init(async || {
aws_config::defaults(BehaviorVersion::latest())
.app_name(AppName::new("vaultwarden").expect("Failed to build AWS app name"))
.http_client(aws_reqwest_connector())
.load()
.await
})
.await
}

13
src/config.rs

@ -919,12 +919,14 @@ make_config! {
smtp_accept_invalid_certs: bool, true, def, false; smtp_accept_invalid_certs: bool, true, def, false;
/// Accept Invalid Hostnames (Know the risks!) |> DANGEROUS: Allow invalid hostnames. This option introduces significant vulnerabilities to man-in-the-middle attacks! /// Accept Invalid Hostnames (Know the risks!) |> DANGEROUS: Allow invalid hostnames. This option introduces significant vulnerabilities to man-in-the-middle attacks!
smtp_accept_invalid_hostnames: bool, true, def, false; smtp_accept_invalid_hostnames: bool, true, def, false;
/// Use AWS SES |> Whether to send mail via AWS Simple Email Service (SES)
use_aws_ses: bool, true, def, false;
}, },
/// Email 2FA Settings /// Email 2FA Settings
email_2fa: _enable_email_2fa { email_2fa: _enable_email_2fa {
/// Enabled |> Disabling will prevent users from setting up new email 2FA and using existing email 2FA configured /// Enabled |> Disabling will prevent users from setting up new email 2FA and using existing email 2FA configured
_enable_email_2fa: bool, true, auto, |c| c._enable_smtp && (c.smtp_host.is_some() || c.use_sendmail); _enable_email_2fa: bool, true, auto, |c| c._enable_smtp && (c.smtp_host.is_some() || c.use_sendmail || c.use_aws_ses);
/// Email token size |> Number of digits in an email 2FA token (min: 6, max: 255). Note that the Bitwarden clients are hardcoded to mention 6 digit codes regardless of this setting. /// Email token size |> Number of digits in an email 2FA token (min: 6, max: 255). Note that the Bitwarden clients are hardcoded to mention 6 digit codes regardless of this setting.
email_token_size: u8, true, def, 6; email_token_size: u8, true, def, 6;
/// Token expiration time |> Maximum time in seconds a token is valid. The time the user has to open email client and copy token. /// Token expiration time |> Maximum time in seconds a token is valid. The time the user has to open email client and copy token.
@ -1169,6 +1171,9 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> {
} }
} }
} }
} else if cfg.use_aws_ses {
#[cfg(not(ses))]
err!("`USE_AWS_SES` is set, but the `ses` feature is not enabled in this build");
} else { } else {
if cfg.smtp_host.is_some() == cfg.smtp_from.is_empty() { if cfg.smtp_host.is_some() == cfg.smtp_from.is_empty() {
err!("Both `SMTP_HOST` and `SMTP_FROM` need to be set for email support without `USE_SENDMAIL`") err!("Both `SMTP_HOST` and `SMTP_FROM` need to be set for email support without `USE_SENDMAIL`")
@ -1181,7 +1186,7 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> {
} }
} }
if (cfg.smtp_host.is_some() || cfg.use_sendmail) && !is_valid_email(&cfg.smtp_from) { if (cfg.smtp_host.is_some() || cfg.use_sendmail || cfg.use_aws_ses) && !is_valid_email(&cfg.smtp_from) {
err!(format!("SMTP_FROM '{}' is not a valid email address", cfg.smtp_from)) err!(format!("SMTP_FROM '{}' is not a valid email address", cfg.smtp_from))
} }
@ -1190,7 +1195,7 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> {
} }
} }
if cfg._enable_email_2fa && !(cfg.smtp_host.is_some() || cfg.use_sendmail) { if cfg._enable_email_2fa && !(cfg.smtp_host.is_some() || cfg.use_sendmail || cfg.use_aws_ses) {
err!("To enable email 2FA, a mail transport must be configured") err!("To enable email 2FA, a mail transport must be configured")
} }
@ -1612,7 +1617,7 @@ impl Config {
} }
pub fn mail_enabled(&self) -> bool { pub fn mail_enabled(&self) -> bool {
let inner = &self.inner.read().unwrap().config; let inner = &self.inner.read().unwrap().config;
inner._enable_smtp && (inner.smtp_host.is_some() || inner.use_sendmail) inner._enable_smtp && (inner.smtp_host.is_some() || inner.use_sendmail || inner.use_aws_ses)
} }
pub async fn get_duo_akey(&self) -> String { pub async fn get_duo_akey(&self) -> String {

86
src/http_client.rs

@ -14,7 +14,10 @@ use reqwest::{
}; };
use url::Host; use url::Host;
use crate::{CONFIG, util::is_global}; use crate::{
CONFIG,
util::{get_env_bool, is_global},
};
pub fn make_http_request(method: reqwest::Method, url: &str) -> Result<reqwest::RequestBuilder, crate::Error> { pub fn make_http_request(method: reqwest::Method, url: &str) -> Result<reqwest::RequestBuilder, crate::Error> {
static INSTANCE: LazyLock<Client> = static INSTANCE: LazyLock<Client> =
@ -36,7 +39,7 @@ pub fn get_reqwest_client_builder(enforce_block: bool) -> ClientBuilder {
let mut headers = header::HeaderMap::new(); let mut headers = header::HeaderMap::new();
headers.insert(header::USER_AGENT, header::HeaderValue::from_static("Vaultwarden")); headers.insert(header::USER_AGENT, header::HeaderValue::from_static("Vaultwarden"));
let redirect_policy = reqwest::redirect::Policy::custom(|attempt| { let redirect_policy = reqwest::redirect::Policy::custom(move |attempt| {
if attempt.previous().len() >= 5 { if attempt.previous().len() >= 5 {
return attempt.error("Too many redirects"); return attempt.error("Too many redirects");
} }
@ -45,7 +48,7 @@ pub fn get_reqwest_client_builder(enforce_block: bool) -> ClientBuilder {
return attempt.error("Invalid host"); return attempt.error("Invalid host");
}; };
if let Err(e) = should_block_host(&host) { if enforce_block && let Err(e) = should_block_host(&host) {
return attempt.error(e); return attempt.error(e);
} }
@ -59,6 +62,14 @@ pub fn get_reqwest_client_builder(enforce_block: bool) -> ClientBuilder {
.timeout(Duration::from_secs(10)) .timeout(Duration::from_secs(10))
} }
fn dns_prefer_ipv6() -> bool {
// CONFIG may require DNS to initialize, so avoid forcing it during bootstrap.
match LazyLock::get(&CONFIG) {
Some(config) => config.dns_prefer_ipv6(),
None => get_env_bool("DNS_PREFER_IPV6").unwrap_or(false),
}
}
fn should_block_ip(ip: IpAddr) -> bool { fn should_block_ip(ip: IpAddr) -> bool {
if !CONFIG.http_request_block_non_global_ips() { if !CONFIG.http_request_block_non_global_ips() {
return false; return false;
@ -258,12 +269,8 @@ impl CustomDnsResolver {
fn new() -> Arc<Self> { fn new() -> Arc<Self> {
TokioResolver::builder(TokioRuntimeProvider::default()) TokioResolver::builder(TokioRuntimeProvider::default())
.and_then(|mut builder| { .and_then(|mut builder| {
// Hickory's default since v0.26 is `Ipv6AndIpv4`, which sorts IPv6 first // Query both families; the preferred order is applied per lookup below.
// This might cause issues on IPv4 only systems or containers builder.options_mut().ip_strategy = hickory_resolver::config::LookupIpStrategy::Ipv4AndIpv6;
// Unless someone enabled DNS_PREFER_IPV6, use Ipv4AndIpv6, which returns IPv4 first which was our previous default
if !CONFIG.dns_prefer_ipv6() {
builder.options_mut().ip_strategy = hickory_resolver::config::LookupIpStrategy::Ipv4AndIpv6;
}
builder.build() builder.build()
}) })
.inspect_err(|e| warn!("Error creating Hickory resolver, falling back to default: {e:?}")) .inspect_err(|e| warn!("Error creating Hickory resolver, falling back to default: {e:?}"))
@ -289,6 +296,14 @@ impl CustomDnsResolver {
} }
} }
fn sort_addresses(addresses: &mut [SocketAddr], prefer_ipv6: bool) {
if prefer_ipv6 {
addresses.sort_by_key(SocketAddr::is_ipv4);
} else {
addresses.sort_by_key(SocketAddr::is_ipv6);
}
}
fn pre_resolve(name: &str, enforce_block: bool) -> Result<(), CustomHttpClientError> { fn pre_resolve(name: &str, enforce_block: bool) -> Result<(), CustomHttpClientError> {
let Ok(host) = get_valid_host(name) else { let Ok(host) = get_valid_host(name) else {
return Err(CustomHttpClientError::Invalid { return Err(CustomHttpClientError::Invalid {
@ -320,7 +335,9 @@ impl Resolve for CustomDns {
let this = Arc::clone(&self.resolver); let this = Arc::clone(&self.resolver);
Box::pin(async move { Box::pin(async move {
let name = name.as_str(); let name = name.as_str();
let results = this.resolve_domain(name, enforce_block).await?; let mut results = this.resolve_domain(name, enforce_block).await?;
// Recheck after bootstrap so long-lived clients adopt the loaded config.
sort_addresses(&mut results, dns_prefer_ipv6());
if results.is_empty() { if results.is_empty() {
warn!("Unable to resolve {name} to any valid IP address"); warn!("Unable to resolve {name} to any valid IP address");
} }
@ -329,7 +346,7 @@ impl Resolve for CustomDns {
} }
} }
#[cfg(s3)] #[cfg(any(s3, ses))]
pub(crate) mod aws { pub(crate) mod aws {
use aws_smithy_runtime_api::client::{ use aws_smithy_runtime_api::client::{
http::{HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings, SharedHttpConnector}, http::{HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings, SharedHttpConnector},
@ -339,10 +356,29 @@ pub(crate) mod aws {
}; };
use reqwest::Client; use reqwest::Client;
use super::get_reqwest_client_builder;
// Adapter that wraps reqwest to be compatible with the AWS SDK // Adapter that wraps reqwest to be compatible with the AWS SDK
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct AwsReqwestConnector { pub(crate) struct AwsReqwestConnector {
pub(crate) client: Client, client: Client,
}
impl AwsReqwestConnector {
pub(crate) fn new() -> Self {
let client = get_reqwest_client_builder(false).build().expect("Failed to build AWS HTTP client");
Self {
client,
}
}
}
fn connector_error(error: reqwest::Error) -> ConnectorError {
if error.is_timeout() {
ConnectorError::timeout(Box::new(error))
} else {
ConnectorError::io(Box::new(error))
}
} }
impl HttpConnector for AwsReqwestConnector { impl HttpConnector for AwsReqwestConnector {
@ -362,10 +398,10 @@ pub(crate) mod aws {
req_builder = req_builder.body(body_bytes.to_vec()); req_builder = req_builder.body(body_bytes.to_vec());
} }
let response = req_builder.send().await.map_err(|e| ConnectorError::io(Box::new(e)))?; let response = req_builder.send().await.map_err(connector_error)?;
let status = response.status().into(); let status = response.status().into();
let bytes = response.bytes().await.map_err(|e| ConnectorError::io(Box::new(e)))?; let bytes = response.bytes().await.map_err(connector_error)?;
Ok(HttpResponse::new(status, bytes.into())) Ok(HttpResponse::new(status, bytes.into()))
}; };
@ -391,7 +427,7 @@ pub(crate) mod aws {
mod tests { mod tests {
use super::*; use super::*;
use crate::util::is_global_hardcoded; use crate::util::is_global_hardcoded;
use std::net::Ipv4Addr; use std::net::{Ipv4Addr, Ipv6Addr};
use url::Host; use url::Host;
// === // ===
@ -404,6 +440,26 @@ mod tests {
} }
} }
#[test]
fn dns_setup_does_not_initialize_config() {
assert!(LazyLock::get(&CONFIG).is_none());
drop(CustomDns::instance(false));
assert!(LazyLock::get(&CONFIG).is_none());
}
#[test]
fn dns_preference_orders_addresses() {
let ipv4 = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0);
let ipv6 = SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0);
let mut addresses = [ipv6, ipv4];
sort_addresses(&mut addresses, false);
assert_eq!(addresses, [ipv4, ipv6]);
sort_addresses(&mut addresses, true);
assert_eq!(addresses, [ipv6, ipv4]);
}
#[test] #[test]
fn dotted_decimal_loopback_normalizes() { fn dotted_decimal_loopback_normalizes() {
let ip = parse_to_ip("127.0.0.1").unwrap(); let ip = parse_to_ip("127.0.0.1").unwrap();

47
src/mail.rs

@ -96,6 +96,44 @@ fn smtp_transport() -> AsyncSmtpTransport<Tokio1Executor> {
smtp_client.build() smtp_client.build()
} }
#[cfg(ses)]
async fn send_with_aws_ses(email: Message) -> std::io::Result<()> {
use std::io::Error;
use aws_sdk_sesv2::{
Client,
types::{EmailContent, RawMessage},
};
use tokio::sync::OnceCell;
static AWS_SESV2_CLIENT: OnceCell<Client> = OnceCell::const_new();
let client = AWS_SESV2_CLIENT
.get_or_init(async || {
let config = crate::aws::aws_sdk_config().await;
Client::new(config)
})
.await;
client
.send_email()
.content(
EmailContent::builder()
.raw(
RawMessage::builder()
.data(email.formatted().into())
.build()
.map_err(|e| Error::other(format!("Failed to build AWS SESv2 RawMessage: {e:?}")))?,
)
.build(),
)
.send()
.await
.map_err(Error::other)?;
Ok(())
}
// This will sanitize the string values by stripping all the html tags to prevent XSS and HTML Injections // This will sanitize the string values by stripping all the html tags to prevent XSS and HTML Injections
fn sanitize_data(data: &mut serde_json::Value) { fn sanitize_data(data: &mut serde_json::Value) {
use regex::Regex; use regex::Regex;
@ -676,6 +714,15 @@ async fn send_with_selected_transport(email: Message) -> EmptyResult {
err!(format!("Sendmail error: {e}")); err!(format!("Sendmail error: {e}"));
} }
} }
} else if CONFIG.use_aws_ses() {
#[cfg(ses)]
match send_with_aws_ses(email).await {
Ok(()) => Ok(()),
Err(e) => err!("Failed to send email", format!("Failed to send email using AWS SES: {e:?}")),
}
#[cfg(not(ses))]
unreachable!("Failed to send email using AWS SES: `ses` feature is not enabled");
} else { } else {
match smtp_transport().send(email).await { match smtp_transport().send(email).await {
Ok(_) => Ok(()), Ok(_) => Ok(()),

2
src/main.rs

@ -51,6 +51,8 @@ use rocket::data::{Limits, ToByteUnit};
mod error; mod error;
mod api; mod api;
mod auth; mod auth;
#[cfg(any(s3, ses))]
mod aws;
mod config; mod config;
mod crypto; mod crypto;
#[macro_use] #[macro_use]

36
src/storage.rs

@ -77,10 +77,18 @@ pub(crate) fn operator_for_path(path: &str) -> Result<opendal::Operator, crate::
#[cfg(s3)] #[cfg(s3)]
mod s3 { mod s3 {
use std::sync::LazyLock;
use opendal_http_transport_reqwest::ReqwestTransport;
use reqwest::Url; use reqwest::Url;
use crate::error::Error; use crate::error::Error;
static HTTP_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
// Storage endpoints are administrator-configured and may be private.
crate::http_client::get_reqwest_client_builder(false).build().expect("Failed to build OpenDAL HTTP client")
});
pub(super) fn is_uri(path: &str) -> bool { pub(super) fn is_uri(path: &str) -> bool {
path.starts_with("s3://") path.starts_with("s3://")
} }
@ -152,8 +160,6 @@ mod s3 {
} }
pub(super) fn operator_for_path(path: &str) -> Result<opendal::Operator, Error> { pub(super) fn operator_for_path(path: &str) -> Result<opendal::Operator, Error> {
use crate::http_client::aws::AwsReqwestConnector;
use aws_config::{default_provider::credentials::DefaultCredentialsChain, provider_config::ProviderConfig};
use opendal::Configurator; use opendal::Configurator;
use reqsign_aws_v4::Credential; use reqsign_aws_v4::Credential;
use reqsign_core::{Context, ProvideCredential, ProvideCredentialChain}; use reqsign_core::{Context, ProvideCredential, ProvideCredentialChain};
@ -171,24 +177,12 @@ mod s3 {
async fn provide_credential(&self, _ctx: &Context) -> reqsign_core::Result<Option<Self::Credential>> { async fn provide_credential(&self, _ctx: &Context) -> reqsign_core::Result<Option<Self::Credential>> {
use aws_credential_types::provider::ProvideCredentials as _; use aws_credential_types::provider::ProvideCredentials as _;
use reqsign_core::time::Timestamp; use reqsign_core::time::Timestamp;
use tokio::sync::OnceCell;
static DEFAULT_CREDENTIAL_CHAIN: OnceCell<DefaultCredentialsChain> = OnceCell::const_new();
let chain = DEFAULT_CREDENTIAL_CHAIN
.get_or_init(|| {
let reqwest_client = reqwest::Client::builder().build().unwrap();
let connector = AwsReqwestConnector {
client: reqwest_client,
};
let conf = ProviderConfig::default().with_http_client(connector); let credentials_provider =
crate::aws::aws_sdk_config().await.credentials_provider().ok_or_else(|| {
DefaultCredentialsChain::builder().configure(conf).build() reqsign_core::Error::unexpected("failed to load AWS credentials provider from AWS SDK config")
}) })?;
.await; let creds = credentials_provider.provide_credentials().await.map_err(|e| {
let creds = chain.provide_credentials().await.map_err(|e| {
reqsign_core::Error::unexpected("failed to load AWS credentials via AWS SDK").with_source(e) reqsign_core::Error::unexpected("failed to load AWS credentials via AWS SDK").with_source(e)
})?; })?;
@ -236,7 +230,9 @@ mod s3 {
builder.credential_provider_chain(ProvideCredentialChain::new().push(OpenDALS3CredentialProvider)); builder.credential_provider_chain(ProvideCredentialChain::new().push(OpenDALS3CredentialProvider));
} }
Ok(opendal::Operator::new(builder)?) let http_transport = opendal::HttpTransporter::new(ReqwestTransport::new(HTTP_CLIENT.clone()));
let context = opendal::OperationContext::new().with_http_transport(http_transport);
Ok(opendal::Operator::new(builder)?.with_context(context))
} }
fn uri_has_option(uri: &opendal::OperatorUri, names: &[&str]) -> bool { fn uri_has_option(uri: &opendal::OperatorUri, names: &[&str]) -> bool {

Loading…
Cancel
Save