Files
acme-client-rs/src/main.rs

446 lines
17 KiB
Rust

#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate rust_util;
mod config;
mod x509;
mod network;
// 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::exit;
use std::time::{Duration, SystemTime};
use async_std::task;
use async_std::channel;
use async_std::channel::Sender;
use config::AcmeMode;
use crate::config::{CertConfig, CERT_NAME, KEY_NAME};
use crate::x509::{X509PublicKeyAlgo, X509EcPublicKeyAlgo};
use std::path::PathBuf;
use crate::network::{get_local_public_ip, get_resolver, resolve_first_ipv4};
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<BTreeMap<String, String>> = RwLock::new(BTreeMap::new());
}
#[derive(Debug, Default)]
struct AcmeRequest<'a> {
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<String>,
cert_file: Option<String>,
}
#[async_std::main]
async fn main() -> tide::Result<()> {
let matches = App::new(NAME)
.version(VERSION)
.about(DESCRIPTION)
.author(AUTHORS)
.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"))
.arg(Arg::with_name("algo").short("a").long("algo").takes_value(true).default_value("ec384").help("Pki algo"))
.arg(Arg::with_name("timeout").long("timeout").takes_value(true).default_value("5000").help("Timeout (ms)"))
.arg(Arg::with_name("mode").short("m").long("mode").takes_value(true).default_value("prod").help("Mode"))
.arg(Arg::with_name("dir").long("dir").takes_value(true).default_value("acme_dir").help("Account key dir"))
.arg(Arg::with_name("cert-dir").long("cert-dir").takes_value(true).help("Certificate dir"))
.arg(Arg::with_name("config").short("c").long("config").takes_value(true).help("Cert config"))
.arg(Arg::with_name("check").long("check").help("Check cert config"))
.arg(Arg::with_name("hide-logo").long("hide-logo").help("Hide logo"))
.arg(Arg::with_name("skip-verify-ip").long("skip-verify-ip").help("Skip verify public ip"))
.get_matches();
if matches.is_present("verbose") {
env::set_var("LOGGER_LEVEL", "*");
}
if matches.is_present("version") {
println!("{}", include_str!("logo.txt"));
information!("{} v{}", NAME, VERSION);
exit(1);
}
if !matches.is_present("hide-logo") {
println!("{}", include_str!("logo.txt"));
}
let skip_verify_ip = matches.is_present("skip-verify-ip");
let local_public_ip = if skip_verify_ip {
None
} else {
Some(get_local_public_ip().unwrap_or_else(|e| {
failure!("Get local public ip failed: {}", e);
exit(1);
}))
};
debugging!("Clap matches: {:?}", matches);
let account_dir = matches.value_of("dir").unwrap_or("acme_dir");
information!("Acme dir: {}", account_dir);
fs::create_dir_all(account_dir).ok();
let mut account_email = PathBuf::from(account_dir);
account_email.push("account_email.conf");
let email = if account_email.exists() {
match fs::read_to_string(&account_email) {
Err(e) => {
failure!("Read from file: {:?}, failed: {}", account_email, e);
exit(1);
}
Ok(email) => {
if let Some(email_from_args) = matches.value_of("email") {
if email != email_from_args {
warning!("Get email from account config: {}", email);
}
}
email.trim().to_string()
}
}
} else {
let email = matches.value_of("email").unwrap_or_else(|| {
failure!("Email is not assigned.");
exit(1);
});
information!("Write email to account config: {:?}", account_email);
if let Err(e) = fs::write(&account_email, email) {
warning!("Write email to account config: {:?}, failed: {}", account_email, e);
}
email.to_string()
};
match matches.value_of("type") {
Some("http") => {}
_ => {
failure!("Type is not assigned or must be http.");
exit(1);
}
}
let port: u16 = match matches.value_of("port") {
Some(p) => p.parse().unwrap_or_else(|e| {
failure!("Parse port: {}, failed: {}", p, e);
exit(1);
}),
None => {
failure!("Port is not assigned.");
exit(1);
}
};
let timeout: u64 = match matches.value_of("timeout") {
Some(p) => p.parse().unwrap_or_else(|e| {
failure!("Parse timeout: {}, failed: {}", p, e);
exit(1);
}),
None => {
failure!("Timeout is not assigned.");
exit(1);
}
};
let algo = match matches.value_of("algo") {
Some(a) => X509PublicKeyAlgo::from_str(a).unwrap_or_else(|e| {
failure!("{}", e);
exit(1);
}),
_ => {
failure!("Algo is not assigned, should be: ec256, ec384, rsa2048, rsa3073 or rsa4096.");
exit(1);
}
};
let mode = match matches.value_of("mode") {
Some(m) => AcmeMode::parse(m).unwrap_or_else(|e| {
failure!("{}", e);
exit(1);
}),
_ => {
failure!("AcmeMode is not assigned, should be: prod or test");
exit(1);
}
};
let cert_config_file = matches.value_of("config");
let cert_config = cert_config_file.map(|f|
CertConfig::load(f).unwrap_or_else(|e| {
failure!("Load cert config: {}, failed: {}", f, e);
exit(1);
}));
let port = cert_config.as_ref().map(|c| c.port).flatten().unwrap_or(port);
let check = matches.is_present("check");
if !check {
let (s, r) = channel::bounded(1);
startup_http_server(s, port);
r.recv().await.ok();
task::sleep(Duration::from_millis(500)).await;
}
match cert_config {
None => { // cert config is not assigned
if check {
failure!("Bad argument `--check`");
exit(1);
}
let domains_val = matches.values_of("domain").unwrap_or_else(|| {
failure!("Domains is not assigned.");
exit(1);
});
let domains: Vec<&str> = domains_val.collect::<Vec<_>>();
let primary_name = domains[0];
let alt_names: Vec<&str> = domains.into_iter().skip(1).collect();
information!("Domains, main: {}, alt: {:?}", primary_name, alt_names);
let (cert_file, key_file) = match matches.value_of("cert-dir") {
None => (None, None),
Some(cert_dir) => {
information!("Certificate output dir: {}", cert_dir);
fs::create_dir_all(cert_dir).ok();
(Some(format!("{}/{}", cert_dir, CERT_NAME)),
Some(format!("{}/{}", cert_dir, KEY_NAME)))
}
};
let acme_request = AcmeRequest {
contract_email: &email,
primary_name,
alt_names: &alt_names,
algo,
mode,
account_dir,
timeout,
local_public_ip: local_public_ip.as_deref(),
cert_file,
key_file,
..Default::default()
};
if let Err(e) = request_acme_certificate(acme_request) {
failure!("Request certificate by acme failed: {}", e);
exit(1);
}
}
Some(cert_config) => { // cert config is assigned
if check {
check_cert_config(&cert_config);
return Ok(());
}
let filtered_cert_config = cert_config.filter_cert_config_items(30);
for item in &filtered_cert_config.cert_items {
if item.common_name.as_ref().map(|n| n.contains('*')).unwrap_or(false)
|| item.dns_names.as_ref().map(|dns_names| dns_names.iter().any(|n| n.contains('*'))).unwrap_or(false) {
warning!("Currently not support wide card domain name");
continue;
}
if let (Some(common_name), Some(dns_names)) = (&item.common_name, &item.dns_names) {
information!("Domains, main: {}, alt: {:?}", common_name, dns_names);
let alt_names: Vec<&str> = dns_names.iter().map(|n| n.as_str()).collect();
let acme_request = AcmeRequest {
contract_email: &email,
primary_name: common_name,
alt_names: &alt_names,
algo,
mode,
account_dir,
timeout,
local_public_ip: local_public_ip.as_deref(),
cert_file: Some(format!("{}/{}", item.path, CERT_NAME)),
key_file: Some(format!("{}/{}", item.path, KEY_NAME)),
};
if let Err(e) = request_acme_certificate(acme_request) {
failure!("Request certificate: {}, by acme failed: {}", item.path, e);
}
}
}
}
}
Ok(())
}
fn check_cert_config(cert_config: &CertConfig) {
let secs_from_unix_epoch = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as i64;
let item_count = cert_config.cert_items.len();
for (i, item) in cert_config.cert_items.iter().enumerate() {
information!("Checking: {}, item {} of {}", item.path, i, item_count);
let cert_fn = format!("{}/{}", item.path, CERT_NAME);
let pem = match fs::read_to_string(&cert_fn) {
Ok(pem) => pem,
Err(e) => {
warning!("Read file: {}, failed: {}", cert_fn, e);
continue;
}
};
let x509_certificate = match x509::parse_x509(&cert_fn, &pem) {
Ok(cert) => cert,
Err(e) => {
failure!("Parse x509 file: {}, failed: {}", cert_fn, e);
continue;
}
};
success!("Found certificate: common name: {}, dns names: {:?}, public key algo: {:?}, valid days: {}",
x509_certificate.common_name,
x509_certificate.alt_names,
x509_certificate.public_key_algo,
(x509_certificate.certificate_not_after - secs_from_unix_epoch) / (24 * 3600)
);
}
}
fn request_acme_certificate(acme_request: AcmeRequest) -> 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: {}");
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 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 {
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: {}");
}
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 {
information!("Certificate key: {}", cert.private_key());
information!("Certificate pem: {}", cert.certificate());
}
Ok(())
}
fn startup_http_server(s: Sender<i32>, port: u16) {
task::spawn(async move {
information!("Listen at 0.0.0.0:{}", port);
let mut app = tide::new();
app.at("/").get(|_req: Request<()>| async move {
information!("Request / received");
Ok("acme-client\n")
});
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, query: {:?}, error: {}", req.url().query(), e);
return Ok("400 - bad request\n".to_string());
}
};
let peer = req.peer_addr().unwrap_or("none");
let auth_token = { TOKEN_MAP.read().unwrap().get(token).cloned() };
match auth_token {
Some(auth_token) => {
information!("Request acme challenge: {} -> {}, peer: {:?}", token, auth_token, peer);
Ok(auth_token)
}
None => {
warning!("Request acme challenge not found: {}, peer: {:?}", token, peer);
Ok("404 - not found\n".to_string())
}
}
});
app.at("/*").get(|req: Request<()>| async move {
warning!("Request /* received: {}", req.url());
Ok("acme-client *\n")
});
s.send(1).await.ok();
if let Err(e) = app.listen(&format!("0.0.0.0:{}", port)).await {
failure!("Failed to listen 0.0.0.0:{}, program will exit, error: {}", port, e);
exit(1);
}
});
}