src files
This commit is contained in:
		
							
								
								
									
										89
									
								
								src/certs.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								src/certs.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,89 @@
 | 
			
		||||
use anyhow::{anyhow, Result};
 | 
			
		||||
 | 
			
		||||
use std::{path::PathBuf, sync::Arc};
 | 
			
		||||
 | 
			
		||||
use crate::common::{is_file_exist, read_file, write_file, AppConfig};
 | 
			
		||||
use crate::crypto::ICryptoProvider;
 | 
			
		||||
 | 
			
		||||
pub(crate) struct Certs<T>
 | 
			
		||||
where
 | 
			
		||||
    T: ICryptoProvider,
 | 
			
		||||
{
 | 
			
		||||
    pub(crate) encoding: String,
 | 
			
		||||
    pub(crate) ca_file: PathBuf,
 | 
			
		||||
    pub(crate) key_file: PathBuf,
 | 
			
		||||
    pub(crate) cert_file: PathBuf,
 | 
			
		||||
    pub(crate) config_file: PathBuf,
 | 
			
		||||
    pub(crate) template_file: PathBuf,
 | 
			
		||||
    pub(crate) provider: Arc<T>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<T> Certs<T>
 | 
			
		||||
where
 | 
			
		||||
    T: ICryptoProvider,
 | 
			
		||||
{
 | 
			
		||||
    pub(crate) fn new(cfg: &AppConfig, provider: T) -> Self {
 | 
			
		||||
        let base_dir = PathBuf::from(&cfg.base_directory);
 | 
			
		||||
        let keys_dir = base_dir.clone().join(cfg.keys_subdir.clone());
 | 
			
		||||
        let config_dir = base_dir.clone().join(cfg.config_subdir.clone());
 | 
			
		||||
        let name = cfg.name.clone();
 | 
			
		||||
 | 
			
		||||
        Certs {
 | 
			
		||||
            encoding: cfg.encoding.clone(),
 | 
			
		||||
            ca_file: keys_dir.join(cfg.ca_filename.clone()),
 | 
			
		||||
            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()),
 | 
			
		||||
            provider: Arc::new(provider),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn is_config_exists(&self) -> bool {
 | 
			
		||||
        is_file_exist(&self.config_file).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) async fn request(&self) -> Result<()> {
 | 
			
		||||
        self.provider.request().await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) async fn sign(&self) -> Result<()> {
 | 
			
		||||
        self.provider.sign().await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) async fn build_client_config(&self) -> Result<bool> {
 | 
			
		||||
        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"))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										198
									
								
								src/common.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								src/common.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,198 @@
 | 
			
		||||
use anyhow::{anyhow, Context, Result};
 | 
			
		||||
use async_stream::stream;
 | 
			
		||||
use clap::Parser;
 | 
			
		||||
use encoding::{label::encoding_from_whatwg_label, EncoderTrap};
 | 
			
		||||
use std::{
 | 
			
		||||
    collections::BTreeMap,
 | 
			
		||||
    path::{Path, PathBuf},
 | 
			
		||||
};
 | 
			
		||||
use tokio::{
 | 
			
		||||
    fs::{self, File},
 | 
			
		||||
    io::{AsyncBufReadExt, BufReader},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use futures_core::stream::Stream;
 | 
			
		||||
 | 
			
		||||
pub(crate) type VarsMap = BTreeMap<String, String>;
 | 
			
		||||
 | 
			
		||||
#[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<String>,
 | 
			
		||||
 | 
			
		||||
    /// client email
 | 
			
		||||
    #[arg(short, long)]
 | 
			
		||||
    pub(crate) email: Option<String>,
 | 
			
		||||
 | 
			
		||||
    /// files encoding
 | 
			
		||||
    #[arg(short = 'c', long)]
 | 
			
		||||
    pub(crate) encoding: Option<String>,
 | 
			
		||||
 | 
			
		||||
    /// 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 = "30650")]
 | 
			
		||||
    pub(crate) days: u32,
 | 
			
		||||
 | 
			
		||||
    /// openssl binary
 | 
			
		||||
    #[arg(long, default_value = "openssl")]
 | 
			
		||||
    pub(crate) openssl: String,
 | 
			
		||||
 | 
			
		||||
    /// 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: String,
 | 
			
		||||
    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: "openssl".into(),
 | 
			
		||||
            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<String>
 | 
			
		||||
where
 | 
			
		||||
    S: AsRef<str> + std::cmp::PartialEq<&'a str>,
 | 
			
		||||
    P: AsRef<Path>,
 | 
			
		||||
{
 | 
			
		||||
    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<Box<dyn Stream<Item = String>>> {
 | 
			
		||||
    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()
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										146
									
								
								src/crypto.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								src/crypto.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,146 @@
 | 
			
		||||
use anyhow::{anyhow, Result};
 | 
			
		||||
use std::path::PathBuf;
 | 
			
		||||
 | 
			
		||||
use tokio::process::Command;
 | 
			
		||||
 | 
			
		||||
use crate::common::{is_file_exist, AppConfig, VarsMap};
 | 
			
		||||
 | 
			
		||||
pub(crate) trait ICryptoProvider {
 | 
			
		||||
    async fn request(&self) -> Result<()>;
 | 
			
		||||
    async fn sign(&self) -> Result<()>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub(crate) fn make_certs_provider(cfg: &AppConfig, vars: VarsMap) -> impl ICryptoProvider {
 | 
			
		||||
    OpenSSLProvider::from_cfg(cfg, vars)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub(crate) struct OpenSSLProvider {
 | 
			
		||||
    vars: VarsMap,
 | 
			
		||||
    base_dir: PathBuf,
 | 
			
		||||
    openssl_cnf: PathBuf,
 | 
			
		||||
    openssl: String,
 | 
			
		||||
    ca_file: PathBuf,
 | 
			
		||||
    req_file: PathBuf,
 | 
			
		||||
    key_file: PathBuf,
 | 
			
		||||
    cert_file: PathBuf,
 | 
			
		||||
    req_days: u32,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl OpenSSLProvider {
 | 
			
		||||
    async fn is_ca_exists(&self) -> bool {
 | 
			
		||||
        is_file_exist(&self.ca_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
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) fn from_cfg(cfg: &AppConfig, vars: VarsMap) -> Self {
 | 
			
		||||
        let base_dir = PathBuf::from(&cfg.base_directory);
 | 
			
		||||
        let keys_dir = base_dir.clone().join(cfg.keys_subdir.clone());
 | 
			
		||||
        let name = cfg.name.clone();
 | 
			
		||||
        let mut vars = vars;
 | 
			
		||||
 | 
			
		||||
        vars.insert("KEY_CN".into(), name.clone());
 | 
			
		||||
        vars.insert("KEY_NAME".into(), name.clone());
 | 
			
		||||
        vars.insert("KEY_EMAIL".into(), cfg.email.clone());
 | 
			
		||||
 | 
			
		||||
        let ca_file = keys_dir.join(cfg.ca_filename.clone());
 | 
			
		||||
        let req_file = keys_dir.join(format!("{}.csr", &name));
 | 
			
		||||
        let key_file = keys_dir.join(format!("{}.key", &name));
 | 
			
		||||
        let cert_file = keys_dir.join(format!("{}.crt", &name));
 | 
			
		||||
        let openssl_cnf = base_dir.clone().join(
 | 
			
		||||
            std::env::var(cfg.openssl_cnf_env.clone()).unwrap_or(cfg.openssl_default_cnf.clone()),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            vars,
 | 
			
		||||
            base_dir,
 | 
			
		||||
            openssl_cnf,
 | 
			
		||||
            openssl: cfg.openssl.clone(),
 | 
			
		||||
            ca_file,
 | 
			
		||||
            req_file,
 | 
			
		||||
            key_file,
 | 
			
		||||
            cert_file,
 | 
			
		||||
            req_days: cfg.req_days,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ICryptoProvider for OpenSSLProvider {
 | 
			
		||||
    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(&self.openssl)
 | 
			
		||||
            .args([
 | 
			
		||||
                "req",
 | 
			
		||||
                "-nodes",
 | 
			
		||||
                "-new",
 | 
			
		||||
                "-keyout",
 | 
			
		||||
                self.key_file.to_str().unwrap(),
 | 
			
		||||
                "-out",
 | 
			
		||||
                self.req_file.to_str().unwrap(),
 | 
			
		||||
                "-config",
 | 
			
		||||
                self.openssl_cnf.to_str().unwrap(),
 | 
			
		||||
                "-batch",
 | 
			
		||||
            ])
 | 
			
		||||
            .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(&self.openssl)
 | 
			
		||||
            .args([
 | 
			
		||||
                "ca",
 | 
			
		||||
                "-days",
 | 
			
		||||
                format!("{}", self.req_days).as_str(),
 | 
			
		||||
                "-out",
 | 
			
		||||
                self.cert_file.to_str().unwrap(),
 | 
			
		||||
                "-in",
 | 
			
		||||
                self.req_file.to_str().unwrap(),
 | 
			
		||||
                "-config",
 | 
			
		||||
                self.openssl_cnf.to_str().unwrap(),
 | 
			
		||||
                "-batch",
 | 
			
		||||
            ])
 | 
			
		||||
            .current_dir(&self.base_dir)
 | 
			
		||||
            .envs(&self.vars)
 | 
			
		||||
            .status()
 | 
			
		||||
            .await?;
 | 
			
		||||
 | 
			
		||||
        match status.success() {
 | 
			
		||||
            true => Ok(()),
 | 
			
		||||
            false => Err(anyhow!("ssl ca execution failed")),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										519
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										519
									
								
								src/main.rs
									
									
									
									
									
								
							@@ -1,516 +1,15 @@
 | 
			
		||||
use anyhow::{anyhow, Context, Result};
 | 
			
		||||
use anyhow::{anyhow, Result};
 | 
			
		||||
use clap::Parser;
 | 
			
		||||
use encoding::{label::encoding_from_whatwg_label, EncoderTrap};
 | 
			
		||||
use regex::Regex;
 | 
			
		||||
use std::{
 | 
			
		||||
    collections::BTreeMap,
 | 
			
		||||
    path::{Path, PathBuf},
 | 
			
		||||
    pin::Pin,
 | 
			
		||||
    sync::Arc,
 | 
			
		||||
};
 | 
			
		||||
use tokio::{
 | 
			
		||||
    fs::{self, File},
 | 
			
		||||
    io::{AsyncBufReadExt, BufReader},
 | 
			
		||||
};
 | 
			
		||||
use tokio::{pin, process::Command};
 | 
			
		||||
 | 
			
		||||
use async_stream::stream;
 | 
			
		||||
mod certs;
 | 
			
		||||
mod common;
 | 
			
		||||
mod crypto;
 | 
			
		||||
mod vars;
 | 
			
		||||
 | 
			
		||||
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<String>,
 | 
			
		||||
 | 
			
		||||
    /// client email
 | 
			
		||||
    #[arg(short, long)]
 | 
			
		||||
    email: Option<String>,
 | 
			
		||||
 | 
			
		||||
    /// files encoding
 | 
			
		||||
    #[arg(short = 'c', long)]
 | 
			
		||||
    encoding: Option<String>,
 | 
			
		||||
 | 
			
		||||
    /// keys subdir
 | 
			
		||||
    #[arg(long, default_value = "keys")]
 | 
			
		||||
    keys_dir: String,
 | 
			
		||||
 | 
			
		||||
    /// config subdir
 | 
			
		||||
    #[arg(long, default_value = "config")]
 | 
			
		||||
    config_dir: String,
 | 
			
		||||
 | 
			
		||||
    /// valid days
 | 
			
		||||
    #[arg(long, default_value = "30650")]
 | 
			
		||||
    days: u32,
 | 
			
		||||
 | 
			
		||||
    /// openssl binary
 | 
			
		||||
    #[arg(long, default_value = "openssl")]
 | 
			
		||||
    openssl: String,
 | 
			
		||||
 | 
			
		||||
    /// template file
 | 
			
		||||
    #[arg(long, default_value = "template.ovpn")]
 | 
			
		||||
    template_file: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct VarsFile {
 | 
			
		||||
    filepath: PathBuf,
 | 
			
		||||
    vars: Option<BTreeMap<String, String>>,
 | 
			
		||||
    encoding: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct AppConfig {
 | 
			
		||||
    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,
 | 
			
		||||
    openssl: String,
 | 
			
		||||
    base_directory: String,
 | 
			
		||||
    email: String,
 | 
			
		||||
    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: "openssl".into(),
 | 
			
		||||
            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
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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<String>
 | 
			
		||||
where
 | 
			
		||||
    S: AsRef<str> + std::cmp::PartialEq<&'a str>,
 | 
			
		||||
    P: AsRef<Path>,
 | 
			
		||||
{
 | 
			
		||||
    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<Box<dyn Stream<Item = String>>> {
 | 
			
		||||
    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<Self> {
 | 
			
		||||
        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<Self> {
 | 
			
		||||
        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 from_config(config: &AppConfig) -> Result<Self> {
 | 
			
		||||
        Self::from_dir(
 | 
			
		||||
            PathBuf::from(&config.base_directory),
 | 
			
		||||
            config.encoding.clone(),
 | 
			
		||||
        )
 | 
			
		||||
        .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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<key>[\w\d_]+)\b=\s?"?(?P<value>[^\#]+?)"?$"#)
 | 
			
		||||
                .context("regex v2")?;
 | 
			
		||||
        let re_v3 = Regex::new(r"^set_var\s(?P<key1>[\w\d_]+)\s+(?P<value1>[^\#]+?)$")
 | 
			
		||||
            .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(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
trait ICryptoProvider {
 | 
			
		||||
    async fn request(&self) -> Result<()>;
 | 
			
		||||
    async fn sign(&self) -> Result<()>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct OpenSSLProvider {
 | 
			
		||||
    vars: BTreeMap<String, String>,
 | 
			
		||||
    base_dir: PathBuf,
 | 
			
		||||
    openssl_cnf: PathBuf,
 | 
			
		||||
    openssl: String,
 | 
			
		||||
    ca_file: PathBuf,
 | 
			
		||||
    req_file: PathBuf,
 | 
			
		||||
    key_file: PathBuf,
 | 
			
		||||
    cert_file: PathBuf,
 | 
			
		||||
    req_days: u32,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl OpenSSLProvider {
 | 
			
		||||
    async fn is_ca_exists(&self) -> bool {
 | 
			
		||||
        is_file_exist(&self.ca_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
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn from_cfg(cfg: &AppConfig, vars: BTreeMap<String, String>) -> Self {
 | 
			
		||||
        let base_dir = PathBuf::from(&cfg.base_directory);
 | 
			
		||||
        let keys_dir = base_dir.clone().join(cfg.keys_subdir.clone());
 | 
			
		||||
        let name = cfg.name.clone();
 | 
			
		||||
        let mut vars = vars;
 | 
			
		||||
 | 
			
		||||
        vars.insert("KEY_CN".into(), name.clone());
 | 
			
		||||
        vars.insert("KEY_NAME".into(), name.clone());
 | 
			
		||||
        vars.insert("KEY_EMAIL".into(), cfg.email.clone());
 | 
			
		||||
 | 
			
		||||
        let ca_file = keys_dir.join(cfg.ca_filename.clone());
 | 
			
		||||
        let req_file = keys_dir.join(format!("{}.csr", &name));
 | 
			
		||||
        let key_file = keys_dir.join(format!("{}.key", &name));
 | 
			
		||||
        let cert_file = keys_dir.join(format!("{}.crt", &name));
 | 
			
		||||
        let openssl_cnf = base_dir.clone().join(
 | 
			
		||||
            std::env::var(cfg.openssl_cnf_env.clone()).unwrap_or(cfg.openssl_default_cnf.clone()),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        Self {
 | 
			
		||||
            vars,
 | 
			
		||||
            base_dir,
 | 
			
		||||
            openssl_cnf,
 | 
			
		||||
            openssl: cfg.openssl.clone(),
 | 
			
		||||
            ca_file,
 | 
			
		||||
            req_file,
 | 
			
		||||
            key_file,
 | 
			
		||||
            cert_file,
 | 
			
		||||
            req_days: cfg.req_days,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ICryptoProvider for OpenSSLProvider {
 | 
			
		||||
    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(&self.openssl)
 | 
			
		||||
            .args([
 | 
			
		||||
                "req",
 | 
			
		||||
                "-nodes",
 | 
			
		||||
                "-new",
 | 
			
		||||
                "-keyout",
 | 
			
		||||
                self.key_file.to_str().unwrap(),
 | 
			
		||||
                "-out",
 | 
			
		||||
                self.req_file.to_str().unwrap(),
 | 
			
		||||
                "-config",
 | 
			
		||||
                self.openssl_cnf.to_str().unwrap(),
 | 
			
		||||
                "-batch",
 | 
			
		||||
            ])
 | 
			
		||||
            .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(&self.openssl)
 | 
			
		||||
            .args([
 | 
			
		||||
                "ca",
 | 
			
		||||
                "-days",
 | 
			
		||||
                format!("{}", self.req_days).as_str(),
 | 
			
		||||
                "-out",
 | 
			
		||||
                self.cert_file.to_str().unwrap(),
 | 
			
		||||
                "-in",
 | 
			
		||||
                self.req_file.to_str().unwrap(),
 | 
			
		||||
                "-config",
 | 
			
		||||
                self.openssl_cnf.to_str().unwrap(),
 | 
			
		||||
                "-batch",
 | 
			
		||||
            ])
 | 
			
		||||
            .current_dir(&self.base_dir)
 | 
			
		||||
            .envs(&self.vars)
 | 
			
		||||
            .status()
 | 
			
		||||
            .await?;
 | 
			
		||||
 | 
			
		||||
        match status.success() {
 | 
			
		||||
            true => Ok(()),
 | 
			
		||||
            false => Err(anyhow!("ssl ca execution failed")),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct Certs<T>
 | 
			
		||||
where
 | 
			
		||||
    T: ICryptoProvider,
 | 
			
		||||
{
 | 
			
		||||
    encoding: String,
 | 
			
		||||
    ca_file: PathBuf,
 | 
			
		||||
    key_file: PathBuf,
 | 
			
		||||
    cert_file: PathBuf,
 | 
			
		||||
    config_file: PathBuf,
 | 
			
		||||
    template_file: PathBuf,
 | 
			
		||||
    provider: Arc<T>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn make_certs_provider(cfg: &AppConfig, vars: BTreeMap<String, String>) -> impl ICryptoProvider {
 | 
			
		||||
    OpenSSLProvider::from_cfg(cfg, vars)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<T> Certs<T>
 | 
			
		||||
where
 | 
			
		||||
    T: ICryptoProvider,
 | 
			
		||||
{
 | 
			
		||||
    fn new(cfg: &AppConfig, provider: T) -> Self {
 | 
			
		||||
        let base_dir = PathBuf::from(&cfg.base_directory);
 | 
			
		||||
        let keys_dir = base_dir.clone().join(cfg.keys_subdir.clone());
 | 
			
		||||
        let config_dir = base_dir.clone().join(cfg.config_subdir.clone());
 | 
			
		||||
        let name = cfg.name.clone();
 | 
			
		||||
 | 
			
		||||
        Certs {
 | 
			
		||||
            encoding: cfg.encoding.clone(),
 | 
			
		||||
            ca_file: keys_dir.join(cfg.ca_filename.clone()),
 | 
			
		||||
            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()),
 | 
			
		||||
            provider: Arc::new(provider),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn is_config_exists(&self) -> bool {
 | 
			
		||||
        is_file_exist(&self.config_file).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn request(&self) -> Result<()> {
 | 
			
		||||
        self.provider.request().await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn sign(&self) -> Result<()> {
 | 
			
		||||
        self.provider.sign().await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn build_client_config(&self) -> Result<bool> {
 | 
			
		||||
        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"))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
use crate::certs::Certs;
 | 
			
		||||
use crate::common::{AppConfig, Args};
 | 
			
		||||
use crate::crypto::make_certs_provider;
 | 
			
		||||
use crate::vars::VarsFile;
 | 
			
		||||
 | 
			
		||||
#[tokio::main(flavor = "current_thread")]
 | 
			
		||||
async fn main() -> Result<()> {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										96
									
								
								src/vars.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								src/vars.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,96 @@
 | 
			
		||||
use anyhow::{anyhow, Context, Result};
 | 
			
		||||
use regex::Regex;
 | 
			
		||||
use std::{path::PathBuf, pin::Pin};
 | 
			
		||||
use tokio::pin;
 | 
			
		||||
 | 
			
		||||
use futures_util::stream::StreamExt;
 | 
			
		||||
 | 
			
		||||
use crate::common::{read_file_by_lines, AppConfig, VarsMap};
 | 
			
		||||
 | 
			
		||||
pub(crate) struct VarsFile {
 | 
			
		||||
    pub(crate) filepath: PathBuf,
 | 
			
		||||
    pub(crate) vars: Option<VarsMap>,
 | 
			
		||||
    pub(crate) encoding: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl VarsFile {
 | 
			
		||||
    async fn from_file(filepath: &PathBuf, encoding: String) -> Result<Self> {
 | 
			
		||||
        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<Self> {
 | 
			
		||||
        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)),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) async fn from_config(config: &AppConfig) -> Result<Self> {
 | 
			
		||||
        Self::from_dir(
 | 
			
		||||
            PathBuf::from(&config.base_directory),
 | 
			
		||||
            config.encoding.clone(),
 | 
			
		||||
        )
 | 
			
		||||
        .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) async fn parse(&mut self) -> Result<()> {
 | 
			
		||||
        let mut result = VarsMap::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<key>[\w\d_]+)\b=\s?"?(?P<value>[^\#]+?)"?$"#)
 | 
			
		||||
                .context("regex v2")?;
 | 
			
		||||
        let re_v3 = Regex::new(r"^set_var\s(?P<key1>[\w\d_]+)\s+(?P<value1>[^\#]+?)$")
 | 
			
		||||
            .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(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user