Compare commits

...

52 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
d85ac9416e Merge pull request 'v1.1.0-dns-challenge' (#1) from v1.1.0-dns-challenge into master
Reviewed-on: #1
2022-02-03 16:58:40 +08:00
8d8b1c24f0 dns challenge for aliyun works 2022-02-03 16:55:13 +08:00
53e71c62d6 add responses 2022-02-03 01:18:30 +08:00
e52d9c02cb add ali dns 2022-02-03 01:00:14 +08:00
2bb2c80768 v1.1.0 add dns challenge support 2022-02-02 00:55:56 +08:00
d685636417 v1.0.3 2022-01-31 00:47:18 +08:00
82e6761c8d v1.0.2 2022-01-04 22:06:42 +08:00
b576b49b0e update readme.md 2022-01-02 17:17:23 +08:00
c601f31bca v1.0.1 2022-01-02 17:15:08 +08:00
af1ab9ad6c feat dingtalk and restart nginx 2022-01-02 10:35:47 +08:00
e25650099a feat dingtalk and restart nginx 2022-01-02 10:35:28 +08:00
716b442458 feat: add acme statics 2021-10-15 16:36:09 +08:00
4ccd306d40 build musl 2021-09-04 17:57:35 +08:00
0264badcbe chore: update dependencies 2021-09-04 17:47:42 +08:00
42d729264f update version to 0.6.0 2021-09-04 16:24:24 +08:00
ce2dd9fe67 feat: use local config 2021-09-04 14:47:32 +08:00
fbed7fb2ef chore: get port 2021-09-04 14:38:31 +08:00
60a732ef0c feat: add port for cert config 2021-09-04 01:06:09 +08:00
ee17791b57 feat: add arg --cert-dir 2021-09-04 00:41:13 +08:00
d094cf1e6e feat: update dependency 2021-08-19 00:42:22 +08:00
2a2be6402b chore: comment 2021-07-17 09:49:16 +08:00
5e9e021c50 chore: fix clippy 2021-07-17 09:47:12 +08:00
854b8fb5de chore: clippy 2021-07-16 01:03:24 +08:00
17 changed files with 3000 additions and 1016 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
certs/
test_cert_config.json
acme_dir/ acme_dir/
.idea/ .idea/
__temp_dir/ __temp_dir/

2722
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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,20 +13,24 @@ USAGE:
acme-client [FLAGS] [OPTIONS] acme-client [FLAGS] [OPTIONS]
FLAGS: FLAGS:
--allow-interact Allow interact
--check Check cert config --check Check cert config
-h, --help Prints help information -h, --help Prints help information
--hide-logo Hide logo --hide-logo Hide logo
--skip-verify-ip Skip verify public ip -K, --skip-verify-certificate Skip verify certificate
-k, --skip-verify-ip Skip verify public ip
-v, --verbose Verbose -v, --verbose Verbose
-V, --version Print version -V, --version Print version
OPTIONS: OPTIONS:
-a, --algo <algo> Pki algo [default: ec384] -a, --algo <algo> Pki algo [default: ec384]
--cert-dir <cert-dir> Certificate dir
-c, --config <config> Cert config -c, --config <config> Cert config
--dir <dir> Account key dir [default: acme_dir] --dir <dir> Account key dir [default: acme_dir]
-d, --domain <domain>... Domains -d, --domain <domain>... Domains
--email <email> Contract email --email <email> Contract email
-m, --mode <mode> Mode [default: prod] -m, --mode <mode> Mode [default: prod]
-o, --outputs <outputs> Outputs file
-p, --port <port> Http port [default: 80] -p, --port <port> Http port [default: 80]
--timeout <timeout> Timeout (ms) [default: 5000] --timeout <timeout> Timeout (ms) [default: 5000]
-t, --type <type> Type http or dns [default: http] -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

View File

@@ -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
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(())
}

200
src/ali_dns.rs Normal file
View 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),
}
}
}
}

View File

@@ -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
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,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) {
if item.get_acme_challenge() != AcmeChallenge::Dns {
warning!("Currently not support wide card domain name"); warning!("Currently not support wide card domain name");
continue; 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);

View File

@@ -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
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::{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
View 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
View 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))
}

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::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);
}; };