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 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 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 let endpoint = format!("https://{}/{object_key}", self.host);
70
71 let payload_hash = hex::encode(Sha256::digest(payload));
73
74 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 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 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 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}