diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index d19e21b..9fdd9c9 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -10,7 +10,7 @@ name: 'Dependency review' on: pull_request: - branches: [ "main" ] + branches: [ "main", "develop" ] # If using a dependency submission action in this workflow this permission will need to be set to: # diff --git a/.github/workflows/rust-clippy.yml b/.github/workflows/rust-clippy.yml index ff7b29f..a2b9543 100644 --- a/.github/workflows/rust-clippy.yml +++ b/.github/workflows/rust-clippy.yml @@ -11,10 +11,10 @@ name: Rust Clippy Analyze on: push: - branches: [ "main" ] + branches: [ "main", "develop" ] pull_request: # The branches below must be a subset of the branches above - branches: [ "main" ] + branches: [ "main", "develop" ] schedule: - cron: '36 18 * * 3' @@ -42,9 +42,9 @@ jobs: run: cargo install clippy-sarif sarif-fmt - name: Run rust-clippy - run: - cargo clippy - --all-features + run: | + cargo clippy \ + --all-features \ --message-format=json | clippy-sarif | tee rust-clippy-results.sarif | sarif-fmt continue-on-error: true diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 27058ac..48995cf 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -4,9 +4,11 @@ on: push: branches: - main + - develop pull_request: branches: - main + - develop jobs: build: @@ -14,7 +16,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 # Add a step to set file permissions for the working directory (if needed) - name: Set file permissions diff --git a/src/download.rs b/src/download.rs index d526dad..70153f6 100644 --- a/src/download.rs +++ b/src/download.rs @@ -1,9 +1,10 @@ use std::io::{self, Write}; -use std::fs::File; -use std::path::Path; +use std::fs::{self, File}; +use std::path::{Path, PathBuf}; use serde::Deserialize; use reqwest::blocking; use zip; +use std::time::Duration; // Define a struct to deserialize the JSON configuration #[derive(Debug, Deserialize)] @@ -13,51 +14,105 @@ struct Config { // Function to encapsulate download functionality pub fn download() -> io::Result<()> { - // Read JSON file and deserialize into Config struct - let config_file = File::open("config.json")?; - let config: Config = serde_json::from_reader(config_file)?; + // Load configuration + let config = load_config("config.json")?; - // URL to download HumHub - let humhub_download_url = "https://download.humhub.com/downloads/install/humhub-1.16.2.zip"; + // Download and extract HumHub + let humhub_version = "1.16.2"; + let humhub_download_url = format!( + "https://download.humhub.com/downloads/install/humhub-{}.zip", + humhub_version + ); + let humhub_zip_path = Path::new(&format!("humhub-{}.zip", humhub_version)); + let humhub_extract_dir = Path::new("/var/www/html"); - // File path to save the downloaded HumHub ZIP file - let humhub_zip_path = "humhub-1.16.2.zip"; + download_file(&humhub_download_url, humhub_zip_path)?; + extract_zip(humhub_zip_path, humhub_extract_dir)?; - // Directory to extract HumHub ZIP file (root directory) - let humhub_extract_dir = "/var/www/html"; + println!( + "HumHub version {} downloaded and extracted successfully to {}", + humhub_version, + humhub_extract_dir.display() + ); - // Initialize HTTP client - let client = blocking::Client::new(); + Ok(()) +} + +// Function to load configuration from a JSON file +fn load_config(path: &str) -> io::Result { + let config_file = File::open(path)?; + serde_json::from_reader(config_file).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) +} + +// Function to download a file from a URL +fn download_file(url: &str, output_path: &Path) -> io::Result<()> { + println!("Downloading file from: {}", url); + + let client = blocking::Client::builder() + .timeout(Duration::from_secs(60)) // Add timeout for the request + .danger_accept_invalid_certs(false) // Make sure SSL validation is done (set to true for secure config) + .build() + .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("Failed to create HTTP client: {}", e)))?; + + let mut response = client + .get(url) + .send() + .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("Failed to send request: {}", e)))?; + + if !response.status().is_success() { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("Failed to download file: HTTP {}", response.status()) + )); + } - // Download HumHub - let mut response = client.get(humhub_download_url).send()?; - let mut zip_file = File::create(humhub_zip_path)?; - io::copy(&mut response, &mut zip_file)?; + let mut file = File::create(output_path)?; + io::copy(&mut response, &mut file)?; - // Extract HumHub ZIP file to the root directory - let extract_dir = Path::new(humhub_extract_dir); - let zip_file = File::open(humhub_zip_path)?; - let mut archive = zip::ZipArchive::new(zip_file)?; + println!("File downloaded to: {}", output_path.display()); + Ok(()) +} + +// Function to extract a ZIP file to a target directory +fn extract_zip(zip_path: &Path, extract_dir: &Path) -> io::Result<()> { + println!("Extracting ZIP file: {}", zip_path.display()); + + let zip_file = File::open(zip_path)?; + let mut archive = zip::ZipArchive::new(zip_file) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("Failed to read ZIP archive: {}", e)))?; for i in 0..archive.len() { let mut file = archive.by_index(i)?; - let outpath = extract_dir.join(file.sanitized_name()); + let outpath = extract_dir.join(sanitize_path(&file.sanitized_name(), extract_dir)?); if let Some(parent) = outpath.parent() { if !parent.exists() { - std::fs::create_dir_all(parent)?; + fs::create_dir_all(parent)?; } } - if (&*file.name()).ends_with('/') { - std::fs::create_dir_all(&outpath)?; + if file.name().ends_with('/') { + fs::create_dir_all(&outpath)?; } else { let mut outfile = File::create(&outpath)?; io::copy(&mut file, &mut outfile)?; } } - println!("HumHub downloaded and extracted successfully to {}", humhub_extract_dir); - + println!("Extraction completed to: {}", extract_dir.display()); Ok(()) } + +// Function to sanitize file paths and ensure they are within the target directory +fn sanitize_path(path: &Path, base_dir: &Path) -> io::Result { + let sanitized = path + .strip_prefix("/") + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "Invalid file path"))?; + + let full_path = base_dir.join(sanitized); + if full_path.starts_with(base_dir) { + Ok(full_path) + } else { + Err(io::Error::new(io::ErrorKind::InvalidInput, "Path traversal detected")) + } +} diff --git a/src/main.rs b/src/main.rs index 920734d..d9bb66a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,8 @@ use std::fs::File; use serde::Deserialize; use trust_dns_resolver::{Resolver, config::ResolverConfig, config::ResolverOpts}; use std::net::TcpStream; +use ssh2::Session; +use std::time::Duration; #[derive(Debug, Deserialize)] struct Config { @@ -20,57 +22,91 @@ fn main() { } fn run() -> io::Result<()> { - // Read configuration from file - let config_file = File::open("config.json")?; - let config: Config = serde_json::from_reader(config_file)?; + // Load configuration + let config = load_config("config.json")?; + // Initialize DNS resolver and resolve domain + resolve_domain(&config.domain_name)?; - // Initialize DNS resolver + // Establish SSH connection + let mut sess = establish_ssh_connection(&config)?; + + // Install required software on the remote server + execute_remote_commands( + &mut sess, + &[ + "sudo apt update", + "sudo apt upgrade -y", + "sudo apt install -y apache2", + "sudo add-apt-repository -y ppa:ondrej/php", + "sudo apt update", + "sudo apt install -y php8.1 libapache2-mod-php8.1 php8.1-mysql php8.1-common php8.1-cli php8.1-curl php8.1-json php8.1-zip php8.1-gd php8.1-mbstring php8.1-xml", + "sudo apt install -y mariadb-server", + "sudo mysql_secure_installation", + "sudo a2enmod php8.1", + "sudo systemctl restart apache2", + ], + )?; + + // Configure Apache + configure_apache(&mut sess, &config)?; + + println!("Setup completed successfully."); + Ok(()) +} + +fn load_config(path: &str) -> io::Result { + let config_file = File::open(path)?; + serde_json::from_reader(config_file).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) +} + +fn resolve_domain(domain: &str) -> io::Result<()> { let resolver = Resolver::new(ResolverConfig::default(), ResolverOpts::default())?; - let response = resolver.lookup_ip(&config.domain_name)?; - println!("DNS record updated successfully: {:?}", response); + match resolver.lookup_ip(domain) { + Ok(response) => { + println!("Resolved DNS for {}: {:?}", domain, response); + Ok(()) + } + Err(e) => Err(io::Error::new(io::ErrorKind::NotFound, format!("Failed to resolve domain: {}", e))), + } +} + +fn establish_ssh_connection(config: &Config) -> io::Result { + let tcp = TcpStream::connect(format!("{}:22", config.host))?; + tcp.set_read_timeout(Some(Duration::from_secs(30)))?; + tcp.set_write_timeout(Some(Duration::from_secs(30)))?; - // Connect to SSH server - let _tcp = TcpStream::connect(format!("{}:22", config.host))?; - let mut sess = ssh2::Session::new()?; + let mut sess = Session::new().map_err(|e| { + io::Error::new(io::ErrorKind::Other, format!("Failed to create SSH session: {}", e)) + })?; + + sess.set_tcp_stream(tcp); + sess.handshake()?; - // Handshake with SSH server - if let Err(err) = sess.handshake() { - return Err(io::Error::new(io::ErrorKind::Other, err)); + // Try to authenticate using password + if let Err(e) = sess.userauth_password(&config.username, &config.password) { + return Err(io::Error::new(io::ErrorKind::PermissionDenied, format!("SSH Authentication failed: {}", e))); } - // Authenticate with SSH server using username and password - if let Err(err) = sess.userauth_password(&config.username, &config.password) { - return Err(io::Error::new(io::ErrorKind::Other, err)); + if !sess.authenticated() { + return Err(io::Error::new(io::ErrorKind::PermissionDenied, "Authentication failed")); } - // Define commands to install required software - let commands = [ - "sudo apt update", - "sudo apt upgrade -y", - "sudo apt install -y apache2", - "sudo add-apt-repository -y ppa:ondrej/php", - "sudo apt update", - "sudo apt install -y php8.1 libapache2-mod-php8.1 php8.1-mysql php8.1-common php8.1-cli php8.1-curl php8.1-json php8.1-zip php8.1-gd php8.1-mbstring php8.1-xml", - "sudo apt install -y mariadb-server", - "sudo mysql_secure_installation", - "sudo a2enmod php8.1", - "sudo systemctl restart apache2", - ]; + Ok(sess) +} - // Execute commands remotely +fn execute_remote_commands(sess: &mut Session, commands: &[&str]) -> io::Result<()> { let mut channel = sess.channel_session()?; - for cmd in &commands { - if let Err(err) = channel.exec(cmd) { - return Err(io::Error::new(io::ErrorKind::Other, err)); - } - let mut output = String::new(); - if let Err(err) = channel.read_to_string(&mut output) { - return Err(io::Error::new(io::ErrorKind::Other, err)); - } - println!("{}", output); - } + let commands_str = commands.join(" && "); + println!("Executing: {}", commands_str); + channel.exec(&commands_str)?; + let mut output = String::new(); + channel.read_to_string(&mut output)?; + println!("{}", output); + channel.wait_close()?; + Ok(()) +} - // Modify Apache virtual host configuration +fn configure_apache(sess: &mut Session, config: &Config) -> io::Result<()> { let apache_config = format!( r#" @@ -83,29 +119,17 @@ fn run() -> io::Result<()> { "#, - &config.domain_name, &config.domain_name, &config.domain_name + config.domain_name, config.domain_name, config.domain_name ); - if let Err(err) = channel.exec(&format!( - "echo '{}' | sudo tee /etc/apache2/sites-available/{}.conf", - apache_config, &config.domain_name - )) { - return Err(io::Error::new(io::ErrorKind::Other, err)); - } - if let Err(err) = channel.exec(&format!("sudo a2ensite {}.conf", &config.domain_name)) { - return Err(io::Error::new(io::ErrorKind::Other, err)); - } - if let Err(err) = channel.exec("sudo systemctl reload apache2") { - return Err(io::Error::new(io::ErrorKind::Other, err)); - } - - // Close the SSH session - if let Err(err) = channel.send_eof() { - return Err(io::Error::new(io::ErrorKind::Other, err)); - } - if let Err(err) = channel.wait_close() { - return Err(io::Error::new(io::ErrorKind::Other, err)); - } + let commands = [ + format!( + "echo '{}' | sudo tee /etc/apache2/sites-available/{}.conf", + apache_config, config.domain_name + ), + format!("sudo a2ensite {}.conf", config.domain_name), + "sudo systemctl reload apache2".to_string(), + ]; - Ok(()) + execute_remote_commands(sess, &commands.iter().map(String::as_str).collect::>()) }