226 lines
6.2 KiB
Rust
226 lines
6.2 KiB
Rust
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<String, String>;
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub enum OpenSSLProviderArg {
|
|
Internal,
|
|
ExternalBin(String),
|
|
}
|
|
|
|
impl FromStr for OpenSSLProviderArg {
|
|
type Err = anyhow::Error;
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
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<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 = "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<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()
|
|
}
|
|
})
|
|
})
|
|
}
|