Compare commits

...

29 Commits

Author SHA1 Message Date
76334e3fa4 feat: updates 2025-04-05 01:01:16 +08:00
a1c7bcba30 feat: v1.3.9, kms encrypts dns credential 2025-04-05 00:58:16 +08:00
b5c142838f feat: v1.3.8, update dependencies 2025-03-26 23:55:34 +08:00
87e052e67b feat: v1.3.7, add webhook 2025-03-25 23:04:11 +08:00
2019fde054 feat: update acme lb 2024-08-14 00:22:04 +08:00
cb47c5c4e6 feat: v1.3.5, add ecdsa with sha384 signature algorithm 2024-08-12 23:56:57 +08:00
d36335a885 feat: update dependencies 2023-09-23 14:04:47 +08:00
9636eba5f7 feat: fix typo 2023-09-23 13:39:39 +08:00
732ad63dbe v1.3.2, update readme 2023-01-15 13:27:56 +08:00
edce2cdcd5 v1.3.2, update readme 2023-01-15 13:19:10 +08:00
b6267a0c76 v1.3.2, add hint 2023-01-15 12:19:39 +08:00
8135d1ceed feat: v1.3.1 2022-11-11 01:19:32 +08:00
09291d553c feat: v1.3.1 2022-11-11 01:19:02 +08:00
7b768c9302 feat: v1.3.1 update dependencies 2022-11-10 01:05:18 +08:00
315fddaa15 feat: v1.3.0 add directory-url support 2022-11-10 01:02:06 +08:00
10d680a364 v1.2.0 2022-05-22 11:29:14 +08:00
9e3ae38447 chore: update dns fn name 2022-02-05 15:55:29 +08:00
31e480a7b2 feat: add dns.rs 2022-02-05 15:38:13 +08:00
5ebe0a2dae chore: use openssl or rustls 2022-02-05 12:29:42 +08:00
835373dd12 chore: clean code 2022-02-05 00:28:48 +08:00
0a51e5de08 chore: clean code 2022-02-05 00:25:48 +08:00
8c8173de79 chore: clean clode 2022-02-05 00:22:59 +08:00
f655c6a87f chore: update use 2022-02-05 00:12:03 +08:00
41f81ebdd9 a 2022-02-05 00:05:45 +08:00
3564e6738c chore: update readme 2022-02-04 20:28:24 +08:00
4a00f96763 chore: update denpendcies 2022-02-04 14:41:07 +08:00
6639276e11 feat: command line supports dns challenge 2022-02-04 14:38:08 +08:00
ac540437dd update justfile 2022-02-03 19:38:31 +08:00
8282a4c704 update readme 2022-02-03 17:04:27 +08:00
16 changed files with 2382 additions and 2169 deletions

3539
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "acme-client"
version = "1.1.0"
version = "1.3.9"
authors = ["Hatter Jiang <jht5945@gmail.com>"]
edition = "2018"
description = "Acme auto challenge client, acme-client can issue certificates from Let's encrypt"
@@ -11,21 +11,22 @@ description = "Acme auto challenge client, acme-client can issue certificates fr
lazy_static = "1.4"
clap = "2.33"
rust_util = "0.6"
acme-lib = "0.8"
acme-lib = "0.9"
tide = "0.16"
async-std = { version = "1.8", features = ["attributes"] }
serde = { version = "1.0", features = ["derive"] }
deser-hjson = "0.1"
deser-hjson = "2.2"
x509-parser = "0.9"
reqwest = { version = "0.11", features = ["blocking"] }
trust-dns-resolver = "0.20"
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"
serde_json = "1.0"
urlencoding = "1.0.0"
base64 = "0.11.0"
hmac = "0.7.1"
sha2 = "0.8.1"
aliyun-openapi-core-rust-sdk = "0.3.0"
urlencoding = "2.1"
base64 = "0.21"
hmac = "0.12"
sha2 = "0.10"
aliyun-openapi-core-rust-sdk = "1.1.0"
[profile.release]
codegen-units = 1

View File

