Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 107 additions & 17 deletions backend/src/routes/updates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use axum::{
Json, Router,
};
use serde::{Deserialize, Serialize};
use std::process::Command;
use tokio::process::Command;

use crate::AppState;

Expand Down Expand Up @@ -254,39 +254,129 @@ pub async fn install_update(
tokio::spawn(async move {
tracing::info!("Spawning update process for version {}", version_clone);

// Check if we're running as root or need sudo
// On Unix systems, check effective user ID via USER env var
let use_sudo = std::env::var("USER").map(|u| u != "root").unwrap_or(true);
// Log current user for debugging
let current_user = std::env::var("USER").unwrap_or_else(|_| "unknown".to_string());
tracing::info!("Running as user: {}", current_user);

let mut command = if use_sudo {
// Check if we're running as root
let is_root = std::fs::metadata("/etc/shadow")
.and_then(|_| std::fs::File::open("/etc/shadow"))
.is_ok();

tracing::info!("Running as root: {}", is_root);

// Build command - use sudo with direct script path
// This matches the sudoers configuration: /bin/bash /opt/csf-core/scripts/update.sh*
let mut command = if is_root {
// Running as root, execute script directly
Command::new(&script_path_clone)
} else {
// Need sudo - call script via sudo bash
let mut cmd = Command::new("sudo");
// Use full path to bash for better compatibility with sudoers
cmd.arg("/bin/bash");
cmd.arg(&script_path_clone);
cmd
} else {
Command::new("/bin/bash")
};

match command
.arg(&script_path_clone)
// Set up environment variables that the script might need
command.env(
"PATH",
"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
);
command.env(
"HOME",
std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()),
);
command.env("LANG", "C.UTF-8");
command.env("LC_ALL", "C.UTF-8");

// Set working directory to script location
if let Some(script_dir) = script_path_clone.parent() {
command.current_dir(script_dir);
tracing::info!("Working directory: {:?}", script_dir);
}

command
.arg(&version_clone)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
{
.stderr(std::process::Stdio::piped());

tracing::info!("Executing command: {:?}", command);

match command.spawn() {
Ok(mut child) => {
tracing::info!(
"Update process started successfully (PID: {:?})",
child.id()
);

// Wait for process to complete in background
match child.wait() {
// Capture and log output in real-time
let stdout = child.stdout.take();
let stderr = child.stderr.take();

// Spawn tasks to read stdout and stderr
let stdout_handle = tokio::spawn(async move {
if let Some(stdout) = stdout {
use tokio::io::{AsyncBufReadExt, BufReader};
let reader = BufReader::new(stdout);
let mut lines = reader.lines();
while let Ok(Some(line)) = lines.next_line().await {
tracing::info!("[UPDATE STDOUT] {}", line);
}
}
});

let stderr_handle = tokio::spawn(async move {
if let Some(stderr) = stderr {
use tokio::io::{AsyncBufReadExt, BufReader};
let reader = BufReader::new(stderr);
let mut lines = reader.lines();
while let Ok(Some(line)) = lines.next_line().await {
tracing::warn!("[UPDATE STDERR] {}", line);
}
}
});

// Wait for process to complete
match child.wait().await {
Ok(status) => {
// Wait for output handlers to finish
let _ = tokio::join!(stdout_handle, stderr_handle);

if status.success() {
tracing::info!("Update process completed successfully");
tracing::info!(
"Update process completed successfully with exit code: {:?}",
status.code()
);
} else {
tracing::error!("Update process failed with status: {}", status);
tracing::error!(
"Update process failed with status: {} (exit code: {:?})",
status,
status.code()
);

// Write detailed error to status file
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();

let error_status = format!(
r#"{{
"status": "error",
"message": "Update script failed with exit code {}",
"progress": 0,
"version": "{}",
"timestamp": "{}"
}}"#,
status.code().unwrap_or(-1),
version_clone,
ts
);

let _ =
tokio::fs::write("/tmp/csf-core-update-status.json", error_status)
.await;
}
}
Err(e) => {
Expand Down
22 changes: 17 additions & 5 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -279,14 +279,26 @@ create_service_user() {

# Configure sudo access for update script (passwordless, specific command only)
print_step "Konfiguriere sudo-Zugriff für Updates..."
cat > /etc/sudoers.d/csf-core << EOF
cat > /etc/sudoers.d/csf-core << 'EOF'
# Allow csf-core user to run update script without password
$SERVICE_USER ALL=(ALL) NOPASSWD: /opt/csf-core/scripts/update.sh
$SERVICE_USER ALL=(ALL) NOPASSWD: /bin/bash /opt/csf-core/scripts/update.sh*
$SERVICE_USER ALL=(ALL) NOPASSWD: /usr/bin/bash /opt/csf-core/scripts/update.sh*
csf-core ALL=(ALL) NOPASSWD: /opt/csf-core/scripts/update.sh
csf-core ALL=(ALL) NOPASSWD: /bin/bash /opt/csf-core/scripts/update.sh*
csf-core ALL=(ALL) NOPASSWD: /usr/bin/bash /opt/csf-core/scripts/update.sh*

# Allow csf-core to preserve environment and run non-interactively
Defaults:csf-core !requiretty
Defaults:csf-core env_keep += "PATH HOME LANG LC_ALL"
EOF
chmod 0440 /etc/sudoers.d/csf-core
print_success "sudo-Zugriff für Updates konfiguriert"

# Validate sudoers file
if visudo -c -f /etc/sudoers.d/csf-core > /dev/null 2>&1; then
print_success "sudo-Zugriff für Updates konfiguriert"
else
print_error "Sudoers-Datei ist ungültig"
rm -f /etc/sudoers.d/csf-core
print_warning "sudo-Zugriff konnte nicht konfiguriert werden, Updates erfordern manuelle sudo-Rechte"
fi

# Add user to docker group if Docker is installed
if command -v docker &> /dev/null; then
Expand Down
Loading