use clap::{Parser, Subcommand}; use message_tools::BoxedMessage; use std::{ fs::OpenOptions, io::{BufRead, Write}, os::unix::process::ExitStatusExt, process::{Command, ExitStatus}, }; const WEB_BUILD_DIR: &str = "web-build"; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Cli { #[command(subcommand)] command: Commands, } #[derive(Subcommand, Debug)] enum Commands { /// Builds the website and server, including wasm files Build, /// Runs the server locally Start { /// Whether to build again or not #[arg(long)] rebuild: bool, }, /// Generates new encryption keys GenKeys { /// How many keypairs to generate #[arg(short, long, default_value = "100")] number: usize, /// Directory to put the generated secret key files #[arg(short, long, default_value = ".local/secrets.json")] sk_out_dir: String, /// Directory to put the generated pubklic key pool #[arg(long, default_value = "target/public_keys.json")] pk_out_dir: String, }, /// Reads fetched messages ReadMessages { /// Source file path #[arg(short, long, default_value = ".local/messages")] source: String, /// Where to find the secret keys for decrypting boxed messages #[arg(long, default_value = ".local/secrets.json")] sk_dir: String, /// Optional output file path for decoded messages /// Default: stdout #[arg(short, long)] out_dir: Option, }, } fn main() { let cli = Cli::parse(); match &cli.command { Commands::Build => { println!("Executing build command..."); build(); } Commands::Start { rebuild } => { println!("Starting server..."); if *rebuild { println!("Rebuilding web pages..."); build(); } // Change the current working directory of the running Rust program println!("Changing directory to {WEB_BUILD_DIR}..."); let cd_result = std::env::set_current_dir(WEB_BUILD_DIR); // Use the existing helper to check the result handle_command_status( cd_result.map(|_| std::process::ExitStatus::from_raw(0)), "cd build dir", ); println!("Running server executable..."); // Now run the server executable from within the new current directory handle_command_status(Command::new("./server").status(), "run server"); } Commands::GenKeys { number, sk_out_dir, pk_out_dir, } => { println!("Executing gen-keys command..."); message_tools::gen_keys::generate_keys_to_file(pk_out_dir, sk_out_dir, *number) .expect("write key files"); } Commands::ReadMessages { source, sk_dir, out_dir, } => { println!("Executing read-messages command..."); let kps = load_kps(sk_dir); let msgs = { let ms = fetch_all_messages_from_dir(source).expect("failed to read messages"); let mut unboxed: Vec = vec![]; // for loop go brrr, its less than 1000 keys for m in ms { if let Some(sk) = fetch_sk(&kps, &m.remote_pk().expect("failed to parse pk bytes")) { let who = m.tag(); let message = message_tools::unbox_message(m, sk).expect("failed to unbox message"); unboxed.push(UnboxedMessage { who, message }); } else { eprintln!("No secret key found for message {m:?}"); continue; } } unboxed }; if let Some(dir) = out_dir { let mut f = OpenOptions::new() .create(true) .write(true) .append(true) .open(dir) .expect("failed to create message output file"); for msg in msgs { writeln!(f, "{:?}", msg).expect("failed to write message"); } } else { println!("{msgs:?}"); } } } } fn fetch_sk( kps: &Vec, pk: &message_tools::PublicKey, ) -> Option { for kp in kps { if &kp.pk == pk { return Some(kp.sk.clone()); } } return None; } #[allow(dead_code)] // struct for easy display using debug #[derive(Debug)] struct UnboxedMessage { who: String, message: String, } struct KeyPair { pub pk: message_tools::PublicKey, pub sk: message_tools::StaticSecret, } fn load_kps(path: impl AsRef) -> Vec { let sks_bytes: Vec> = process_json_file(path).expect("failed to parse sk file"); sks_bytes .into_iter() .map(|sk| { let arr: [u8; 32] = sk.try_into().expect("invalid secret"); message_tools::StaticSecret::from(arr) }) .map(|sk| KeyPair { pk: message_tools::PublicKey::from(&sk), sk, }) .collect() } fn fetch_all_messages_from_dir( dir_path: impl AsRef, ) -> std::io::Result> { let mut all_messages: Vec = Vec::new(); for entry in std::fs::read_dir(dir_path)? { let entry = entry?; let path = entry.path(); if path.is_file() && path.extension().unwrap_or_default() == "json" { let file_messages = process_json_file(&path)?; all_messages.extend(file_messages); } } Ok(all_messages) } fn process_json_file( file_path: impl AsRef, ) -> std::io::Result> { let f = std::fs::OpenOptions::new() .read(true) .write(false) .open(file_path.as_ref()) .expect(&format!("failed to open file at {:?}", file_path.as_ref())); let reader = std::io::BufReader::new(f); let mut ds: Vec = vec![]; for line in reader.lines() { let line = line.expect("failed to read line"); let line = line.trim(); if line.is_empty() { continue; } let d: D = serde_json::from_str::(line).expect(&format!( "failed to parse message from file {:?}", file_path.as_ref() )); ds.push(d); } Ok(ds) } // A helper function to process the Result fn handle_command_status(status_result: std::io::Result, command_name: &str) { match status_result { Ok(exit_status) => { if !exit_status.success() { eprintln!( "\nCommand '{}' failed with exit code: {:?}", command_name, exit_status.code() ); std::process::exit(exit_status.code().unwrap_or(1)); } } Err(e) => { eprintln!("\nFailed to execute command '{}': {}", command_name, e); std::process::exit(1); // Exit with an error code } } } fn build() { println!("Building wasm package..."); // build the wasm package let status = Command::new("wasm-pack") .arg("build") .arg("--out-dir") .arg("../../routes/contact/pkg") .arg("crates/message-tools") .arg("--target") .arg("no-modules") .status(); handle_command_status(status, "wasm-pack build"); println!("Building server executable..."); // build the server executable let status = Command::new("cargo") .arg("build") .arg("--release") .args(["-p", "server"]) .args(["--target", "x86_64-unknown-linux-musl"]) .status(); handle_command_status(status, "cargo build server"); let server_bin = "target/x86_64-unknown-linux-musl/release/server"; println!("Creating build directory..."); // make webpage build directory if it doesn't already exist let status = Command::new("mkdir").args(["-p", WEB_BUILD_DIR]).status(); handle_command_status(status, "mkdir web-build"); println!("Copying files..."); // copy relevant files let status = Command::new("cp") .args(["-R", "-f", "routes"]) .arg(format!("{WEB_BUILD_DIR}/routes")) .status(); handle_command_status(status, "cp routes"); let status = Command::new("cp") .arg("-f") .arg(server_bin) .arg(format!("{WEB_BUILD_DIR}")) .status(); handle_command_status(status, "cp server bin"); let pubkey_file = "target/public_keys.json"; if !std::path::Path::new(pubkey_file).exists() { panic!("No public key file. Please generate keys first."); } let status = Command::new("mkdir") .args(["-p", &format!("{WEB_BUILD_DIR}/pubkeys")]) .status(); handle_command_status(status, "mkdir web-build"); let status = Command::new("cp") .arg("-f") .arg("target/public_keys.json") .arg(format!("{WEB_BUILD_DIR}/pubkeys/")) .status(); handle_command_status(status, "cp pubkeys"); println!("\nBuild successful!"); }