@@ -5,7 +5,7 @@ ACME Client in Rust
Acme client help:
```shell
$ acme-client --help
acme-client 1.0.1
acme-client 1.1.0
Hatter Jiang <jht5945@gmail.com>
Acme auto challenge client, acme-client can issue certificates from Let's encrypt
@@ -13,6 +13,7 @@ USAGE:
acme-client [FLAGS] [OPTIONS]
FLAGS:
--allow-interact Allow interact
--check Check cert config
-h, --help Prints help information
--hide-logo Hide logo
@@ -29,6 +30,7 @@ OPTIONS:
-d, --domain <domain>... Domains
--email <email> Contract email
-m, --mode <mode> Mode [default: prod]
-o, --outputs <outputs> Outputs file
-p, --port <port> Http port [default: 80]
--timeout <timeout> Timeout (ms) [default: 5000]
-t, --type <type> Type http or dns [default: http]
@@ -42,11 +44,19 @@ OPTIONS:
```json
{
"port": 18342,
"credentialSuppliers": {
"alibabacloud": "account://access_key_id:access_key_secret@alibabacloud?id=dns"
},
"triggerAfterUpdate": ["/usr/local/nginx/nginx", "-s", "reload"],
"notifyToken": "dingtalk:access_token?sec_token",
"certItems": [{
"path": "dir_cryptofan_org",
"dnsNames": ["cryptofan.org", "www.cryptofan.org"]
}, {
"path": "dir_webauthn_host",
"dnsNames": ["webauthn.host", "*.webauthn.host"],
"type": "dns",
"supplier": "alibabacloud"
}]
}
```
@@ -59,8 +69,23 @@ location /.well-known/acme-challenge/ {
}
```
通过命令行交互创建DNS挑战证书
```shell
acme-client --port 0 -t dns --allow-interact --email email@example.com -d example.net
```
* `email@example.com` -- your email
* `example.net` -- your domain
出现以下提示时需要自行配置DNS配置完成后按"回车"
```shell
[INFO ] You need to config dns manually, press enter to continue...
```
<br>
Cross build uses: https://github.com/messense/rust-musl-cross
Cross build uses:
- ~~https://github.com/messense/rust-musl-cross~~
- https://github.com/emk/rust-musl-builder

View File

@@ -1,23 +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

206
src/acme.rs Normal file
View File

