use super::{AcmeDnsTask, Error};
use async_trait::async_trait;
use base64::{Engine, engine::general_purpose::STANDARD};
use chrono::Utc;
use hmac::{Hmac, Mac};
use serde::Deserialize;
use sha1::Sha1;
use std::collections::BTreeMap;
use tokio::sync::Mutex;
use url::Url;
use url::form_urlencoded::byte_serialize;
type Result<T, E = Error> = std::result::Result<T, E>;
fn new_error(err: impl ToString) -> Error {
Error::Fail {
category: "ali".to_string(),
message: err.to_string(),
}
}
#[derive(Deserialize, Debug)]
struct AddRecordResponse {
#[serde(rename = "RecordId")]
record_id: String,
}
fn percent_encode(input: &str) -> String {
byte_serialize(input.as_bytes()).collect()
}
async fn ali_api_request(
endpoint: &str,
access_key_id: &str,
access_key_secret: &str,
params: &mut BTreeMap<&str, String>,
) -> Result<String> {
params.insert("Format", "JSON".to_string());
params.insert("Version", "2015-01-09".to_string());
params.insert("AccessKeyId", access_key_id.to_string());
params.insert("SignatureMethod", "HMAC-SHA1".to_string());
params.insert("SignatureVersion", "1.0".to_string());
params.insert(
"Timestamp",
Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string(),
);
params.insert("SignatureNonce", uuid::Uuid::new_v4().to_string());
let canonicalized_query_string = params
.iter()
.map(|(k, v)| format!("{}={}", percent_encode(k), percent_encode(v)))
.collect::<Vec<String>>()
.join("&");
let string_to_sign = format!(
"GET&{}&{}",
percent_encode("/"),
percent_encode(&canonicalized_query_string)
);
let signing_key = format!("{access_key_secret}&");
type HmacSha1 = Hmac<Sha1>;
let mut mac =
HmacSha1::new_from_slice(signing_key.as_bytes()).map_err(new_error)?;
mac.update(string_to_sign.as_bytes());
let signature = STANDARD.encode(mac.finalize().into_bytes());
let request_url = format!(
"{endpoint}?{canonicalized_query_string}&Signature={}",
percent_encode(&signature)
);
let client = reqwest::Client::new();
let response = client.get(&request_url).send().await.map_err(new_error)?;
if response.status().is_success() {
Ok(response.text().await.map_err(new_error)?)
} else {
let status = response.status();
let error_body = response.text().await.map_err(new_error)?;
Err(new_error(format!("API Error: {status} - {error_body}")))
}
}
async fn add_ali_dns_record(
endpoint: &str,
access_key_id: &str,
access_key_secret: &str,
domain: &str,
value: &str,
) -> Result<AddRecordResponse> {
let mut params = BTreeMap::new();
let (rr, domain_name) =
domain.split_once(".").ok_or(new_error("invalid domain"))?;
params.insert("Action", "AddDomainRecord".to_string());
params.insert("DomainName", domain_name.to_string());
params.insert("RR", rr.to_string());
params.insert("Type", "TXT".to_string());
params.insert("Value", value.to_string());
let response_body = ali_api_request(
endpoint,
access_key_id,
access_key_secret,
&mut params,
)
.await?;
let response: AddRecordResponse =
serde_json::from_str(&response_body).map_err(new_error)?;
Ok(response)
}
async fn delete_ali_dns_record(
endpoint: &str,
access_key_id: &str,
access_key_secret: &str,
record_id: &str,
) -> Result<String> {
let mut params = BTreeMap::new();
params.insert("Action", "DeleteDomainRecord".to_string());
params.insert("RecordId", record_id.to_string());
ali_api_request(endpoint, access_key_id, access_key_secret, &mut params)
.await
}
pub(crate) struct AliDnsTask {
access_key_id: String,
access_key_secret: String,
endpoint: String,
record: Mutex<String>,
}
impl AliDnsTask {
pub fn new(url: &str) -> Result<Self> {
let info = Url::parse(url).map_err(new_error)?;
let endpoint = info.origin().ascii_serialization();
let mut access_key_id = "".to_string();
let mut access_key_secret = "".to_string();
for (k, v) in info.query_pairs() {
match k.as_ref() {
"access_key_id" => {
access_key_id = v.to_string();
},
"access_key_secret" => {
access_key_secret = v.to_string();
},
_ => {},
}
}
if access_key_id.is_empty() || access_key_secret.is_empty() {
return Err(new_error(
"access_key_id or access_key_secret is required",
));
}
Ok(Self {
access_key_id,
access_key_secret,
endpoint,
record: Mutex::new(String::new()),
})
}
}
#[async_trait]
impl AcmeDnsTask for AliDnsTask {
async fn add_txt_record(&self, domain: &str, value: &str) -> Result<()> {
let response = add_ali_dns_record(
&self.endpoint,
&self.access_key_id,
&self.access_key_secret,
domain,
value,
)
.await?;
let mut record = self.record.lock().await;
*record = response.record_id;
Ok(())
}
async fn done(&self) -> Result<()> {
let mut record = self.record.lock().await;
delete_ali_dns_record(
&self.endpoint,
&self.access_key_id,
&self.access_key_secret,
&record,
)
.await?;
*record = String::new();
Ok(())
}
}