use super::error::Error;
use super::helper::now_iso8601;
use hmac::{Hmac, Mac};
use rand::Rng;
use rand::distr::Alphanumeric;
use serde::Serialize;
use serde_json::Value;
use sha2::{Digest, Sha256};
use std::collections::{BTreeMap, HashMap};
use url::{Url, form_urlencoded};
pub struct SignParams<'a, T: Serialize> {
pub host: &'a str,
pub x_acs_action: &'a str,
pub x_acs_version: &'a str,
pub x_acs_security_token: Option<&'a str>,
pub style: &'a OpenApiStyle,
pub req_method: &'a str,
pub query_map: T,
pub request_body: Option<&'a RequestBody<'a>>,
}
fn replace_percent_encode(s: &str) -> String {
s.replace('+', "%20")
.replace('*', "%2A")
.replace("%7E", "~")
}
pub enum OpenApiStyle {
RPC,
ROA,
}
pub fn generate_can_uri(
host: &str,
query: &impl Serialize,
style: &OpenApiStyle,
) -> Result<(String, String, String), Error> {
let query_map = to_query_map(query);
let u = Url::parse_with_params(&format!("https://{}", host), query_map)
.map_err(|_| Error::Common("url parsed failed!".to_owned()))?;
let can_uri = match style {
OpenApiStyle::RPC => "/".to_owned(),
OpenApiStyle::ROA => replace_percent_encode(u.path()),
};
let can_query_str = if let Some(s) = u.query() {
replace_percent_encode(s)
} else {
"".to_owned()
};
Ok((can_uri, u.to_string(), can_query_str))
}
pub struct GenerateCanHeadersRes {
pub can_headers: String,
pub can_signed_headers: String,
pub common_headers: HashMap<String, String>,
}
pub enum RequestBody<'a> {
FormData(&'a [(&'a str, &'a str)]),
Json(&'a str),
Binary(&'a [u8]),
}
pub fn generate_can_headers(
host: &str,
x_acs_action: &str,
x_acs_version: &str,
x_acs_security_token: Option<&str>,
x_acs_content_sha256: &str,
content_type: Option<&str>,
) -> GenerateCanHeadersRes {
let mut need_signed_headers = BTreeMap::new();
need_signed_headers.insert("host".to_owned(), host.trim().to_owned());
need_signed_headers.insert("x-acs-action".to_owned(), x_acs_action.to_owned());
need_signed_headers.insert(
"x-acs-content-sha256".to_owned(),
x_acs_content_sha256.to_owned(),
);
let date = now_iso8601();
need_signed_headers.insert("x-acs-date".to_owned(), date.clone());
need_signed_headers.insert("x-acs-signature-nonce".to_owned(), generate_nonce());
need_signed_headers.insert("x-acs-version".to_owned(), x_acs_version.to_owned());
if let Some(s) = x_acs_security_token {
need_signed_headers.insert("x-acs-security-token".to_owned(), s.trim().to_owned());
}
if let Some(ct) = content_type {
need_signed_headers.insert("content-type".to_owned(), ct.to_owned());
}
let mut can_headers = String::new();
let mut can_signed_headers = String::new();
for (k, v) in &need_signed_headers {
can_headers.push_str(format!("{k}:{v}\n").as_str());
can_signed_headers.push_str(format!("{k};").as_str());
}
can_signed_headers.pop();
let common_headers = need_signed_headers.into_iter().collect::<HashMap<_, _>>();
GenerateCanHeadersRes {
can_headers,
can_signed_headers,
common_headers,
}
}
pub fn hash_sha256(bytes: Option<&[u8]>) -> String {
let mut hasher = Sha256::new();
if let Some(b) = bytes {
hasher.update(b);
} else {
hasher.update(b"");
};
let hash_str = hasher.finalize();
hex::encode(hash_str)
}
pub fn sign_hmac_sha256(secret: &str, str_to_sign: &str) -> String {
type HmacSha256 = Hmac<Sha256>;
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap();
mac.update(str_to_sign.as_bytes());
let res = mac.finalize().into_bytes();
hex::encode(res)
}
pub fn generate_random_string(len: usize) -> String {
rand::rng()
.sample_iter(&Alphanumeric)
.take(len)
.map(char::from)
.collect()
}
fn generate_nonce() -> String {
generate_random_string(32)
}
pub fn get_openapi_request_header<T: Serialize>(
access_key_secret: &str,
access_key_id: &str,
sign_params: SignParams<T>,
) -> Result<(HashMap<String, String>, String), Error> {
let (can_uri, url_, can_query_str) =
generate_can_uri(sign_params.host, &sign_params.query_map, sign_params.style)?;
let body_hash;
let mut content_type = None;
if let Some(pt) = sign_params.request_body {
match pt {
RequestBody::FormData(form_data) => {
content_type = Some("application/x-www-form-urlencoded");
let mut serializer = form_urlencoded::Serializer::new(String::new());
for (k, v) in form_data.iter() {
serializer.append_pair(k, v);
}
let s = serializer.finish();
body_hash = hash_sha256(Some(s.as_bytes()));
}
RequestBody::Json(s) => {
content_type = Some("application/json");
body_hash = hash_sha256(Some(s.as_bytes()));
}
RequestBody::Binary(bytes) => {
content_type = Some("application/octet-stream");
body_hash = hash_sha256(Some(bytes));
}
};
} else {
body_hash = hash_sha256(None);
}
let generate_can_headers_res = generate_can_headers(
sign_params.host,
sign_params.x_acs_action,
sign_params.x_acs_version,
sign_params.x_acs_security_token,
&body_hash,
content_type,
);
let can_req_str = format!(
"{}\n{}\n{}\n{}\n{}\n{}",
sign_params.req_method,
can_uri,
can_query_str,
generate_can_headers_res.can_headers,
generate_can_headers_res.can_signed_headers,
body_hash
);
let hashed_can_request = hash_sha256(Some(can_req_str.as_bytes()));
let str_to_sign = format!("ACS3-HMAC-SHA256\n{hashed_can_request}");
let signature = sign_hmac_sha256(access_key_secret, &str_to_sign);
let mut common_headers = generate_can_headers_res.common_headers;
let authorization = format!(
"ACS3-HMAC-SHA256 Credential={},SignedHeaders={},Signature={}",
access_key_id, generate_can_headers_res.can_signed_headers, signature
);
common_headers.insert("Authorization".to_owned(), authorization);
Ok((common_headers, url_))
}
fn to_query_map(query: impl Serialize) -> BTreeMap<String, String> {
let v = serde_json::to_value(query).expect("Serialize to serde_json::Value failed");
if !v.is_object() {
panic!("to_query_map: input query is not an object!");
}
flatten_root(&v)
}
fn flatten_root(v: &Value) -> BTreeMap<String, String> {
let mut res = BTreeMap::new();
if let Value::Object(map) = v {
for (k, val) in map {
flatten_with_prefix(k, val, &mut res);
}
}
res
}
fn flatten_with_prefix(prefix: &str, v: &Value, out: &mut BTreeMap<String, String>) {
match v {
Value::Null => {
out.insert(prefix.to_owned(), "".to_owned());
}
Value::Bool(b) => {
out.insert(prefix.to_owned(), b.to_string());
}
Value::Number(n) => {
out.insert(prefix.to_owned(), n.to_string());
}
Value::String(s) => {
out.insert(prefix.to_owned(), s.clone());
}
Value::Array(arr) => {
for (i, item) in arr.iter().enumerate() {
let new_prefix = format!("{prefix}.{}", i + 1);
flatten_with_prefix(&new_prefix, item, out);
}
}
Value::Object(map) => {
for (k, val) in map {
let new_prefix = format!("{prefix}.{k}");
flatten_with_prefix(&new_prefix, val, out);
}
}
}
}