it works!

This commit is contained in:
Dmitry Belyaev 2024-10-19 23:15:56 +03:00
parent 8e2352403e
commit d441cc9a85
Signed by: b4tman
GPG Key ID: 41A00BF15EA7E5F3
3 changed files with 439 additions and 88 deletions

109
Cargo.lock generated
View File

@ -81,6 +81,28 @@ version = "1.0.90"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37bf3594c4c988a53154954629820791dde498571819ae4ca50ca811e060cc95" 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]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.4.0" version = "1.4.0"
@ -102,12 +124,6 @@ dependencies = [
"windows-targets", "windows-targets",
] ]
[[package]]
name = "bitflags"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.7.2" version = "1.7.2"
@ -349,16 +365,6 @@ version = "0.2.161"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" 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]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.4" version = "2.7.4"
@ -395,40 +401,19 @@ dependencies = [
"memchr", "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]] [[package]]
name = "peazyrsa" name = "peazyrsa"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-stream",
"clap", "clap",
"encoding", "encoding",
"futures", "futures",
"futures-core",
"futures-util",
"regex", "regex",
"tokio", "tokio",
"tokio-stream",
] ]
[[package]] [[package]]
@ -461,15 +446,6 @@ dependencies = [
"proc-macro2", "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]] [[package]]
name = "regex" name = "regex"
version = "1.11.0" version = "1.11.0"
@ -505,12 +481,6 @@ version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.2" version = "1.4.2"
@ -529,22 +499,6 @@ dependencies = [
"autocfg", "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]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.1" version = "0.11.1"
@ -572,10 +526,8 @@ dependencies = [
"bytes", "bytes",
"libc", "libc",
"mio", "mio",
"parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
"socket2",
"tokio-macros", "tokio-macros",
"windows-sys", "windows-sys",
] ]
@ -591,17 +543,6 @@ dependencies = [
"syn", "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]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.13" version = "1.0.13"

View File

@ -5,9 +5,11 @@ edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0.90" anyhow = "1.0.90"
async-stream = "0.3.6"
clap = { version = "4.5.20", features = ["derive"] } clap = { version = "4.5.20", features = ["derive"] }
encoding = "0.2.33" encoding = "0.2.33"
futures = "0.3.31" futures = "0.3.31"
futures-core = "0.3.31"
futures-util = "0.3.31"
regex = "1.11.0" regex = "1.11.0"
tokio = { version = "1.40.0", features = ["full"] } tokio = { version = "1.40.0", features = ["fs", "rt", "process", "macros", "io-util"] }
tokio-stream = "0.1.16"

View File

@ -1,3 +1,411 @@
fn main() { use anyhow::{anyhow, Context, Result};
println!("Hello, world!"); 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}"))
}
} }