This commit is contained in:
Mateusz Gruszczyński committed 2026-10-03 18:10:20 +02:00
1 parent 1297ea2920
commit dc0fcabf51
49 files changed
+2632 -252

No files matched your search

+97 -8
View File
@@ -35,12 +35,13 @@ use tokio::{
task::JoinSet,
};
use zeroize::Zeroizing;
use tracing_subscriber::prelude::*;
#[derive(Parser)]
#[command(
name = "rust-proxy",
version,
about = "RustProxy - independent Rust reverse proxy 0.24.8"
about = "RustProxy - independent Rust reverse proxy 0.24.9"
)]
struct Cli {
#[arg(
@@ -94,6 +95,11 @@ enum Command {
#[command(subcommand)]
action: ConfigCommand,
},
/// Create or restore a password-encrypted full application backup.
Backup {
#[command(subcommand)]
action: BackupCommand,
},
}
#[derive(Subcommand)]
enum AdminCommand {
@@ -127,6 +133,20 @@ enum ConfigCommand {
/// Validate JSON, ACLs, headers and backend policy without changing the database.
Validate { file: PathBuf },
}
#[derive(Subcommand)]
enum BackupCommand {
/// Create an encrypted full backup. Stop the RustProxy service first.
Create {
/// Destination .rpb file. Existing files are never overwritten.
#[arg(long)]
output: Option<PathBuf>,
},
/// Restore an encrypted full backup atomically. Stop the RustProxy service first.
Restore {
archive: PathBuf,
},
}
#[derive(Subcommand)]
enum TlsCommand {
/// Generate a self-signed development certificate. Existing files are never overwritten.
@@ -243,13 +263,18 @@ fn resolve_installed_data_dir(configured: PathBuf, explicitly_configured: bool)
}
async fn application() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "rust_proxy=info,warn".into()),
let error_log = rust_proxy::logging::ErrorLogController::start();
rust_proxy::logging::install_error_log_controller(error_log.clone())?;
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "rust_proxy=info,warn".into());
tracing_subscriber::registry()
.with(filter)
.with(
tracing_subscriber::fmt::layer()
.with_target(false)
.with_writer(rust_proxy::logging::RedactingStderr),
)
.with_target(false)
.with_writer(std::io::stderr)
.with(rust_proxy::logging::ErrorLogLayer::new(error_log))
.init();
let matches = Cli::command().get_matches();
let explicit = |matches: &clap::ArgMatches, id: &str| {
@@ -309,6 +334,13 @@ async fn application() -> Result<()> {
)
.await
}
Command::Backup { action } => {
backup_command(
resolve_installed_data_dir(cli.data_dir, data_dir_explicit),
action,
)
.await
}
Command::Serve(options) => {
let update_check = rust_proxy::update_check::Config::embedded()?.with_overrides(
cli.update_repository_url,
@@ -328,6 +360,57 @@ async fn application() -> Result<()> {
}
}
}
async fn backup_command(data_dir: PathBuf, action: BackupCommand) -> Result<()> {
match action {
BackupCommand::Create { output } => {
let output = output.unwrap_or_else(|| {
PathBuf::from(format!("rustproxy-backup-{}.rpb", rust_proxy::auth::now()))
});
let mut generated = false;
let password = if io::stdin().is_terminal() {
let value = Zeroizing::new(
rpassword::prompt_password("Backup password (leave empty to generate): ")?
);
if value.is_empty() {
generated = true;
Zeroizing::new(rust_proxy::backup::generated_password())
} else {
let confirmation = Zeroizing::new(rpassword::prompt_password("Confirm backup password: ")?);
ensure!(value.as_str() == confirmation.as_str(), "backup passwords do not match");
value
}
} else {
generated = true;
Zeroizing::new(rust_proxy::backup::generated_password())
};
let summary = rust_proxy::backup::create(&data_dir, &output, password.as_str()).await?;
println!(
"Encrypted backup created: {} ({} files, configuration revision {})",
summary.path.display(), summary.files, summary.configuration_revision
);
if generated {
println!("Generated backup password (shown once): {}", password.as_str());
println!("Store this password securely; restore is impossible without it.");
}
Ok(())
}
BackupCommand::Restore { archive } => {
ensure!(
io::stdin().is_terminal(),
"backup restore requires an interactive TTY for the password"
);
let password = Zeroizing::new(rpassword::prompt_password("Backup password: ")?);
ensure!(!password.is_empty(), "backup password cannot be empty");
let summary = rust_proxy::backup::restore(&archive, &data_dir, password.as_str()).await?;
println!(
"Backup restored to {} ({} files, configuration revision {})",
data_dir.display(), summary.files, summary.configuration_revision
);
Ok(())
}
}
}
fn validate_admin_binding(admin_addr: SocketAddr, tls: bool, allow_remote: bool) -> Result<bool> {
let remote_admin = !admin_addr.ip().is_loopback();
ensure!(
@@ -445,6 +528,11 @@ async fn run(
let directory = data_dir.join("certs");
let initialize = directory.clone();
tokio::task::spawn_blocking(move || rust_proxy::tls::initialize_default(&initialize)).await??;
rust_proxy::secret_store::initialize(&data_dir)?;
let migrated = rust_proxy::secret_store::migrate_acme_credentials(&store, &data_dir, &directory).await?;
if migrated > 0 {
tracing::info!(migrated, "legacy ACME credentials migrated to encrypted Secret Store");
}
let cert_dir = Some(directory.clone());
let policy = ValidationPolicy {
proxy_addr: options.proxy_listen,
@@ -459,6 +547,7 @@ async fn run(
})
.await?
.context("stored configuration is invalid; no automatic reset was performed")?;
rust_proxy::logging::configure_error_log(snapshot.envelope.config.error_log.clone());
let admin_tls_config = {
let tls = snapshot.envelope.config.tls.clone();
let administration = snapshot.envelope.config.administration.clone();
@@ -1426,7 +1515,7 @@ async fn live_logs_command(dir: PathBuf, addr: SocketAddr) -> Result<()> {
}
});
let interactive = io::stdin().is_terminal() && io::stdout().is_terminal();
println!("RustProxy live logs 0.24.8 - all vhosts. Type 'help' for interactive filters; Ctrl-C or 'quit' exits.");
println!("RustProxy live logs 0.24.9 - all vhosts. Type 'help' for interactive filters; Ctrl-C or 'quit' exits.");
if interactive { print!("> "); io::stdout().flush()?; }
let mut filters = LiveLogFilters::default();
let mut history = std::collections::BTreeMap::<u64, serde_json::Value>::new();