Compare commits
52 Commits
4980d41db7
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
76334e3fa4
|
|||
|
a1c7bcba30
|
|||
|
b5c142838f
|
|||
|
87e052e67b
|
|||
|
2019fde054
|
|||
|
cb47c5c4e6
|
|||
|
d36335a885
|
|||
|
9636eba5f7
|
|||
|
732ad63dbe
|
|||
|
edce2cdcd5
|
|||
|
b6267a0c76
|
|||
|
8135d1ceed
|
|||
|
09291d553c
|
|||
|
7b768c9302
|
|||
|
315fddaa15
|
|||
|
10d680a364
|
|||
|
9e3ae38447
|
|||
|
31e480a7b2
|
|||
|
5ebe0a2dae
|
|||
|
835373dd12
|
|||
|
0a51e5de08
|
|||
|
8c8173de79
|
|||
|
f655c6a87f
|
|||
|
41f81ebdd9
|
|||
|
3564e6738c
|
|||
|
4a00f96763
|
|||
|
6639276e11
|
|||
|
ac540437dd
|
|||
|
8282a4c704
|
|||
| d85ac9416e | |||
|
8d8b1c24f0
|
|||
|
53e71c62d6
|
|||
|
e52d9c02cb
|
|||
|
2bb2c80768
|
|||
|
d685636417
|
|||
|
82e6761c8d
|
|||
|
b576b49b0e
|
|||
|
c601f31bca
|
|||
|
af1ab9ad6c
|
|||
|
e25650099a
|
|||
|
716b442458
|
|||
|
4ccd306d40
|
|||
|
0264badcbe
|
|||
|
42d729264f
|
|||
|
ce2dd9fe67
|
|||
|
fbed7fb2ef
|
|||
|
60a732ef0c
|
|||
|
ee17791b57
|
|||
|
d094cf1e6e
|
|||
|
2a2be6402b
|
|||
|
5e9e021c50
|
|||
|
854b8fb5de
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
|||||||
|
certs/
|
||||||
|
test_cert_config.json
|
||||||
acme_dir/
|
acme_dir/
|
||||||
.idea/
|
.idea/
|
||||||
__temp_dir/
|
__temp_dir/
|
||||||
|
|||||||
2722
Cargo.lock
generated
2722
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
18
Cargo.toml
18
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "acme-client"
|
name = "acme-client"
|
||||||
version = "0.5.0"
|
version = "1.3.9"
|
||||||
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"
|
||||||
@@ -11,14 +11,22 @@ description = "Acme auto challenge client, acme-client can issue certificates fr
|
|||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
clap = "2.33"
|
clap = "2.33"
|
||||||
rust_util = "0.6"
|
rust_util = "0.6"
|
||||||
acme-lib = "0.8"
|
acme-lib = "0.9"
|
||||||
tide = "0.16"
|
tide = "0.16"
|
||||||
async-std = { version = "1.8", features = ["attributes"] }
|
async-std = { version = "1.8", features = ["attributes"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
deser-hjson = "0.1"
|
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"] }
|
||||||
trust-dns-resolver = "0.20"
|
#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 = "2.1"
|
||||||
|
base64 = "0.21"
|
||||||
|
hmac = "0.12"
|
||||||
|
sha2 = "0.10"
|
||||||
|
aliyun-openapi-core-rust-sdk = "1.1.0"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
|||||||
72
README.md
72
README.md
@@ -5,7 +5,7 @@ ACME Client in Rust
|
|||||||
Acme client help:
|
Acme client help:
|
||||||
```shell
|
```shell
|
||||||
$ acme-client --help
|
$ acme-client --help
|
||||||
acme-client 0.5.0
|
acme-client 1.1.0
|
||||||
Hatter Jiang <jht5945@gmail.com>
|
Hatter Jiang <jht5945@gmail.com>
|
||||||
Acme auto challenge client, acme-client can issue certificates from Let's encrypt
|
Acme auto challenge client, acme-client can issue certificates from Let's encrypt
|
||||||
|
|
||||||
@@ -13,23 +13,27 @@ USAGE:
|
|||||||
acme-client [FLAGS] [OPTIONS]
|
acme-client [FLAGS] [OPTIONS]
|
||||||
|
|
||||||
FLAGS:
|
FLAGS:
|
||||||
--check Check cert config
|
--allow-interact Allow interact
|
||||||
-h, --help Prints help information
|
--check Check cert config
|
||||||
--hide-logo Hide logo
|
-h, --help Prints help information
|
||||||
--skip-verify-ip Skip verify public ip
|
--hide-logo Hide logo
|
||||||
-v, --verbose Verbose
|
-K, --skip-verify-certificate Skip verify certificate
|
||||||
-V, --version Print version
|
-k, --skip-verify-ip Skip verify public ip
|
||||||
|
-v, --verbose Verbose
|
||||||
|
-V, --version Print version
|
||||||
|
|
||||||
OPTIONS:
|
OPTIONS:
|
||||||
-a, --algo <algo> Pki algo [default: ec384]
|
-a, --algo <algo> Pki algo [default: ec384]
|
||||||
-c, --config <config> Cert config
|
--cert-dir <cert-dir> Certificate dir
|
||||||
--dir <dir> Account key dir [default: acme_dir]
|
-c, --config <config> Cert config
|
||||||
-d, --domain <domain>... Domains
|
--dir <dir> Account key dir [default: acme_dir]
|
||||||
--email <email> Contract email
|
-d, --domain <domain>... Domains
|
||||||
-m, --mode <mode> Mode [default: prod]
|
--email <email> Contract email
|
||||||
-p, --port <port> Http port [default: 80]
|
-m, --mode <mode> Mode [default: prod]
|
||||||
--timeout <timeout> Timeout (ms) [default: 5000]
|
-o, --outputs <outputs> Outputs file
|
||||||
-t, --type <type> Type http or dns [default: http]
|
-p, --port <port> Http port [default: 80]
|
||||||
|
--timeout <timeout> Timeout (ms) [default: 5000]
|
||||||
|
-t, --type <type> Type http or dns [default: http]
|
||||||
```
|
```
|
||||||
|
|
||||||
签发一张证书示例
|
签发一张证书示例
|
||||||
@@ -39,15 +43,49 @@ OPTIONS:
|
|||||||
使用参数 `--config` 时的配置文件示例:
|
使用参数 `--config` 时的配置文件示例:
|
||||||
```json
|
```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": [{
|
"certItems": [{
|
||||||
"path": "dir_cryptofan_org",
|
"path": "dir_cryptofan_org",
|
||||||
"dnsNames": ["cryptofan.org", "www.cryptofan.org"]
|
"dnsNames": ["cryptofan.org", "www.cryptofan.org"]
|
||||||
|
}, {
|
||||||
|
"path": "dir_webauthn_host",
|
||||||
|
"dnsNames": ["webauthn.host", "*.webauthn.host"],
|
||||||
|
"type": "dns",
|
||||||
|
"supplier": "alibabacloud"
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Nginx.conf 配置:
|
||||||
|
```nginx.conf
|
||||||
|
location /.well-known/acme-challenge/ {
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_pass http://127.0.0.1:18342/.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>
|
<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
|
||||||
|
|
||||||
|
|||||||
12
justfile
12
justfile
@@ -1,14 +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
|
|
||||||
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
|
|
||||||
|
|||||||
206
src/acme.rs
Normal file
206
src/acme.rs
Normal 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(())
|
||||||
|
}
|
||||||
200
src/ali_dns.rs
Normal file
200
src/ali_dns.rs
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
#![allow(deprecated)]
|
||||||
|
use rust_util::XResult;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use aliyun_openapi_core_rust_sdk::RPClient;
|
||||||
|
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,
|
||||||
|
access_key_secret: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// syntax: account://***:***@alibabacloud?id=dns
|
||||||
|
pub fn simple_parse_aliyun_supplier(supplier: &str) -> XResult<AccessCredential> {
|
||||||
|
if !supplier.starts_with("account://") {
|
||||||
|
return simple_error!("Supplier syntax error: {}", supplier);
|
||||||
|
}
|
||||||
|
let access_key_id_and_secret: String = supplier.chars().skip("account://".len()).take_while(|c| *c != '@').collect();
|
||||||
|
let c_pos = opt_value_result!(access_key_id_and_secret.find(":"), "Supplier syntax error: {}", supplier);
|
||||||
|
|
||||||
|
let access_key_id = access_key_id_and_secret.chars().take(c_pos).collect();
|
||||||
|
let access_key_secret = access_key_id_and_secret.chars().skip(c_pos + 1).collect();
|
||||||
|
|
||||||
|
Ok(AccessCredential { access_key_id, access_key_secret })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct CommonSuccessResponse {
|
||||||
|
#[serde(rename = "RequestId")]
|
||||||
|
pub request_id: String,
|
||||||
|
#[serde(rename = "RecordId")]
|
||||||
|
pub record_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct CommonErrorResponse {
|
||||||
|
#[serde(rename = "RequestId")]
|
||||||
|
pub request_id: String,
|
||||||
|
#[serde(rename = "Message")]
|
||||||
|
pub message: String,
|
||||||
|
#[serde(rename = "Recommend")]
|
||||||
|
pub recommend: String,
|
||||||
|
#[serde(rename = "HostId")]
|
||||||
|
pub host_id: String,
|
||||||
|
#[serde(rename = "Code")]
|
||||||
|
pub code: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ListDnsResponse {
|
||||||
|
#[serde(rename = "TotalCount")]
|
||||||
|
pub total_count: i32,
|
||||||
|
#[serde(rename = "RequestId")]
|
||||||
|
pub request_id: String,
|
||||||
|
#[serde(rename = "PageSize")]
|
||||||
|
pub page_size: i32,
|
||||||
|
#[serde(rename = "PageNumber")]
|
||||||
|
pub page_number: i32,
|
||||||
|
#[serde(rename = "DomainRecords")]
|
||||||
|
pub domain_records: DnsRecords,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct DnsRecords {
|
||||||
|
#[serde(rename = "Record")]
|
||||||
|
pub record: Vec<DnsRecord>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct DnsRecord {
|
||||||
|
#[serde(rename = "RR")]
|
||||||
|
pub rr: String,
|
||||||
|
#[serde(rename = "Line")]
|
||||||
|
pub line: String,
|
||||||
|
#[serde(rename = "Status")]
|
||||||
|
pub status: String,
|
||||||
|
#[serde(rename = "Locked")]
|
||||||
|
pub locked: bool,
|
||||||
|
#[serde(rename = "Type")]
|
||||||
|
pub r#type: String,
|
||||||
|
#[serde(rename = "DomainName")]
|
||||||
|
pub domain_name: String,
|
||||||
|
#[serde(rename = "Value")]
|
||||||
|
pub value: String,
|
||||||
|
#[serde(rename = "RecordId")]
|
||||||
|
pub record_id: String,
|
||||||
|
#[serde(rename = "TTL")]
|
||||||
|
pub ttl: i32,
|
||||||
|
#[serde(rename = "Weight")]
|
||||||
|
pub weight: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_dns(client: &RPClient, domain: &str) -> XResult<Result<ListDnsResponse, CommonErrorResponse>> {
|
||||||
|
let describe_domain_records_response = opt_result!(client.get("DescribeDomainRecords")
|
||||||
|
.query(&[
|
||||||
|
("RegionId", "cn-hangzhou"),
|
||||||
|
("DomainName", domain)
|
||||||
|
])
|
||||||
|
.send(), "List domain records: {}, failed: {}", domain);
|
||||||
|
parse_result("DescribeDomainRecords", &describe_domain_records_response)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_dns_record(client: &RPClient, record_id: &str) -> XResult<Result<CommonSuccessResponse, CommonErrorResponse>> {
|
||||||
|
let delete_domain_record_response = opt_result!(client.get("DeleteDomainRecord")
|
||||||
|
.query(&[
|
||||||
|
("RegionId", "cn-hangzhou"),
|
||||||
|
("RecordId", record_id)
|
||||||
|
])
|
||||||
|
.send(), "Delete domain record id: {}, failed: {}", record_id);
|
||||||
|
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)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// domain -> "example.com"
|
||||||
|
// rr -> "@", "_acme-challenge"
|
||||||
|
// t -> "TXT"
|
||||||
|
// value -> "test"
|
||||||
|
pub fn add_dns_record(client: &RPClient, domain: &str, rr: &str, t: &str, value: &str) -> XResult<Result<CommonSuccessResponse, CommonErrorResponse>> {
|
||||||
|
let add_domain_record_response = opt_result!(client.get("AddDomainRecord")
|
||||||
|
.query(&[
|
||||||
|
("RegionId", "cn-hangzhou"),
|
||||||
|
("DomainName", domain),
|
||||||
|
("RR", rr),
|
||||||
|
("Type", t),
|
||||||
|
("Value", value)
|
||||||
|
])
|
||||||
|
.send(), "Add domain record: {}.{} -> {} {} ,failed: {}", rr, domain, t, value);
|
||||||
|
parse_result("AddDomainRecord", &add_domain_record_response)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_dns_client(access_credential: &AccessCredential) -> RPClient {
|
||||||
|
RPClient::new(
|
||||||
|
access_credential.access_key_id.clone(),
|
||||||
|
access_credential.access_key_secret.clone(),
|
||||||
|
String::from(ALI_DNS_ENDPOINT),
|
||||||
|
String::from(ALI_DNS_API_VERSION),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
match describe_domain_records_error_result {
|
||||||
|
Ok(r) => Ok(Err(r)),
|
||||||
|
Err(_) => simple_error!("Parse {} response failed: {}", fn_name, response),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,34 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use rust_util::XResult;
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use acme_lib::DirectoryUrl;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use crate::x509;
|
|
||||||
use crate::x509::{X509PublicKeyAlgo, X509Certificate};
|
|
||||||
use std::time::SystemTime;
|
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 CERT_NAME: &str = "cert.pem";
|
||||||
pub const KEY_NAME: &str = "key.pem";
|
pub const KEY_NAME: &str = "key.pem";
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum AcmeChallenge {
|
||||||
|
#[default]
|
||||||
|
Http,
|
||||||
|
Dns,
|
||||||
|
}
|
||||||
|
|
||||||
|
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)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum AcmeMode {
|
pub enum AcmeMode {
|
||||||
Prod,
|
Prod,
|
||||||
@@ -43,6 +61,9 @@ impl AcmeMode {
|
|||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CertConfigItem {
|
pub struct CertConfigItem {
|
||||||
|
// HTTP, DNS
|
||||||
|
pub r#type: Option<String>,
|
||||||
|
pub supplier: Option<String>,
|
||||||
pub path: String,
|
pub path: String,
|
||||||
pub algo: Option<String>,
|
pub algo: Option<String>,
|
||||||
pub public_key_algo: Option<X509PublicKeyAlgo>,
|
pub public_key_algo: Option<X509PublicKeyAlgo>,
|
||||||
@@ -53,10 +74,19 @@ pub struct CertConfigItem {
|
|||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CertConfig {
|
pub struct CertConfig {
|
||||||
|
pub port: Option<u16>,
|
||||||
|
pub credential_suppliers: Option<HashMap<String, String>>,
|
||||||
pub cert_items: Vec<CertConfigItem>,
|
pub cert_items: Vec<CertConfigItem>,
|
||||||
|
pub trigger_after_update: Option<Vec<String>>,
|
||||||
|
pub notify_token: Option<String>,
|
||||||
|
pub directory_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CertConfig {
|
impl CertConfig {
|
||||||
|
// pub fn get_port(&self, port: Option<u16>) -> u16 {
|
||||||
|
// self.port.or(port).unwrap_or(80)
|
||||||
|
// }
|
||||||
|
|
||||||
pub fn filter_cert_config_items(self, valid_days: i32) -> Self {
|
pub fn filter_cert_config_items(self, valid_days: i32) -> Self {
|
||||||
let mut filtered_cert_items = vec![];
|
let mut filtered_cert_items = vec![];
|
||||||
|
|
||||||
@@ -93,7 +123,14 @@ impl CertConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Self { cert_items: filtered_cert_items }
|
Self {
|
||||||
|
port: self.port,
|
||||||
|
credential_suppliers: self.credential_suppliers,
|
||||||
|
cert_items: filtered_cert_items,
|
||||||
|
trigger_after_update: self.trigger_after_update,
|
||||||
|
notify_token: self.notify_token,
|
||||||
|
directory_url: self.directory_url,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load(config_fn: &str) -> XResult<Self> {
|
pub fn load(config_fn: &str) -> XResult<Self> {
|
||||||
@@ -104,6 +141,10 @@ impl CertConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CertConfigItem {
|
impl CertConfigItem {
|
||||||
|
pub fn get_acme_challenge(&self) -> AcmeChallenge {
|
||||||
|
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>> {
|
||||||
if self.path.is_empty() {
|
if self.path.is_empty() {
|
||||||
return simple_error!("Cert config item path is empty");
|
return simple_error!("Cert config item path is empty");
|
||||||
@@ -112,7 +153,7 @@ impl CertConfigItem {
|
|||||||
let cert_path_buff = path_buff.join(CERT_NAME);
|
let cert_path_buff = path_buff.join(CERT_NAME);
|
||||||
if self.common_name.is_none() && self.dns_names.is_none() {
|
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 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.common_name = Some(x509_certificate.common_name.clone());
|
||||||
self.dns_names = Some(x509_certificate.alt_names.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) {
|
if let Some(pos) = x509_certificate.alt_names.iter().position(|n| n == &x509_certificate.common_name) {
|
||||||
@@ -132,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),
|
||||||
},
|
},
|
||||||
@@ -140,7 +181,7 @@ impl CertConfigItem {
|
|||||||
}
|
}
|
||||||
if cert_path_buff.exists() {
|
if cert_path_buff.exists() {
|
||||||
let pem = opt_result!(fs::read_to_string(cert_path_buff.clone()), "Read file: {:?}, failed: {}", cert_path_buff);
|
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 self_dns_names = vec![];
|
||||||
let mut cert_dns_names = vec![];
|
let mut cert_dns_names = vec![];
|
||||||
|
|||||||
30
src/dns.rs
Normal file
30
src/dns.rs
Normal 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
30
src/kms.rs
Normal 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)
|
||||||
|
}
|
||||||
333
src/main.rs
333
src/main.rs
@@ -3,57 +3,40 @@ extern crate lazy_static;
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate rust_util;
|
extern crate rust_util;
|
||||||
|
|
||||||
|
mod acme;
|
||||||
|
mod util;
|
||||||
mod config;
|
mod config;
|
||||||
mod x509;
|
mod x509;
|
||||||
mod network;
|
mod network;
|
||||||
|
mod statistics;
|
||||||
|
mod notification;
|
||||||
|
mod kms;
|
||||||
|
mod dns;
|
||||||
|
mod ali_dns;
|
||||||
// mod simple_thread_pool;
|
// mod simple_thread_pool;
|
||||||
|
|
||||||
use std::env;
|
use crate::acme::{request_acme_certificate, AcmeRequest};
|
||||||
use std::thread;
|
use crate::config::{AcmeChallenge, AcmeMode, CertConfig, CERT_NAME, KEY_NAME};
|
||||||
use rust_util::XResult;
|
use crate::notification::send_notify_message;
|
||||||
use acme_lib::Directory;
|
use crate::network::get_local_public_ip;
|
||||||
use acme_lib::{create_p384_key, create_p256_key, create_rsa_key};
|
use crate::statistics::{AcmeStatistics, AcmeStatus};
|
||||||
use acme_lib::persist::FilePersist;
|
use crate::x509::X509PublicKeyAlgo;
|
||||||
|
use async_std::{channel, channel::Sender, task};
|
||||||
use clap::{App, Arg};
|
use clap::{App, Arg};
|
||||||
|
use rust_util::util_cmd::run_command_and_wait;
|
||||||
|
use std::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::str::FromStr;
|
|
||||||
use std::sync::RwLock;
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use tide::Request;
|
|
||||||
use std::process::exit;
|
|
||||||
use std::time::{Duration, SystemTime};
|
|
||||||
use async_std::task;
|
|
||||||
use async_std::channel;
|
|
||||||
use async_std::channel::Sender;
|
|
||||||
use config::AcmeMode;
|
|
||||||
use crate::config::{CertConfig, CERT_NAME, KEY_NAME};
|
|
||||||
use crate::x509::{X509PublicKeyAlgo, X509EcPublicKeyAlgo};
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use crate::network::{get_local_public_ip, get_resolver, resolve_first_ipv4};
|
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");
|
||||||
const AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
|
const AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
|
||||||
const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
|
const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref TOKEN_MAP: RwLock<BTreeMap<String, String>> = RwLock::new(BTreeMap::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
struct AcmeRequest<'a> {
|
|
||||||
contract_email: &'a str,
|
|
||||||
primary_name: &'a str,
|
|
||||||
alt_names: &'a [&'a str],
|
|
||||||
algo: X509PublicKeyAlgo,
|
|
||||||
mode: AcmeMode,
|
|
||||||
account_dir: &'a str,
|
|
||||||
timeout: u64,
|
|
||||||
local_public_ip: Option<&'a str>,
|
|
||||||
key_file: Option<String>,
|
|
||||||
cert_file: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_std::main]
|
#[async_std::main]
|
||||||
async fn main() -> tide::Result<()> {
|
async fn main() -> tide::Result<()> {
|
||||||
let matches = App::new(NAME)
|
let matches = App::new(NAME)
|
||||||
@@ -62,18 +45,25 @@ async fn main() -> tide::Result<()> {
|
|||||||
.author(AUTHORS)
|
.author(AUTHORS)
|
||||||
.arg(Arg::with_name("version").short("V").long("version").help("Print version"))
|
.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("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("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("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("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("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("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("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("dir").long("dir").takes_value(true).default_value("acme_dir").help("Account key dir"))
|
||||||
|
.arg(Arg::with_name("cert-dir").long("cert-dir").takes_value(true).help("Certificate dir"))
|
||||||
.arg(Arg::with_name("config").short("c").long("config").takes_value(true).help("Cert config"))
|
.arg(Arg::with_name("config").short("c").long("config").takes_value(true).help("Cert config"))
|
||||||
|
.arg(Arg::with_name("outputs").short("o").long("outputs").takes_value(true).help("Outputs file"))
|
||||||
.arg(Arg::with_name("check").long("check").help("Check cert config"))
|
.arg(Arg::with_name("check").long("check").help("Check cert config"))
|
||||||
.arg(Arg::with_name("hide-logo").long("hide-logo").help("Hide logo"))
|
.arg(Arg::with_name("hide-logo").long("hide-logo").help("Hide logo"))
|
||||||
.arg(Arg::with_name("skip-verify-ip").long("skip-verify-ip").help("Skip verify public ip"))
|
.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();
|
.get_matches();
|
||||||
|
|
||||||
if matches.is_present("verbose") {
|
if matches.is_present("verbose") {
|
||||||
@@ -94,8 +84,9 @@ async fn main() -> tide::Result<()> {
|
|||||||
let local_public_ip = if skip_verify_ip {
|
let local_public_ip = if skip_verify_ip {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(get_local_public_ip().unwrap_or_else(|e| {
|
let skip_verify_certificate = matches.is_present("skip-verify-certificate");
|
||||||
failure!("Get local public ip failed: {}", e);
|
Some(get_local_public_ip(skip_verify_certificate).unwrap_or_else(|e| {
|
||||||
|
failure!("Get local public ip failed: {}, you can turn off verify IP by -k or --skip-verify-ip", e);
|
||||||
exit(1);
|
exit(1);
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
@@ -117,7 +108,7 @@ async fn main() -> tide::Result<()> {
|
|||||||
}
|
}
|
||||||
Ok(email) => {
|
Ok(email) => {
|
||||||
if let Some(email_from_args) = matches.value_of("email") {
|
if let Some(email_from_args) = matches.value_of("email") {
|
||||||
if &email != email_from_args {
|
if email != email_from_args {
|
||||||
warning!("Get email from account config: {}", email);
|
warning!("Get email from account config: {}", email);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,13 +129,6 @@ async fn main() -> tide::Result<()> {
|
|||||||
email.to_string()
|
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") {
|
let port: u16 = match matches.value_of("port") {
|
||||||
Some(p) => p.parse().unwrap_or_else(|e| {
|
Some(p) => p.parse().unwrap_or_else(|e| {
|
||||||
failure!("Parse port: {}, failed: {}", p, e);
|
failure!("Parse port: {}, failed: {}", p, e);
|
||||||
@@ -186,19 +170,29 @@ async fn main() -> tide::Result<()> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let check = matches.is_present("check");
|
let cert_config_file = matches.value_of("config");
|
||||||
|
let cert_config = cert_config_file.map(|f|
|
||||||
|
CertConfig::load(f).unwrap_or_else(|e| {
|
||||||
|
failure!("Load cert config: {}, failed: {}", f, e);
|
||||||
|
exit(1);
|
||||||
|
}));
|
||||||
|
|
||||||
if !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));
|
||||||
|
|
||||||
|
let check_config = matches.is_present("check");
|
||||||
|
|
||||||
|
if !check_config && port > 0 {
|
||||||
let (s, r) = channel::bounded(1);
|
let (s, r) = channel::bounded(1);
|
||||||
startup_http_server(s, port);
|
startup_http_server(s, port);
|
||||||
r.recv().await.ok();
|
r.recv().await.ok();
|
||||||
task::sleep(Duration::from_millis(500)).await;
|
task::sleep(Duration::from_millis(500)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cert_config = matches.value_of("config");
|
let mut dns_cleaned_domains: Vec<String> = vec![];
|
||||||
match cert_config {
|
match cert_config {
|
||||||
None => { // cert config is not assigned
|
None => { // cert config is not assigned
|
||||||
if check {
|
if check_config {
|
||||||
failure!("Bad argument `--check`");
|
failure!("Bad argument `--check`");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
@@ -210,60 +204,173 @@ async fn main() -> tide::Result<()> {
|
|||||||
let primary_name = domains[0];
|
let primary_name = domains[0];
|
||||||
let alt_names: Vec<&str> = domains.into_iter().skip(1).collect();
|
let alt_names: Vec<&str> = domains.into_iter().skip(1).collect();
|
||||||
information!("Domains, main: {}, alt: {:?}", primary_name, alt_names);
|
information!("Domains, main: {}, alt: {:?}", primary_name, alt_names);
|
||||||
|
|
||||||
|
let (cert_file, key_file) = match matches.value_of("cert-dir") {
|
||||||
|
None => (None, None),
|
||||||
|
Some(cert_dir) => {
|
||||||
|
information!("Certificate output dir: {}", cert_dir);
|
||||||
|
fs::create_dir_all(cert_dir).ok();
|
||||||
|
(Some(format!("{}/{}", cert_dir, CERT_NAME)),
|
||||||
|
Some(format!("{}/{}", cert_dir, KEY_NAME)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let acme_request = AcmeRequest {
|
let acme_request = AcmeRequest {
|
||||||
|
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,
|
contract_email: &email,
|
||||||
primary_name,
|
primary_name,
|
||||||
alt_names: &alt_names,
|
alt_names: &alt_names,
|
||||||
algo,
|
algo,
|
||||||
mode,
|
mode,
|
||||||
|
directory_url: matches.value_of("directory-url").map(|u| u.to_string()),
|
||||||
account_dir,
|
account_dir,
|
||||||
timeout,
|
timeout,
|
||||||
local_public_ip: local_public_ip.as_ref().map(|ip| ip.as_str()),
|
local_public_ip: local_public_ip.as_deref(),
|
||||||
..Default::default()
|
cert_file,
|
||||||
|
key_file,
|
||||||
|
outputs_file: matches.value_of("outputs").map(|s| s.into()),
|
||||||
|
//..Default::default()
|
||||||
};
|
};
|
||||||
if let Err(e) = request_acme_certificate(acme_request) {
|
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);
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(cert_config) => { // cert config is assigned
|
Some(cert_config) => { // cert config is assigned
|
||||||
let cert_config = {
|
if check_config {
|
||||||
CertConfig::load(cert_config).unwrap_or_else(|e| {
|
|
||||||
failure!("Load cert config: {}, failed: {}", cert_config, e);
|
|
||||||
exit(1);
|
|
||||||
})
|
|
||||||
};
|
|
||||||
if check {
|
|
||||||
check_cert_config(&cert_config);
|
check_cert_config(&cert_config);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
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) {
|
||||||
warning!("Currently not support wide card domain name");
|
if item.get_acme_challenge() != AcmeChallenge::Dns {
|
||||||
continue;
|
warning!("Currently not support wide card domain name");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let (Some(common_name), Some(dns_names)) = (&item.common_name, &item.dns_names) {
|
if let (Some(common_name), Some(dns_names)) = (&item.common_name, &item.dns_names) {
|
||||||
information!("Domains, main: {}, alt: {:?}", common_name, dns_names);
|
information!("Domains, main: {}, alt: {:?}", common_name, dns_names);
|
||||||
let alt_names: Vec<&str> = dns_names.iter().map(|n| n.as_str()).collect();
|
let alt_names: Vec<&str> = dns_names.iter().map(|n| n.as_str()).collect();
|
||||||
|
let challenge = item.get_acme_challenge();
|
||||||
|
let credential_supplier = if challenge == AcmeChallenge::Dns {
|
||||||
|
match &item.supplier {
|
||||||
|
None => None,
|
||||||
|
Some(supplier) => {
|
||||||
|
let credential_supplier = filtered_cert_config.credential_suppliers.as_ref()
|
||||||
|
.and_then(|m| m.get(supplier));
|
||||||
|
match credential_supplier {
|
||||||
|
None => {
|
||||||
|
warning!("DNS challenge no credential supplier found");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { None };
|
||||||
let acme_request = AcmeRequest {
|
let acme_request = AcmeRequest {
|
||||||
|
challenge,
|
||||||
|
credential_supplier,
|
||||||
|
allow_interact: matches.is_present("allow-interact"),
|
||||||
contract_email: &email,
|
contract_email: &email,
|
||||||
primary_name: common_name,
|
primary_name: common_name,
|
||||||
alt_names: &alt_names,
|
alt_names: &alt_names,
|
||||||
algo,
|
algo,
|
||||||
mode,
|
mode,
|
||||||
|
directory_url: matches.value_of("directory-url").map(|u| u.to_string()).or(filtered_cert_config.directory_url.clone()),
|
||||||
account_dir,
|
account_dir,
|
||||||
timeout,
|
timeout,
|
||||||
local_public_ip: local_public_ip.as_ref().map(|ip| ip.as_str()),
|
local_public_ip: local_public_ip.as_deref(),
|
||||||
cert_file: Some(format!("{}/{}", item.path, CERT_NAME)),
|
cert_file: Some(format!("{}/{}", item.path, CERT_NAME)),
|
||||||
key_file: Some(format!("{}/{}", item.path, KEY_NAME)),
|
key_file: Some(format!("{}/{}", item.path, KEY_NAME)),
|
||||||
|
outputs_file: None,
|
||||||
};
|
};
|
||||||
if let Err(e) = request_acme_certificate(acme_request) {
|
let mut domains = vec![common_name.clone()];
|
||||||
|
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);
|
failure!("Request certificate: {}, by acme failed: {}", item.path, e);
|
||||||
|
acme_statistics.add_item(domains, AcmeStatus::Fail(format!("{}", e)));
|
||||||
|
} else {
|
||||||
|
acme_statistics.add_item(domains, AcmeStatus::Success);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
acme_statistics.end();
|
||||||
|
|
||||||
|
let mut success_count = 0;
|
||||||
|
for acme_statistic in &acme_statistics.items {
|
||||||
|
if let AcmeStatus::Success = acme_statistic.status {
|
||||||
|
success_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
success!("Statistics: \n{}", acme_statistics);
|
||||||
|
|
||||||
|
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.is_empty() {
|
||||||
|
let mut cmd = Command::new(&trigger_after_update[0]);
|
||||||
|
for arg in trigger_after_update.iter().skip(1) {
|
||||||
|
cmd.arg(arg);
|
||||||
|
}
|
||||||
|
match run_command_and_wait(&mut cmd) {
|
||||||
|
Ok(_) => {
|
||||||
|
success!("Restart nginx success");
|
||||||
|
dingtalk_message.push_str(&format!("\n\ntrigger after update success: {:?}", cmd));
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
failure!("Restart nginx failed: {:?}", err);
|
||||||
|
dingtalk_message.push_str(&format!("\n\ntrigger after update failed: {:?}, message: {:?}", cmd, err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warning!("No trigger after update is configured but is empty");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warning!("No trigger after update configured");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut success_domains = vec![];
|
||||||
|
let mut failed_domains = vec![];
|
||||||
|
for acme_item in &acme_statistics.items {
|
||||||
|
if let AcmeStatus::Success = acme_item.status {
|
||||||
|
success_domains.push(format!("* {}", acme_item.domains.join(", ")));
|
||||||
|
}
|
||||||
|
if let AcmeStatus::Fail(_) = acme_item.status {
|
||||||
|
failed_domains.push(format!("* {}", acme_item.domains.join(", ")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !success_domains.is_empty() {
|
||||||
|
dingtalk_message.push_str("\nsuccess domains:\n");
|
||||||
|
dingtalk_message.push_str(&success_domains.join("\n"));
|
||||||
|
}
|
||||||
|
if !failed_domains.is_empty() {
|
||||||
|
dingtalk_message.push_str("\nfailed domains:\n");
|
||||||
|
dingtalk_message.push_str(&failed_domains.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 sent, or no configured notification token");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,96 +406,6 @@ fn check_cert_config(cert_config: &CertConfig) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn request_acme_certificate(acme_request: AcmeRequest) -> XResult<()> {
|
|
||||||
if let Some(local_public_ip) = acme_request.local_public_ip {
|
|
||||||
let mut all_domains = vec![acme_request.primary_name.to_string()];
|
|
||||||
for alt_name in acme_request.alt_names {
|
|
||||||
all_domains.push(alt_name.to_string());
|
|
||||||
}
|
|
||||||
information!("Checking domain dns records, domains: {:?}", all_domains);
|
|
||||||
let resolver = opt_result!(get_resolver(), "Get resolver failed: {}");
|
|
||||||
|
|
||||||
for domain in &all_domains {
|
|
||||||
debugging!("Checking domain: {}", domain);
|
|
||||||
let ipv4 = opt_result!(resolve_first_ipv4(&resolver, domain), "{}");
|
|
||||||
match ipv4 {
|
|
||||||
None => return simple_error!("Resolve domain ip failed: {}", domain),
|
|
||||||
Some(ipv4) => if local_public_ip != &ipv4 {
|
|
||||||
return simple_error!("Check domain ip: {}, mis-match, local: {} vs domain: {}", domain, local_public_ip, ipv4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
information!("Acme mode: {:?}", acme_request.mode);
|
|
||||||
let url = acme_request.mode.directory_url();
|
|
||||||
let persist = FilePersist::new(acme_request.account_dir);
|
|
||||||
let dir = opt_result!(Directory::from_url(persist, url), "Create directory from url failed: {}");
|
|
||||||
let acc = opt_result!(dir.account(acme_request.contract_email), "Directory set account failed: {}");
|
|
||||||
let mut ord_new = opt_result!( acc.new_order(acme_request.primary_name, acme_request.alt_names), "Create order failed: {}");
|
|
||||||
|
|
||||||
let mut order_csr_index = 0;
|
|
||||||
let ord_csr = loop {
|
|
||||||
if let Some(ord_csr) = ord_new.confirm_validations() {
|
|
||||||
debugging!("Valid acme certificate http challenge success");
|
|
||||||
break ord_csr;
|
|
||||||
}
|
|
||||||
|
|
||||||
information!("Loop for acme challenge auth, #{}", order_csr_index);
|
|
||||||
order_csr_index += 1;
|
|
||||||
|
|
||||||
debugging!("Start acme certificate http challenge");
|
|
||||||
let auths = opt_result!(ord_new.authorizations(), "Order auth failed: {}");
|
|
||||||
for auth in &auths {
|
|
||||||
let chall = auth.http_challenge();
|
|
||||||
let token = chall.http_token();
|
|
||||||
let proof = chall.http_proof();
|
|
||||||
|
|
||||||
{
|
|
||||||
information!("Add acme http challenge: {} -> {}",token, proof);
|
|
||||||
TOKEN_MAP.write().unwrap().insert(token.to_string(), proof);
|
|
||||||
}
|
|
||||||
debugging!("Valid acme certificate http challenge");
|
|
||||||
opt_result!(chall.validate(acme_request.timeout), "Validate http challenge failed: {}");
|
|
||||||
}
|
|
||||||
|
|
||||||
debugging!("Refresh acme certificate order");
|
|
||||||
opt_result!(ord_new.refresh(), "Refresh order failed: {}");
|
|
||||||
};
|
|
||||||
|
|
||||||
information!("Generate private key, key type: {:?}", acme_request.algo);
|
|
||||||
let pkey_pri = match acme_request.algo {
|
|
||||||
X509PublicKeyAlgo::EcKey(X509EcPublicKeyAlgo::Secp256r1) => create_p256_key(),
|
|
||||||
X509PublicKeyAlgo::EcKey(X509EcPublicKeyAlgo::Secp384r1) => create_p384_key(),
|
|
||||||
X509PublicKeyAlgo::EcKey(X509EcPublicKeyAlgo::Secp521r1) => return simple_error!("Algo ec521 is not supported"),
|
|
||||||
X509PublicKeyAlgo::Rsa(bits) => create_rsa_key(bits),
|
|
||||||
};
|
|
||||||
|
|
||||||
debugging!("Invoking csr finalize pkey");
|
|
||||||
let ord_cert = opt_result!( ord_csr.finalize_pkey(pkey_pri, acme_request.timeout), "Submit CSR failed: {}");
|
|
||||||
debugging!("Downloading and save cert");
|
|
||||||
let cert = opt_result!( ord_cert.download_and_save_cert(), "Download and save certificate failed: {}");
|
|
||||||
|
|
||||||
if let (Some(cert_file), Some(key_file)) = (&acme_request.cert_file, &acme_request.key_file) {
|
|
||||||
debugging!("Certificate key: {}", cert.private_key());
|
|
||||||
debugging!("Certificate pem: {}", cert.certificate());
|
|
||||||
information!("Write file: {}", cert_file);
|
|
||||||
if let Err(e) = fs::write(cert_file, cert.certificate()) {
|
|
||||||
failure!("Write file: {}, failed: {}", cert_file, e);
|
|
||||||
}
|
|
||||||
information!("Write file: {}", key_file);
|
|
||||||
if let Err(e) = fs::write(key_file, cert.private_key()) {
|
|
||||||
failure!("Write file: {}, failed: {}", key_file, e);
|
|
||||||
}
|
|
||||||
success!("Write files success: {} and {}", cert_file, key_file);
|
|
||||||
} else {
|
|
||||||
information!("Certificate key: {}", cert.private_key());
|
|
||||||
information!("Certificate pem: {}", cert.certificate());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn startup_http_server(s: Sender<i32>, port: u16) {
|
fn startup_http_server(s: Sender<i32>, port: u16) {
|
||||||
task::spawn(async move {
|
task::spawn(async move {
|
||||||
information!("Listen at 0.0.0.0:{}", port);
|
information!("Listen at 0.0.0.0:{}", port);
|
||||||
@@ -406,7 +423,7 @@ fn startup_http_server(s: Sender<i32>, port: u16) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let peer = req.peer_addr().unwrap_or("none");
|
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 {
|
match auth_token {
|
||||||
Some(auth_token) => {
|
Some(auth_token) => {
|
||||||
information!("Request acme challenge: {} -> {}, peer: {:?}", token, auth_token, peer);
|
information!("Request acme challenge: {} -> {}, peer: {:?}", token, auth_token, peer);
|
||||||
|
|||||||
@@ -12,8 +12,51 @@ pub struct PublicIpResponse {
|
|||||||
pub user_agent: Option<String>,
|
pub user_agent: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_local_public_ip() -> XResult<String> {
|
pub fn get_local_public_ip(skip_verify_certificate: bool) -> XResult<String> {
|
||||||
let response = opt_result!(reqwest::blocking::get("https://hatter.ink/ip/ip.jsonp"), "Get local public ip failed: {}");
|
let mut client_builder = reqwest::blocking::Client::builder();
|
||||||
|
if skip_verify_certificate {
|
||||||
|
warning!("Skip verify certificate is turned on");
|
||||||
|
client_builder = client_builder.danger_accept_invalid_certs(true);
|
||||||
|
}
|
||||||
|
let root_certificate_isrg_x_pem = r#"-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFYDCCBEigAwIBAgIQQAF3ITfU6UK47naqPGQKtzANBgkqhkiG9w0BAQsFADA/
|
||||||
|
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
|
||||||
|
DkRTVCBSb290IENBIFgzMB4XDTIxMDEyMDE5MTQwM1oXDTI0MDkzMDE4MTQwM1ow
|
||||||
|
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
||||||
|
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwggIiMA0GCSqGSIb3DQEB
|
||||||
|
AQUAA4ICDwAwggIKAoICAQCt6CRz9BQ385ueK1coHIe+3LffOJCMbjzmV6B493XC
|
||||||
|
ov71am72AE8o295ohmxEk7axY/0UEmu/H9LqMZshftEzPLpI9d1537O4/xLxIZpL
|
||||||
|
wYqGcWlKZmZsj348cL+tKSIG8+TA5oCu4kuPt5l+lAOf00eXfJlII1PoOK5PCm+D
|
||||||
|
LtFJV4yAdLbaL9A4jXsDcCEbdfIwPPqPrt3aY6vrFk/CjhFLfs8L6P+1dy70sntK
|
||||||
|
4EwSJQxwjQMpoOFTJOwT2e4ZvxCzSow/iaNhUd6shweU9GNx7C7ib1uYgeGJXDR5
|
||||||
|
bHbvO5BieebbpJovJsXQEOEO3tkQjhb7t/eo98flAgeYjzYIlefiN5YNNnWe+w5y
|
||||||
|
sR2bvAP5SQXYgd0FtCrWQemsAXaVCg/Y39W9Eh81LygXbNKYwagJZHduRze6zqxZ
|
||||||
|
Xmidf3LWicUGQSk+WT7dJvUkyRGnWqNMQB9GoZm1pzpRboY7nn1ypxIFeFntPlF4
|
||||||
|
FQsDj43QLwWyPntKHEtzBRL8xurgUBN8Q5N0s8p0544fAQjQMNRbcTa0B7rBMDBc
|
||||||
|
SLeCO5imfWCKoqMpgsy6vYMEG6KDA0Gh1gXxG8K28Kh8hjtGqEgqiNx2mna/H2ql
|
||||||
|
PRmP6zjzZN7IKw0KKP/32+IVQtQi0Cdd4Xn+GOdwiK1O5tmLOsbdJ1Fu/7xk9TND
|
||||||
|
TwIDAQABo4IBRjCCAUIwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw
|
||||||
|
SwYIKwYBBQUHAQEEPzA9MDsGCCsGAQUFBzAChi9odHRwOi8vYXBwcy5pZGVudHJ1
|
||||||
|
c3QuY29tL3Jvb3RzL2RzdHJvb3RjYXgzLnA3YzAfBgNVHSMEGDAWgBTEp7Gkeyxx
|
||||||
|
+tvhS5B1/8QVYIWJEDBUBgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEEAYLfEwEB
|
||||||
|
ATAwMC4GCCsGAQUFBwIBFiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2VuY3J5cHQu
|
||||||
|
b3JnMDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6Ly9jcmwuaWRlbnRydXN0LmNvbS9E
|
||||||
|
U1RST09UQ0FYM0NSTC5jcmwwHQYDVR0OBBYEFHm0WeZ7tuXkAXOACIjIGlj26Ztu
|
||||||
|
MA0GCSqGSIb3DQEBCwUAA4IBAQAKcwBslm7/DlLQrt2M51oGrS+o44+/yQoDFVDC
|
||||||
|
5WxCu2+b9LRPwkSICHXM6webFGJueN7sJ7o5XPWioW5WlHAQU7G75K/QosMrAdSW
|
||||||
|
9MUgNTP52GE24HGNtLi1qoJFlcDyqSMo59ahy2cI2qBDLKobkx/J3vWraV0T9VuG
|
||||||
|
WCLKTVXkcGdtwlfFRjlBz4pYg1htmf5X6DYO8A4jqv2Il9DjXA6USbW1FzXSLr9O
|
||||||
|
he8Y4IWS6wY7bCkjCWDcRQJMEhg76fsO3txE+FiYruq9RUWhiF1myv4Q6W+CyBFC
|
||||||
|
Dfvp7OOGAN6dEOM4+qR9sdjoSYKEBpsr6GtPAQw4dy753ec5
|
||||||
|
-----END CERTIFICATE-----"#;
|
||||||
|
match reqwest::Certificate::from_pem(root_certificate_isrg_x_pem.as_bytes()) {
|
||||||
|
Err(err) => { warning!("Add ISRG X1 root failed: {}", err); },
|
||||||
|
Ok(certificate) => {
|
||||||
|
client_builder = client_builder.add_root_certificate(certificate);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
let client = opt_result!(client_builder.build(), "Build http client failed: {}");
|
||||||
|
let response = opt_result!(client.get("https://hatter.ink/ip/ip.jsonp").send(), "Get local public ip failed: {}");
|
||||||
let response_text = opt_result!(response.text(), "Get local public ip failed: {}");
|
let response_text = opt_result!(response.text(), "Get local public ip failed: {}");
|
||||||
debugging!("Get local public ip response: {}", response_text);
|
debugging!("Get local public ip response: {}", response_text);
|
||||||
let response_json: PublicIpResponse = opt_result!(deser_hjson::from_str(&response_text), "Parse get public ip response failed: {}");
|
let response_json: PublicIpResponse = opt_result!(deser_hjson::from_str(&response_text), "Parse get public ip response failed: {}");
|
||||||
@@ -32,6 +75,6 @@ pub fn resolve_first_ipv4(resolver: &Resolver, domain: &str) -> XResult<Option<S
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test() {
|
fn test() {
|
||||||
println!("{:?}", resolve_first_ipv4(&get_resolver().unwrap(),"hatter.ink"));
|
println!("{:?}", resolve_first_ipv4(&get_resolver().unwrap(), "hatter.ink"));
|
||||||
println!("{:?}", get_local_public_ip());
|
println!("{:?}", get_local_public_ip(false));
|
||||||
}
|
}
|
||||||
|
|||||||
106
src/notification.rs
Normal file
106
src/notification.rs
Normal 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("×tamp=");
|
||||||
|
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())
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
use rust_util::XResult;
|
use std::thread::{JoinHandle, spawn, sleep};
|
||||||
use std::thread;
|
|
||||||
use std::thread::JoinHandle;
|
|
||||||
use std::sync::atomic::{AtomicU32, Ordering};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use rust_util::XResult;
|
||||||
|
|
||||||
pub struct SimpleThreadPool {
|
pub struct SimpleThreadPool {
|
||||||
max_pool_size: u32,
|
max_pool_size: u32,
|
||||||
@@ -28,7 +27,7 @@ impl SimpleThreadPool {
|
|||||||
let running = self.running_pool_size.fetch_add(1, Ordering::SeqCst);
|
let running = self.running_pool_size.fetch_add(1, Ordering::SeqCst);
|
||||||
let running_pool_size_clone = self.running_pool_size.clone();
|
let running_pool_size_clone = self.running_pool_size.clone();
|
||||||
if running < self.max_pool_size {
|
if running < self.max_pool_size {
|
||||||
Some(thread::spawn(move || {
|
Some(spawn(move || {
|
||||||
f();
|
f();
|
||||||
running_pool_size_clone.fetch_sub(1, Ordering::SeqCst);
|
running_pool_size_clone.fetch_sub(1, Ordering::SeqCst);
|
||||||
}))
|
}))
|
||||||
@@ -47,7 +46,7 @@ fn test_simple_thread_pool() {
|
|||||||
for i in 1..10 {
|
for i in 1..10 {
|
||||||
if let Some(h) = stp.submit(move || {
|
if let Some(h) = stp.submit(move || {
|
||||||
println!("Task start: {}", i);
|
println!("Task start: {}", i);
|
||||||
thread::sleep(Duration::from_secs(1));
|
sleep(Duration::from_secs(1));
|
||||||
println!("Task end: {}", i);
|
println!("Task end: {}", i);
|
||||||
}) {
|
}) {
|
||||||
handlers.push(h);
|
handlers.push(h);
|
||||||
|
|||||||
94
src/statistics.rs
Normal file
94
src/statistics.rs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
use std::time::SystemTime;
|
||||||
|
use std::fmt::{Display, Formatter, Result};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AcmeStatistics {
|
||||||
|
pub started: SystemTime,
|
||||||
|
pub ended: Option<SystemTime>,
|
||||||
|
pub items: Vec<AcmeItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum AcmeStatus {
|
||||||
|
Success,
|
||||||
|
// Skipped,
|
||||||
|
Fail(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for AcmeStatus {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||||
|
match self {
|
||||||
|
AcmeStatus::Success => write!(f, "Success"),
|
||||||
|
AcmeStatus::Fail(message) => write!(f, "Failed: {}", message),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AcmeItem {
|
||||||
|
pub domains: Vec<String>,
|
||||||
|
pub status: AcmeStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for AcmeItem {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||||
|
write!(f, "Domains: {}; {}", self.domains.join(", "), &self.status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
sb.push_str(&format!("Started: {}", &started_time));
|
||||||
|
if let Some(ended) = &self.ended {
|
||||||
|
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 {
|
||||||
|
sb.push_str(&format!(", cost: {}", simpledateformat::format_human(cost)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut success_count: i32 = 0;
|
||||||
|
let mut failed_count: i32 = 0;
|
||||||
|
for item in &self.items {
|
||||||
|
match &item.status {
|
||||||
|
AcmeStatus::Success => {
|
||||||
|
success_count += 1;
|
||||||
|
}
|
||||||
|
AcmeStatus::Fail(_) => {
|
||||||
|
failed_count += 1;
|
||||||
|
sb.push_str(&format!("\n - {}", &item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.push_str(&format!("\nTotal count: {}, success count: {}, failed count: {}",
|
||||||
|
success_count + failed_count,
|
||||||
|
success_count,
|
||||||
|
failed_count,
|
||||||
|
));
|
||||||
|
write!(f, "{}", &sb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AcmeStatistics {
|
||||||
|
pub fn start() -> AcmeStatistics {
|
||||||
|
AcmeStatistics {
|
||||||
|
started: SystemTime::now(),
|
||||||
|
ended: None,
|
||||||
|
items: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn end(&mut self) {
|
||||||
|
self.ended = Some(SystemTime::now());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_item(&mut self, domains: Vec<String>, status: AcmeStatus) {
|
||||||
|
self.items.push(AcmeItem {
|
||||||
|
domains,
|
||||||
|
status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/util.rs
Normal file
37
src/util.rs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
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()
|
||||||
|
} else {
|
||||||
|
record.to_ascii_lowercase()
|
||||||
|
};
|
||||||
|
|
||||||
|
let parts: Vec<&str> = r.split(".").collect();
|
||||||
|
if parts.len() < 2 {
|
||||||
|
return simple_error!("Invalid record : {}", record);
|
||||||
|
}
|
||||||
|
|
||||||
|
let last_part = parts[parts.len() - 1];
|
||||||
|
let last_part_2 = parts[parts.len() - 2];
|
||||||
|
|
||||||
|
// SHOULD read from: https://publicsuffix.org/
|
||||||
|
let domain_parts_len = match last_part {
|
||||||
|
"cn" => match last_part_2 {
|
||||||
|
"com" | "net" | "org" | "gov" | "edu" => 3,
|
||||||
|
_ => 2,
|
||||||
|
},
|
||||||
|
_ => 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
if parts.len() < domain_parts_len {
|
||||||
|
return simple_error!("Invalid record: {}", record);
|
||||||
|
}
|
||||||
|
|
||||||
|
let domain = parts.iter().skip(parts.len() - domain_parts_len).map(|s| s.to_string()).collect::<Vec<String>>().join(".");
|
||||||
|
let rr = parts.iter().take(parts.len() - domain_parts_len).map(|s| s.to_string()).collect::<Vec<String>>().join(".");
|
||||||
|
|
||||||
|
Ok((if rr.is_empty() { "@".to_string() } else { rr }, domain))
|
||||||
|
}
|
||||||
33
src/x509.rs
33
src/x509.rs
@@ -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::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! {
|
lazy_static! {
|
||||||
static ref OID_COMMON_NAME: Oid<'static> = Oid::from_str("2.5.4.3").unwrap();
|
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_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_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_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();
|
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 {
|
pub enum X509IssuerAlgo {
|
||||||
RsaWithSha256,
|
RsaWithSha256,
|
||||||
EcdsaWithSha256,
|
EcdsaWithSha256,
|
||||||
|
EcdsaWithSha384,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
@@ -48,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,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 {
|
||||||
@@ -145,6 +146,8 @@ pub fn parse_x509(pem_id: &str, pem: &str) -> XResult<X509Certificate> {
|
|||||||
X509IssuerAlgo::RsaWithSha256
|
X509IssuerAlgo::RsaWithSha256
|
||||||
} else if cert_algorithm_oid == &*OID_ECDSA_WITH_SHA256 {
|
} else if cert_algorithm_oid == &*OID_ECDSA_WITH_SHA256 {
|
||||||
X509IssuerAlgo::EcdsaWithSha256
|
X509IssuerAlgo::EcdsaWithSha256
|
||||||
|
} else if cert_algorithm_oid == &*OID_ECDSA_WITH_SHA384 {
|
||||||
|
X509IssuerAlgo::EcdsaWithSha384
|
||||||
} else {
|
} else {
|
||||||
return simple_error!("Parse pem: {}, unknown x509 algorithm oid: {:?}", pem_id, cert_algorithm_oid);
|
return simple_error!("Parse pem: {}, unknown x509 algorithm oid: {:?}", pem_id, cert_algorithm_oid);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user