mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2026-05-07 20:11:53 +02:00
OpenDAL S3 storage accepts bucket and root path data today, but serverless deployments also need URI query parameters to describe provider behavior in one DATA_FOLDER value. Update OpenDAL to 0.56.0 and build S3 operators with S3Config::from_uri(). Keep Vaultwarden's AWS SDK credential chain by installing a reqsign provider when the URI does not explicitly request OpenDAL-native credential handling. Move path handling and operator construction into storage.rs so S3-specific parsing, credential setup, and URI path manipulation stay out of configuration handling. Local filesystem behavior is unchanged, and S3 child paths are derived before query strings.
756 lines
28 KiB
Rust
756 lines
28 KiB
Rust
#![cfg_attr(feature = "unstable", feature(ip))]
|
|
// The recursion_limit is mainly triggered by the json!() macro.
|
|
// The more key/value pairs there are the more recursion occurs.
|
|
// We want to keep this as low as possible!
|
|
#![recursion_limit = "165"]
|
|
|
|
// When enabled use MiMalloc as malloc instead of the default malloc
|
|
#[cfg(feature = "enable_mimalloc")]
|
|
use mimalloc::MiMalloc;
|
|
#[cfg(feature = "enable_mimalloc")]
|
|
#[cfg_attr(feature = "enable_mimalloc", global_allocator)]
|
|
static GLOBAL: MiMalloc = MiMalloc;
|
|
|
|
#[macro_use]
|
|
extern crate rocket;
|
|
#[macro_use]
|
|
extern crate serde;
|
|
#[macro_use]
|
|
extern crate serde_json;
|
|
#[macro_use]
|
|
extern crate log;
|
|
#[macro_use]
|
|
extern crate diesel;
|
|
#[macro_use]
|
|
extern crate diesel_migrations;
|
|
#[macro_use]
|
|
extern crate diesel_derive_newtype;
|
|
|
|
use std::{
|
|
collections::HashMap,
|
|
fs::{canonicalize, create_dir_all},
|
|
panic,
|
|
path::Path,
|
|
process::exit,
|
|
str::FromStr,
|
|
thread,
|
|
};
|
|
|
|
use tokio::{
|
|
fs::File,
|
|
io::{AsyncBufReadExt, BufReader},
|
|
};
|
|
|
|
#[cfg(unix)]
|
|
use tokio::signal::unix::SignalKind;
|
|
|
|
#[macro_use]
|
|
mod error;
|
|
mod api;
|
|
mod auth;
|
|
mod config;
|
|
mod crypto;
|
|
#[macro_use]
|
|
mod db;
|
|
mod http_client;
|
|
mod mail;
|
|
mod ratelimit;
|
|
mod sso;
|
|
mod sso_client;
|
|
mod storage;
|
|
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::{PathType, CONFIG};
|
|
pub use error::{Error, MapResult};
|
|
use rocket::data::{Limits, ToByteUnit};
|
|
use std::sync::{atomic::Ordering, Arc};
|
|
pub use util::is_running_in_container;
|
|
|
|
#[rocket::main]
|
|
async fn main() -> Result<(), Error> {
|
|
parse_args();
|
|
launch_info();
|
|
|
|
let level = init_logging()?;
|
|
|
|
check_data_folder().await;
|
|
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.tmp_folder(), "tmp folder");
|
|
|
|
let pool = create_db_pool().await;
|
|
schedule_jobs(pool.clone());
|
|
db::models::TwoFactor::migrate_u2f_to_webauthn(&pool.get().await.unwrap()).await.unwrap();
|
|
db::models::TwoFactor::migrate_credential_to_passkey(&pool.get().await.unwrap()).await.unwrap();
|
|
|
|
let extra_debug = matches!(level, log::LevelFilter::Trace | log::LevelFilter::Debug);
|
|
launch_rocket(pool, extra_debug).await // Blocks until program termination.
|
|
}
|
|
|
|
const HELP: &str = "\
|
|
Alternative implementation of the Bitwarden server API written in Rust
|
|
|
|
USAGE:
|
|
vaultwarden [FLAGS|COMMAND]
|
|
|
|
FLAGS:
|
|
-h, --help Prints help information
|
|
-v, --version Prints the app and web-vault version
|
|
|
|
COMMAND:
|
|
hash [--preset {bitwarden|owasp}] Generate an Argon2id PHC ADMIN_TOKEN
|
|
backup Create a backup of the SQLite database
|
|
You can also send the USR1 signal to trigger a backup
|
|
|
|
PRESETS: m= t= p=
|
|
bitwarden (default) 64MiB, 3 Iterations, 4 Threads
|
|
owasp 19MiB, 2 Iterations, 1 Thread
|
|
|
|
";
|
|
|
|
pub const VERSION: Option<&str> = option_env!("VW_VERSION");
|
|
|
|
fn parse_args() {
|
|
let mut pargs = pico_args::Arguments::from_env();
|
|
let version = VERSION.unwrap_or("(Version info from Git not present)");
|
|
|
|
if pargs.contains(["-h", "--help"]) {
|
|
println!("Vaultwarden {version}");
|
|
print!("{HELP}");
|
|
exit(0);
|
|
} else if pargs.contains(["-v", "--version"]) {
|
|
config::SKIP_CONFIG_VALIDATION.store(true, Ordering::Relaxed);
|
|
let web_vault_version = util::get_active_web_release();
|
|
println!("Vaultwarden {version}");
|
|
println!("Web-Vault {web_vault_version}");
|
|
exit(0);
|
|
}
|
|
|
|
if let Some(command) = pargs.subcommand().unwrap_or_default() {
|
|
if command == "hash" {
|
|
use argon2::{
|
|
password_hash::SaltString, Algorithm::Argon2id, Argon2, ParamsBuilder, PasswordHasher, Version::V0x13,
|
|
};
|
|
|
|
let mut argon2_params = ParamsBuilder::new();
|
|
let preset: Option<String> = pargs.opt_value_from_str(["-p", "--preset"]).unwrap_or_default();
|
|
let selected_preset;
|
|
match preset.as_deref() {
|
|
Some("owasp") => {
|
|
selected_preset = "owasp";
|
|
argon2_params.m_cost(19456);
|
|
argon2_params.t_cost(2);
|
|
argon2_params.p_cost(1);
|
|
}
|
|
_ => {
|
|
// Bitwarden preset is the default
|
|
selected_preset = "bitwarden";
|
|
argon2_params.m_cost(65540);
|
|
argon2_params.t_cost(3);
|
|
argon2_params.p_cost(4);
|
|
}
|
|
}
|
|
|
|
println!("Generate an Argon2id PHC string using the '{selected_preset}' preset:\n");
|
|
|
|
let password = rpassword::prompt_password("Password: ").unwrap();
|
|
if password.len() < 8 {
|
|
println!("\nPassword must contain at least 8 characters");
|
|
exit(1);
|
|
}
|
|
|
|
let password_verify = rpassword::prompt_password("Confirm Password: ").unwrap();
|
|
if password != password_verify {
|
|
println!("\nPasswords do not match");
|
|
exit(1);
|
|
}
|
|
|
|
let argon2 = Argon2::new(Argon2id, V0x13, argon2_params.build().unwrap());
|
|
let salt = SaltString::encode_b64(&crypto::get_random_bytes::<32>()).unwrap();
|
|
|
|
let argon2_timer = tokio::time::Instant::now();
|
|
if let Ok(password_hash) = argon2.hash_password(password.as_bytes(), &salt) {
|
|
println!(
|
|
"\n\
|
|
ADMIN_TOKEN='{password_hash}'\n\n\
|
|
Generation of the Argon2id PHC string took: {:?}",
|
|
argon2_timer.elapsed()
|
|
);
|
|
} else {
|
|
println!("Unable to generate Argon2id PHC hash.");
|
|
exit(1);
|
|
}
|
|
} else if command == "backup" {
|
|
match db::backup_sqlite() {
|
|
Ok(f) => {
|
|
println!("Backup to '{f}' was successful");
|
|
exit(0);
|
|
}
|
|
Err(e) => {
|
|
println!("Backup failed. {e:?}");
|
|
exit(1);
|
|
}
|
|
}
|
|
}
|
|
exit(0);
|
|
}
|
|
}
|
|
|
|
fn launch_info() {
|
|
println!(
|
|
"\
|
|
/--------------------------------------------------------------------\\\n\
|
|
| Starting Vaultwarden |"
|
|
);
|
|
|
|
if let Some(version) = VERSION {
|
|
println!("|{:^68}|", format!("Version {version}"));
|
|
}
|
|
|
|
println!(
|
|
"\
|
|
|--------------------------------------------------------------------|\n\
|
|
| This is an *unofficial* Bitwarden implementation, DO NOT use the |\n\
|
|
| official channels to report bugs/features, regardless of client. |\n\
|
|
| Send usage/configuration questions or feature requests to: |\n\
|
|
| https://github.com/dani-garcia/vaultwarden/discussions or |\n\
|
|
| https://vaultwarden.discourse.group/ |\n\
|
|
| Report suspected bugs/issues in the software itself at: |\n\
|
|
| https://github.com/dani-garcia/vaultwarden/issues/new |\n\
|
|
\\--------------------------------------------------------------------/\n"
|
|
);
|
|
}
|
|
|
|
fn init_logging() -> Result<log::LevelFilter, Error> {
|
|
let levels = log::LevelFilter::iter().map(|lvl| lvl.as_str().to_lowercase()).collect::<Vec<String>>().join("|");
|
|
let log_level_rgx_str = format!("^({levels})((,[^,=]+=({levels}))*)$");
|
|
let log_level_rgx = regex::Regex::new(&log_level_rgx_str)?;
|
|
let config_str = CONFIG.log_level().to_lowercase();
|
|
|
|
let (level, levels_override) = if let Some(caps) = log_level_rgx.captures(&config_str) {
|
|
let level = caps
|
|
.get(1)
|
|
.and_then(|m| log::LevelFilter::from_str(m.as_str()).ok())
|
|
.ok_or(Error::new("Failed to parse global log level".to_string(), ""))?;
|
|
|
|
let levels_override: Vec<(&str, log::LevelFilter)> = caps
|
|
.get(2)
|
|
.map(|m| {
|
|
m.as_str()
|
|
.split(',')
|
|
.collect::<Vec<&str>>()
|
|
.into_iter()
|
|
.flat_map(|s| match s.split_once('=') {
|
|
Some((log, lvl_str)) => log::LevelFilter::from_str(lvl_str).ok().map(|lvl| (log, lvl)),
|
|
_ => None,
|
|
})
|
|
.collect()
|
|
})
|
|
.ok_or(Error::new("Failed to parse overrides".to_string(), ""))?;
|
|
|
|
(level, levels_override)
|
|
} else {
|
|
err!(format!("LOG_LEVEL should follow the format info,vaultwarden::api::icons=debug, invalid: {config_str}"))
|
|
};
|
|
|
|
// Depending on the main log level we either want to disable or enable logging for hickory.
|
|
// Else if there are timeouts it will clutter the logs since hickory uses warn for this.
|
|
let hickory_level = if level >= log::LevelFilter::Debug {
|
|
level
|
|
} else {
|
|
log::LevelFilter::Off
|
|
};
|
|
|
|
// Only show Rocket underscore `_` logs when the level is Debug or higher
|
|
// Else this will bloat the log output with useless messages.
|
|
let rocket_underscore_level = if level >= log::LevelFilter::Debug {
|
|
log::LevelFilter::Warn
|
|
} else {
|
|
log::LevelFilter::Off
|
|
};
|
|
|
|
// Only show handlebar logs when the level is Trace
|
|
let handlebars_level = if level >= log::LevelFilter::Trace {
|
|
log::LevelFilter::Trace
|
|
} else {
|
|
log::LevelFilter::Warn
|
|
};
|
|
|
|
// Enable smtp debug logging only specifically for smtp when need.
|
|
// This can contain sensitive information we do not want in the default debug/trace logging.
|
|
let smtp_log_level = if CONFIG.smtp_debug() {
|
|
log::LevelFilter::Debug
|
|
} else {
|
|
log::LevelFilter::Off
|
|
};
|
|
|
|
let mut default_levels = HashMap::from([
|
|
// Hide unknown certificate errors if using self-signed
|
|
("rustls::session", log::LevelFilter::Off),
|
|
// Hide failed to close stream messages
|
|
("hyper::server", log::LevelFilter::Warn),
|
|
// Silence Rocket `_` logs
|
|
("_", rocket_underscore_level),
|
|
("rocket::response::responder::_", rocket_underscore_level),
|
|
("rocket::server::_", rocket_underscore_level),
|
|
("vaultwarden::api::admin::_", rocket_underscore_level),
|
|
("vaultwarden::api::notifications::_", rocket_underscore_level),
|
|
// Silence Rocket logs
|
|
("rocket::launch", log::LevelFilter::Error),
|
|
("rocket::launch_", log::LevelFilter::Error),
|
|
("rocket::rocket", log::LevelFilter::Warn),
|
|
("rocket::server", log::LevelFilter::Warn),
|
|
("rocket::fairing::fairings", log::LevelFilter::Warn),
|
|
("rocket::shield::shield", log::LevelFilter::Warn),
|
|
("hyper::proto", log::LevelFilter::Off),
|
|
("hyper::client", log::LevelFilter::Off),
|
|
// Filter handlebars logs
|
|
("handlebars::render", handlebars_level),
|
|
// Prevent cookie_store logs
|
|
("cookie_store", log::LevelFilter::Off),
|
|
// Variable level for hickory used by reqwest
|
|
("hickory_resolver::name_server::name_server", hickory_level),
|
|
("hickory_proto::xfer", hickory_level),
|
|
// SMTP
|
|
("lettre::transport::smtp", smtp_log_level),
|
|
// Set query_logger default to Off, but can be overwritten manually
|
|
// You can set LOG_LEVEL=info,vaultwarden::db::query_logger=<LEVEL> to overwrite it.
|
|
// This makes it possible to do the following:
|
|
// warn = Print slow queries only, 5 seconds or longer
|
|
// info = Print slow queries only, 1 second or longer
|
|
// debug = Print all queries
|
|
("vaultwarden::db::query_logger", log::LevelFilter::Off),
|
|
]);
|
|
|
|
for (path, level) in levels_override.into_iter() {
|
|
let _ = default_levels.insert(path, level);
|
|
}
|
|
|
|
if Some(&log::LevelFilter::Debug) == default_levels.get("lettre::transport::smtp") {
|
|
println!(
|
|
"[WARNING] SMTP Debugging is enabled (SMTP_DEBUG=true). Sensitive information could be disclosed via logs!\n\
|
|
[WARNING] Only enable SMTP_DEBUG during troubleshooting!\n"
|
|
);
|
|
}
|
|
|
|
let mut logger = fern::Dispatch::new().level(level).chain(std::io::stdout());
|
|
|
|
for (path, level) in default_levels {
|
|
logger = logger.level_for(path.to_string(), level);
|
|
}
|
|
|
|
if CONFIG.extended_logging() {
|
|
logger = logger.format(|out, message, record| {
|
|
out.finish(format_args!(
|
|
"[{}][{}][{}] {}",
|
|
chrono::Local::now().format(&CONFIG.log_timestamp_format()),
|
|
record.target(),
|
|
record.level(),
|
|
message
|
|
))
|
|
});
|
|
} else {
|
|
logger = logger.format(|out, message, _| out.finish(format_args!("{message}")));
|
|
}
|
|
|
|
if let Some(log_file) = CONFIG.log_file() {
|
|
#[cfg(windows)]
|
|
{
|
|
logger = logger.chain(fern::log_file(log_file)?);
|
|
}
|
|
#[cfg(unix)]
|
|
{
|
|
const SIGHUP: i32 = SignalKind::hangup().as_raw_value();
|
|
let path = Path::new(&log_file);
|
|
logger = logger.chain(fern::log_reopen1(path, [SIGHUP])?);
|
|
}
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
{
|
|
if cfg!(feature = "enable_syslog") || CONFIG.use_syslog() {
|
|
logger = chain_syslog(logger);
|
|
}
|
|
}
|
|
|
|
if let Err(err) = logger.apply() {
|
|
err!(format!("Failed to activate logger: {err}"))
|
|
}
|
|
|
|
// Catch panics and log them instead of default output to StdErr
|
|
panic::set_hook(Box::new(|info| {
|
|
let thread = thread::current();
|
|
let thread = thread.name().unwrap_or("unnamed");
|
|
|
|
let msg = match info.payload().downcast_ref::<&'static str>() {
|
|
Some(s) => *s,
|
|
None => match info.payload().downcast_ref::<String>() {
|
|
Some(s) => &**s,
|
|
None => "Box<Any>",
|
|
},
|
|
};
|
|
|
|
let backtrace = std::backtrace::Backtrace::force_capture();
|
|
|
|
match info.location() {
|
|
Some(location) => {
|
|
error!(
|
|
target: "panic", "thread '{}' panicked at '{}': {}:{}\n{:}",
|
|
thread,
|
|
msg,
|
|
location.file(),
|
|
location.line(),
|
|
backtrace
|
|
);
|
|
}
|
|
None => error!(
|
|
target: "panic",
|
|
"thread '{thread}' panicked at '{msg}'\n{backtrace:}"
|
|
),
|
|
}
|
|
}));
|
|
|
|
Ok(level)
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch {
|
|
let syslog_fmt = syslog::Formatter3164 {
|
|
facility: syslog::Facility::LOG_USER,
|
|
hostname: None,
|
|
process: "vaultwarden".into(),
|
|
pid: 0,
|
|
};
|
|
|
|
match syslog::unix(syslog_fmt) {
|
|
Ok(sl) => logger.chain(sl),
|
|
Err(e) => {
|
|
error!("Unable to connect to syslog: {e:?}");
|
|
logger
|
|
}
|
|
}
|
|
}
|
|
|
|
fn create_dir(path: &str, description: &str) {
|
|
// Try to create the specified dir, if it doesn't already exist.
|
|
let err_msg = format!("Error creating {description} directory '{path}'");
|
|
create_dir_all(path).expect(&err_msg);
|
|
}
|
|
|
|
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.");
|
|
if is_running_in_container() {
|
|
error!("Verify that your data volume is mounted at the correct location.");
|
|
} else {
|
|
error!("Create the data folder and try again.");
|
|
}
|
|
exit(1);
|
|
}
|
|
if !path.is_dir() {
|
|
error!("Data folder '{data_folder}' is not a directory.");
|
|
exit(1);
|
|
}
|
|
|
|
if is_running_in_container()
|
|
&& std::env::var("I_REALLY_WANT_VOLATILE_STORAGE").is_err()
|
|
&& !container_data_folder_is_persistent(data_folder).await
|
|
{
|
|
error!(
|
|
"No persistent volume!\n\
|
|
########################################################################################\n\
|
|
# It looks like you did not configure a persistent volume! #\n\
|
|
# This will result in permanent data loss when the container is removed or updated! #\n\
|
|
# If you really want to use volatile storage set `I_REALLY_WANT_VOLATILE_STORAGE=true` #\n\
|
|
########################################################################################\n"
|
|
);
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
/// Detect when using Docker or Podman the DATA_FOLDER is either a bind-mount or a volume created manually.
|
|
/// If not created manually, then the data will not be persistent.
|
|
/// A none persistent volume in either Docker or Podman is represented by a 64 alphanumerical string.
|
|
/// If we detect this string, we will alert about not having a persistent self defined volume.
|
|
/// This probably means that someone forgot to add `-v /path/to/vaultwarden_data/:/data`
|
|
async fn container_data_folder_is_persistent(data_folder: &str) -> bool {
|
|
if let Ok(mountinfo) = File::open("/proc/self/mountinfo").await {
|
|
// Since there can only be one mountpoint to the DATA_FOLDER
|
|
// We do a basic check for this mountpoint surrounded by a space.
|
|
let data_folder_match = if data_folder.starts_with('/') {
|
|
format!(" {data_folder} ")
|
|
} else {
|
|
format!(" /{data_folder} ")
|
|
};
|
|
let mut lines = BufReader::new(mountinfo).lines();
|
|
let re = regex::Regex::new(r"/volumes/[a-z0-9]{64}/_data /").unwrap();
|
|
while let Some(line) = lines.next_line().await.unwrap_or_default() {
|
|
// Only execute a regex check if we find the base match
|
|
if line.contains(&data_folder_match) {
|
|
if re.is_match(&line) {
|
|
return false;
|
|
}
|
|
// If we did found a match for the mountpoint, but not the regex, then still stop searching.
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// In all other cases, just assume a true.
|
|
// This is just an informative check to try and prevent data loss.
|
|
true
|
|
}
|
|
|
|
fn check_web_vault() {
|
|
if !CONFIG.web_vault_enabled() {
|
|
return;
|
|
}
|
|
|
|
let index_path = Path::new(&CONFIG.web_vault_folder()).join("index.html");
|
|
|
|
if !index_path.exists() {
|
|
error!(
|
|
"Web vault is not found at '{}'. To install it, please follow the steps in: ",
|
|
CONFIG.web_vault_folder()
|
|
);
|
|
error!("https://github.com/dani-garcia/vaultwarden/wiki/Building-binary#install-the-web-vault");
|
|
error!("You can also set the environment variable 'WEB_VAULT_ENABLED=false' to disable it");
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
async fn create_db_pool() -> db::DbPool {
|
|
match util::retry_db(db::DbPool::from_config, CONFIG.db_connection_retries()).await {
|
|
Ok(p) => p,
|
|
Err(e) => {
|
|
error!("Error creating database pool: {e:?}");
|
|
exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error> {
|
|
let basepath = &CONFIG.domain_path();
|
|
|
|
let mut config = rocket::Config::from(rocket::Config::figment());
|
|
|
|
// We install our own signal handlers below; disable Rocket's built-in handlers
|
|
config.shutdown.ctrlc = false;
|
|
#[cfg(unix)]
|
|
config.shutdown.signals.clear();
|
|
|
|
config.temp_dir = canonicalize(CONFIG.tmp_folder()).unwrap().into();
|
|
config.cli_colors = false; // Make sure Rocket does not color any values for logging.
|
|
config.limits = Limits::new()
|
|
.limit("json", 20.megabytes()) // 20MB should be enough for very large imports, something like 5000+ vault entries
|
|
.limit("data-form", 525.megabytes()) // This needs to match the maximum allowed file size for Send
|
|
.limit("file", 525.megabytes()); // This needs to match the maximum allowed file size for attachments
|
|
|
|
// If adding more paths here, consider also adding them to
|
|
// crate::utils::LOGGED_ROUTES to make sure they appear in the log
|
|
let instance = rocket::custom(config)
|
|
.mount([basepath, "/"].concat(), api::web_routes())
|
|
.mount([basepath, "/api"].concat(), api::core_routes())
|
|
.mount([basepath, "/admin"].concat(), api::admin_routes())
|
|
.mount([basepath, "/events"].concat(), api::core_events_routes())
|
|
.mount([basepath, "/identity"].concat(), api::identity_routes())
|
|
.mount([basepath, "/icons"].concat(), api::icons_routes())
|
|
.mount([basepath, "/notifications"].concat(), api::notifications_routes())
|
|
.register([basepath, "/"].concat(), api::web_catchers())
|
|
.register([basepath, "/api"].concat(), api::core_catchers())
|
|
.register([basepath, "/admin"].concat(), api::admin_catchers())
|
|
.manage(pool)
|
|
.manage(Arc::clone(&WS_USERS))
|
|
.manage(Arc::clone(&WS_ANONYMOUS_SUBSCRIPTIONS))
|
|
.attach(util::AppHeaders())
|
|
.attach(util::Cors())
|
|
.attach(util::BetterLogging(extra_debug))
|
|
.ignite()
|
|
.await?;
|
|
|
|
CONFIG.set_rocket_shutdown_handle(instance.shutdown());
|
|
|
|
spawn_shutdown_signal_handler();
|
|
|
|
#[cfg(all(unix, sqlite))]
|
|
{
|
|
if db::ACTIVE_DB_TYPE.get() != Some(&db::DbConnType::Sqlite) {
|
|
debug!("PostgreSQL and MySQL/MariaDB do not support this backup feature, skip adding USR1 signal.");
|
|
} else {
|
|
tokio::spawn(async move {
|
|
let mut signal_user1 = tokio::signal::unix::signal(SignalKind::user_defined1()).unwrap();
|
|
loop {
|
|
// If we need more signals to act upon, we might want to use select! here.
|
|
// With only one item to listen for this is enough.
|
|
let _ = signal_user1.recv().await;
|
|
match db::backup_sqlite() {
|
|
Ok(f) => info!("Backup to '{f}' was successful"),
|
|
Err(e) => error!("Backup failed. {e:?}"),
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
instance.launch().await?;
|
|
|
|
info!("Vaultwarden process exited!");
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn spawn_shutdown_signal_handler() {
|
|
tokio::spawn(async move {
|
|
use tokio::signal::unix::signal;
|
|
|
|
let mut sigint = signal(SignalKind::interrupt()).expect("Error setting SIGINT handler");
|
|
let mut sigterm = signal(SignalKind::terminate()).expect("Error setting SIGTERM handler");
|
|
let mut sigquit = signal(SignalKind::quit()).expect("Error setting SIGQUIT handler");
|
|
|
|
let signal_name = tokio::select! {
|
|
_ = sigint.recv() => "SIGINT",
|
|
_ = sigterm.recv() => "SIGTERM",
|
|
_ = sigquit.recv() => "SIGQUIT",
|
|
};
|
|
|
|
info!("Received {signal_name}, initiating graceful shutdown");
|
|
CONFIG.shutdown();
|
|
});
|
|
}
|
|
|
|
#[cfg(not(unix))]
|
|
fn spawn_shutdown_signal_handler() {
|
|
tokio::spawn(async move {
|
|
tokio::signal::ctrl_c().await.expect("Error setting Ctrl-C handler");
|
|
info!("Received Ctrl-C, initiating graceful shutdown");
|
|
CONFIG.shutdown();
|
|
});
|
|
}
|
|
|
|
fn schedule_jobs(pool: db::DbPool) {
|
|
if CONFIG.job_poll_interval_ms() == 0 {
|
|
info!("Job scheduler disabled.");
|
|
return;
|
|
}
|
|
|
|
let runtime = tokio::runtime::Runtime::new().unwrap();
|
|
|
|
thread::Builder::new()
|
|
.name("job-scheduler".to_string())
|
|
.spawn(move || {
|
|
use job_scheduler_ng::{Job, JobScheduler};
|
|
let _runtime_guard = runtime.enter();
|
|
|
|
let mut sched = JobScheduler::new();
|
|
|
|
// Purge sends that are past their deletion date.
|
|
if !CONFIG.send_purge_schedule().is_empty() {
|
|
sched.add(Job::new(CONFIG.send_purge_schedule().parse().unwrap(), || {
|
|
runtime.spawn(api::purge_sends(pool.clone()));
|
|
}));
|
|
}
|
|
|
|
// Purge trashed items that are old enough to be auto-deleted.
|
|
if !CONFIG.trash_purge_schedule().is_empty() {
|
|
sched.add(Job::new(CONFIG.trash_purge_schedule().parse().unwrap(), || {
|
|
runtime.spawn(api::purge_trashed_ciphers(pool.clone()));
|
|
}));
|
|
}
|
|
|
|
// Send email notifications about incomplete 2FA logins, which potentially
|
|
// indicates that a user's master password has been compromised.
|
|
if !CONFIG.incomplete_2fa_schedule().is_empty() {
|
|
sched.add(Job::new(CONFIG.incomplete_2fa_schedule().parse().unwrap(), || {
|
|
runtime.spawn(api::send_incomplete_2fa_notifications(pool.clone()));
|
|
}));
|
|
}
|
|
|
|
// Grant emergency access requests that have met the required wait time.
|
|
// This job should run before the emergency access reminders job to avoid
|
|
// sending reminders for requests that are about to be granted anyway.
|
|
if !CONFIG.emergency_request_timeout_schedule().is_empty() {
|
|
sched.add(Job::new(CONFIG.emergency_request_timeout_schedule().parse().unwrap(), || {
|
|
runtime.spawn(api::emergency_request_timeout_job(pool.clone()));
|
|
}));
|
|
}
|
|
|
|
// Send reminders to emergency access grantors that there are pending
|
|
// emergency access requests.
|
|
if !CONFIG.emergency_notification_reminder_schedule().is_empty() {
|
|
sched.add(Job::new(CONFIG.emergency_notification_reminder_schedule().parse().unwrap(), || {
|
|
runtime.spawn(api::emergency_notification_reminder_job(pool.clone()));
|
|
}));
|
|
}
|
|
|
|
if !CONFIG.auth_request_purge_schedule().is_empty() {
|
|
sched.add(Job::new(CONFIG.auth_request_purge_schedule().parse().unwrap(), || {
|
|
runtime.spawn(purge_auth_requests(pool.clone()));
|
|
}));
|
|
}
|
|
|
|
// Clean unused, expired Duo authentication contexts.
|
|
if !CONFIG.duo_context_purge_schedule().is_empty() && CONFIG._enable_duo() && !CONFIG.duo_use_iframe() {
|
|
sched.add(Job::new(CONFIG.duo_context_purge_schedule().parse().unwrap(), || {
|
|
runtime.spawn(purge_duo_contexts(pool.clone()));
|
|
}));
|
|
}
|
|
|
|
// Cleanup the event table of records x days old.
|
|
if CONFIG.org_events_enabled()
|
|
&& !CONFIG.event_cleanup_schedule().is_empty()
|
|
&& CONFIG.events_days_retain().is_some()
|
|
{
|
|
sched.add(Job::new(CONFIG.event_cleanup_schedule().parse().unwrap(), || {
|
|
runtime.spawn(api::event_cleanup_job(pool.clone()));
|
|
}));
|
|
}
|
|
|
|
// Purge sso auth from incomplete flow (default to daily at 00h20).
|
|
if !CONFIG.purge_incomplete_sso_auth().is_empty() {
|
|
sched.add(Job::new(CONFIG.purge_incomplete_sso_auth().parse().unwrap(), || {
|
|
runtime.spawn(db::models::SsoAuth::delete_expired(pool.clone()));
|
|
}));
|
|
}
|
|
|
|
// Periodically check for jobs to run. We probably won't need any
|
|
// jobs that run more often than once a minute, so a default poll
|
|
// interval of 30 seconds should be sufficient. Users who want to
|
|
// schedule jobs to run more frequently for some reason can reduce
|
|
// the poll interval accordingly.
|
|
//
|
|
// Note that the scheduler checks jobs in the order in which they
|
|
// were added, so if two jobs are both eligible to run at a given
|
|
// tick, the one that was added earlier will run first.
|
|
loop {
|
|
sched.tick();
|
|
runtime.block_on(tokio::time::sleep(tokio::time::Duration::from_millis(CONFIG.job_poll_interval_ms())));
|
|
}
|
|
})
|
|
.expect("Error spawning job scheduler thread");
|
|
}
|