use anyhow::{anyhow, Context, Result}; use async_stream::stream; use clap::Parser; use encoding::{label::encoding_from_whatwg_label, EncoderTrap}; use std::{ collections::BTreeMap, fmt::Display, path::{Path, PathBuf}, str::FromStr, }; use tokio::{ fs::{self, File}, io::{AsyncBufReadExt, BufReader}, }; use futures_core::stream::Stream; pub(crate) type VarsMap = BTreeMap; #[derive(Debug, Clone, PartialEq)] pub enum OpenSSLProviderArg { Internal, ExternalBin(String), } impl FromStr for OpenSSLProviderArg { type Err = anyhow::Error; fn from_str(s: &str) -> Result { match s.to_ascii_lowercase().as_str() { "internal" => Ok(OpenSSLProviderArg::Internal), x => Ok(OpenSSLProviderArg::ExternalBin(x.to_string())), } } } impl Display for OpenSSLProviderArg { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { OpenSSLProviderArg::ExternalBin(x) => write!(f, "{}", x), _ => write!(f, "internal"), } } } #[derive(Parser)] #[command(author, version, about, long_about = None)] pub(crate) struct Args { /// new client name pub(crate) name: String, /// pki directory #[arg(short, long)] pub(crate) directory: Option, /// client email #[arg(short, long)] pub(crate) email: Option, /// files encoding #[arg(short = 'c', long)] pub(crate) encoding: Option, /// keys subdir #[arg(long, default_value = "keys")] pub(crate) keys_dir: String, /// config subdir #[arg(long, default_value = "config")] pub(crate) config_dir: String, /// valid days #[arg(long, default_value = "3650")] pub(crate) days: u32, /// openssl binary or (internal) #[arg(long, short, default_value = "internal")] pub(crate) openssl: OpenSSLProviderArg, /// template file #[arg(long, default_value = "template.ovpn")] pub(crate) template_file: String, } pub(crate) struct AppConfig { pub(crate) encoding: String, pub(crate) req_days: u32, pub(crate) keys_subdir: String, pub(crate) config_subdir: String, pub(crate) template_file: String, pub(crate) openssl_default_cnf: String, pub(crate) openssl_cnf_env: String, pub(crate) ca_filename: String, pub(crate) default_email_domain: String, pub(crate) openssl: OpenSSLProviderArg, pub(crate) base_directory: String, pub(crate) email: String, pub(crate) name: String, } impl Default for AppConfig { 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(), openssl: OpenSSLProviderArg::Internal, base_directory: ".".into(), email: "name@example.com".into(), name: "user".into(), } } } impl From<&Args> for AppConfig { fn from(args: &Args) -> Self { let defaults = Self::default(); let base_directory = args .directory .as_ref() .unwrap_or(&defaults.base_directory) .clone(); let email = args.email.clone().unwrap_or(format!( "{}@{}", &args.name, defaults.default_email_domain.clone() )); let encoding = if let Some(enc) = args.encoding.clone() { enc.to_string() } else { defaults.encoding.clone() }; let name = args.name.clone(); let openssl = args.openssl.clone(); let template_file = args.template_file.clone(); let req_days = args.days; let keys_subdir = args.keys_dir.clone(); let config_subdir = args.config_dir.clone(); Self { base_directory, email, encoding, name, openssl, template_file, req_days, keys_subdir, config_subdir, ..defaults } } } pub(crate) 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 } pub(crate) 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")) } pub(crate) 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") } pub(crate) 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() } }) }) }