diff --git a/src/acme.rs b/src/acme.rs new file mode 100644 index 0000000..43b02b8 --- /dev/null +++ b/src/acme.rs @@ -0,0 +1,198 @@ +use std::sync::RwLock; +use std::collections::BTreeMap; +use std::fs; +use acme_lib::{create_p256_key, create_p384_key, create_rsa_key}; +use acme_lib::persist::FilePersist; +use acme_lib::Directory; +use aliyun_openapi_core_rust_sdk::RPClient; +use rust_util::XResult; +use crate::util::parse_dns_record; +use crate::network::{get_resolver, resolve_first_ipv4}; +use crate::ali_dns::{add_txt_dns_record, build_dns_client, delete_dns_record, list_dns, simple_parse_aliyun_supplier}; +use crate::config::{AcmeChallenge, AcmeMode}; +use crate::x509::{X509PublicKeyAlgo, X509EcPublicKeyAlgo}; + + +lazy_static! { + pub static ref TOKEN_MAP: RwLock> = RwLock::new(BTreeMap::new()); +} + +#[derive(Debug, Default)] +pub struct AcmeRequest<'a> { + pub challenge: AcmeChallenge, + // issue, single acme request can only process one supplier + pub credential_supplier: Option<&'a str>, + pub allow_interact: bool, + pub contract_email: &'a str, + pub primary_name: &'a str, + pub alt_names: &'a [&'a str], + pub algo: X509PublicKeyAlgo, + pub mode: AcmeMode, + pub account_dir: &'a str, + pub timeout: u64, + pub local_public_ip: Option<&'a str>, + pub key_file: Option, + pub cert_file: Option, + pub outputs_file: Option, +} + +pub fn request_acme_certificate(acme_request: AcmeRequest, dns_cleaned_domains: &mut Vec) -> XResult<()> { + if let Some(local_public_ip) = acme_request.local_public_ip { + let mut all_domains = vec![acme_request.primary_name.to_string()]; + for alt_name in acme_request.alt_names { + all_domains.push(alt_name.to_string()); + } + information!("Checking domain dns records, domains: {:?}", all_domains); + let resolver = opt_result!(get_resolver(), "Get resolver failed: {}"); + + if acme_request.challenge == AcmeChallenge::Http { + for domain in &all_domains { + debugging!("Checking domain: {}", domain); + let ipv4 = opt_result!(resolve_first_ipv4(&resolver, domain), "{}"); + match ipv4 { + None => return simple_error!("Resolve domain ip failed: {}", domain), + Some(ipv4) => if local_public_ip != ipv4 { + return simple_error!("Check domain ip: {}, mis-match, local: {} vs domain: {}", domain, local_public_ip, ipv4); + } + } + } + } + } + + information!("Acme mode: {:?}", acme_request.mode); + let url = acme_request.mode.directory_url(); + let persist = FilePersist::new(acme_request.account_dir); + let dir = opt_result!(Directory::from_url(persist, url), "Create directory from url failed: {}"); + let acc = opt_result!(dir.account(acme_request.contract_email), "Directory set account failed: {}"); + let mut ord_new = opt_result!( acc.new_order(acme_request.primary_name, acme_request.alt_names), "Create order failed: {}"); + let ali_yun_client: Option = match acme_request.credential_supplier { + Some(credential_supplier) => Some(build_dns_client( + &opt_result!(simple_parse_aliyun_supplier(credential_supplier), "Parse credential supplier failed: {}"))), + None => None, + }; + + let mut order_csr_index = 0; + let ord_csr = loop { + if let Some(ord_csr) = ord_new.confirm_validations() { + debugging!("Valid acme certificate http challenge success"); + break ord_csr; + } + + information!("Loop for acme challenge auth, #{}", order_csr_index); + order_csr_index += 1; + + debugging!("Start acme certificate http challenge"); + let auths = opt_result!(ord_new.authorizations(), "Order auth failed: {}"); + for auth in &auths { + match acme_request.challenge { + AcmeChallenge::Http => { + let chall = auth.http_challenge(); + let token = chall.http_token(); + let proof = chall.http_proof(); + + { + information!("Add acme http challenge: {} -> {}",token, proof); + TOKEN_MAP.write().unwrap().insert(token.to_string(), proof); + } + debugging!("Valid acme certificate http challenge"); + opt_result!(chall.validate(acme_request.timeout), "Validate http challenge failed: {}"); + } + AcmeChallenge::Dns => { + let chall = auth.dns_challenge(); + let record = format!("_acme-challenge.{}.", auth.domain_name()); + let proof = chall.dns_proof(); + information!("Add acme dns challenge: {} -> {}", record, proof); + + let rr_and_domain = opt_result!(parse_dns_record(&record), "Parse record to rr&domain failed: {}"); + + if !dns_cleaned_domains.contains(&rr_and_domain.1) { + information!("Clearing domain: {}", &rr_and_domain.1); + dns_cleaned_domains.push(rr_and_domain.1.clone()); + ali_yun_client.as_ref().map(|client| { + match list_dns(client, &rr_and_domain.1) { + Err(e) => warning!("List dns for: {}, failed: {}", &rr_and_domain.1, e), + Ok(Err(e)) => warning!("List dns for: {}, failed: {:?}", &rr_and_domain.1, e), + Ok(Ok(s)) => { + for r in &s.domain_records.record { + let rr = &r.rr; + if rr == "_acme-challenge" || rr.starts_with("_acme-challenge.") { + match delete_dns_record(client, &r.record_id) { + Err(e) => warning!("Delete dns: {}.{}, failed: {}", r.rr, r.domain_name, e), + Ok(Err(e)) => warning!("Delete dns: {}.{}, failed: {:?}", r.rr, r.domain_name, e), + Ok(Ok(_)) => success!("Delete dns: {}.{}", r.rr, r.domain_name), + } + } + } + } + } + }); + } + + match &ali_yun_client { + Some(client) => { + let add_txt_dns_result = opt_result!(add_txt_dns_record(client, &rr_and_domain.1, &rr_and_domain.0, &proof), "Add DNS TXT record failed: {}"); + match add_txt_dns_result { + Ok(s) => success!("Add dns txt record successes: {}", s.record_id), + Err(e) => return simple_error!("Add dns txt record failed: {:?}", e), + } + } + None => if acme_request.allow_interact { + let mut line = String::new(); + information!("You need to config dns manually, press enter to continue..."); + let _ = std::io::stdin().read_line(&mut line).unwrap(); + } else { + return simple_error!("Interact is not allowed, --allow-interact to allow interact"); + } + } + + debugging!("Valid acme certificate dns challenge"); + opt_result!(chall.validate(acme_request.timeout), "Validate dns challenge failed: {}"); + } + } + } + + debugging!("Refresh acme certificate order"); + opt_result!(ord_new.refresh(), "Refresh order failed: {}"); + }; + + information!("Generate private key, key type: {:?}", acme_request.algo); + let pkey_pri = match acme_request.algo { + X509PublicKeyAlgo::EcKey(X509EcPublicKeyAlgo::Secp256r1) => create_p256_key(), + X509PublicKeyAlgo::EcKey(X509EcPublicKeyAlgo::Secp384r1) => create_p384_key(), + X509PublicKeyAlgo::EcKey(X509EcPublicKeyAlgo::Secp521r1) => return simple_error!("Algo ec521 is not supported"), + X509PublicKeyAlgo::Rsa(bits) => create_rsa_key(bits), + }; + + debugging!("Invoking csr finalize pkey"); + let ord_cert = opt_result!( ord_csr.finalize_pkey(pkey_pri, acme_request.timeout), "Submit CSR failed: {}"); + debugging!("Downloading and save cert"); + let cert = opt_result!( ord_cert.download_and_save_cert(), "Download and save certificate failed: {}"); + + if let (Some(cert_file), Some(key_file)) = (&acme_request.cert_file, &acme_request.key_file) { + debugging!("Certificate key: {}", cert.private_key()); + debugging!("Certificate pem: {}", cert.certificate()); + information!("Write file: {}", cert_file); + if let Err(e) = fs::write(cert_file, cert.certificate()) { + failure!("Write file: {}, failed: {}", cert_file, e); + } + information!("Write file: {}", key_file); + if let Err(e) = fs::write(key_file, cert.private_key()) { + failure!("Write file: {}, failed: {}", key_file, e); + } + success!("Write files success: {} and {}", cert_file, key_file); + } else if let Some(outputs_file) = &acme_request.outputs_file { + let mut outputs = String::new(); + outputs.push_str("private key:\n"); + outputs.push_str(cert.private_key()); + outputs.push_str("\n\ncertificates:\n"); + outputs.push_str(cert.certificate()); + if let Err(e) = fs::write(outputs_file, outputs) { + failure!("Write file: {}, failed: {}", outputs_file, e); + } + } else { + information!("Certificate key: {}", cert.private_key()); + information!("Certificate pem: {}", cert.certificate()); + } + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index a93900c..1b8d378 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ extern crate lazy_static; #[macro_use] extern crate rust_util; +mod acme; mod util; mod config; mod x509; @@ -13,15 +14,9 @@ mod ali_dns; // mod simple_thread_pool; use std::env; -use rust_util::XResult; -use acme_lib::Directory; -use acme_lib::{create_p384_key, create_p256_key, create_rsa_key}; -use acme_lib::persist::FilePersist; use clap::{App, Arg}; use std::fs; use std::str::FromStr; -use std::sync::RwLock; -use std::collections::BTreeMap; use tide::Request; use std::process::{Command, exit}; use std::time::{Duration, SystemTime}; @@ -29,45 +24,20 @@ use async_std::task; use async_std::channel; use async_std::channel::Sender; use config::AcmeMode; -use crate::config::{AcmeChallenge, CertConfig, CERT_NAME, KEY_NAME}; -use crate::x509::{X509PublicKeyAlgo, X509EcPublicKeyAlgo}; use std::path::PathBuf; -use aliyun_openapi_core_rust_sdk::RPClient; use rust_util::util_cmd::run_command_and_wait; -use crate::ali_dns::{add_txt_dns_record, build_dns_client, delete_dns_record, list_dns, simple_parse_aliyun_supplier}; +use crate::config::{AcmeChallenge, CertConfig, CERT_NAME, KEY_NAME}; +use crate::x509::{X509PublicKeyAlgo}; use crate::dingtalk::send_dingtalk_message; -use crate::network::{get_local_public_ip, get_resolver, resolve_first_ipv4}; use crate::statics::{AcmeStatics, AcmeStatus}; -use crate::util::parse_dns_record; +use crate::acme::{AcmeRequest, request_acme_certificate}; +use crate::network::get_local_public_ip; const NAME: &str = env!("CARGO_PKG_NAME"); const VERSION: &str = env!("CARGO_PKG_VERSION"); const AUTHORS: &str = env!("CARGO_PKG_AUTHORS"); const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION"); -lazy_static! { - static ref TOKEN_MAP: RwLock> = RwLock::new(BTreeMap::new()); -} - -#[derive(Debug, Default)] -struct AcmeRequest<'a> { - challenge: AcmeChallenge, - // issue, single acme request can only process one supplier - credential_supplier: Option<&'a str>, - allow_interact: bool, - contract_email: &'a str, - primary_name: &'a str, - alt_names: &'a [&'a str], - algo: X509PublicKeyAlgo, - mode: AcmeMode, - account_dir: &'a str, - timeout: u64, - local_public_ip: Option<&'a str>, - key_file: Option, - cert_file: Option, - outputs_file: Option, -} - #[async_std::main] async fn main() -> tide::Result<()> { let matches = App::new(NAME) @@ -432,167 +402,6 @@ fn check_cert_config(cert_config: &CertConfig) { } } -fn request_acme_certificate(acme_request: AcmeRequest, dns_cleaned_domains: &mut Vec) -> XResult<()> { - if let Some(local_public_ip) = acme_request.local_public_ip { - let mut all_domains = vec![acme_request.primary_name.to_string()]; - for alt_name in acme_request.alt_names { - all_domains.push(alt_name.to_string()); - } - information!("Checking domain dns records, domains: {:?}", all_domains); - let resolver = opt_result!(get_resolver(), "Get resolver failed: {}"); - - if acme_request.challenge == AcmeChallenge::Http { - for domain in &all_domains { - debugging!("Checking domain: {}", domain); - let ipv4 = opt_result!(resolve_first_ipv4(&resolver, domain), "{}"); - match ipv4 { - None => return simple_error!("Resolve domain ip failed: {}", domain), - Some(ipv4) => if local_public_ip != ipv4 { - return simple_error!("Check domain ip: {}, mis-match, local: {} vs domain: {}", domain, local_public_ip, ipv4); - } - } - } - } - } - - information!("Acme mode: {:?}", acme_request.mode); - let url = acme_request.mode.directory_url(); - let persist = FilePersist::new(acme_request.account_dir); - let dir = opt_result!(Directory::from_url(persist, url), "Create directory from url failed: {}"); - let acc = opt_result!(dir.account(acme_request.contract_email), "Directory set account failed: {}"); - let mut ord_new = opt_result!( acc.new_order(acme_request.primary_name, acme_request.alt_names), "Create order failed: {}"); - let ali_yun_client: Option = match acme_request.credential_supplier { - Some(credential_supplier) => Some(build_dns_client( - &opt_result!(simple_parse_aliyun_supplier(credential_supplier), "Parse credential supplier failed: {}"))), - None => None, - }; - - let mut order_csr_index = 0; - let ord_csr = loop { - if let Some(ord_csr) = ord_new.confirm_validations() { - debugging!("Valid acme certificate http challenge success"); - break ord_csr; - } - - information!("Loop for acme challenge auth, #{}", order_csr_index); - order_csr_index += 1; - - debugging!("Start acme certificate http challenge"); - let auths = opt_result!(ord_new.authorizations(), "Order auth failed: {}"); - for auth in &auths { - match acme_request.challenge { - AcmeChallenge::Http => { - let chall = auth.http_challenge(); - let token = chall.http_token(); - let proof = chall.http_proof(); - - { - information!("Add acme http challenge: {} -> {}",token, proof); - TOKEN_MAP.write().unwrap().insert(token.to_string(), proof); - } - debugging!("Valid acme certificate http challenge"); - opt_result!(chall.validate(acme_request.timeout), "Validate http challenge failed: {}"); - } - AcmeChallenge::Dns => { - let chall = auth.dns_challenge(); - let record = format!("_acme-challenge.{}.", auth.domain_name()); - let proof = chall.dns_proof(); - information!("Add acme dns challenge: {} -> {}", record, proof); - - let rr_and_domain = opt_result!(parse_dns_record(&record), "Parse record to rr&domain failed: {}"); - - if !dns_cleaned_domains.contains(&rr_and_domain.1) { - information!("Clearing domain: {}", &rr_and_domain.1); - dns_cleaned_domains.push(rr_and_domain.1.clone()); - ali_yun_client.as_ref().map(|client| { - match list_dns(client, &rr_and_domain.1) { - Err(e) => warning!("List dns for: {}, failed: {}", &rr_and_domain.1, e), - Ok(Err(e)) => warning!("List dns for: {}, failed: {:?}", &rr_and_domain.1, e), - Ok(Ok(s)) => { - for r in &s.domain_records.record { - let rr = &r.rr; - if rr == "_acme-challenge" || rr.starts_with("_acme-challenge.") { - match delete_dns_record(client, &r.record_id) { - Err(e) => warning!("Delete dns: {}.{}, failed: {}", r.rr, r.domain_name, e), - Ok(Err(e)) => warning!("Delete dns: {}.{}, failed: {:?}", r.rr, r.domain_name, e), - Ok(Ok(_)) => success!("Delete dns: {}.{}", r.rr, r.domain_name), - } - } - } - } - } - }); - } - - match &ali_yun_client { - Some(client) => { - let add_txt_dns_result = opt_result!(add_txt_dns_record(client, &rr_and_domain.1, &rr_and_domain.0, &proof), "Add DNS TXT record failed: {}"); - match add_txt_dns_result { - Ok(s) => success!("Add dns txt record successes: {}", s.record_id), - Err(e) => return simple_error!("Add dns txt record failed: {:?}", e), - } - } - None => if acme_request.allow_interact { - let mut line = String::new(); - information!("You need to config dns manually, press enter to continue..."); - let _ = std::io::stdin().read_line(&mut line).unwrap(); - } else { - return simple_error!("Interact is not allowed, --allow-interact to allow interact"); - } - } - - debugging!("Valid acme certificate dns challenge"); - opt_result!(chall.validate(acme_request.timeout), "Validate dns challenge failed: {}"); - } - } - } - - debugging!("Refresh acme certificate order"); - opt_result!(ord_new.refresh(), "Refresh order failed: {}"); - }; - - information!("Generate private key, key type: {:?}", acme_request.algo); - let pkey_pri = match acme_request.algo { - X509PublicKeyAlgo::EcKey(X509EcPublicKeyAlgo::Secp256r1) => create_p256_key(), - X509PublicKeyAlgo::EcKey(X509EcPublicKeyAlgo::Secp384r1) => create_p384_key(), - X509PublicKeyAlgo::EcKey(X509EcPublicKeyAlgo::Secp521r1) => return simple_error!("Algo ec521 is not supported"), - X509PublicKeyAlgo::Rsa(bits) => create_rsa_key(bits), - }; - - debugging!("Invoking csr finalize pkey"); - let ord_cert = opt_result!( ord_csr.finalize_pkey(pkey_pri, acme_request.timeout), "Submit CSR failed: {}"); - debugging!("Downloading and save cert"); - let cert = opt_result!( ord_cert.download_and_save_cert(), "Download and save certificate failed: {}"); - - if let (Some(cert_file), Some(key_file)) = (&acme_request.cert_file, &acme_request.key_file) { - debugging!("Certificate key: {}", cert.private_key()); - debugging!("Certificate pem: {}", cert.certificate()); - information!("Write file: {}", cert_file); - if let Err(e) = fs::write(cert_file, cert.certificate()) { - failure!("Write file: {}, failed: {}", cert_file, e); - } - information!("Write file: {}", key_file); - if let Err(e) = fs::write(key_file, cert.private_key()) { - failure!("Write file: {}, failed: {}", key_file, e); - } - success!("Write files success: {} and {}", cert_file, key_file); - } else if let Some(outputs_file) = &acme_request.outputs_file { - let mut outputs = String::new(); - outputs.push_str("private key:\n"); - outputs.push_str(cert.private_key()); - outputs.push_str("\n\ncertificates:\n"); - outputs.push_str(cert.certificate()); - if let Err(e) = fs::write(outputs_file, outputs) { - failure!("Write file: {}, failed: {}", outputs_file, e); - } - } else { - information!("Certificate key: {}", cert.private_key()); - information!("Certificate pem: {}", cert.certificate()); - } - - Ok(()) -} - fn startup_http_server(s: Sender, port: u16) { task::spawn(async move { information!("Listen at 0.0.0.0:{}", port); @@ -610,7 +419,7 @@ fn startup_http_server(s: Sender, port: u16) { } }; let peer = req.peer_addr().unwrap_or("none"); - let auth_token = { TOKEN_MAP.read().unwrap().get(token).cloned() }; + let auth_token = { crate::acme::TOKEN_MAP.read().unwrap().get(token).cloned() }; match auth_token { Some(auth_token) => { information!("Request acme challenge: {} -> {}, peer: {:?}", token, auth_token, peer);