@@ -0,0 +1,206 @@
use std::fs;
use std::sync::RwLock;
use std::collections::BTreeMap;
use acme_lib::{Directory, create_p256_key, create_p384_key, create_rsa_key, DirectoryUrl};
use acme_lib::persist::FilePersist;
use rust_util::XResult;
use crate::util::parse_dns_record;
use crate::network::{get_resolver, resolve_first_ipv4};
use crate::config::{AcmeChallenge, AcmeMode};
use crate::dns::{DnsClient, DnsClientFactory, DnsRecord};
use crate::x509::{X509PublicKeyAlgo, X509EcPublicKeyAlgo};
lazy_static! {
pub static ref TOKEN_MAP: RwLock<BTreeMap<String, String>> = 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<String>,
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 directory_url: Option<String>,
pub account_dir: &'a str,
pub timeout: u64,
pub local_public_ip: Option<&'a str>,
pub key_file: Option<String>,
pub cert_file: Option<String>,
pub outputs_file: Option<String>,
}
pub fn request_acme_certificate(acme_request: AcmeRequest, dns_cleaned_domains: &mut Vec<String>) -> 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 = if let Some(directory_url) = &acme_request.directory_url {
DirectoryUrl::Other(directory_url)
} else {
acme_request.mode.directory_url()
};
debugging!("Directory URL: {:?}", 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 dns_client: Option<Box<dyn DnsClient>> = match &acme_request.credential_supplier {
Some(credential_supplier) => Some(
opt_result!(DnsClientFactory::build(credential_supplier), "Build dns client 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());
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) => {
for r in &records {
let rr = &r.rr;
if rr == "_acme-challenge" || rr.starts_with("_acme-challenge.") {
match client.delete_dns_record(&r.id) {
Err(e) => warning!("Delete dns: {}.{}, failed: {}", rr, r.domain, e),
Ok(_) => success!("Delete dns: {}.{}", rr, r.domain),
}
}
}
}
}
}
}
match &mut dns_client {
Some(client) => {
let dns_record = DnsRecord {
id: String::new(),
domain: rr_and_domain.1,
rr: rr_and_domain.0,
r#type: "TXT".into(),
ttl: -1,
value: proof,
};
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 {
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();
information!("Continued")
} 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(())
}

View File

@@ -1,10 +1,54 @@
#![allow(deprecated)]
use rust_util::XResult;
use serde::{Deserialize, Serialize};
use aliyun_openapi_core_rust_sdk::RPClient;
use rust_util::XResult;
use crate::dns::DnsClient;
static ALI_DNS_ENDPOINT: &str = "https://alidns.aliyuncs.com";
static ALI_DNS_API_VERSION: &str = "2015-01-09";
pub struct AlibabaCloudDnsClient {
client: RPClient,
}
impl AlibabaCloudDnsClient {
pub fn build(supplier: &str) -> XResult<AlibabaCloudDnsClient> {
let access_credential = simple_parse_aliyun_supplier(supplier)?;
Ok(AlibabaCloudDnsClient {
client: build_dns_client(&access_credential)
})
}
}
impl DnsClient for AlibabaCloudDnsClient {
fn list_dns_records(&mut self, domain: &str) -> XResult<Vec<crate::dns::DnsRecord>> {
let list_dns_response = opt_result!(list_dns(&self.client, domain)?, "List dns records failed: {:?}");
let mut dns_records = vec![];
list_dns_response.domain_records.record.into_iter().for_each(|record| {
dns_records.push(crate::dns::DnsRecord {
id: record.record_id,
domain: record.domain_name,
rr: record.rr,
r#type: record.r#type,
ttl: record.ttl,
value: record.value,
});
});
Ok(dns_records)
}
fn delete_dns_record(&mut self, record_id: &str) -> XResult<()> {
opt_result!(delete_dns_record(&self.client, record_id)?, "Delete dns record failed: {:?}");
Ok(())
}
fn add_dns_record(&mut self, dns_record: &crate::dns::DnsRecord) -> XResult<()> {
let _ = opt_result!(add_dns_record(&self.client, &dns_record.domain, &dns_record.rr, &dns_record.r#type, &dns_record.value),
"Add dns record failed: {}");
Ok(())
}
}
#[derive(Debug)]
pub struct AccessCredential {
access_key_id: String,
@@ -111,9 +155,9 @@ pub fn delete_dns_record(client: &RPClient, record_id: &str) -> XResult<Result<C
parse_result("DeleteDomainRecord", &delete_domain_record_response)
}
pub fn add_txt_dns_record(client: &RPClient, domain: &str, rr: &str, value: &str) -> XResult<Result<CommonSuccessResponse, CommonErrorResponse>> {
add_dns_record(client, domain, rr, "TXT", value)
}
// pub fn add_txt_dns_record(client: &RPClient, domain: &str, rr: &str, value: &str) -> XResult<Result<CommonSuccessResponse, CommonErrorResponse>> {
// add_dns_record(client, domain, rr, "TXT", value)
// }
// domain -> "example.com"
// rr -> "@", "_acme-challenge"
@@ -142,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),

View File

@@ -1,27 +1,32 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use rust_util::XResult;
use std::fs;
use acme_lib::DirectoryUrl;
use std::collections::HashMap;
use std::path::PathBuf;
use std::str::FromStr;
use crate::x509;
use crate::x509::{X509PublicKeyAlgo, X509Certificate};
use std::time::SystemTime;
use serde::{Deserialize, Serialize};
use rust_util::XResult;
use acme_lib::DirectoryUrl;
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 {
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());
if t == "dns" {
AcmeChallenge::Dns
} else {
AcmeChallenge::Http
}
}
}
#[derive(Debug, Clone, Copy)]
@@ -74,6 +79,7 @@ pub struct CertConfig {
pub cert_items: Vec<CertConfigItem>,
pub trigger_after_update: Option<Vec<String>>,
pub notify_token: Option<String>,
pub directory_url: Option<String>,
}
impl CertConfig {
@@ -123,6 +129,7 @@ impl CertConfig {
cert_items: filtered_cert_items,
trigger_after_update: self.trigger_after_update,
notify_token: self.notify_token,
directory_url: self.directory_url,
}
}
@@ -135,12 +142,7 @@ impl CertConfig {
impl CertConfigItem {
pub fn get_acme_challenge(&self) -> AcmeChallenge {
let t = self.r#type.as_ref().map(|t| t.to_ascii_lowercase()).unwrap_or_else(|| "http".to_string());
if t == "dns" {
AcmeChallenge::Dns
} else {
AcmeChallenge::Http
}
AcmeChallenge::from_str(self.r#type.as_deref())
}
pub fn fill_dns_names(&mut self) -> XResult<Option<X509Certificate>> {
@@ -151,7 +153,7 @@ impl CertConfigItem {
let cert_path_buff = path_buff.join(CERT_NAME);
if self.common_name.is_none() && self.dns_names.is_none() {
let pem = opt_result!(fs::read_to_string(cert_path_buff.clone()), "Read file: {:?}, failed: {}", cert_path_buff);
let x509_certificate = opt_result!(x509::parse_x509(&format!("{}/{}", self.path, CERT_NAME), &pem), "Parse x509: {}/{}, faield: {}", self.path, CERT_NAME);
let x509_certificate = opt_result!(parse_x509(&format!("{}/{}", self.path, CERT_NAME), &pem), "Parse x509: {}/{}, faield: {}", self.path, CERT_NAME);
self.common_name = Some(x509_certificate.common_name.clone());
self.dns_names = Some(x509_certificate.alt_names.clone());
if let Some(pos) = x509_certificate.alt_names.iter().position(|n| n == &x509_certificate.common_name) {
@@ -171,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),
},
@@ -179,7 +181,7 @@ impl CertConfigItem {
}
if cert_path_buff.exists() {
let pem = opt_result!(fs::read_to_string(cert_path_buff.clone()), "Read file: {:?}, failed: {}", cert_path_buff);
let x509_certificate = opt_result!(x509::parse_x509(&format!("{}/{}", self.path, CERT_NAME), &pem), "Parse x509: {}/{}, faield: {}", self.path, CERT_NAME);
let x509_certificate = opt_result!(parse_x509(&format!("{}/{}", self.path, CERT_NAME), &pem), "Parse x509: {}/{}, faield: {}", self.path, CERT_NAME);
let mut self_dns_names = vec![];
let mut cert_dns_names = vec![];

View File

@@ -1,88 +0,0 @@
use std::io::{Error, ErrorKind};
use serde::{Serialize, Deserialize};
use std::time::SystemTime;
use rust_util::XResult;
use hmac::{Hmac, Mac};
use sha2::Sha256;
use crate::CertConfig;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct InnerTextMessageText {
pub content: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct InnerTextMessage {
pub msgtype: String,
pub text: InnerTextMessageText,
}
pub fn send_dingtalk_message(cert_config: &CertConfig, message: &str) -> XResult<()> {
let dintalk_notify_token = get_dingtalk_notify_token(cert_config);
if let Some((access_token, sec_token)) = &dintalk_notify_token {
inner_send_dingtalk_message(access_token, sec_token, 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 {
msgtype: "text".into(),
text: InnerTextMessageText {
content: message.into(),
},
})?;
let client = reqwest::blocking::Client::new();
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));
if !sec_token.is_empty() {
let timestamp = &format!("{}", SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_millis());
let timestamp_and_secret = &format!("{}\n{}", timestamp, sec_token);
let hmac_sha256 = base64::encode(&calc_hmac_sha256(sec_token.as_bytes(), timestamp_and_secret.as_bytes())?[..]);
webhook_url.push_str("&timestamp=");
webhook_url.push_str(timestamp);
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(())
}
// format: dingtalk:access_token?sec_token
fn get_dingtalk_notify_token(cert_config: &CertConfig) -> Option<(String, String)> {
let token = cert_config.notify_token.as_ref()?;
if token.starts_with("dingtalk:") {
let token_and_or_sec = &token["dingtalk:".len()..];
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()))
} else {
None
}
}
/// calc hma_sha256 digest
fn calc_hmac_sha256(key: &[u8], message: &[u8]) -> XResult<Vec<u8>> {
let mut mac = match Hmac::<Sha256>::new_varkey(key) {
Ok(m) => m,
Err(e) => {
return Err(Box::new(Error::new(ErrorKind::Other, format!("Hmac error: {}", e))));
}
};
mac.input(message);
Ok(mac.result().code().to_vec())
}

30
src/dns.rs Normal file
View File

@@ -0,0 +1,30 @@
use serde::{Deserialize, Serialize};
use rust_util::XResult;
use crate::ali_dns::AlibabaCloudDnsClient;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DnsRecord {
pub id: String,
pub domain: String,
pub rr: String,
pub r#type: String,
pub ttl: i32,
pub value: String,
}
pub trait DnsClient {
fn list_dns_records(&mut self, domain: &str) -> XResult<Vec<DnsRecord>>;
fn delete_dns_record(&mut self, record_id: &str) -> XResult<()>;
fn add_dns_record(&mut self, dns_record: &DnsRecord) -> XResult<()>;
}
pub struct DnsClientFactory {}
impl DnsClientFactory {
pub fn build(supplier: &str) -> XResult<Box<dyn DnsClient>> {
Ok(Box::new(AlibabaCloudDnsClient::build(supplier)?))
//simple_error!("Build dns client failed: {}", supplier)
}
}

30
src/kms.rs Normal file
View File

@@ -0,0 +1,30 @@
use reqwest::blocking::{Body, Client, Request};
use reqwest::{Method, Url};
use rust_util::XResult;
use serde::Deserialize;
use serde_json::json;
#[derive(Debug, Deserialize)]
struct DecryptResponse {
value: String,
}
pub fn try_kms_decrypt(ciphertext: &str) -> XResult<String> {
if !ciphertext.starts_with("LKMS:") {
return Ok(ciphertext.to_string());
}
debugging!("Try decrypt: {}", ciphertext);
let body = json!({ "encrypted_value": ciphertext });
let body = serde_json::to_string(&body)?;
let client = Client::new();
let uri = format!("http://{}/{}", "127.0.0.1:5567", "decrypt");
let mut request = Request::new(Method::POST, Url::parse(&uri)?);
let _ = request.body_mut().insert(Body::from(body));
let response = client.execute(request)?;
debugging!("KMS response text: {:?}", &response);
let response_text = response.text()?;
debugging!("KMS response text: {}", &response_text);
let decrypt_response: DecryptResponse = serde_json::from_str(&response_text)?;
debugging!("Decrypt value: {}", &decrypt_response.value);
Ok(decrypt_response.value)
}

View File

@@ -3,71 +3,40 @@ extern crate lazy_static;
#[macro_use]
extern crate rust_util;
mod acme;
mod util;
mod config;
mod x509;
mod network;
mod statics;
mod dingtalk;
mod statistics;
mod notification;
mod kms;
mod dns;
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 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 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};
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::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 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");
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> {
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<String>,
cert_file: Option<String>,
outputs_file: Option<String>,
}
#[async_std::main]
async fn main() -> tide::Result<()> {
let matches = App::new(NAME)
@@ -76,13 +45,13 @@ async fn main() -> tide::Result<()> {
.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("directory-url").long("directory-url").takes_value(true).help("ACME directory URL"))
.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"))
@@ -91,7 +60,10 @@ async fn main() -> tide::Result<()> {
.arg(Arg::with_name("hide-logo").long("hide-logo").help("Hide logo"))
.arg(Arg::with_name("skip-verify-ip").short("k").long("skip-verify-ip").help("Skip verify public ip"))
.arg(Arg::with_name("skip-verify-certificate").short("K").long("skip-verify-certificate").help("Skip verify certificate"))
.arg(Arg::with_name("skip-listen").long("skip-listen").help("Skip http challenge listen"))
.arg(Arg::with_name("allow-interact").long("allow-interact").help("Allow interact"))
.arg(Arg::with_name("challenge-type").short("t").long("challenge-type").takes_value(true).default_value("http").help("Challenge type, http or dns"))
.arg(Arg::with_name("dns-supplier").short("s").long("dns-supplier").takes_value(true).help("DNS supplier, e.g. account://***:****@**?id=*"))
.get_matches();
if matches.is_present("verbose") {
@@ -114,7 +86,7 @@ async fn main() -> tide::Result<()> {
} else {
let skip_verify_certificate = matches.is_present("skip-verify-certificate");
Some(get_local_public_ip(skip_verify_certificate).unwrap_or_else(|e| {
failure!("Get local public ip failed: {}", e);
failure!("Get local public ip failed: {}, you can turn off verify IP by -k or --skip-verify-ip", e);
exit(1);
}))
};
@@ -157,13 +129,6 @@ async fn main() -> tide::Result<()> {
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);
@@ -211,11 +176,13 @@ async fn main() -> tide::Result<()> {
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");
let skip_listen = matches.is_present("skip-listen");
let port = iff!(skip_listen, 0, cert_config.as_ref().and_then(|c| c.port).unwrap_or(port));
if !check {
let check_config = matches.is_present("check");
if !check_config && port > 0 {
let (s, r) = channel::bounded(1);
startup_http_server(s, port);
r.recv().await.ok();
@@ -225,7 +192,7 @@ async fn main() -> tide::Result<()> {
let mut dns_cleaned_domains: Vec<String> = vec![];
match cert_config {
None => { // cert config is not assigned
if check {
if check_config {
failure!("Bad argument `--check`");
exit(1);
}
@@ -249,21 +216,22 @@ async fn main() -> tide::Result<()> {
};
let acme_request = AcmeRequest {
challenge: AcmeChallenge::Http,
credential_supplier: None,
allow_interact: false,
challenge: AcmeChallenge::from_str(matches.value_of("challenge-type")),
credential_supplier: matches.value_of("dns-supplier").map(ToString::to_string),
allow_interact: matches.is_present("allow-interact"),
contract_email: &email,
primary_name,
alt_names: &alt_names,
algo,
mode,
directory_url: matches.value_of("directory-url").map(|u| u.to_string()),
account_dir,
timeout,
local_public_ip: local_public_ip.as_deref(),
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);
@@ -271,13 +239,14 @@ async fn main() -> tide::Result<()> {
}
}
Some(cert_config) => { // cert config is assigned
if check {
if check_config {
check_cert_config(&cert_config);
return Ok(());
}
let mut acme_statics = AcmeStatics::start();
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 {
@@ -294,13 +263,19 @@ 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");
None
}
Some(credential_supplier) => Some(credential_supplier.as_str()),
Some(credential_supplier) => match kms::try_kms_decrypt(credential_supplier) {
Ok(credential_supplier) => Some(credential_supplier),
Err(e) => {
failure!("Decrypt DNS challenge credential supplier failed: {}", e);
None
}
},
}
}
}
@@ -314,6 +289,7 @@ async fn main() -> tide::Result<()> {
alt_names: &alt_names,
algo,
mode,
directory_url: matches.value_of("directory-url").map(|u| u.to_string()).or(filtered_cert_config.directory_url.clone()),
account_dir,
timeout,
local_public_ip: local_public_ip.as_deref(),
@@ -325,30 +301,30 @@ async fn main() -> tide::Result<()> {
dns_names.iter().for_each(|dns_name| domains.push(dns_name.clone()));
if let Err(e) = request_acme_certificate(acme_request, &mut dns_cleaned_domains) {
failure!("Request certificate: {}, by acme failed: {}", item.path, e);
acme_statics.add_item(domains, AcmeStatus::Fail(format!("{}", e)));
acme_statistics.add_item(domains, AcmeStatus::Fail(format!("{}", e)));
} else {
acme_statics.add_item(domains, AcmeStatus::Success);
acme_statistics.add_item(domains, AcmeStatus::Success);
}
}
}
acme_statics.end();
acme_statistics.end();
let mut success_count = 0;
for acme_static in &acme_statics.items {
if let AcmeStatus::Success = acme_static.status {
for acme_statistic in &acme_statistics.items {
if let AcmeStatus::Success = acme_statistic.status {
success_count += 1;
}
}
success!("Statics: \n{}", acme_statics);
success!("Statistics: \n{}", acme_statistics);
let mut dingtalk_message = format!("Statics: \n{}", acme_statics);
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(_) => {
@@ -370,7 +346,7 @@ async fn main() -> tide::Result<()> {
let mut success_domains = vec![];
let mut failed_domains = vec![];
for acme_item in &acme_statics.items {
for acme_item in &acme_statistics.items {
if let AcmeStatus::Success = acme_item.status {
success_domains.push(format!("* {}", acme_item.domains.join(", ")));
}
@@ -388,12 +364,12 @@ async fn main() -> tide::Result<()> {
dingtalk_message.push_str(&failed_domains.join("\n"));
}
if !acme_statics.items.is_empty() && filtered_cert_config.notify_token.is_some() {
if let Err(err) = send_dingtalk_message(&filtered_cert_config, &dingtalk_message) {
if !acme_statistics.items.is_empty() && filtered_cert_config.notify_token.is_some() {
if let Err(err) = send_notify_message(&filtered_cert_config, &dingtalk_message) {
failure!("Send notification message failed: {:?}", err);
}
} else {
information!("No notification message need to send, or not configed notification token");
information!("No notification message sent, or no configured notification token");
}
}
}
@@ -430,167 +406,6 @@ fn check_cert_config(cert_config: &CertConfig) {
}
}
fn request_acme_certificate(acme_request: AcmeRequest, dns_cleaned_domains: &mut Vec<String>) -> 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<RPClient> = 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<i32>, port: u16) {
task::spawn(async move {
information!("Listen at 0.0.0.0:{}", port);
@@ -608,7 +423,7 @@ fn startup_http_server(s: Sender<i32>, 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);

106
src/notification.rs Normal file
View File

@@ -0,0 +1,106 @@
use std::io::{Error, ErrorKind};
use std::time::SystemTime;
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use serde::{Serialize, Deserialize};
use rust_util::XResult;
use hmac::{Hmac, Mac};
use hmac::digest::FixedOutput;
use sha2::Sha256;
use crate::config::CertConfig;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct InnerTextMessageText {
pub content: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct InnerTextMessage {
pub msgtype: String,
pub text: InnerTextMessageText,
}
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 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));
if !sec_token.is_empty() {
let timestamp = &format!("{}", SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_millis());
let timestamp_and_secret = &format!("{}\n{}", timestamp, sec_token);
let hmac_sha256 = STANDARD.encode(&calc_hmac_sha256(sec_token.as_bytes(), timestamp_and_secret.as_bytes())?[..]);
webhook_url.push_str("&timestamp=");
webhook_url.push_str(timestamp);
webhook_url.push_str("&sign=");
webhook_url.push_str(&urlencoding::encode(&hmac_sha256));
}
Ok(webhook_url)
}
// format: dingtalk:access_token?sec_token
#[allow(clippy::manual_map)]
fn parse_notify_token(cert_config: &CertConfig) -> Option<NotifyToken> {
let token = cert_config.notify_token.as_ref()?;
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 = 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
}
}
/// calc hma_sha256 digest
fn calc_hmac_sha256(key: &[u8], message: &[u8]) -> XResult<Vec<u8>> {
let mut mac = match Hmac::<Sha256>::new_from_slice(key) {
Ok(m) => m,
Err(e) => {
return Err(Box::new(Error::new(ErrorKind::Other, format!("Hmac error: {}", e))));
}
};
mac.update(message);
Ok(mac.finalize_fixed().to_vec())
}

View File

@@ -1,9 +1,8 @@
use rust_util::XResult;
use std::thread;
use std::thread::JoinHandle;
use std::sync::atomic::{AtomicU32, Ordering};
use std::thread::{JoinHandle, spawn, sleep};
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Duration;
use rust_util::XResult;
pub struct SimpleThreadPool {
max_pool_size: u32,
@@ -28,7 +27,7 @@ impl SimpleThreadPool {
let running = self.running_pool_size.fetch_add(1, Ordering::SeqCst);
let running_pool_size_clone = self.running_pool_size.clone();
if running < self.max_pool_size {
Some(thread::spawn(move || {
Some(spawn(move || {
f();
running_pool_size_clone.fetch_sub(1, Ordering::SeqCst);
}))
@@ -47,7 +46,7 @@ fn test_simple_thread_pool() {
for i in 1..10 {
if let Some(h) = stp.submit(move || {
println!("Task start: {}", i);
thread::sleep(Duration::from_secs(1));
sleep(Duration::from_secs(1));
println!("Task end: {}", i);
}) {
handlers.push(h);

View File

@@ -2,7 +2,7 @@ use std::time::SystemTime;
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub struct AcmeStatics {
pub struct AcmeStatistics {
pub started: SystemTime,
pub ended: Option<SystemTime>,
pub items: Vec<AcmeItem>,
@@ -36,14 +36,14 @@ impl Display for AcmeItem {
}
}
impl Display for AcmeStatics {
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 {
@@ -72,9 +72,9 @@ impl Display for AcmeStatics {
}
}
impl AcmeStatics {
pub fn start() -> AcmeStatics {
AcmeStatics {
impl AcmeStatistics {
pub fn start() -> AcmeStatistics {
AcmeStatistics {
started: SystemTime::now(),
ended: None,
items: Vec::new(),

View File

@@ -1,5 +1,7 @@
use rust_util::XResult;
// "example.com" -> ("@", "example.com")
// "www.example.com" -> ("www", "example.com")
pub fn parse_dns_record(record: &str) -> XResult<(String, String)> {
let r = if record.ends_with(".") {
record.chars().take(record.len() - 1).collect::<String>().to_ascii_lowercase()
@@ -18,7 +20,7 @@ pub fn parse_dns_record(record: &str) -> XResult<(String, String)> {
// SHOULD read from: https://publicsuffix.org/
let domain_parts_len = match last_part {
"cn" => match last_part_2 {
"com" | "net" | "org" | "gov" => 3,
"com" | "net" | "org" | "gov" | "edu" => 3,
_ => 2,
},
_ => 2,

View File

@@ -1,19 +1,18 @@
use serde::{Deserialize, Serialize};
use x509_parser::parse_x509_certificate;
use x509_parser::pem::parse_x509_pem;
use x509_parser::extensions::{ParsedExtension, GeneralName};
use x509_parser::der_parser::oid::Oid;
use std::str::FromStr;
use rust_util::XResult;
use x509_parser::der_parser::parse_der;
use x509_parser::x509::SubjectPublicKeyInfo;
use x509_parser::der_parser::ber::BerObjectContent;
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use rust_util::XResult;
use x509_parser::{pem::parse_x509_pem, parse_x509_certificate};
use x509_parser::extensions::{ParsedExtension, GeneralName};
use x509_parser::der_parser::{ber::BerObjectContent, oid::Oid, parse_der};
use x509_parser::x509::SubjectPublicKeyInfo;
lazy_static! {
static ref OID_COMMON_NAME: Oid<'static> = Oid::from_str("2.5.4.3").unwrap();
static ref OID_RSA_WITH_SHA256: Oid<'static> = Oid::from_str("1.2.840.113549.1.1.11").unwrap();
static ref OID_ECDSA_WITH_SHA256: Oid<'static> = Oid::from_str("1.2.840.10045.4.3.2").unwrap();
static ref OID_ECDSA_WITH_SHA384: Oid<'static> = Oid::from_str("1.2.840.10045.4.3.3").unwrap();
static ref OID_EC_PUBLIC_KEY: Oid<'static> = Oid::from_str("1.2.840.10045.2.1").unwrap();
static ref OID_RSA_PUBLIC_KEY: Oid<'static> = Oid::from_str("1.2.840.113549.1.1.1").unwrap();
@@ -27,6 +26,7 @@ lazy_static! {
pub enum X509IssuerAlgo {
RsaWithSha256,
EcdsaWithSha256,
EcdsaWithSha384,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
@@ -48,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)
}
}
@@ -76,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 {
@@ -145,6 +146,8 @@ pub fn parse_x509(pem_id: &str, pem: &str) -> XResult<X509Certificate> {
X509IssuerAlgo::RsaWithSha256
} else if cert_algorithm_oid == &*OID_ECDSA_WITH_SHA256 {
X509IssuerAlgo::EcdsaWithSha256
} else if cert_algorithm_oid == &*OID_ECDSA_WITH_SHA384 {
X509IssuerAlgo::EcdsaWithSha384
} else {
return simple_error!("Parse pem: {}, unknown x509 algorithm oid: {:?}", pem_id, cert_algorithm_oid);
};