From 041a20156a0c9fc1fd24edc576d90d81a46f88dd Mon Sep 17 00:00:00 2001 From: Hatter Jiang Date: Fri, 30 Apr 2021 01:27:22 +0800 Subject: [PATCH] feat: init commit --- .gitignore | 2 + Cargo.toml | 16 +++++++ justfile | 10 ++++ src/main.rs | 135 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 163 insertions(+) create mode 100644 Cargo.toml create mode 100644 justfile create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore index bb487ee..7e0c669 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.idea/ +__temp_dir/ # ---> Rust # Generated by Cargo # will have compiled files and executables diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ac66ae4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "acme-client" +version = "0.1.0" +authors = ["Hatter Jiang "] +edition = "2018" +description = "Acme auto challenge client" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +lazy_static = "1.4.0" +clap = "2.33.3" +rust_util = "0.6" +acme-lib = "0.8.1" +tide = "0.16.0" +async-std = { version = "1.8.0", features = ["attributes"] } diff --git a/justfile b/justfile new file mode 100644 index 0000000..66e792a --- /dev/null +++ b/justfile @@ -0,0 +1,10 @@ +_: + @just --list + +# cross build x86-64 musl debug +cross-build-x64-debug: + cross build --target x86_64-unknown-linux-musl + +# cross build x86-64 musl release +cross-build-x64: + cross build --target x86_64-unknown-linux-musl --release diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..9a56bfa --- /dev/null +++ b/src/main.rs @@ -0,0 +1,135 @@ +#[macro_use] +extern crate lazy_static; +#[macro_use] +extern crate rust_util; + +use rust_util::XResult; +use acme_lib::{DirectoryUrl, Directory, create_p384_key}; +use acme_lib::persist::FilePersist; +use clap::{App, Arg}; +use std::sync::{Arc, RwLock}; +use std::collections::BTreeMap; +use tide::Request; + +const NAME: &str = env!("CARGO_PKG_NAME"); +const VERSION: &str = env!("CARGO_PKG_VERSION"); +const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION"); + +lazy_static! { + static ref TOKEN_MAP: Arc>> = Arc::new(RwLock::new(BTreeMap::new())); +} + +#[async_std::main] +async fn main() -> tide::Result<()> { + let matches = App::new(NAME) + .version(VERSION) + .about(DESCRIPTION) + .arg(Arg::with_name("version").short("V").long("version").help("Print version")) + .arg(Arg::with_name("verbose").short("v").long("verbose").help("Verbose")) + .arg(Arg::with_name("type").short("t").long("type").default_value("http").takes_value(true).help("Type http or dns")) + .arg(Arg::with_name("port").short("p").long("port").default_value("80").takes_value(true).help("Http port")) + .arg(Arg::with_name("domain").short("d").long("domain").multiple(true).takes_value(true).help("Domains")) + .arg(Arg::with_name("email").long("email").takes_value(true).help("Contract email")) + .get_matches(); + + if matches.is_present("version") { + information!("{} v{}", NAME, VERSION); + return Ok(()); + } + + let email = match matches.value_of("email") { + Some(email) => email, + None => { + failure!("Email is not assigned."); + return Ok(()); + } + }; + let _type = matches.value_of("type").expect("Failed to get type"); + let port: u16 = matches.value_of("port").expect("Failed to get port").parse().expect("Failed to parse port"); + + let domains_val = match matches.values_of("domain") { + Some(domain_val) => domain_val, + None => { + failure!("Domains is not assigned."); + return Ok(()); + } + }; + + let domains: Vec<&str> = domains_val.collect::>(); + information!("Domains: {:?}", domains); + + async_std::task::spawn(async move { + information!("Listen at 0.0.0.0:{}", port); + let mut app = tide::new(); + app.at("/.well-known/acme-challenge/:token").get(|req: Request<()>| async move { + let token = match req.param("token") { + Ok(token) => token, + Err(e) => { + warning!("Cannot get token from url, error: {}", e); + return Ok("400 - bad request".to_string()); + } + }; + let auth_token = { TOKEN_MAP.read().unwrap().get(token).cloned() }; + match auth_token { + Some(auth_token) => { + information!("Request acme challenge: {} -> {}", token, auth_token); + Ok(auth_token) + } + None => { + warning!("Request acme challenge not found: {}", token); + Ok("404 - not found".to_string()) + } + } + }); + if let Err(e) = app.listen(&format!("0.0.0.0:{}", port)).await { + failure!("Failed to listen 0.0.0.0:{}, error: {}", port, e); + } + }); + + let primary_name = domains[0]; + let alt_names: Vec<&str> = domains.into_iter().skip(1).collect(); + request_domains(email, primary_name, &alt_names).expect("Request domain failed"); + + Ok(()) +} + + +fn request_domains(contract_email: &str, primary_name: &str, alt_names: &[&str]) -> XResult<()> { + let url = DirectoryUrl::LetsEncrypt; + std::fs::create_dir("__temp_dir").expect("Create temp dir failed!"); + let persist = FilePersist::new("__temp_dir"); + let dir = Directory::from_url(persist, url)?; + let acc = dir.account(contract_email)?; + let mut ord_new = acc.new_order(primary_name, alt_names)?; + + let ord_csr = loop { + if let Some(ord_csr) = ord_new.confirm_validations() { + break ord_csr; + } + + let auths = ord_new.authorizations()?; + for auth in &auths { + let chall = auth.http_challenge(); + let token = chall.http_token(); + // let path = format!(".well-known/acme-challenge/{}", token); + let proof = chall.http_proof(); + + information!("Add acme challenge: {} -> {}",token, proof); + TOKEN_MAP.write().unwrap().insert(token.to_string(), proof); + + chall.validate(5000)?; + } + + ord_new.refresh()?; + }; + + let pkey_pri = create_p384_key(); + println!("{:?}", pkey_pri); + + let ord_cert = ord_csr.finalize_pkey(pkey_pri, 5000)?; + let cert = ord_cert.download_and_save_cert()?; + + println!("{:?}", cert); + + Ok(()) +}