rusty-cat 0.2.0

Async HTTP client for resumable file upload and download.
Documentation
use hmac::{Hmac, Mac};
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use sha2::{Digest, Sha256};

use super::constants::OSS_UNSIGNED_PAYLOAD;
use crate::{InnerErrorCode, MeowError};

type HmacSha256 = Hmac<Sha256>;

pub(crate) fn signed_headers(
    method: &str,
    canonical_uri: &str,
    raw_query: Option<&str>,
    sign_pairs: &[(&str, &str)],
    additional_headers: Option<&str>,
    access_key_id: &str,
    access_key_secret: &str,
    region: &str,
) -> Result<HeaderMap, MeowError> {
    let now = time::OffsetDateTime::now_utc();
    let iso8601 = format_iso8601_basic_z(now);
    let date = format!(
        "{:04}{:02}{:02}",
        now.year(),
        u8::from(now.month()),
        now.day()
    );
    let mut entries = vec![
        (
            "x-oss-content-sha256".to_string(),
            OSS_UNSIGNED_PAYLOAD.to_string(),
        ),
        ("x-oss-date".to_string(), iso8601.clone()),
    ];
    for (k, v) in sign_pairs {
        entries.push((k.to_ascii_lowercase(), (*v).to_string()));
    }
    entries.sort_by(|a, b| a.0.cmp(&b.0));
    let canonical_headers = entries
        .iter()
        .map(|(k, v)| format!("{k}:{v}\n"))
        .collect::<String>();
    let additional_headers = additional_headers.unwrap_or_default();
    let canonical_request = format!(
        "{method}\n{canonical_uri}\n{}\n{canonical_headers}\n{additional_headers}\n{OSS_UNSIGNED_PAYLOAD}",
        raw_query.unwrap_or_default()
    );
    let scope = format!("{date}/{region}/oss/aliyun_v4_request");
    let string_to_sign = format!(
        "OSS4-HMAC-SHA256\n{iso8601}\n{scope}\n{}",
        hex_encode(Sha256::digest(canonical_request.as_bytes()).as_slice())
    );
    let signing_key = hmac_sha256(
        format!("aliyun_v4{access_key_secret}").as_bytes(),
        date.as_bytes(),
    );
    let signing_key = hmac_sha256(&signing_key, region.as_bytes());
    let signing_key = hmac_sha256(&signing_key, b"oss");
    let signing_key = hmac_sha256(&signing_key, b"aliyun_v4_request");
    let signature = hex_encode(&hmac_sha256(&signing_key, string_to_sign.as_bytes()));
    let mut headers = HeaderMap::new();
    headers.insert("x-oss-date", header_value(&iso8601)?);
    headers.insert("x-oss-content-sha256", header_value(OSS_UNSIGNED_PAYLOAD)?);
    for (k, v) in sign_pairs {
        let name = HeaderName::from_bytes(k.as_bytes()).map_err(|e| {
            MeowError::from_code(
                InnerErrorCode::ParameterEmpty,
                format!("invalid header name '{k}': {e}"),
            )
        })?;
        headers.insert(name, header_value(v)?);
    }
    let mut auth = format!("OSS4-HMAC-SHA256 Credential={access_key_id}/{scope}");
    if !additional_headers.is_empty() {
        auth.push_str(",AdditionalHeaders=");
        auth.push_str(additional_headers);
    }
    auth.push_str(",Signature=");
    auth.push_str(&signature);
    headers.insert("authorization", header_value(&auth)?);
    Ok(headers)
}

fn hmac_sha256(key: &[u8], data: &[u8]) -> Vec<u8> {
    let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts any key length");
    mac.update(data);
    mac.finalize().into_bytes().to_vec()
}

pub(crate) fn hex_encode(bytes: &[u8]) -> String {
    const HEX: &[u8; 16] = b"0123456789abcdef";
    let mut out = String::with_capacity(bytes.len() * 2);
    for b in bytes {
        out.push(HEX[(b >> 4) as usize] as char);
        out.push(HEX[(b & 0x0f) as usize] as char);
    }
    out
}

fn format_iso8601_basic_z(t: time::OffsetDateTime) -> String {
    let t = t.to_offset(time::UtcOffset::UTC);
    format!(
        "{:04}{:02}{:02}T{:02}{:02}{:02}Z",
        t.year(),
        u8::from(t.month()),
        t.day(),
        t.hour(),
        t.minute(),
        t.second()
    )
}

pub(crate) fn header_value(v: &str) -> Result<HeaderValue, MeowError> {
    HeaderValue::from_str(v).map_err(|e| {
        MeowError::from_code(
            InnerErrorCode::ParameterEmpty,
            format!("invalid header value: {e}"),
        )
    })
}

pub(crate) fn param_error(e: impl std::fmt::Display) -> MeowError {
    MeowError::from_code(InnerErrorCode::ParameterEmpty, e.to_string())
}

#[cfg(test)]
mod tests {
    use super::hex_encode;

    #[test]
    fn test_hex_encode() {
        assert_eq!(hex_encode(&[0, 15, 16, 255]), "000f10ff");
    }
}