use std::collections::HashMap;
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine};
use chrono::{DateTime, Utc};
use hmac::{Hmac, Mac};
use md5::Md5;
use sha1::Sha1;
use sha2::{Digest, Sha256};
use uuid::Uuid;
use super::base::RESTAuthParameter;
use super::dlf_provider::DLFToken;
type HmacSha256 = Hmac<Sha256>;
type HmacSha1 = Hmac<Sha1>;
pub trait DLFRequestSigner: Send + Sync {
fn sign_headers(
&self,
body: Option<&str>,
now: &DateTime<Utc>,
security_token: Option<&str>,
host: &str,
) -> HashMap<String, String>;
fn authorization(
&self,
rest_auth_parameter: &RESTAuthParameter,
token: &DLFToken,
host: &str,
sign_headers: &HashMap<String, String>,
) -> String;
#[allow(dead_code)]
fn identifier(&self) -> &str;
}
pub struct DLFDefaultSigner {
region: String,
}
impl DLFDefaultSigner {
pub const IDENTIFIER: &'static str = "default";
const VERSION: &'static str = "v1";
const SIGNATURE_ALGORITHM: &'static str = "DLF4-HMAC-SHA256";
const PRODUCT: &'static str = "DlfNext";
const REQUEST_TYPE: &'static str = "aliyun_v4_request";
const SIGNATURE_KEY: &'static str = "Signature";
const NEW_LINE: &'static str = "\n";
const DLF_CONTENT_MD5_HEADER_KEY: &'static str = "Content-MD5";
const DLF_CONTENT_TYPE_KEY: &'static str = "Content-Type";
const DLF_DATE_HEADER_KEY: &'static str = "x-dlf-date";
const DLF_SECURITY_TOKEN_HEADER_KEY: &'static str = "x-dlf-security-token";
const DLF_AUTH_VERSION_HEADER_KEY: &'static str = "x-dlf-version";
const DLF_CONTENT_SHA256_HEADER_KEY: &'static str = "x-dlf-content-sha256";
const DLF_CONTENT_SHA256_VALUE: &'static str = "UNSIGNED-PAYLOAD";
const AUTH_DATE_TIME_FORMAT: &'static str = "%Y%m%dT%H%M%SZ";
const MEDIA_TYPE: &'static str = "application/json";
const SIGNED_HEADERS: &'static [&'static str] = &[
"content-md5",
"content-type",
"x-dlf-content-sha256",
"x-dlf-date",
"x-dlf-version",
"x-dlf-security-token",
];
pub fn new(region: impl Into<String>) -> Self {
Self {
region: region.into(),
}
}
fn md5_base64(raw: &str) -> String {
let mut hasher = Md5::new();
hasher.update(raw.as_bytes());
let hash = hasher.finalize();
BASE64_STANDARD.encode(hash)
}
fn hmac_sha256(key: &[u8], data: &str) -> Vec<u8> {
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC can take key of any size");
mac.update(data.as_bytes());
mac.finalize().into_bytes().to_vec()
}
fn sha256_hex(raw: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(raw.as_bytes());
hex::encode(hasher.finalize())
}
fn hex_encode(raw: &[u8]) -> String {
hex::encode(raw)
}
fn trim(value: &str) -> &str {
value.trim()
}
fn get_canonical_request(
&self,
rest_auth_parameter: &RESTAuthParameter,
headers: &HashMap<String, String>,
) -> String {
let mut parts = vec![
rest_auth_parameter.method.clone(),
rest_auth_parameter.path.clone(),
];
let canonical_query_string =
self.build_canonical_query_string(&rest_auth_parameter.parameters);
parts.push(canonical_query_string);
let sorted_headers = self.build_sorted_signed_headers_map(headers);
for (key, value) in sorted_headers {
parts.push(format!("{key}:{value}"));
}
let content_sha256 = headers
.get(Self::DLF_CONTENT_SHA256_HEADER_KEY)
.map(|s| s.as_str())
.unwrap_or(Self::DLF_CONTENT_SHA256_VALUE);
parts.push(content_sha256.to_string());
parts.join(Self::NEW_LINE)
}
fn build_canonical_query_string(&self, parameters: &HashMap<String, String>) -> String {
if parameters.is_empty() {
return String::new();
}
let mut sorted_params: Vec<_> = parameters.iter().collect();
sorted_params.sort_by(|a, b| a.0.cmp(b.0));
let query_parts: Vec<String> = sorted_params
.iter()
.map(|(key, value)| {
let key = Self::trim(key);
if !value.is_empty() {
let value = Self::trim(value);
format!("{key}={value}")
} else {
key.to_string()
}
})
.collect();
query_parts.join("&")
}
fn build_sorted_signed_headers_map(
&self,
headers: &HashMap<String, String>,
) -> Vec<(String, String)> {
let mut sorted_headers: Vec<(String, String)> = headers
.iter()
.filter(|(key, _)| {
let lower_key = key.to_lowercase();
Self::SIGNED_HEADERS.contains(&lower_key.as_str())
})
.map(|(key, value)| (key.to_lowercase(), Self::trim(value).to_string()))
.collect();
sorted_headers.sort_by(|a, b| a.0.cmp(&b.0));
sorted_headers
}
}
impl DLFRequestSigner for DLFDefaultSigner {
fn sign_headers(
&self,
body: Option<&str>,
now: &DateTime<Utc>,
security_token: Option<&str>,
_host: &str,
) -> HashMap<String, String> {
let mut sign_headers = HashMap::new();
let date_time = now.format(Self::AUTH_DATE_TIME_FORMAT).to_string();
sign_headers.insert(Self::DLF_DATE_HEADER_KEY.to_string(), date_time);
sign_headers.insert(
Self::DLF_CONTENT_SHA256_HEADER_KEY.to_string(),
Self::DLF_CONTENT_SHA256_VALUE.to_string(),
);
sign_headers.insert(
Self::DLF_AUTH_VERSION_HEADER_KEY.to_string(),
Self::VERSION.to_string(),
);
if let Some(body_content) = body {
if !body_content.is_empty() {
sign_headers.insert(
Self::DLF_CONTENT_TYPE_KEY.to_string(),
Self::MEDIA_TYPE.to_string(),
);
sign_headers.insert(
Self::DLF_CONTENT_MD5_HEADER_KEY.to_string(),
Self::md5_base64(body_content),
);
}
}
if let Some(token) = security_token {
sign_headers.insert(
Self::DLF_SECURITY_TOKEN_HEADER_KEY.to_string(),
token.to_string(),
);
}
sign_headers
}
fn authorization(
&self,
rest_auth_parameter: &RESTAuthParameter,
token: &DLFToken,
_host: &str,
sign_headers: &HashMap<String, String>,
) -> String {
let date_time = sign_headers.get(Self::DLF_DATE_HEADER_KEY).unwrap();
let date = &date_time[..8];
let canonical_request = self.get_canonical_request(rest_auth_parameter, sign_headers);
let string_to_sign = [
Self::SIGNATURE_ALGORITHM.to_string(),
date_time.clone(),
format!(
"{}/{}/{}/{}",
date,
self.region,
Self::PRODUCT,
Self::REQUEST_TYPE
),
Self::sha256_hex(&canonical_request),
]
.join(Self::NEW_LINE);
let date_key = Self::hmac_sha256(
format!("aliyun_v4{}", token.access_key_secret).as_bytes(),
date,
);
let date_region_key = Self::hmac_sha256(&date_key, &self.region);
let date_region_service_key = Self::hmac_sha256(&date_region_key, Self::PRODUCT);
let signing_key = Self::hmac_sha256(&date_region_service_key, Self::REQUEST_TYPE);
let signature_bytes = Self::hmac_sha256(&signing_key, &string_to_sign);
let signature = Self::hex_encode(&signature_bytes);
format!(
"{} Credential={}/{}/{}/{}/{},{}={}",
Self::SIGNATURE_ALGORITHM,
token.access_key_id,
date,
self.region,
Self::PRODUCT,
Self::REQUEST_TYPE,
Self::SIGNATURE_KEY,
signature
)
}
fn identifier(&self) -> &str {
Self::IDENTIFIER
}
}
pub struct DLFOpenApiSigner;
impl DLFOpenApiSigner {
pub const IDENTIFIER: &'static str = "openapi";
const DATE_HEADER: &'static str = "Date";
const ACCEPT_HEADER: &'static str = "Accept";
const CONTENT_MD5_HEADER: &'static str = "Content-MD5";
const CONTENT_TYPE_HEADER: &'static str = "Content-Type";
const HOST_HEADER: &'static str = "Host";
const X_ACS_SIGNATURE_METHOD: &'static str = "x-acs-signature-method";
const X_ACS_SIGNATURE_NONCE: &'static str = "x-acs-signature-nonce";
const X_ACS_SIGNATURE_VERSION: &'static str = "x-acs-signature-version";
const X_ACS_VERSION: &'static str = "x-acs-version";
const X_ACS_SECURITY_TOKEN: &'static str = "x-acs-security-token";
const DATE_FORMAT: &'static str = "%a, %d %b %Y %H:%M:%S GMT";
const ACCEPT_VALUE: &'static str = "application/json";
const CONTENT_TYPE_VALUE: &'static str = "application/json";
const SIGNATURE_METHOD_VALUE: &'static str = "HMAC-SHA1";
const SIGNATURE_VERSION_VALUE: &'static str = "1.0";
const API_VERSION: &'static str = "2026-01-18";
fn md5_base64(data: &str) -> String {
let mut hasher = Md5::new();
hasher.update(data.as_bytes());
let hash = hasher.finalize();
BASE64_STANDARD.encode(hash)
}
fn hmac_sha1_base64(key: &str, data: &str) -> String {
let mut mac =
HmacSha1::new_from_slice(key.as_bytes()).expect("HMAC can take key of any size");
mac.update(data.as_bytes());
BASE64_STANDARD.encode(mac.finalize().into_bytes())
}
fn trim(value: &str) -> &str {
value.trim()
}
fn build_canonicalized_headers(&self, headers: &HashMap<String, String>) -> String {
let mut sorted_headers: Vec<(String, String)> = headers
.iter()
.filter(|(key, _)| key.to_lowercase().starts_with("x-acs-"))
.map(|(key, value)| (key.to_lowercase(), Self::trim(value).to_string()))
.collect();
sorted_headers.sort_by(|a, b| a.0.cmp(&b.0));
let mut result = String::new();
for (key, value) in sorted_headers {
result.push_str(&format!("{key}:{value}\n"));
}
result
}
fn build_canonicalized_resource(&self, rest_auth_parameter: &RESTAuthParameter) -> String {
let path = urlencoding::decode(&rest_auth_parameter.path).unwrap_or_default();
if rest_auth_parameter.parameters.is_empty() {
return path.to_string();
}
let mut sorted_params: Vec<_> = rest_auth_parameter.parameters.iter().collect();
sorted_params.sort_by(|a, b| a.0.cmp(b.0));
let query_parts: Vec<String> = sorted_params
.iter()
.map(|(key, value)| {
let decoded_value = urlencoding::decode(value).unwrap_or_default();
if !decoded_value.is_empty() {
format!("{key}={decoded_value}")
} else {
key.to_string()
}
})
.collect();
format!("{}?{}", path, query_parts.join("&"))
}
fn build_string_to_sign(
&self,
rest_auth_parameter: &RESTAuthParameter,
headers: &HashMap<String, String>,
canonicalized_headers: &str,
canonicalized_resource: &str,
) -> String {
let parts = [
rest_auth_parameter.method.clone(),
headers
.get(Self::ACCEPT_HEADER)
.cloned()
.unwrap_or_default(),
headers
.get(Self::CONTENT_MD5_HEADER)
.cloned()
.unwrap_or_default(),
headers
.get(Self::CONTENT_TYPE_HEADER)
.cloned()
.unwrap_or_default(),
headers.get(Self::DATE_HEADER).cloned().unwrap_or_default(),
canonicalized_headers.to_string(),
];
parts.join("\n") + canonicalized_resource
}
}
impl DLFRequestSigner for DLFOpenApiSigner {
fn sign_headers(
&self,
body: Option<&str>,
now: &DateTime<Utc>,
security_token: Option<&str>,
host: &str,
) -> HashMap<String, String> {
let mut headers = HashMap::new();
headers.insert(
Self::DATE_HEADER.to_string(),
now.format(Self::DATE_FORMAT).to_string(),
);
headers.insert(
Self::ACCEPT_HEADER.to_string(),
Self::ACCEPT_VALUE.to_string(),
);
if let Some(body_content) = body {
if !body_content.is_empty() {
headers.insert(
Self::CONTENT_MD5_HEADER.to_string(),
Self::md5_base64(body_content),
);
headers.insert(
Self::CONTENT_TYPE_HEADER.to_string(),
Self::CONTENT_TYPE_VALUE.to_string(),
);
}
}
headers.insert(Self::HOST_HEADER.to_string(), host.to_string());
headers.insert(
Self::X_ACS_SIGNATURE_METHOD.to_string(),
Self::SIGNATURE_METHOD_VALUE.to_string(),
);
headers.insert(
Self::X_ACS_SIGNATURE_NONCE.to_string(),
Uuid::new_v4().to_string(),
);
headers.insert(
Self::X_ACS_SIGNATURE_VERSION.to_string(),
Self::SIGNATURE_VERSION_VALUE.to_string(),
);
headers.insert(
Self::X_ACS_VERSION.to_string(),
Self::API_VERSION.to_string(),
);
if let Some(token) = security_token {
headers.insert(Self::X_ACS_SECURITY_TOKEN.to_string(), token.to_string());
}
headers
}
fn authorization(
&self,
rest_auth_parameter: &RESTAuthParameter,
token: &DLFToken,
_host: &str,
sign_headers: &HashMap<String, String>,
) -> String {
let canonicalized_headers = self.build_canonicalized_headers(sign_headers);
let canonicalized_resource = self.build_canonicalized_resource(rest_auth_parameter);
let string_to_sign = self.build_string_to_sign(
rest_auth_parameter,
sign_headers,
&canonicalized_headers,
&canonicalized_resource,
);
let signature = Self::hmac_sha1_base64(&token.access_key_secret, &string_to_sign);
format!("acs {}:{}", token.access_key_id, signature)
}
fn identifier(&self) -> &str {
Self::IDENTIFIER
}
}
pub struct DLFSignerFactory;
impl DLFSignerFactory {
pub fn create_signer(signing_algorithm: &str, region: &str) -> Box<dyn DLFRequestSigner> {
if signing_algorithm == DLFOpenApiSigner::IDENTIFIER {
Box::new(DLFOpenApiSigner)
} else {
Box::new(DLFDefaultSigner::new(region))
}
}
}