Skip to main content

volcengine_rs/
volce.rs

1use chrono::Utc;
2use hmac::{Hmac, Mac};
3use reqwest::Client;
4use serde::de::DeserializeOwned;
5use sha2::{Digest, Sha256};
6use std::collections::BTreeMap;
7
8type HmacSha256 = Hmac<Sha256>;
9
10fn sign(key: &[u8], msg: &str) -> Vec<u8> {
11    let mut mac = HmacSha256::new_from_slice(key).expect("HMAC can be created");
12    mac.update(msg.as_bytes());
13    mac.finalize().into_bytes().to_vec()
14}
15
16fn get_signature_key(secret_key: &str, date_stamp: &str, region: &str, service: &str) -> Vec<u8> {
17    let k_date = sign(secret_key.as_bytes(), date_stamp);
18    let k_region = sign(&k_date, region);
19    let k_service = sign(&k_region, service);
20    sign(&k_service, "request")
21}
22
23fn format_query(parameters: &BTreeMap<&str, &str>) -> String {
24    parameters
25        .iter()
26        .map(|(k, v)| format!("{}={}", k, v))
27        .collect::<Vec<_>>()
28        .join("&")
29}
30
31#[allow(clippy::too_many_arguments)]
32fn signature_v4(
33    access_key: &str,
34    secret_key: &str,
35    host: &str,
36    region: &str,
37    service: &str,
38    method: &str,
39    req_query: &str,
40    req_body: &str,
41) -> Result<(String, String), Box<dyn std::error::Error>> {
42    let now = Utc::now();
43    let current_date = now.format("%Y%m%dT%H%M%SZ").to_string();
44    let datestamp = now.format("%Y%m%d").to_string();
45
46    let payload_hash = {
47        let mut hasher = Sha256::new();
48        hasher.update(req_body.as_bytes());
49        hex::encode(hasher.finalize())
50    };
51
52    let content_type = "application/json";
53    let signed_headers = "content-type;host;x-content-sha256;x-date";
54
55    let canonical_headers = format!(
56        "content-type:{}\nhost:{}\nx-content-sha256:{}\nx-date:{}\n",
57        content_type, host, payload_hash, current_date
58    );
59
60    let canonical_request = format!(
61        "{}\n/\n{}\n{}\n{}\n{}",
62        method, req_query, canonical_headers, signed_headers, payload_hash
63    );
64
65    let algorithm = "HMAC-SHA256";
66    let credential_scope = format!("{}/{}/{}/request", datestamp, region, service);
67
68    let string_to_sign = format!(
69        "{}\n{}\n{}\n{}",
70        algorithm,
71        current_date,
72        credential_scope,
73        hex::encode(Sha256::digest(canonical_request.as_bytes()))
74    );
75
76    let signing_key = get_signature_key(secret_key, &datestamp, region, service);
77    let signature = hex::encode(sign(&signing_key, &string_to_sign));
78
79    let authorization_header = format!(
80        "{} Credential={}/{}, SignedHeaders={}, Signature={}",
81        algorithm, access_key, credential_scope, signed_headers, signature
82    );
83
84    Ok((authorization_header, current_date))
85}
86
87fn get_host(endpoint: &str) -> Result<String, String> {
88    reqwest::Url::parse(endpoint)
89        .map_err(|e| e.to_string())?
90        .host_str()
91        .map(|s| s.to_string())
92        .ok_or_else(|| "Invalid endpoint".into())
93}
94
95#[allow(clippy::too_many_arguments)]
96pub async fn send_request<T: DeserializeOwned>(
97    access_key: &str,
98    secret_key: &str,
99    endpoint: &str,
100    region: &str,
101    service: &str,
102    method: &str,
103    content_type: &str,
104    query_params: BTreeMap<&str, &str>,
105    body_params: serde_json::Value,
106) -> Result<T, Box<dyn std::error::Error>> {
107    let client = Client::new();
108    let formatted_query = format_query(&query_params);
109    let formatted_body = body_params.to_string();
110
111    let host = get_host(endpoint)?;
112
113    let (authorization_header, current_date) = signature_v4(
114        access_key,
115        secret_key,
116        &host,
117        region,
118        service,
119        method,
120        &formatted_query,
121        &formatted_body,
122    )?;
123
124    let payload_hash = hex::encode(Sha256::digest(formatted_body.as_bytes()));
125
126    let request_url = format!("{}?{}", endpoint, formatted_query);
127
128    let response = client
129        .request(method.parse()?, request_url)
130        .header("X-Date", current_date)
131        .header("Authorization", authorization_header)
132        .header("X-Content-Sha256", payload_hash)
133        .header("Content-Type", content_type)
134        .body(formatted_body)
135        .send()
136        .await?;
137
138    let data = response.json::<T>().await?;
139
140    Ok(data)
141}