feat: v1.3.7, add webhook

This commit is contained in:
2025-03-25 23:04:11 +08:00
parent 2019fde054
commit 87e052e67b
10 changed files with 98 additions and 106 deletions

14
Cargo.lock generated
View File

@@ -1,10 +1,10 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 4
[[package]] [[package]]
name = "acme-client" name = "acme-client"
version = "1.3.6" version = "1.3.7"
dependencies = [ dependencies = [
"acme-lib", "acme-lib",
"aliyun-openapi-core-rust-sdk", "aliyun-openapi-core-rust-sdk",
@@ -1817,6 +1817,15 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 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]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.103" version = "0.9.103"
@@ -1825,6 +1834,7 @@ checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
"openssl-src",
"pkg-config", "pkg-config",
"vcpkg", "vcpkg",
] ]

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "acme-client" name = "acme-client"
version = "1.3.6" version = "1.3.7"
authors = ["Hatter Jiang <jht5945@gmail.com>"] authors = ["Hatter Jiang <jht5945@gmail.com>"]
edition = "2018" edition = "2018"
description = "Acme auto challenge client, acme-client can issue certificates from Let's encrypt" 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"] } serde = { version = "1.0", features = ["derive"] }
deser-hjson = "2.2" deser-hjson = "2.2"
x509-parser = "0.9" 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"] } #reqwest = { version = "0.11", default-features = false, features = ["blocking", "rustls-tls"] }
trust-dns-resolver = "0.23" trust-dns-resolver = "0.23"
simpledateformat = "0.1.3" simpledateformat = "0.1.3"

View File

@@ -1,37 +1,6 @@
_: _:
@just --list @just --list
## cross build x86-64 musl debug build-linux-x64-musl:
#cross-build-x64-debug: cargo zigbuild --release --target x86_64-unknown-linux-musl
# 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

View File

