use crate::oss::Error;
use crate::oss::sign_v4::{HTTPVerb, SignV4Param, generate_v4_signature, sign_v4};
use base64::{Engine, engine::general_purpose};
use md5::{Digest, Md5};
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use serde::de::DeserializeOwned;
use serde_json::{Map, Value};
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::path::Path;
use time::macros::format_description;
use tokio::io::AsyncReadExt;
use u_sdk_common::helper::gmt_format;
use url::Url;
pub fn utc_date_str(date_time: &time::OffsetDateTime) -> String {
date_time
.to_utc()
.format(format_description!("[year][month][day]"))
.unwrap()
}
pub fn utc_date_time_str(date_time: &time::OffsetDateTime) -> String {
date_time
.to_utc()
.format(format_description!(
"[year][month][day]T[hour][minute][second]Z"
))
.unwrap()
}
pub fn get_content_md5(bytes: &[u8]) -> String {
use md5::{Digest, Md5};
let mut hasher = Md5::new();
hasher.update(bytes);
let res = hasher.finalize();
general_purpose::STANDARD.encode(res)
}
pub fn hmac_sha256_bytes(key: &[u8], msg: &str) -> Vec<u8> {
use hmac::{Hmac, Mac};
use sha2::Sha256;
let mut mac = Hmac::<Sha256>::new_from_slice(key).unwrap();
mac.update(msg.as_bytes());
let result = mac.finalize();
result.into_bytes().to_vec()
}
#[test]
fn get_content_md5_test() {
let s = get_content_md5(b"0123456789");
assert_eq!(&s, "eB5eJF1ptWaXm4bijSPyxw==")
}
pub(crate) fn into_request_header(map: HashMap<&str, &str>) -> HeaderMap {
map.into_iter()
.map(|(k, v)| {
let name = HeaderName::from_bytes(k.as_bytes()).unwrap();
let value = HeaderValue::from_bytes(v.as_bytes()).unwrap();
(name, value)
})
.collect()
}
pub(crate) async fn into_request_failed_error(resp: reqwest::Response) -> Error {
let status = resp.status();
let body = resp.text().await;
match body {
Ok(text) => Error::RequestAPIFailed {
status: status.to_string(),
text,
},
Err(e) => Error::Reqwest(e),
}
}
pub(crate) async fn parse_xml_response<T: DeserializeOwned>(
resp: reqwest::Response,
) -> Result<T, Error> {
let status = resp.status();
if !status.is_success() {
return Err(into_request_failed_error(resp).await);
}
let text = resp.text().await?;
let data = quick_xml::de::from_str(&text)
.map_err(|e| Error::Common(format!("XML parse error: {}", e)))?;
Ok(data)
}
pub(crate) async fn compute_md5_from_file(path: &Path) -> Result<String, Error> {
let mut file = tokio::fs::File::open(path).await?;
let mut hasher = Md5::new();
let mut buf = vec![0u8; 64 * 1024];
loop {
let n = file.read(&mut buf).await?;
if n == 0 {
break;
}
hasher.update(&buf[..n]);
}
let result = hasher.finalize();
Ok(general_purpose::STANDARD.encode(result))
}
pub(crate) fn validate_object_name(name: &str) -> Result<(), Error> {
let len = name.len();
if len == 0 {
return Err(Error::Common("object_name cannot be empty".to_owned()));
}
if len > 1023 {
return Err(Error::Common(
"object_name is too long, max is 1023 bytes".to_owned(),
));
}
if let Some(first) = name.chars().next()
&& (first == '/' || first == '\\')
{
return Err(Error::Common(
"object_name cannot start with '/' or '\\'".to_owned(),
));
}
if name.bytes().any(|b| b == b'\r' || b == b'\n') {
return Err(Error::Common(
"object_name cannot contain control characters".to_owned(),
));
}
let segments: Vec<&str> = name.split('/').collect();
let to_check = if name.ends_with('/') {
&segments[..segments.len().saturating_sub(1)]
} else {
&segments[..]
};
if to_check.iter().any(|seg| seg.is_empty()) {
return Err(Error::Common(
"object_name cannot contain empty path segments".to_owned(),
));
}
for segment in to_check {
if *segment == "." || *segment == ".." {
return Err(Error::Common(
"object_name cannot contain relative path segments '.' or '..'".to_owned(),
));
}
}
Ok(())
}
#[test]
fn validate_object_name_test() {
assert!(validate_object_name("exampleobject.txt").is_ok());
assert!(validate_object_name("dir/subdir/file_测试-01.log").is_ok());
assert!(validate_object_name("a/b").is_ok());
assert!(validate_object_name("a/b/").is_ok());
assert!(validate_object_name("").is_err());
assert!(validate_object_name(&"a".repeat(1024)).is_err());
assert!(validate_object_name("/badname").is_err());
assert!(validate_object_name("\\badname").is_err());
assert!(validate_object_name("bad\r\nname").is_err());
assert!(validate_object_name("a//abc").is_err());
assert!(validate_object_name("x/y//z").is_err());
assert!(validate_object_name("./abc").is_err());
assert!(validate_object_name("../abc").is_err());
assert!(validate_object_name("abc/./def").is_err());
assert!(validate_object_name("abc/../def").is_err());
}
pub(crate) fn get_request_header(
access_key_id: &str,
access_key_secret: &str,
req_header_map: HashMap<String, String>,
request_url: &Url,
http_verb: HTTPVerb,
signing_region: &str,
bucket: Option<&str>,
) -> HeaderMap {
let (sign_map, remaining_map) = partition_header(req_header_map);
let mut canonical_header = BTreeMap::new();
canonical_header.extend(sign_map.iter().map(|(k, v)| (k.as_str(), v.as_str())));
canonical_header.insert("x-oss-content-sha256", "UNSIGNED-PAYLOAD");
canonical_header.insert("host", request_url.host_str().unwrap());
let mut additional_header = BTreeSet::new();
additional_header.insert("host");
let now = time::OffsetDateTime::now_utc();
let sign_v4_param = SignV4Param {
signing_region,
http_verb,
uri: request_url,
bucket,
header_map: &canonical_header,
additional_header: Some(&additional_header),
date_time: &now,
};
let authorization = sign_v4(access_key_id, access_key_secret, sign_v4_param);
let mut header = canonical_header.into_iter().collect::<HashMap<_, _>>();
header.insert("Authorization", &authorization);
let gmt = gmt_format(&now);
header.insert("Date", &gmt);
header.extend(remaining_map.iter().map(|(k, v)| (k.as_str(), v.as_str())));
into_request_header(header)
}
pub(crate) fn get_date_str(data: &time::OffsetDateTime) -> String {
let date_format = format_description!("[year][month][day]");
data.format(&date_format).unwrap()
}
pub(crate) fn get_date_time_str(data: &time::OffsetDateTime) -> String {
let data_time_format = format_description!("[year][month][day]T[hour][minute][second]Z");
data.format(&data_time_format).unwrap()
}
pub(crate) struct PresignParams<'a> {
pub access_key_id: &'a str,
pub access_key_secret: &'a str,
pub header_map: HashMap<String, String>,
pub presigned_url: Url,
pub http_verb: HTTPVerb,
pub url_expires: i32,
pub bucket: &'a str,
pub signing_region: &'a str,
}
pub(crate) fn generate_presigned_url(mut params: PresignParams<'_>) -> String {
let (header_map, remaining_map) = partition_header(params.header_map);
let mut canonical_header = BTreeMap::new();
canonical_header.extend(header_map.iter().map(|(k, v)| (k.as_str(), v.as_str())));
let host = params.presigned_url.host_str().unwrap().to_owned();
canonical_header.insert("host", host.as_str());
let mut additional_header = BTreeSet::new();
additional_header.insert("host");
for (k, v) in &remaining_map {
canonical_header.insert(k.as_str(), v.as_str());
additional_header.insert(k.as_str());
}
let now = time::OffsetDateTime::now_utc();
params
.presigned_url
.query_pairs_mut()
.append_pair("x-oss-signature-version", "OSS4-HMAC-SHA256")
.append_pair(
"x-oss-credential",
&format!(
"{}/{}/{}/oss/aliyun_v4_request",
params.access_key_id,
&get_date_str(&now),
params.signing_region
),
)
.append_pair("x-oss-date", &get_date_time_str(&now))
.append_pair("x-oss-expires", ¶ms.url_expires.to_string())
.append_pair(
"x-oss-additional-headers",
additional_header
.iter()
.cloned()
.collect::<Vec<&str>>()
.join(";")
.as_str(),
)
.finish();
let sign_v4_param = SignV4Param {
signing_region: params.signing_region,
http_verb: params.http_verb,
uri: ¶ms.presigned_url,
bucket: Some(params.bucket),
header_map: &canonical_header,
additional_header: Some(&additional_header),
date_time: &now,
};
let signature = generate_v4_signature(params.access_key_secret, sign_v4_param);
params
.presigned_url
.query_pairs_mut()
.append_pair("x-oss-signature", &signature)
.finish();
params.presigned_url.to_string()
}
fn partition_header(
header_map: HashMap<String, String>,
) -> (HashMap<String, String>, HashMap<String, String>) {
let mut sign_map = HashMap::new();
let mut remaining_map = HashMap::new();
for (k, v) in header_map {
let k = k.to_lowercase();
if k == "content-type" || k == "content-md5" || k.starts_with("x-oss-") {
sign_map.insert(k, v);
} else {
remaining_map.insert(k, v);
}
}
(sign_map, remaining_map)
}
pub(crate) fn parse_get_object_response_header<T: DeserializeOwned>(
header: &HeaderMap,
) -> (T, HashMap<String, String>) {
let mut map = Map::new();
let mut custom_meta_map = HashMap::new();
for (name, val) in header {
let name_s = name.as_str();
if let Ok(s) = val.to_str() {
if name_s.starts_with("x-oss-meta-") {
custom_meta_map.insert(
name_s.trim_start_matches("x-oss-meta-").to_string(),
s.to_string(),
);
} else {
map.insert(name_s.to_string(), Value::String(s.to_string()));
}
}
}
let response_header = serde_json::from_value::<T>(Value::Object(map)).unwrap();
(response_header, custom_meta_map)
}