volcengine-rs 0.1.0

Volcengine API SDK
Documentation
use chrono::Utc;
use hmac::{Hmac, Mac};
use reqwest::Client;
use serde::de::DeserializeOwned;
use sha2::{Digest, Sha256};
use std::collections::BTreeMap;

type HmacSha256 = Hmac<Sha256>;

fn sign(key: &[u8], msg: &str) -> Vec<u8> {
    let mut mac = HmacSha256::new_from_slice(key).expect("HMAC can be created");
    mac.update(msg.as_bytes());
    mac.finalize().into_bytes().to_vec()
}

fn get_signature_key(secret_key: &str, date_stamp: &str, region: &str, service: &str) -> Vec<u8> {
    let k_date = sign(secret_key.as_bytes(), date_stamp);
    let k_region = sign(&k_date, region);
    let k_service = sign(&k_region, service);
    sign(&k_service, "request")
}

fn format_query(parameters: &BTreeMap<&str, &str>) -> String {
    parameters
        .iter()
        .map(|(k, v)| format!("{}={}", k, v))
        .collect::<Vec<_>>()
        .join("&")
}

#[allow(clippy::too_many_arguments)]
fn signature_v4(
    access_key: &str,
    secret_key: &str,
    host: &str,
    region: &str,
    service: &str,
    method: &str,
    req_query: &str,
    req_body: &str,
) -> Result<(String, String), Box<dyn std::error::Error>> {
    let now = Utc::now();
    let current_date = now.format("%Y%m%dT%H%M%SZ").to_string();
    let datestamp = now.format("%Y%m%d").to_string();

    let payload_hash = {
        let mut hasher = Sha256::new();
        hasher.update(req_body.as_bytes());
        hex::encode(hasher.finalize())
    };

    let content_type = "application/json";
    let signed_headers = "content-type;host;x-content-sha256;x-date";

    let canonical_headers = format!(
        "content-type:{}\nhost:{}\nx-content-sha256:{}\nx-date:{}\n",
        content_type, host, payload_hash, current_date
    );

    let canonical_request = format!(
        "{}\n/\n{}\n{}\n{}\n{}",
        method, req_query, canonical_headers, signed_headers, payload_hash
    );

    let algorithm = "HMAC-SHA256";
    let credential_scope = format!("{}/{}/{}/request", datestamp, region, service);

    let string_to_sign = format!(
        "{}\n{}\n{}\n{}",
        algorithm,
        current_date,
        credential_scope,
        hex::encode(Sha256::digest(canonical_request.as_bytes()))
    );

    let signing_key = get_signature_key(secret_key, &datestamp, region, service);
    let signature = hex::encode(sign(&signing_key, &string_to_sign));

    let authorization_header = format!(
        "{} Credential={}/{}, SignedHeaders={}, Signature={}",
        algorithm, access_key, credential_scope, signed_headers, signature
    );

    Ok((authorization_header, current_date))
}

fn get_host(endpoint: &str) -> Result<String, String> {
    reqwest::Url::parse(endpoint)
        .map_err(|e| e.to_string())?
        .host_str()
        .map(|s| s.to_string())
        .ok_or_else(|| "Invalid endpoint".into())
}

#[allow(clippy::too_many_arguments)]
pub async fn send_request<T: DeserializeOwned>(
    access_key: &str,
    secret_key: &str,
    endpoint: &str,
    region: &str,
    service: &str,
    method: &str,
    content_type: &str,
    query_params: BTreeMap<&str, &str>,
    body_params: serde_json::Value,
) -> Result<T, Box<dyn std::error::Error>> {
    let client = Client::new();
    let formatted_query = format_query(&query_params);
    let formatted_body = body_params.to_string();

    let host = get_host(endpoint)?;

    let (authorization_header, current_date) = signature_v4(
        access_key,
        secret_key,
        &host,
        region,
        service,
        method,
        &formatted_query,
        &formatted_body,
    )?;

    let payload_hash = hex::encode(Sha256::digest(formatted_body.as_bytes()));

    let request_url = format!("{}?{}", endpoint, formatted_query);

    let response = client
        .request(method.parse()?, request_url)
        .header("X-Date", current_date)
        .header("Authorization", authorization_header)
        .header("X-Content-Sha256", payload_hash)
        .header("Content-Type", content_type)
        .body(formatted_body)
        .send()
        .await?;

    let data = response.json::<T>().await?;

    Ok(data)
}