commit d76faf35065fa6616b6d68994ae64b4b7d74ae35 Author: sisungo Date: Fri Jul 3 21:41:14 2026 +0800 initial commit Signed-off-by: sisungo diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4235f6a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "certutil" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1" +clap = { version = "4", features = ["derive"] } diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ce476ae --- /dev/null +++ b/src/main.rs @@ -0,0 +1,18 @@ +mod refresh; + +use clap::Parser; + +#[derive(Debug, Parser)] +enum Cli { + Refresh(refresh::Cli), +} + +fn main() { + let cli = Cli::parse(); + let result = match cli { + Cli::Refresh(cli) => refresh::main(cli), + }; + if let Err(err) = result { + eprintln!("error: {err}"); + } +} diff --git a/src/refresh.rs b/src/refresh.rs new file mode 100644 index 0000000..ed5901f --- /dev/null +++ b/src/refresh.rs @@ -0,0 +1,135 @@ +use anyhow::anyhow; +use clap::Parser; +use std::{ + fs::File, + io::{Read, Write}, + path::{Path, PathBuf}, + time::SystemTime, +}; + +#[derive(Debug, Parser)] +pub struct Cli { + #[arg(long, default_value = "/share/ca-certificates")] + scan: Vec, + + #[arg(long, default_value = "/var/config/ssl")] + ssl_config_dir: PathBuf, +} + +pub fn main(cli: Cli) -> anyhow::Result<()> { + _ = std::fs::create_dir_all(&cli.ssl_config_dir); + + let certs_dir = cli.ssl_config_dir.join("certs"); + let bundle_path = cli.ssl_config_dir.join("ca-certificates.crt"); + + let certs_obj_dir = certs_dir.with_added_extension(timestamp_ms()); + std::fs::create_dir(&certs_obj_dir) + .map_err(|e| anyhow!("failed to create \"{}\": {e}", certs_obj_dir.display()))?; + + let mut bundle_content = Vec::new(); + + for scan in cli.scan { + let read_dir = std::fs::read_dir(&scan) + .map_err(|e| anyhow!("failed to open \"{}\": {e}", scan.display()))?; + for it in read_dir { + let it = it.map_err(|e| anyhow!("failed to read \"{}\": {e}", scan.display()))?; + + let content = match read_file_limited(&it.path(), 4096) { + Ok(data) => data, + Err(err) => { + eprintln!("warning: {err}"); + continue; + } + }; + bundle_content.append(&mut content.clone()); + bundle_content.push(b'\n'); + + let copy_dst = certs_obj_dir.join(it.file_name()); + let mut copy_dst_file = File::options() + .create(true) + .truncate(true) + .write(true) + .open(©_dst) + .map_err(|e| anyhow!("failed to open \"{}\": {e}", copy_dst.display()))?; + copy_dst_file + .write_all(&content) + .map_err(|e| anyhow!("failed to write \"{}\": {e}", copy_dst.display()))?; + } + } + + run_openssl_rehash(&certs_obj_dir)?; + write_swp(&bundle_path, &bundle_content)?; + + let old_certs_obj_dir = std::fs::read_link(&certs_dir).ok(); + let certs_swp_dir = certs_dir.with_added_extension("swp"); + std::os::unix::fs::symlink(&certs_obj_dir, &certs_swp_dir).map_err(|e| { + anyhow!( + "failed to create symbolic link at \"{}\": {e}", + certs_swp_dir.display(), + ) + })?; + std::fs::rename(&certs_swp_dir, &certs_dir).map_err(|e| { + anyhow!( + "failed to rename \"{}\" to \"{}\": {e}", + certs_swp_dir.display(), + certs_dir.display() + ) + })?; + + if let Some(path) = old_certs_obj_dir { + _ = std::fs::remove_dir_all(&path); + } + + Ok(()) +} + +fn read_file_limited(path: &Path, limit: u64) -> anyhow::Result> { + let mut buf = Vec::with_capacity(limit as _); + let file = + File::open(path).map_err(|e| anyhow!("failed to open \"{}\": {e}", path.display()))?; + file.take(limit).read_to_end(&mut buf)?; + Ok(buf) +} + +fn write_swp(path: &Path, content: &[u8]) -> anyhow::Result<()> { + let swp = path.with_added_extension("swp"); + std::fs::write(&swp, content) + .map_err(|e| anyhow!("failed to write \"{}\": {e}", swp.display()))?; + std::fs::rename(&swp, path).map_err(|e| { + anyhow!( + "failed to rename \"{}\" to \"{}\": {e}", + swp.display(), + path.display(), + ) + })?; + Ok(()) +} + +fn find_openssl() -> String { + "openssl".into() +} + +fn run_openssl_rehash(path: &Path) -> anyhow::Result<()> { + let openssl_status = std::process::Command::new(find_openssl()) + .arg("rehash") + .arg(&path) + .spawn() + .map(|mut x| x.wait()) + .flatten() + .map_err(|e| anyhow!("failed to run \"{}\": {e}", find_openssl()))?; + if openssl_status.success() { + Ok(()) + } else { + Err(anyhow!( + "openssl rehash failed, see the messages above to get more information" + )) + } +} + +fn timestamp_ms() -> String { + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_millis() + .to_string() +}