@@ -112,7 +112,7 @@ pub fn request_acme_certificate(acme_request: AcmeRequest, dns_cleaned_domains:
if !dns_cleaned_domains.contains(&rr_and_domain.1) { if !dns_cleaned_domains.contains(&rr_and_domain.1) {
information!("Clearing domain: {}", &rr_and_domain.1); information!("Clearing domain: {}", &rr_and_domain.1);
dns_cleaned_domains.push(rr_and_domain.1.clone()); 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) { match client.list_dns_records(&rr_and_domain.1) {
Err(e) => warning!("List dns for: {}, failed: {}", &rr_and_domain.1, e), Err(e) => warning!("List dns for: {}, failed: {}", &rr_and_domain.1, e),
Ok(records) => { Ok(records) => {
@@ -127,7 +127,7 @@ pub fn request_acme_certificate(acme_request: AcmeRequest, dns_cleaned_domains:
} }
} }
} }
}); }
} }
match &mut dns_client { match &mut dns_client {
@@ -140,7 +140,7 @@ pub fn request_acme_certificate(acme_request: AcmeRequest, dns_cleaned_domains:
ttl: -1, ttl: -1,
value: proof, 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); success!("Add dns txt record successes: {}.{} -> {}", dns_record.rr, dns_record.domain, dns_record.value);
} }
None => if acme_request.allow_interact { None => if acme_request.allow_interact {

View File

@@ -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<Result<S, E>> where S: Deserialize<'a>, E: Deserialize<'a> { fn parse_result<'a, S, E>(fn_name: &str, response: &'a str) -> XResult<Result<S, E>> where S: Deserialize<'a>, E: Deserialize<'a> {
let describe_domain_records_result: serde_json::Result<S> = serde_json::from_str(&response); let describe_domain_records_result: serde_json::Result<S> = serde_json::from_str(response);
match describe_domain_records_result { match describe_domain_records_result {
Ok(r) => Ok(Ok(r)), Ok(r) => Ok(Ok(r)),
Err(_) => { Err(_) => {
let describe_domain_records_error_result: serde_json::Result<E> = serde_json::from_str(&response); let describe_domain_records_error_result: serde_json::Result<E> = serde_json::from_str(response);
match describe_domain_records_error_result { match describe_domain_records_error_result {
Ok(r) => Ok(Err(r)), Ok(r) => Ok(Err(r)),
Err(_) => simple_error!("Parse {} response failed: {}", fn_name, response), Err(_) => simple_error!("Parse {} response failed: {}", fn_name, response),

View File

@@ -11,18 +11,13 @@ use crate::x509::{X509PublicKeyAlgo, X509Certificate, parse_x509};
pub const CERT_NAME: &str = "cert.pem"; pub const CERT_NAME: &str = "cert.pem";
pub const KEY_NAME: &str = "key.pem"; pub const KEY_NAME: &str = "key.pem";
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub enum AcmeChallenge { pub enum AcmeChallenge {
#[default]
Http, Http,
Dns, Dns,
} }
impl Default for AcmeChallenge {
fn default() -> Self {
AcmeChallenge::Http
}
}
impl AcmeChallenge { impl AcmeChallenge {
pub fn from_str(t: Option<&str>) -> Self { pub fn from_str(t: Option<&str>) -> Self {
let t = t.map(|t| t.to_ascii_lowercase()).unwrap_or_else(|| "http".to_string()); let t = t.map(|t| t.to_ascii_lowercase()).unwrap_or_else(|| "http".to_string());
@@ -147,7 +142,7 @@ impl CertConfig {
impl CertConfigItem { impl CertConfigItem {
pub fn get_acme_challenge(&self) -> AcmeChallenge { 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<Option<X509Certificate>> { pub fn fill_dns_names(&mut self) -> XResult<Option<X509Certificate>> {
@@ -178,7 +173,7 @@ impl CertConfigItem {
if self.public_key_algo.is_none() { if self.public_key_algo.is_none() {
self.public_key_algo = match &self.algo { self.public_key_algo = match &self.algo {
None => Some(X509PublicKeyAlgo::Rsa(2048)), None => Some(X509PublicKeyAlgo::Rsa(2048)),
Some(algo) => match X509PublicKeyAlgo::from_str(&algo) { Some(algo) => match X509PublicKeyAlgo::from_str(algo) {
Ok(algo) => Some(algo), Ok(algo) => Some(algo),
Err(_) => return simple_error!("Unknown algo: {}", algo), Err(_) => return simple_error!("Unknown algo: {}", algo),
}, },

View File

@@ -9,27 +9,27 @@ mod config;
mod x509; mod x509;
mod network; mod network;
mod statistics; mod statistics;
mod dingtalk; mod notification;
mod dns; mod dns;
mod ali_dns; mod ali_dns;
// mod simple_thread_pool; // mod simple_thread_pool;
use std::fs; use crate::acme::{request_acme_certificate, AcmeRequest};
use std::env; use crate::config::{AcmeChallenge, AcmeMode, CertConfig, CERT_NAME, KEY_NAME};
use std::path::PathBuf; use crate::notification::send_notify_message;
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::network::get_local_public_ip; 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 NAME: &str = env!("CARGO_PKG_NAME");
const VERSION: &str = env!("CARGO_PKG_VERSION"); 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 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"); let check_config = matches.is_present("check");
@@ -230,7 +230,7 @@ async fn main() -> tide::Result<()> {
cert_file, cert_file,
key_file, key_file,
outputs_file: matches.value_of("outputs").map(|s| s.into()), 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) { if let Err(e) = request_acme_certificate(acme_request, &mut dns_cleaned_domains) {
failure!("Request certificate by acme failed: {}", e); failure!("Request certificate by acme failed: {}", e);
@@ -245,6 +245,7 @@ async fn main() -> tide::Result<()> {
let mut acme_statistics = AcmeStatistics::start(); let mut acme_statistics = AcmeStatistics::start();
let filtered_cert_config = cert_config.filter_cert_config_items(30); let filtered_cert_config = cert_config.filter_cert_config_items(30);
for item in &filtered_cert_config.cert_items { 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) 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) { || 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 { if item.get_acme_challenge() != AcmeChallenge::Dns {
@@ -261,7 +262,7 @@ async fn main() -> tide::Result<()> {
None => None, None => None,
Some(supplier) => { Some(supplier) => {
let credential_supplier = filtered_cert_config.credential_suppliers.as_ref() 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 { match credential_supplier {
None => { None => {
warning!("DNS challenge no credential supplier found"); 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); let mut dingtalk_message = format!("Statistics: \n{}", acme_statistics);
if success_count > 0 { if success_count > 0 {
if let Some(trigger_after_update) = &filtered_cert_config.trigger_after_update { 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]); let mut cmd = Command::new(&trigger_after_update[0]);
for i in 1..trigger_after_update.len() { for arg in trigger_after_update.iter().skip(1) {
cmd.arg(&trigger_after_update[i]); cmd.arg(arg);
} }
match run_command_and_wait(&mut cmd) { match run_command_and_wait(&mut cmd) {
Ok(_) => { Ok(_) => {
@@ -357,7 +358,7 @@ async fn main() -> tide::Result<()> {
} }
if !acme_statistics.items.is_empty() && filtered_cert_config.notify_token.is_some() { 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); failure!("Send notification message failed: {:?}", err);
} }
} else { } else {

View File

@@ -20,22 +20,48 @@ pub struct InnerTextMessage {
pub text: InnerTextMessageText, pub text: InnerTextMessageText,
} }
pub fn send_dingtalk_message(cert_config: &CertConfig, message: &str) -> XResult<()> { pub enum NotifyToken {
let dingtalk_notify_token = get_dingtalk_notify_token(cert_config); Dingtalk((String, String)),
if let Some((access_token, sec_token)) = &dingtalk_notify_token { Webhook(String),
inner_send_dingtalk_message(access_token, sec_token, message)?; }
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(()) Ok(())
} }
fn inner_send_dingtalk_message(access_token: &str, sec_token: &str, message: &str) -> XResult<()> { 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(), msgtype: "text".into(),
text: InnerTextMessageText { text: InnerTextMessageText {
content: message.into(), content: message.into(),
}, },
})?; })?;
let client = reqwest::blocking::Client::new(); 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<String> {
let mut webhook_url = "https://oapi.dingtalk.com/robot/send".to_string(); let mut webhook_url = "https://oapi.dingtalk.com/robot/send".to_string();
webhook_url.push_str("?access_token="); webhook_url.push_str("?access_token=");
webhook_url.push_str(&urlencoding::encode(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("&sign=");
webhook_url.push_str(&urlencoding::encode(&hmac_sha256)); webhook_url.push_str(&urlencoding::encode(&hmac_sha256));
} }
let response = client.post(webhook_url) Ok(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(())
} }
// format: dingtalk:access_token?sec_token // 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<NotifyToken> {
let token = cert_config.notify_token.as_ref()?; let token = cert_config.notify_token.as_ref()?;
if token.starts_with("dingtalk:") { if let Some(token_and_or_sec) = token.strip_prefix("dingtalk:") {
let token_and_or_sec = &token["dingtalk:".len()..];
let mut token_and_or_sec_vec = token_and_or_sec.split('?'); let mut token_and_or_sec_vec = token_and_or_sec.split('?');
let access_token = match token_and_or_sec_vec.next() { let access_token = token_and_or_sec_vec.next().unwrap_or(token_and_or_sec);
Some(t) => t, let sec_token = token_and_or_sec_vec.next().unwrap_or("");
None => token_and_or_sec, Some(NotifyToken::Dingtalk((access_token.into(), sec_token.into())))
}; } else if let Some(webhook_url) = token.strip_prefix("webhook:") {
let sec_token = match token_and_or_sec_vec.next() { Some(NotifyToken::Webhook(webhook_url.into()))
Some(t) => t,
None => "",
};
Some((access_token.into(), sec_token.into()))
} else { } else {
None None
} }

View File

@@ -40,10 +40,10 @@ impl Display for AcmeStatistics {
fn fmt(&self, f: &mut Formatter<'_>) -> Result { fn fmt(&self, f: &mut Formatter<'_>) -> Result {
let mut sb = String::with_capacity(512); let mut sb = String::with_capacity(512);
let df = simpledateformat::fmt("yyyy-MM-dd HH:mm:ss z").unwrap(); 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)); sb.push_str(&format!("Started: {}", &started_time));
if let Some(ended) = &self.ended { 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)); sb.push_str(&format!("\nEnded: {}", &ended_time));
let cost_result = ended.duration_since(self.started); let cost_result = ended.duration_since(self.started);
if let Ok(cost) = cost_result { if let Ok(cost) = cost_result {

View File

@@ -1,4 +1,5 @@
use std::error::Error; use std::error::Error;
use std::fmt::{Display, Formatter};
use std::str::FromStr; use std::str::FromStr;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use rust_util::XResult; use rust_util::XResult;
@@ -47,14 +48,15 @@ impl Default for X509PublicKeyAlgo {
} }
} }
impl ToString for X509PublicKeyAlgo { impl Display for X509PublicKeyAlgo {
fn to_string(&self) -> String { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self { let algo = match self {
Self::Rsa(bit_length) => format!("rsa{}", bit_length), Self::Rsa(bit_length) => format!("rsa{}", bit_length),
Self::EcKey(X509EcPublicKeyAlgo::Secp256r1) => "p256".into(), Self::EcKey(X509EcPublicKeyAlgo::Secp256r1) => "p256".into(),
Self::EcKey(X509EcPublicKeyAlgo::Secp384r1) => "p384".into(), Self::EcKey(X509EcPublicKeyAlgo::Secp384r1) => "p384".into(),
Self::EcKey(X509EcPublicKeyAlgo::Secp521r1) => "p521".into(), Self::EcKey(X509EcPublicKeyAlgo::Secp521r1) => "p521".into(),
} };
write!(f, "{}", algo)
} }
} }
@@ -75,7 +77,7 @@ impl FromStr for X509PublicKeyAlgo {
} }
impl X509PublicKeyAlgo { impl X509PublicKeyAlgo {
pub fn parse<'a>(pem_id: &str, public_key_info: &SubjectPublicKeyInfo<'a>) -> XResult<Self> { pub fn parse(pem_id: &str, public_key_info: &SubjectPublicKeyInfo<'_>) -> XResult<Self> {
let algorithm = &public_key_info.algorithm; let algorithm = &public_key_info.algorithm;
let public_key_algo_oid = &algorithm.algorithm; let public_key_algo_oid = &algorithm.algorithm;
if public_key_algo_oid == &*OID_EC_PUBLIC_KEY { if public_key_algo_oid == &*OID_EC_PUBLIC_KEY {