s3_wasi_http/
lib.rs

1use std::env;
2
3use anyhow::Result;
4use chrono::Utc;
5use hmac::{Hmac, Mac};
6use sha2::{Digest, Sha256};
7use wstd::http::{Client, IntoBody, Method, Request};
8
9fn get_signature_key(secret_key: &str, date: &str, region: &str, service: &str) -> Result<Vec<u8>> {
10    let k_secret = format!("AWS4{}", secret_key);
11    let k_date = hmac_sha256(k_secret.as_bytes(), date.as_bytes())?;
12    let k_region = hmac_sha256(&k_date, region.as_bytes())?;
13    let k_service = hmac_sha256(&k_region, service.as_bytes())?;
14    hmac_sha256(&k_service, b"aws4_request")
15}
16
17fn hmac_sha256(key: &[u8], data: &[u8]) -> Result<Vec<u8>> {
18    let mut mac = Hmac::<Sha256>::new_from_slice(key)?;
19    mac.update(data);
20    Ok(mac.finalize().into_bytes().to_vec())
21}
22
23pub struct S3Client {
24    client: Client,
25    access_key: String,
26    secret_key: String,
27    region: String,
28    host: String,
29}
30
31impl S3Client {
32    pub fn new(access_key: String, secret_key: String, region: String, host: String) -> Self {
33        Self {
34            client: Client::new(),
35            access_key,
36            secret_key,
37            region,
38            host,
39        }
40    }
41
42    /// Panics if AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION and AWS_ENDPOINT_URL_S3 isn't set.
43    pub fn new_from_env() -> Self {
44        let access_key = env::var("AWS_ACCESS_KEY_ID").expect("ENV \"AWS_ACCESS_KEY\" isn't set");
45        let secret_key =
46            env::var("AWS_SECRET_ACCESS_KEY").expect("ENV \"AWS_SECRET_ACCESS_KEY\" isn't set");
47        let region = env::var("AWS_DEFAULT_REGION").expect("ENV \"AWS_DEFAULT_REGION\" isn't set");
48        let host = env::var("AWS_ENDPOINT_URL_S3").expect("ENV \"AWS_ENDPOINT_URL_S3\" isn't set");
49
50        Self {
51            client: Client::new(),
52            access_key,
53            secret_key,
54            region,
55            host,
56        }
57    }
58
59    pub async fn put_object(&self, object_key: &str, payload: &[u8]) -> Result<()> {
60        let service = "s3";
61        let algorithm = "AWS4-HMAC-SHA256";
62
63        // Get current time in AWS format
64        let now = Utc::now();
65        let date_stamp = now.format("%Y%m%d").to_string();
66        let amz_date = now.format("%Y%m%dT%H%M%SZ").to_string();
67
68        // Host and endpoint
69        let endpoint = format!("https://{}/{object_key}", self.host);
70
71        // SHA-256 hash of the payload
72        let payload_hash = hex::encode(Sha256::digest(payload));
73
74        // Canonical Request
75        let canonical_headers = format!(
76            "host:{}\nx-amz-content-sha256:{payload_hash}\nx-amz-date:{amz_date}\n",
77            self.host
78        );
79        let signed_headers = "host;x-amz-content-sha256;x-amz-date";
80        let canonical_request =
81            format!("PUT\n/{object_key}\n\n{canonical_headers}\n{signed_headers}\n{payload_hash}",);
82        let canonical_request_hash = hex::encode(Sha256::digest(canonical_request.as_bytes()));
83
84        // String-to-Sign
85        let credential_scope = format!("{date_stamp}/{}/{service}/aws4_request", self.region);
86        let string_to_sign =
87            format!("{algorithm}\n{amz_date}\n{credential_scope}\n{canonical_request_hash}");
88
89        let signing_key = get_signature_key(&self.secret_key, &date_stamp, &self.region, service)?;
90
91        // Compute the Signature
92        let mut mac = Hmac::<Sha256>::new_from_slice(&signing_key)?;
93        mac.update(string_to_sign.as_bytes());
94        let signature = hex::encode(mac.finalize().into_bytes());
95
96        // Authorization Header
97        let authorization_header = format!(
98            "{algorithm} Credential={}/{credential_scope}, SignedHeaders={signed_headers}, Signature={signature}", self.access_key
99        );
100
101        let request = Request::builder()
102            .uri(endpoint)
103            .method(Method::PUT)
104            .header("x-amz-content-sha256", payload_hash)
105            .header("x-amz-date", amz_date)
106            .header("authorization", authorization_header)
107            .header("content-length", payload.len().to_string())
108            .body(payload.into_body())?;
109
110        let res = self.client.send(request).await?;
111        let (parts, mut body) = res.into_parts();
112
113        if parts.status != 200 {
114            let bytes = body.bytes().await?;
115            let message = std::str::from_utf8(&bytes)?;
116            anyhow::bail!(
117                "Filed to put object to S3 bucket, HTTP code: {}, Message: {message}",
118                parts.status
119            )
120        }
121
122        Ok(())
123    }
124}