feat: v1.3.7, add webhook
This commit is contained in:
14
Cargo.lock
generated
14
Cargo.lock
generated
@@ -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",
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "acme-client"
|
||||
version = "1.3.6"
|
||||
version = "1.3.7"
|
||||
authors = ["Hatter Jiang <jht5945@gmail.com>"]
|
||||
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"
|
||||
|
||||
35
justfile
35
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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> {
|
||||
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 {
|
||||
Ok(r) => Ok(Ok(r)),
|
||||
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 {
|
||||
Ok(r) => Ok(Err(r)),
|
||||
Err(_) => simple_error!("Parse {} response failed: {}", fn_name, response),
|
||||
|
||||
@@ -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<Option<X509Certificate>> {
|
||||
@@ -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),
|
||||
},
|
||||
|
||||
47
src/main.rs
47
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 {
|
||||
|
||||
@@ -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<String> {
|
||||
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<NotifyToken> {
|
||||
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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
12
src/x509.rs
12
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<Self> {
|
||||
pub fn parse(pem_id: &str, public_key_info: &SubjectPublicKeyInfo<'_>) -> XResult<Self> {
|
||||
let algorithm = &public_key_info.algorithm;
|
||||
let public_key_algo_oid = &algorithm.algorithm;
|
||||
if public_key_algo_oid == &*OID_EC_PUBLIC_KEY {
|
||||
|
||||
Reference in New Issue
Block a user