From d441cc9a85ce14527b5cfab7285d508915c01b90 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Sat, 19 Oct 2024 23:15:56 +0300 Subject: [PATCH] it works! --- Cargo.lock | 109 ++++---------- Cargo.toml | 6 +- src/main.rs | 412 +++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 439 insertions(+), 88 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7d6e3bc..1b565dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,6 +81,28 @@ version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37bf3594c4c988a53154954629820791dde498571819ae4ca50ca811e060cc95" +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "autocfg" version = "1.4.0" @@ -102,12 +124,6 @@ dependencies = [ "windows-targets", ] -[[package]] -name = "bitflags" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" - [[package]] name = "bytes" version = "1.7.2" @@ -349,16 +365,6 @@ version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" -[[package]] -name = "lock_api" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" -dependencies = [ - "autocfg", - "scopeguard", -] - [[package]] name = "memchr" version = "2.7.4" @@ -395,40 +401,19 @@ dependencies = [ "memchr", ] -[[package]] -name = "parking_lot" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets", -] - [[package]] name = "peazyrsa" version = "0.1.0" dependencies = [ "anyhow", + "async-stream", "clap", "encoding", "futures", + "futures-core", + "futures-util", "regex", "tokio", - "tokio-stream", ] [[package]] @@ -461,15 +446,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "redox_syscall" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" -dependencies = [ - "bitflags", -] - [[package]] name = "regex" version = "1.11.0" @@ -505,12 +481,6 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -529,22 +499,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "smallvec" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" - -[[package]] -name = "socket2" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" -dependencies = [ - "libc", - "windows-sys", -] - [[package]] name = "strsim" version = "0.11.1" @@ -572,10 +526,8 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", "tokio-macros", "windows-sys", ] @@ -591,17 +543,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tokio-stream" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - [[package]] name = "unicode-ident" version = "1.0.13" diff --git a/Cargo.toml b/Cargo.toml index c59c5ad..017be9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,9 +5,11 @@ edition = "2021" [dependencies] anyhow = "1.0.90" +async-stream = "0.3.6" clap = { version = "4.5.20", features = ["derive"] } encoding = "0.2.33" futures = "0.3.31" +futures-core = "0.3.31" +futures-util = "0.3.31" regex = "1.11.0" -tokio = { version = "1.40.0", features = ["full"] } -tokio-stream = "0.1.16" +tokio = { version = "1.40.0", features = ["fs", "rt", "process", "macros", "io-util"] } diff --git a/src/main.rs b/src/main.rs index e7a11a9..b9c0004 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,411 @@ -fn main() { - println!("Hello, world!"); +use anyhow::{anyhow, Context, Result}; +use clap::Parser; +use encoding::{label::encoding_from_whatwg_label, EncoderTrap}; +use regex::Regex; +use std::{collections::BTreeMap, path::{Path, PathBuf}, pin::Pin}; +use tokio::{ + fs::{self, File}, + io::{AsyncBufReadExt, BufReader}, +}; +use tokio::{pin, process::Command}; + +use async_stream::stream; + +use futures_core::stream::Stream; +use futures_util::stream::StreamExt; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Args { + /// new client name + name: String, + + /// pki directory + #[arg(short, long)] + directory: Option, + + /// client email + #[arg(short, long)] + email: Option, + + /// files encoding + #[arg(short = 'c', long)] + encoding: Option, +} + +struct VarsFile { + filepath: PathBuf, + vars: Option>, + encoding: String, +} + +struct Config { + encoding: String, + req_days: u32, + keys_subdir: String, + config_subdir: String, + template_file: String, + openssl_default_cnf: String, + openssl_cnf_env: String, + ca_filename: String, + default_email_domain: String, +} + +impl Default for Config { + fn default() -> Self { + Self { + encoding: "cp866".into(), + req_days: 30650, + keys_subdir: "keys".into(), + config_subdir: "config".into(), + template_file: "template.ovpn".into(), + openssl_default_cnf: "openssl-1.0.0.cnf".into(), + openssl_cnf_env: "KEY_CONFIG".into(), + ca_filename: "ca.crt".into(), + default_email_domain: "example.com".into(), + } + } +} + +async fn is_file_exist(filepath: &PathBuf) -> bool { + let metadata = tokio::fs::metadata(&filepath).await; + if metadata.is_err() { + return false; + } + + if !metadata.unwrap().is_file() { + return false; + } + + true +} + +async fn read_file<'a, S, P>(filepath: P, encoding: S) -> Result +where S: AsRef + std::cmp::PartialEq<&'a str>, P: AsRef { + let filepath = PathBuf::from(filepath.as_ref()); + if encoding == "utf8" { + return Ok(fs::read_to_string(filepath).await?); + } + + let enc = encoding_from_whatwg_label(encoding.as_ref()).ok_or(anyhow!("encoding not found"))?; + + let bytes = fs::read(filepath).await?; + enc.decode(&bytes, encoding::DecoderTrap::Ignore) + .map_err(|_| anyhow!("could not read file")) +} + +async fn write_file(filepath: &PathBuf, text: String, encoding: &str) -> Result<()> { + if encoding == "utf8" { + return Ok(fs::write(filepath, text).await?); + } + + let enc = encoding_from_whatwg_label(encoding).ok_or(anyhow!("encoding not found"))?; + let mut bytes = Vec::new(); + enc.encode_to(&text, EncoderTrap::Ignore, &mut bytes) + .map_err(|_| anyhow!("can't encode"))?; + + fs::write(filepath, bytes).await.context("can't write file") +} + +async fn read_file_by_lines( + filepath: &PathBuf, + encoding: &str, +) -> Result>> { + Ok(if encoding == "utf8" { + let f = File::open(filepath).await?; + let reader = BufReader::new(f); + let mut lines = reader.lines(); + Box::new(stream! { + while let Ok(Some(line)) = lines.next_line().await { + yield line + } + }) + } else { + let text = read_file(filepath, encoding).await?; + Box::new(stream! { + for line in text.lines() { + yield line.to_string() + } + }) + }) +} + +impl VarsFile { + async fn from_file(filepath: &PathBuf, encoding: String) -> Result { + let metadata = tokio::fs::metadata(&filepath).await.context(format!( + "file not found {}", + filepath.to_str().expect("str") + ))?; + if !metadata.is_file() { + Err(anyhow!("{} is not a file", filepath.to_str().expect("str")))? + } + Ok(VarsFile { + filepath: filepath.to_path_buf(), + vars: None, + encoding, + }) + } + + async fn from_dir(dir: PathBuf, encoding: String) -> Result { + let filepath = dir.join("vars"); + let err_context = format!( + "vars or vars.bat file not found in {}", + dir.to_str().expect("str") + ); + + match Self::from_file(&filepath, encoding.clone()).await { + Ok(res) => Ok(res), + Err(_) => Self::from_file(&filepath.with_extension("bat"), encoding.clone()) + .await + .map_err(|e| e.context(err_context)), + } + } + + async fn parse(&mut self) -> Result<()> { + let mut result = BTreeMap::new(); + let lines = read_file_by_lines(&self.filepath, &self.encoding).await?; + let lines = Pin::from(lines); + pin!(lines); + + let re_v2 = + Regex::new(r#"^(export|set)\s\b(?P[\w\d_]+)\b=\s?"?(?P[^\#]+?)"?$"#) + .context("regex v2")?; + let re_v3 = Regex::new(r"^set_var\s(?P[\w\d_]+)\s+(?P[^\#]+?)$") + .context("regex v3")?; + + while let Some(line) = lines.next().await { + if let Some(caps) = re_v2.captures(line.as_str()) { + result.insert(caps["key"].to_string(), caps["value"].to_string()); + continue; + } + + if let Some(caps) = re_v3.captures(line.as_str()) { + result.insert(caps["key"].to_string(), caps["value"].to_string()); + }; + } + + self.vars = Some(result); + Ok(()) + } + + #[allow(dead_code)] + fn apply(&self) -> Result<()> { + if let Some(vars) = self.vars.clone() { + for (key, value) in vars.iter() { + unsafe { + std::env::set_var(key, value); + } + } + } else { + Err(anyhow!("vars not parsed"))? + } + + Ok(()) + } +} + +struct Certs { + base_dir: PathBuf, + encoding: String, + req_days: u32, + ca_file: PathBuf, + req_file: PathBuf, + key_file: PathBuf, + cert_file: PathBuf, + config_file: PathBuf, + template_file: PathBuf, + openssl_cnf: PathBuf, + vars: BTreeMap, +} + +impl Certs { + fn new( + name: String, + email: String, + dir: PathBuf, + vars: BTreeMap, + cfg: &Config, + ) -> Self { + let base_dir = dir; + let keys_dir = base_dir.clone().join(cfg.keys_subdir.clone()); + let config_dir = base_dir.clone().join(cfg.config_subdir.clone()); + + let mut vars = vars; + + vars.insert("KEY_CN".into(), name.clone()); + vars.insert("KEY_NAME".into(), name.clone()); + vars.insert("KEY_EMAIL".into(), email); + + Self { + base_dir: base_dir.clone(), + encoding: cfg.encoding.clone(), + req_days: cfg.req_days, + ca_file: keys_dir.join(cfg.ca_filename.clone()), + req_file: keys_dir.join(format!("{}.csr", &name)), + key_file: keys_dir.join(format!("{}.key", &name)), + cert_file: keys_dir.join(format!("{}.crt", &name)), + config_file: config_dir.join(format!("{}.ovpn", &name)), + template_file: base_dir.clone().join(cfg.template_file.clone()), + openssl_cnf: base_dir.clone().join( + std::env::var(cfg.openssl_cnf_env.clone()) + .unwrap_or(cfg.openssl_default_cnf.clone()), + ), + vars, + } + } + + async fn is_ca_exists(&self) -> bool { + is_file_exist(&self.ca_file).await + } + + async fn is_config_exists(&self) -> bool { + is_file_exist(&self.config_file).await + } + + async fn is_cert_exists(&self) -> bool { + is_file_exist(&self.cert_file).await + } + + async fn is_req_exists(&self) -> bool { + is_file_exist(&self.req_file).await + } + + async fn request(&self) -> Result<()> { + if self.is_req_exists().await { + return Ok(()); + } + + if !self.is_ca_exists().await { + return Err(anyhow!( + "ca file not found: {}", + &self.ca_file.to_str().unwrap() + )); + } + + let status = Command::new("openssl") + .raw_arg(format!( + "req -nodes -new -keyout {} -out {} -config {} -batch", + &self.key_file.to_str().unwrap(), + &self.req_file.to_str().unwrap(), + &self.openssl_cnf.to_str().unwrap() + )) + .current_dir(&self.base_dir) + .envs(&self.vars) + .status() + .await?; + + match status.success() { + true => Ok(()), + false => Err(anyhow!("openssl req execution failed")), + } + } + + async fn sign(&self) -> Result<()> { + if self.is_cert_exists().await { + return Ok(()); + } + + if !self.is_ca_exists().await { + return Err(anyhow!( + "ca file not found: {}", + &self.ca_file.to_str().unwrap() + )); + } + + let status = Command::new("openssl") + .raw_arg(format!( + "ca -days {} -out {} -in {} -config {} -batch", + self.req_days, + &self.cert_file.to_str().unwrap(), + &self.req_file.to_str().unwrap(), + &self.openssl_cnf.to_str().unwrap() + )) + .current_dir(&self.base_dir) + .envs(&self.vars) + .status() + .await?; + + match status.success() { + true => Ok(()), + false => Err(anyhow!("openssl ca execution failed")), + } + } + + async fn build_client_config(&self) -> Result { + if self.is_config_exists().await { + return Ok(false); + } + + self.request().await?; + self.sign().await?; + + let (template_file, ca_file, cert_file, key_file) = ( + self.template_file.clone(), + self.ca_file.clone(), + self.cert_file.clone(), + self.key_file.clone() + ); + let enc = self.encoding.clone(); + let (enc1, enc2, enc3, enc4) = ( + enc.clone(), enc.clone(), enc.clone(), enc.clone() + ); + + if let (Ok(Ok(template)), Ok(Ok(ca)), Ok(Ok(cert)), Ok(Ok(key))) = tokio::join!( + tokio::spawn(read_file(template_file, enc1)), + tokio::spawn(read_file(ca_file, enc2)), + tokio::spawn(read_file(cert_file, enc3)), + tokio::spawn(read_file(key_file, enc4)) + ) { + let text = template + .replace("{{ca}}", ca.trim()) + .replace("{{cert}}", cert.trim()) + .replace("{{key}}", key.trim()); + + write_file(&self.config_file, text, &self.encoding).await?; + + Ok(true) + } else { + Err(anyhow!("files read error")) + } + } +} + +#[tokio::main(flavor="current_thread")] +async fn main() -> Result<()> { + let args = Args::parse(); + let default_directory = ".".to_string(); + let directory = args.directory.as_ref().unwrap_or(&default_directory); + let mut config = Config::default(); + let email = args.email.clone().unwrap_or(format!( + "{}@{}", + &args.name, + config.default_email_domain.clone() + )); + if let Some(encoding) = args.encoding.clone() { + config.encoding = encoding.to_string(); + } + + let mut vars = VarsFile::from_dir(PathBuf::from(directory), config.encoding.clone()).await?; + vars.parse().await?; + + println!("found vars: {}", vars.filepath.to_str().expect("fff")); + println!("loaded: {:#?}", &vars.vars); + + let certs = Certs::new( + args.name, + email, + directory.into(), + vars.vars.unwrap(), + &config, + ); + let created = certs.build_client_config().await?; + + let result_file = certs.config_file.to_str().unwrap(); + + if created { + println!("created: {result_file}"); + Ok(()) + } else { + Err(anyhow!("file exists: {result_file}")) + } }