it works!
This commit is contained in:
412
src/main.rs
412
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<String>,
|
||||
|
||||
/// client email
|
||||
#[arg(short, long)]
|
||||
email: Option<String>,
|
||||
|
||||
/// files encoding
|
||||
#[arg(short = 'c', long)]
|
||||
encoding: Option<String>,
|
||||
}
|
||||
|
||||
struct VarsFile {
|
||||
filepath: PathBuf,
|
||||
vars: Option<BTreeMap<String, String>>,
|
||||
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<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 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(())
|
||||
}
|
||||
}
|
||||
|
||||
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<String, String>,
|
||||
}
|
||||
|
||||
impl Certs {
|
||||
fn new(
|
||||
name: String,
|
||||
email: String,
|
||||
dir: PathBuf,
|
||||
vars: BTreeMap<String, String>,
|
||||
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<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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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}"))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user