From 87e052e67bd60845aedde1b170c11f00727831fb Mon Sep 17 00:00:00 2001 From: Hatter Jiang Date: Tue, 25 Mar 2025 23:04:11 +0800 Subject: [PATCH] feat: v1.3.7, add webhook --- Cargo.lock | 14 +++++- Cargo.toml | 4 +- justfile | 35 +-------------- src/acme.rs | 6 +-- src/ali_dns.rs | 4 +- src/config.rs | 13 ++---- src/main.rs | 47 ++++++++++---------- src/{dingtalk.rs => notification.rs} | 65 +++++++++++++++++----------- src/statistics.rs | 4 +- src/x509.rs | 12 ++--- 10 files changed, 98 insertions(+), 106 deletions(-) rename src/{dingtalk.rs => notification.rs} (59%) diff --git a/Cargo.lock b/Cargo.lock index c237224..447f99c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,10 +1,10 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "acme-client" -version = "1.3.6" +version = "1.3.7" dependencies = [ "acme-lib", "aliyun-openapi-core-rust-sdk", @@ -1817,6 +1817,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-src" +version = "300.4.2+3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168ce4e058f975fe43e89d9ccf78ca668601887ae736090aacc23ae353c298e2" +dependencies = [ + "cc", +] + [[package]] name = "openssl-sys" version = "0.9.103" @@ -1825,6 +1834,7 @@ checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] diff --git a/Cargo.toml b/Cargo.toml index 675c40b..d3c2f49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "acme-client" -version = "1.3.6" +version = "1.3.7" authors = ["Hatter Jiang "] edition = "2018" description = "Acme auto challenge client, acme-client can issue certificates from Let's encrypt" @@ -17,7 +17,7 @@ async-std = { version = "1.8", features = ["attributes"] } serde = { version = "1.0", features = ["derive"] } deser-hjson = "2.2" x509-parser = "0.9" -reqwest = { version = "0.11", features = ["blocking"] } +reqwest = { version = "0.11", default-features = false, features = ["blocking", "native-tls-vendored"] } #reqwest = { version = "0.11", default-features = false, features = ["blocking", "rustls-tls"] } trust-dns-resolver = "0.23" simpledateformat = "0.1.3" diff --git a/justfile b/justfile index 4b7ef2e..ef83a23 100644 --- a/justfile +++ b/justfile @@ -1,37 +1,6 @@ _: @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 - -# cross build x86-64 musl release(for openssl) -cross-build-x64-musl: - docker run --rm -it \ - -v "$(echo $HOME)/.cargo_messense_rust-musl-cross_x86_64-musl_registry":/root/.cargo/registry/ \ - -v "$(pwd)":/home/rust/src \ - messense/rust-musl-cross:x86_64-musl \ - cargo build --release - -# cross build x86-64 musl release(for openssl) 2 -cross-build-x64-musl-2: - docker run --rm -it \ - -v "$(echo $HOME)/.cargo/config":/root/.cargo/config \ - -v "$(echo $HOME)/.cargo_messense_rust-musl-cross_x86_64-musl_registry":/root/.cargo/registry/ \ - -v "$(pwd)":/home/rust/src \ - messense/rust-musl-cross:x86_64-musl \ - cargo build --release - -# cross build x86-64 musl release(for openssl) 3 -cross-build-x64-musl-3: - docker run --rm -it \ - -v "$(echo $HOME)/.cargo/config":/root/.cargo/config \ - -v "$(echo $HOME)/.cargo_messense_rust-musl-cross_x86_64-musl_registry":/root/.cargo/registry/ \ - -v "$(pwd)":/volume \ - clux/muslrust \ - cargo build --release --target=x86_64-unknown-linux-musl +build-linux-x64-musl: + cargo zigbuild --release --target x86_64-unknown-linux-musl diff --git a/src/acme.rs b/src/acme.rs index 2a95778..9c04ee6 100644 --- a/src/acme.rs +++ b/src/acme.rs @@ -112,7 +112,7 @@ pub fn request_acme_certificate(acme_request: AcmeRequest, dns_cleaned_domains: 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()); - dns_client.as_mut().map(|client| { + if let Some(client) = dns_client.as_mut() { match client.list_dns_records(&rr_and_domain.1) { Err(e) => warning!("List dns for: {}, failed: {}", &rr_and_domain.1, e), Ok(records) => { @@ -127,7 +127,7 @@ pub fn request_acme_certificate(acme_request: AcmeRequest, dns_cleaned_domains: } } } - }); + } } match &mut dns_client { @@ -140,7 +140,7 @@ pub fn request_acme_certificate(acme_request: AcmeRequest, dns_cleaned_domains: ttl: -1, value: proof, }; - let _ = opt_result!(client.add_dns_record(&dns_record), "Add DNS TXT record failed: {}"); + opt_result!(client.add_dns_record(&dns_record), "Add DNS TXT record failed: {}"); success!("Add dns txt record successes: {}.{} -> {}", dns_record.rr, dns_record.domain, dns_record.value); } None => if acme_request.allow_interact { diff --git a/src/ali_dns.rs b/src/ali_dns.rs index 863bcb0..41feefc 100644 --- a/src/ali_dns.rs +++ b/src/ali_dns.rs @@ -186,11 +186,11 @@ pub fn build_dns_client(access_credential: &AccessCredential) -> RPClient { } fn parse_result<'a, S, E>(fn_name: &str, response: &'a str) -> XResult> where S: Deserialize<'a>, E: Deserialize<'a> { - let describe_domain_records_result: serde_json::Result = serde_json::from_str(&response); + let describe_domain_records_result: serde_json::Result = serde_json::from_str(response); match describe_domain_records_result { Ok(r) => Ok(Ok(r)), Err(_) => { - let describe_domain_records_error_result: serde_json::Result = serde_json::from_str(&response); + let describe_domain_records_error_result: serde_json::Result = serde_json::from_str(response); match describe_domain_records_error_result { Ok(r) => Ok(Err(r)), Err(_) => simple_error!("Parse {} response failed: {}", fn_name, response), diff --git a/src/config.rs b/src/config.rs index 68774b3..3271b57 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,18 +11,13 @@ use crate::x509::{X509PublicKeyAlgo, X509Certificate, parse_x509}; pub const CERT_NAME: &str = "cert.pem"; pub const KEY_NAME: &str = "key.pem"; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] pub enum AcmeChallenge { + #[default] Http, Dns, } -impl Default for AcmeChallenge { - fn default() -> Self { - AcmeChallenge::Http - } -} - impl AcmeChallenge { pub fn from_str(t: Option<&str>) -> Self { let t = t.map(|t| t.to_ascii_lowercase()).unwrap_or_else(|| "http".to_string()); @@ -147,7 +142,7 @@ impl CertConfig { impl CertConfigItem { pub fn get_acme_challenge(&self) -> AcmeChallenge { - AcmeChallenge::from_str(self.r#type.as_ref().map(|s| s.as_str())) + AcmeChallenge::from_str(self.r#type.as_deref()) } pub fn fill_dns_names(&mut self) -> XResult> { @@ -178,7 +173,7 @@ impl CertConfigItem { if self.public_key_algo.is_none() { self.public_key_algo = match &self.algo { None => Some(X509PublicKeyAlgo::Rsa(2048)), - Some(algo) => match X509PublicKeyAlgo::from_str(&algo) { + Some(algo) => match X509PublicKeyAlgo::from_str(algo) { Ok(algo) => Some(algo), Err(_) => return simple_error!("Unknown algo: {}", algo), }, diff --git a/src/main.rs b/src/main.rs index 780c2c5..0788b1a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,27 +9,27 @@ mod config; mod x509; mod network; mod statistics; -mod dingtalk; +mod notification; mod dns; mod ali_dns; // mod simple_thread_pool; -use std::fs; -use std::env; -use std::path::PathBuf; -use std::process::{Command, exit}; -use std::time::{Duration, SystemTime}; -use std::str::FromStr; -use tide::Request; -use clap::{App, Arg}; -use async_std::{task, channel, channel::Sender}; -use rust_util::util_cmd::run_command_and_wait; -use crate::config::{AcmeMode, AcmeChallenge, CertConfig, CERT_NAME, KEY_NAME}; -use crate::x509::{X509PublicKeyAlgo}; -use crate::dingtalk::send_dingtalk_message; -use crate::statistics::{AcmeStatistics, AcmeStatus}; -use crate::acme::{AcmeRequest, request_acme_certificate}; +use crate::acme::{request_acme_certificate, AcmeRequest}; +use crate::config::{AcmeChallenge, AcmeMode, CertConfig, CERT_NAME, KEY_NAME}; +use crate::notification::send_notify_message; use crate::network::get_local_public_ip; +use crate::statistics::{AcmeStatistics, AcmeStatus}; +use crate::x509::X509PublicKeyAlgo; +use async_std::{channel, channel::Sender, task}; +use clap::{App, Arg}; +use rust_util::util_cmd::run_command_and_wait; +use std::env; +use std::fs; +use std::path::PathBuf; +use std::process::{exit, Command}; +use std::str::FromStr; +use std::time::{Duration, SystemTime}; +use tide::Request; const NAME: &str = env!("CARGO_PKG_NAME"); const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -177,7 +177,7 @@ async fn main() -> tide::Result<()> { })); let skip_listen = matches.is_present("skip-listen"); - let port = iff!(skip_listen, 0, cert_config.as_ref().map(|c| c.port).flatten().unwrap_or(port)); + let port = iff!(skip_listen, 0, cert_config.as_ref().and_then(|c| c.port).unwrap_or(port)); let check_config = matches.is_present("check"); @@ -230,7 +230,7 @@ async fn main() -> tide::Result<()> { cert_file, key_file, outputs_file: matches.value_of("outputs").map(|s| s.into()), - ..Default::default() + //..Default::default() }; if let Err(e) = request_acme_certificate(acme_request, &mut dns_cleaned_domains) { failure!("Request certificate by acme failed: {}", e); @@ -245,6 +245,7 @@ async fn main() -> tide::Result<()> { let mut acme_statistics = AcmeStatistics::start(); let filtered_cert_config = cert_config.filter_cert_config_items(30); for item in &filtered_cert_config.cert_items { + #[allow(clippy::collapsible_if)] 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) { if item.get_acme_challenge() != AcmeChallenge::Dns { @@ -261,7 +262,7 @@ async fn main() -> tide::Result<()> { None => None, Some(supplier) => { let credential_supplier = filtered_cert_config.credential_suppliers.as_ref() - .map(|m| m.get(supplier)).flatten(); + .and_then(|m| m.get(supplier)); match credential_supplier { None => { warning!("DNS challenge no credential supplier found"); @@ -313,10 +314,10 @@ async fn main() -> tide::Result<()> { let mut dingtalk_message = format!("Statistics: \n{}", acme_statistics); if success_count > 0 { if let Some(trigger_after_update) = &filtered_cert_config.trigger_after_update { - if trigger_after_update.len() > 0 { + if !trigger_after_update.is_empty() { let mut cmd = Command::new(&trigger_after_update[0]); - for i in 1..trigger_after_update.len() { - cmd.arg(&trigger_after_update[i]); + for arg in trigger_after_update.iter().skip(1) { + cmd.arg(arg); } match run_command_and_wait(&mut cmd) { Ok(_) => { @@ -357,7 +358,7 @@ async fn main() -> tide::Result<()> { } if !acme_statistics.items.is_empty() && filtered_cert_config.notify_token.is_some() { - if let Err(err) = send_dingtalk_message(&filtered_cert_config, &dingtalk_message) { + if let Err(err) = send_notify_message(&filtered_cert_config, &dingtalk_message) { failure!("Send notification message failed: {:?}", err); } } else { diff --git a/src/dingtalk.rs b/src/notification.rs similarity index 59% rename from src/dingtalk.rs rename to src/notification.rs index 4eaade7..c24f96d 100644 --- a/src/dingtalk.rs +++ b/src/notification.rs @@ -20,22 +20,48 @@ pub struct InnerTextMessage { pub text: InnerTextMessageText, } -pub fn send_dingtalk_message(cert_config: &CertConfig, message: &str) -> XResult<()> { - let dingtalk_notify_token = get_dingtalk_notify_token(cert_config); - if let Some((access_token, sec_token)) = &dingtalk_notify_token { - inner_send_dingtalk_message(access_token, sec_token, message)?; +pub enum NotifyToken { + Dingtalk((String, String)), + Webhook(String), +} + +pub fn send_notify_message(cert_config: &CertConfig, message: &str) -> XResult<()> { + let notify_token = parse_notify_token(cert_config); + match notify_token { + None => { /* DO NOTHING */ } + Some(NotifyToken::Dingtalk((access_token, sec_token))) => { + inner_send_dingtalk_message(&access_token, &sec_token, message)?; + } + Some(NotifyToken::Webhook(webhook)) => { + inner_send_message(&webhook, message)?; + } } Ok(()) } fn inner_send_dingtalk_message(access_token: &str, sec_token: &str, message: &str) -> XResult<()> { - let dingtalk_message_json = serde_json::to_string(&InnerTextMessage { + let webhook_url = build_dingtalk_post_message_url(access_token, sec_token)?; + inner_send_message(&webhook_url, message) +} + +fn inner_send_message(webhook_url: &str, message: &str) -> XResult<()> { + let message_json = serde_json::to_string(&InnerTextMessage { msgtype: "text".into(), text: InnerTextMessageText { content: message.into(), }, })?; let client = reqwest::blocking::Client::new(); + let response = client.post(webhook_url) + .header("Content-Type", "application/json; charset=utf-8") + .body(message_json.as_bytes().to_vec()) + .send()?; + + information!("Send notify message: {:?}", response); + Ok(()) +} + +fn build_dingtalk_post_message_url(access_token: &str, sec_token: &str) -> XResult { let mut webhook_url = "https://oapi.dingtalk.com/robot/send".to_string(); webhook_url.push_str("?access_token="); webhook_url.push_str(&urlencoding::encode(access_token)); @@ -48,31 +74,20 @@ fn inner_send_dingtalk_message(access_token: &str, sec_token: &str, message: &st webhook_url.push_str("&sign="); webhook_url.push_str(&urlencoding::encode(&hmac_sha256)); } - let response = client.post(webhook_url) - .header("Content-Type", "application/json; charset=utf-8") - .body(dingtalk_message_json.as_bytes().to_vec()) - .send()?; - - information!("Send dingtalk message: {:?}", response); - - Ok(()) + Ok(webhook_url) } // format: dingtalk:access_token?sec_token -fn get_dingtalk_notify_token(cert_config: &CertConfig) -> Option<(String, String)> { +#[allow(clippy::manual_map)] +fn parse_notify_token(cert_config: &CertConfig) -> Option { let token = cert_config.notify_token.as_ref()?; - if token.starts_with("dingtalk:") { - let token_and_or_sec = &token["dingtalk:".len()..]; + if let Some(token_and_or_sec) = token.strip_prefix("dingtalk:") { let mut token_and_or_sec_vec = token_and_or_sec.split('?'); - let access_token = match token_and_or_sec_vec.next() { - Some(t) => t, - None => token_and_or_sec, - }; - let sec_token = match token_and_or_sec_vec.next() { - Some(t) => t, - None => "", - }; - Some((access_token.into(), sec_token.into())) + let access_token = token_and_or_sec_vec.next().unwrap_or(token_and_or_sec); + let sec_token = token_and_or_sec_vec.next().unwrap_or(""); + Some(NotifyToken::Dingtalk((access_token.into(), sec_token.into()))) + } else if let Some(webhook_url) = token.strip_prefix("webhook:") { + Some(NotifyToken::Webhook(webhook_url.into())) } else { None } diff --git a/src/statistics.rs b/src/statistics.rs index 1f9e198..2506a71 100644 --- a/src/statistics.rs +++ b/src/statistics.rs @@ -40,10 +40,10 @@ impl Display for AcmeStatistics { fn fmt(&self, f: &mut Formatter<'_>) -> Result { let mut sb = String::with_capacity(512); let df = simpledateformat::fmt("yyyy-MM-dd HH:mm:ss z").unwrap(); - let started_time = df.format_local(self.started.clone()); + let started_time = df.format_local(self.started); sb.push_str(&format!("Started: {}", &started_time)); if let Some(ended) = &self.ended { - let ended_time = df.format_local(ended.clone()); + let ended_time = df.format_local(*ended); sb.push_str(&format!("\nEnded: {}", &ended_time)); let cost_result = ended.duration_since(self.started); if let Ok(cost) = cost_result { diff --git a/src/x509.rs b/src/x509.rs index afdacda..0399384 100644 --- a/src/x509.rs +++ b/src/x509.rs @@ -1,4 +1,5 @@ use std::error::Error; +use std::fmt::{Display, Formatter}; use std::str::FromStr; use serde::{Deserialize, Serialize}; use rust_util::XResult; @@ -47,14 +48,15 @@ impl Default for X509PublicKeyAlgo { } } -impl ToString for X509PublicKeyAlgo { - fn to_string(&self) -> String { - match self { +impl Display for X509PublicKeyAlgo { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let algo = match self { Self::Rsa(bit_length) => format!("rsa{}", bit_length), Self::EcKey(X509EcPublicKeyAlgo::Secp256r1) => "p256".into(), Self::EcKey(X509EcPublicKeyAlgo::Secp384r1) => "p384".into(), Self::EcKey(X509EcPublicKeyAlgo::Secp521r1) => "p521".into(), - } + }; + write!(f, "{}", algo) } } @@ -75,7 +77,7 @@ impl FromStr for X509PublicKeyAlgo { } impl X509PublicKeyAlgo { - pub fn parse<'a>(pem_id: &str, public_key_info: &SubjectPublicKeyInfo<'a>) -> XResult { + pub fn parse(pem_id: &str, public_key_info: &SubjectPublicKeyInfo<'_>) -> XResult { let algorithm = &public_key_info.algorithm; let public_key_algo_oid = &algorithm.algorithm; if public_key_algo_oid == &*OID_EC_PUBLIC_KEY {