dns challenge for aliyun works
This commit is contained in:
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/
|
||||||
|
|||||||
154
src/ali_dns.rs
154
src/ali_dns.rs
@@ -1,11 +1,40 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use aliyun_openapi_core_rust_sdk::RPClient;
|
use aliyun_openapi_core_rust_sdk::RPClient;
|
||||||
use rust_util::XResult;
|
use rust_util::XResult;
|
||||||
|
|
||||||
static ALI_DNS_ENDPOINT: &str = "https://alidns.aliyuncs.com";
|
static ALI_DNS_ENDPOINT: &str = "https://alidns.aliyuncs.com";
|
||||||
static ALI_DNS_API_VERSION: &str = "2015-01-09";
|
static ALI_DNS_API_VERSION: &str = "2015-01-09";
|
||||||
|
|
||||||
|
#[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)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct ListDnsResponseErrorResponse {
|
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")]
|
#[serde(rename = "RequestId")]
|
||||||
pub request_id: String,
|
pub request_id: String,
|
||||||
#[serde(rename = "Message")]
|
#[serde(rename = "Message")]
|
||||||
@@ -29,7 +58,13 @@ pub struct ListDnsResponse {
|
|||||||
#[serde(rename = "PageNumber")]
|
#[serde(rename = "PageNumber")]
|
||||||
pub page_number: i32,
|
pub page_number: i32,
|
||||||
#[serde(rename = "DomainRecords")]
|
#[serde(rename = "DomainRecords")]
|
||||||
pub domain_records: Vec<DnsRecord>,
|
pub domain_records: DnsRecords,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct DnsRecords {
|
||||||
|
#[serde(rename = "Record")]
|
||||||
|
pub record: Vec<DnsRecord>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
@@ -53,102 +88,51 @@ pub struct DnsRecord {
|
|||||||
#[serde(rename = "TTL")]
|
#[serde(rename = "TTL")]
|
||||||
pub ttl: i32,
|
pub ttl: i32,
|
||||||
#[serde(rename = "Weight")]
|
#[serde(rename = "Weight")]
|
||||||
pub weight: i32,
|
pub weight: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Response {
|
pub fn list_dns(client: &RPClient, domain: &str) -> XResult<Result<ListDnsResponse, CommonErrorResponse>> {
|
||||||
#[serde(rename = "RequestId")]
|
let describe_domain_records_response = opt_result!(client.get("DescribeDomainRecords")
|
||||||
pub request_id: String,
|
|
||||||
#[serde(rename = "RecordId")]
|
|
||||||
pub record_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct AccessCredential {
|
|
||||||
access_key_id: String,
|
|
||||||
access_key_secret: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test() {
|
|
||||||
let a = AccessCredential {
|
|
||||||
access_key_id: "***".to_string(),
|
|
||||||
access_key_secret: "***".to_string(),
|
|
||||||
};
|
|
||||||
let client = create_dns_client(&a);
|
|
||||||
println!("{}", list_dns(&client, "webauthn1.host").unwrap());
|
|
||||||
// println!("{}", add_dns_txt(&client, "webauthn.host").unwrap());
|
|
||||||
// println!("{}", delete_dns(&client, "744459160027659264").unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
// {
|
|
||||||
// "TotalCount": 2,
|
|
||||||
// "RequestId": "8993B447-F1FF-58ED-9D4B-1027B551DC5E",
|
|
||||||
// "PageSize": 20,
|
|
||||||
// "DomainRecords": {
|
|
||||||
// "Record": [
|
|
||||||
// {
|
|
||||||
// "RR": "www",
|
|
||||||
// "Line": "default",
|
|
||||||
// "Status": "ENABLE",
|
|
||||||
// "Locked": false,
|
|
||||||
// "Type": "A",
|
|
||||||
// "DomainName": "webauthn.host",
|
|
||||||
// "Value": "47.52.7.223",
|
|
||||||
// "RecordId": "714019124998091776",
|
|
||||||
// "TTL": 1800,
|
|
||||||
// "Weight": 1
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// "RR": "@",
|
|
||||||
// "Line": "default",
|
|
||||||
// "Status": "ENABLE",
|
|
||||||
// "Locked": false,
|
|
||||||
// "Type": "A",
|
|
||||||
// "DomainName": "webauthn.host",
|
|
||||||
// "Value": "47.52.7.223",
|
|
||||||
// "RecordId": "714019101941960704",
|
|
||||||
// "TTL": 1800,
|
|
||||||
// "Weight": 1
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
// },
|
|
||||||
// "PageNumber": 1
|
|
||||||
// }
|
|
||||||
pub fn list_dns(client: &RPClient, domain: &str) -> XResult<String> {
|
|
||||||
Ok(client.get("DescribeDomainRecords")
|
|
||||||
.query(&[
|
.query(&[
|
||||||
("RegionId", "cn-hangzhou"),
|
("RegionId", "cn-hangzhou"),
|
||||||
("DomainName", domain)
|
("DomainName", domain)
|
||||||
])
|
])
|
||||||
.send()?)
|
.send(), "List domain records: {}, failed: {}", domain);
|
||||||
|
parse_result("DescribeDomainRecords", &describe_domain_records_response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// {"RequestId":"AD997158-68D2-5084-B6B9-5F5A0893DDC1","RecordId":"744459160027659264"}
|
pub fn delete_dns_record(client: &RPClient, record_id: &str) -> XResult<Result<CommonSuccessResponse, CommonErrorResponse>> {
|
||||||
pub fn delete_dns(client: &RPClient, record_id: &str) -> XResult<String> {
|
let delete_domain_record_response = opt_result!(client.get("DeleteDomainRecord")
|
||||||
Ok(client.get("DeleteDomainRecord")
|
|
||||||
.query(&[
|
.query(&[
|
||||||
("RegionId", "cn-hangzhou"),
|
("RegionId", "cn-hangzhou"),
|
||||||
("RecordId", record_id)
|
("RecordId", record_id)
|
||||||
])
|
])
|
||||||
.send()?)
|
.send(), "Delete domain record id: {}, failed: {}", record_id);
|
||||||
|
parse_result("DeleteDomainRecord", &delete_domain_record_response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// {"RequestId":"F3D54AB2-7058-54FD-AAF3-566FB8EC9BD1","RecordId":"744459160027659264"}
|
pub fn add_txt_dns_record(client: &RPClient, domain: &str, rr: &str, value: &str) -> XResult<Result<CommonSuccessResponse, CommonErrorResponse>> {
|
||||||
pub fn add_dns_txt(client: &RPClient, domain: &str) -> XResult<String> {
|
add_dns_record(client, domain, rr, "TXT", value)
|
||||||
Ok(client.get("AddDomainRecord")
|
}
|
||||||
|
|
||||||
|
// 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(&[
|
.query(&[
|
||||||
("RegionId", "cn-hangzhou"),
|
("RegionId", "cn-hangzhou"),
|
||||||
("DomainName", domain),
|
("DomainName", domain),
|
||||||
("RR", "_acme-challenge_test"),
|
("RR", rr),
|
||||||
("Type", "TXT"),
|
("Type", t),
|
||||||
("Value", "test")
|
("Value", value)
|
||||||
])
|
])
|
||||||
.send()?)
|
.send(), "Add domain record: {}.{} -> {} {} ,failed: {}", rr, domain, t, value);
|
||||||
|
parse_result("AddDomainRecord", &add_domain_record_response)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_dns_client(access_credential: &AccessCredential) -> RPClient {
|
pub fn build_dns_client(access_credential: &AccessCredential) -> RPClient {
|
||||||
RPClient::new(
|
RPClient::new(
|
||||||
access_credential.access_key_id.clone(),
|
access_credential.access_key_id.clone(),
|
||||||
access_credential.access_key_secret.clone(),
|
access_credential.access_key_secret.clone(),
|
||||||
@@ -156,3 +140,17 @@ fn create_dns_client(access_credential: &AccessCredential) -> RPClient {
|
|||||||
String::from(ALI_DNS_API_VERSION),
|
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,3 +1,4 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use rust_util::XResult;
|
use rust_util::XResult;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@@ -11,6 +12,18 @@ use std::time::SystemTime;
|
|||||||
pub const CERT_NAME: &str = "cert.pem";
|
pub const CERT_NAME: &str = "cert.pem";
|
||||||
pub const KEY_NAME: &str = "key.pem";
|
pub const KEY_NAME: &str = "key.pem";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum AcmeChallenge {
|
||||||
|
Http,
|
||||||
|
Dns,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AcmeChallenge {
|
||||||
|
fn default() -> Self {
|
||||||
|
AcmeChallenge::Http
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum AcmeMode {
|
pub enum AcmeMode {
|
||||||
Prod,
|
Prod,
|
||||||
@@ -43,6 +56,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>,
|
||||||
@@ -54,6 +70,7 @@ pub struct CertConfigItem {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CertConfig {
|
pub struct CertConfig {
|
||||||
pub port: Option<u16>,
|
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 trigger_after_update: Option<Vec<String>>,
|
||||||
pub notify_token: Option<String>,
|
pub notify_token: Option<String>,
|
||||||
@@ -102,6 +119,7 @@ impl CertConfig {
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
port: self.port,
|
port: self.port,
|
||||||
|
credential_suppliers: self.credential_suppliers,
|
||||||
cert_items: filtered_cert_items,
|
cert_items: filtered_cert_items,
|
||||||
trigger_after_update: self.trigger_after_update,
|
trigger_after_update: self.trigger_after_update,
|
||||||
notify_token: self.notify_token,
|
notify_token: self.notify_token,
|
||||||
@@ -116,6 +134,15 @@ impl CertConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CertConfigItem {
|
impl CertConfigItem {
|
||||||
|
pub fn get_acme_challenge(&self) -> AcmeChallenge {
|
||||||
|
let t = self.r#type.as_ref().map(|t| t.to_ascii_lowercase()).unwrap_or_else(|| "http".to_string());
|
||||||
|
if t == "dns" {
|
||||||
|
AcmeChallenge::Dns
|
||||||
|
} else {
|
||||||
|
AcmeChallenge::Http
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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");
|
||||||
|
|||||||
130
src/main.rs
130
src/main.rs
@@ -3,6 +3,7 @@ extern crate lazy_static;
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate rust_util;
|
extern crate rust_util;
|
||||||
|
|
||||||
|
mod util;
|
||||||
mod config;
|
mod config;
|
||||||
mod x509;
|
mod x509;
|
||||||
mod network;
|
mod network;
|
||||||
@@ -28,14 +29,16 @@ use async_std::task;
|
|||||||
use async_std::channel;
|
use async_std::channel;
|
||||||
use async_std::channel::Sender;
|
use async_std::channel::Sender;
|
||||||
use config::AcmeMode;
|
use config::AcmeMode;
|
||||||
use crate::config::{CertConfig, CERT_NAME, KEY_NAME};
|
use crate::config::{AcmeChallenge, CertConfig, CERT_NAME, KEY_NAME};
|
||||||
use crate::x509::{X509PublicKeyAlgo, X509EcPublicKeyAlgo};
|
use crate::x509::{X509PublicKeyAlgo, X509EcPublicKeyAlgo};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use aliyun_openapi_core_rust_sdk::RPClient;
|
||||||
use rust_util::util_cmd::run_command_and_wait;
|
use rust_util::util_cmd::run_command_and_wait;
|
||||||
use crate::AcmeChallenge::Http;
|
use crate::ali_dns::{add_txt_dns_record, build_dns_client, delete_dns_record, list_dns, simple_parse_aliyun_supplier};
|
||||||
use crate::dingtalk::send_dingtalk_message;
|
use crate::dingtalk::send_dingtalk_message;
|
||||||
use crate::network::{get_local_public_ip, get_resolver, resolve_first_ipv4};
|
use crate::network::{get_local_public_ip, get_resolver, resolve_first_ipv4};
|
||||||
use crate::statics::{AcmeStatics, AcmeStatus};
|
use crate::statics::{AcmeStatics, AcmeStatus};
|
||||||
|
use crate::util::parse_dns_record;
|
||||||
|
|
||||||
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");
|
||||||
@@ -46,21 +49,12 @@ lazy_static! {
|
|||||||
static ref TOKEN_MAP: RwLock<BTreeMap<String, String>> = RwLock::new(BTreeMap::new());
|
static ref TOKEN_MAP: RwLock<BTreeMap<String, String>> = RwLock::new(BTreeMap::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
enum AcmeChallenge {
|
|
||||||
Http,
|
|
||||||
Dns,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for AcmeChallenge {
|
|
||||||
fn default() -> Self {
|
|
||||||
Http
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
struct AcmeRequest<'a> {
|
struct AcmeRequest<'a> {
|
||||||
challenge: AcmeChallenge,
|
challenge: AcmeChallenge,
|
||||||
|
// issue, single acme request can only process one supplier
|
||||||
|
credential_supplier: Option<&'a str>,
|
||||||
|
allow_interact: bool,
|
||||||
contract_email: &'a str,
|
contract_email: &'a str,
|
||||||
primary_name: &'a str,
|
primary_name: &'a str,
|
||||||
alt_names: &'a [&'a str],
|
alt_names: &'a [&'a str],
|
||||||
@@ -97,6 +91,7 @@ async fn main() -> tide::Result<()> {
|
|||||||
.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").short("k").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-verify-certificate").short("K").long("skip-verify-certificate").help("Skip verify certificate"))
|
||||||
|
.arg(Arg::with_name("allow-interact").long("allow-interact").help("Allow interact"))
|
||||||
.get_matches();
|
.get_matches();
|
||||||
|
|
||||||
if matches.is_present("verbose") {
|
if matches.is_present("verbose") {
|
||||||
@@ -227,6 +222,7 @@ async fn main() -> tide::Result<()> {
|
|||||||
task::sleep(Duration::from_millis(500)).await;
|
task::sleep(Duration::from_millis(500)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
@@ -253,7 +249,9 @@ async fn main() -> tide::Result<()> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let acme_request = AcmeRequest {
|
let acme_request = AcmeRequest {
|
||||||
challenge: Http,
|
challenge: AcmeChallenge::Http,
|
||||||
|
credential_supplier: None,
|
||||||
|
allow_interact: false,
|
||||||
contract_email: &email,
|
contract_email: &email,
|
||||||
primary_name,
|
primary_name,
|
||||||
alt_names: &alt_names,
|
alt_names: &alt_names,
|
||||||
@@ -267,7 +265,7 @@ async fn main() -> tide::Result<()> {
|
|||||||
outputs_file: matches.value_of("outputs").map(|s| s.into()),
|
outputs_file: matches.value_of("outputs").map(|s| s.into()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
if let Err(e) = request_acme_certificate(acme_request) {
|
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);
|
||||||
}
|
}
|
||||||
@@ -282,14 +280,35 @@ async fn main() -> tide::Result<()> {
|
|||||||
for item in &filtered_cert_config.cert_items {
|
for item in &filtered_cert_config.cert_items {
|
||||||
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()
|
||||||
|
.map(|m| m.get(supplier)).flatten();
|
||||||
|
match credential_supplier {
|
||||||
|
None => {
|
||||||
|
warning!("DNS challenge no credential supplier found");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Some(credential_supplier) => Some(credential_supplier.as_str()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { None };
|
||||||
let acme_request = AcmeRequest {
|
let acme_request = AcmeRequest {
|
||||||
challenge: Http,
|
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,
|
||||||
@@ -304,7 +323,7 @@ async fn main() -> tide::Result<()> {
|
|||||||
};
|
};
|
||||||
let mut domains = vec![common_name.clone()];
|
let mut domains = vec![common_name.clone()];
|
||||||
dns_names.iter().for_each(|dns_name| domains.push(dns_name.clone()));
|
dns_names.iter().for_each(|dns_name| domains.push(dns_name.clone()));
|
||||||
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: {}", item.path, e);
|
failure!("Request certificate: {}, by acme failed: {}", item.path, e);
|
||||||
acme_statics.add_item(domains, AcmeStatus::Fail(format!("{}", e)));
|
acme_statics.add_item(domains, AcmeStatus::Fail(format!("{}", e)));
|
||||||
} else {
|
} else {
|
||||||
@@ -411,7 +430,7 @@ fn check_cert_config(cert_config: &CertConfig) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn request_acme_certificate(acme_request: AcmeRequest) -> XResult<()> {
|
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 {
|
if let Some(local_public_ip) = acme_request.local_public_ip {
|
||||||
let mut all_domains = vec![acme_request.primary_name.to_string()];
|
let mut all_domains = vec![acme_request.primary_name.to_string()];
|
||||||
for alt_name in acme_request.alt_names {
|
for alt_name in acme_request.alt_names {
|
||||||
@@ -420,13 +439,15 @@ fn request_acme_certificate(acme_request: AcmeRequest) -> XResult<()> {
|
|||||||
information!("Checking domain dns records, domains: {:?}", all_domains);
|
information!("Checking domain dns records, domains: {:?}", all_domains);
|
||||||
let resolver = opt_result!(get_resolver(), "Get resolver failed: {}");
|
let resolver = opt_result!(get_resolver(), "Get resolver failed: {}");
|
||||||
|
|
||||||
for domain in &all_domains {
|
if acme_request.challenge == AcmeChallenge::Http {
|
||||||
debugging!("Checking domain: {}", domain);
|
for domain in &all_domains {
|
||||||
let ipv4 = opt_result!(resolve_first_ipv4(&resolver, domain), "{}");
|
debugging!("Checking domain: {}", domain);
|
||||||
match ipv4 {
|
let ipv4 = opt_result!(resolve_first_ipv4(&resolver, domain), "{}");
|
||||||
None => return simple_error!("Resolve domain ip failed: {}", domain),
|
match ipv4 {
|
||||||
Some(ipv4) => if local_public_ip != ipv4 {
|
None => return simple_error!("Resolve domain ip failed: {}", domain),
|
||||||
return simple_error!("Check domain ip: {}, mis-match, local: {} vs domain: {}", domain, local_public_ip, ipv4);
|
Some(ipv4) => if local_public_ip != ipv4 {
|
||||||
|
return simple_error!("Check domain ip: {}, mis-match, local: {} vs domain: {}", domain, local_public_ip, ipv4);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -438,6 +459,11 @@ fn request_acme_certificate(acme_request: AcmeRequest) -> XResult<()> {
|
|||||||
let dir = opt_result!(Directory::from_url(persist, url), "Create directory from url failed: {}");
|
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 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 ord_new = opt_result!( acc.new_order(acme_request.primary_name, acme_request.alt_names), "Create order failed: {}");
|
||||||
|
let ali_yun_client: Option<RPClient> = match acme_request.credential_supplier {
|
||||||
|
Some(credential_supplier) => Some(build_dns_client(
|
||||||
|
&opt_result!(simple_parse_aliyun_supplier(credential_supplier), "Parse credential supplier failed: {}"))),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
let mut order_csr_index = 0;
|
let mut order_csr_index = 0;
|
||||||
let ord_csr = loop {
|
let ord_csr = loop {
|
||||||
@@ -453,7 +479,7 @@ fn request_acme_certificate(acme_request: AcmeRequest) -> XResult<()> {
|
|||||||
let auths = opt_result!(ord_new.authorizations(), "Order auth failed: {}");
|
let auths = opt_result!(ord_new.authorizations(), "Order auth failed: {}");
|
||||||
for auth in &auths {
|
for auth in &auths {
|
||||||
match acme_request.challenge {
|
match acme_request.challenge {
|
||||||
Http => {
|
AcmeChallenge::Http => {
|
||||||
let chall = auth.http_challenge();
|
let chall = auth.http_challenge();
|
||||||
let token = chall.http_token();
|
let token = chall.http_token();
|
||||||
let proof = chall.http_proof();
|
let proof = chall.http_proof();
|
||||||
@@ -465,11 +491,53 @@ fn request_acme_certificate(acme_request: AcmeRequest) -> XResult<()> {
|
|||||||
debugging!("Valid acme certificate http challenge");
|
debugging!("Valid acme certificate http challenge");
|
||||||
opt_result!(chall.validate(acme_request.timeout), "Validate http challenge failed: {}");
|
opt_result!(chall.validate(acme_request.timeout), "Validate http challenge failed: {}");
|
||||||
}
|
}
|
||||||
Dns => {
|
AcmeChallenge::Dns => {
|
||||||
let chall = auth.dns_challenge();
|
let chall = auth.dns_challenge();
|
||||||
let record = format!("_acme-challenge.{}.", auth.domain_name());
|
let record = format!("_acme-challenge.{}.", auth.domain_name());
|
||||||
let proof = chall.dns_proof();
|
let proof = chall.dns_proof();
|
||||||
information!("Add acme dns challenge: {} -> {}",record, proof);
|
information!("Add acme dns challenge: {} -> {}", record, proof);
|
||||||
|
|
||||||
|
let rr_and_domain = opt_result!(parse_dns_record(&record), "Parse record to rr&domain failed: {}");
|
||||||
|
|
||||||
|
if !dns_cleaned_domains.contains(&rr_and_domain.1) {
|
||||||
|
information!("Clearing domain: {}", &rr_and_domain.1);
|
||||||
|
dns_cleaned_domains.push(rr_and_domain.1.clone());
|
||||||
|
ali_yun_client.as_ref().map(|client| {
|
||||||
|
match list_dns(client, &rr_and_domain.1) {
|
||||||
|
Err(e) => warning!("List dns for: {}, failed: {}", &rr_and_domain.1, e),
|
||||||
|
Ok(Err(e)) => warning!("List dns for: {}, failed: {:?}", &rr_and_domain.1, e),
|
||||||
|
Ok(Ok(s)) => {
|
||||||
|
for r in &s.domain_records.record {
|
||||||
|
let rr = &r.rr;
|
||||||
|
if rr == "_acme-challenge" || rr.starts_with("_acme-challenge.") {
|
||||||
|
match delete_dns_record(client, &r.record_id) {
|
||||||
|
Err(e) => warning!("Delete dns: {}.{}, failed: {}", r.rr, r.domain_name, e),
|
||||||
|
Ok(Err(e)) => warning!("Delete dns: {}.{}, failed: {:?}", r.rr, r.domain_name, e),
|
||||||
|
Ok(Ok(_)) => success!("Delete dns: {}.{}", r.rr, r.domain_name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
match &ali_yun_client {
|
||||||
|
Some(client) => {
|
||||||
|
let add_txt_dns_result = opt_result!(add_txt_dns_record(client, &rr_and_domain.1, &rr_and_domain.0, &proof), "Add DNS TXT record failed: {}");
|
||||||
|
match add_txt_dns_result {
|
||||||
|
Ok(s) => success!("Add dns txt record successes: {}", s.record_id),
|
||||||
|
Err(e) => return simple_error!("Add dns txt record failed: {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => if acme_request.allow_interact {
|
||||||
|
let mut line = String::new();
|
||||||
|
information!("You need to config dns manually, press enter to continue...");
|
||||||
|
let _ = std::io::stdin().read_line(&mut line).unwrap();
|
||||||
|
} else {
|
||||||
|
return simple_error!("Interact is not allowed, --allow-interact to allow interact");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
debugging!("Valid acme certificate dns challenge");
|
debugging!("Valid acme certificate dns challenge");
|
||||||
opt_result!(chall.validate(acme_request.timeout), "Validate dns challenge failed: {}");
|
opt_result!(chall.validate(acme_request.timeout), "Validate dns challenge failed: {}");
|
||||||
|
|||||||
35
src/util.rs
Normal file
35
src/util.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
use rust_util::XResult;
|
||||||
|
|
||||||
|
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" => 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))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user