wae_storage/providers/
s3.rs1use crate::{StorageConfig, StorageProvider, StorageResult};
4
5use chrono::Utc;
6use hmac::{Hmac, Mac};
7use sha2::{Digest, Sha256};
8use url::Url;
9use wae_types::WaeError;
10
11pub struct S3Provider;
15
16impl S3Provider {
17 fn hex_encode(data: &[u8]) -> String {
19 data.iter().map(|b| format!("{:02x}", b)).collect()
20 }
21
22 fn sha256_hash(data: &[u8]) -> Vec<u8> {
24 let mut hasher = Sha256::new();
25 hasher.update(data);
26 hasher.finalize().to_vec()
27 }
28
29 fn hmac_sha256(key: &[u8], data: &[u8]) -> Vec<u8> {
31 let mut mac = Hmac::<Sha256>::new_from_slice(key).expect("HMAC can take key of any size");
32 mac.update(data);
33 mac.finalize().into_bytes().to_vec()
34 }
35
36 fn get_signature_key(key: &str, date_stamp: &str, region_name: &str, service_name: &str) -> Vec<u8> {
38 let k_date = Self::hmac_sha256(format!("AWS4{}", key).as_bytes(), date_stamp.as_bytes());
39 let k_region = Self::hmac_sha256(&k_date, region_name.as_bytes());
40 let k_service = Self::hmac_sha256(&k_region, service_name.as_bytes());
41 Self::hmac_sha256(&k_service, b"aws4_request")
42 }
43
44 fn uri_encode(s: &str, encode_slash: bool) -> String {
46 let mut result = String::new();
47 for c in s.chars() {
48 match c {
49 'A'..='Z' | 'a'..='z' | '0'..='9' | '_' | '-' | '~' | '.' => {
50 result.push(c);
51 }
52 '/' => {
53 if encode_slash {
54 result.push_str("%2F");
55 }
56 else {
57 result.push('/');
58 }
59 }
60 _ => {
61 let mut buf = [0; 4];
62 let bytes = c.encode_utf8(&mut buf).as_bytes();
63 for byte in bytes {
64 result.push_str(&format!("%{:02X}", byte));
65 }
66 }
67 }
68 }
69 result
70 }
71}
72
73impl StorageProvider for S3Provider {
74 fn sign_url(&self, path: &str, config: &StorageConfig) -> StorageResult<Url> {
75 if path.is_empty() {
76 return Err(WaeError::invalid_params("path", "Empty path"));
77 }
78
79 if path.starts_with("http://") || path.starts_with("https://") {
80 return Url::parse(path).map_err(|e| WaeError::invalid_params("path", e.to_string()));
81 }
82
83 let clean_path = path.trim_start_matches('/');
84 let now = Utc::now();
85 let amz_date = now.format("%Y%m%dT%H%M%SZ").to_string();
86 let date_stamp = now.format("%Y%m%d").to_string();
87 let expires = 3600;
88
89 let host_str = config.cdn_url.clone().unwrap_or_else(|| {
90 if let Some(endpoint) = &config.endpoint {
91 endpoint.clone()
92 }
93 else {
94 format!("https://{}.s3.{}.amazonaws.com", config.bucket, config.region)
95 }
96 });
97
98 let host_url =
99 if host_str.starts_with("http") { Url::parse(&host_str) } else { Url::parse(&format!("https://{}", host_str)) }
100 .map_err(|e| WaeError::invalid_params("host", e.to_string()))?;
101
102 let mut url = host_url.join(clean_path).map_err(|e| WaeError::invalid_params("path", e.to_string()))?;
103
104 let host = url.host_str().unwrap_or("");
105
106 let method = "GET";
107 let canonical_uri = format!("/{}", clean_path);
108 let canonical_querystring = format!(
109 "X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={}%2F{}%2F{}%2Fs3%2Faws4_request&X-Amz-Date={}&X-Amz-Expires={}&X-Amz-SignedHeaders=host",
110 Self::uri_encode(&config.secret_id, true),
111 date_stamp,
112 config.region,
113 amz_date,
114 expires
115 );
116 let canonical_headers = format!("host:{}\n", host);
117 let signed_headers = "host";
118 let payload_hash = Self::hex_encode(&Self::sha256_hash(b""));
119
120 let canonical_request = format!(
121 "{}\n{}\n{}\n{}\n{}\n{}",
122 method, canonical_uri, canonical_querystring, canonical_headers, signed_headers, payload_hash
123 );
124
125 let credential_scope = format!("{}/{}/s3/aws4_request", date_stamp, config.region);
126 let string_to_sign = format!(
127 "AWS4-HMAC-SHA256\n{}\n{}\n{}",
128 amz_date,
129 credential_scope,
130 Self::hex_encode(&Self::sha256_hash(canonical_request.as_bytes()))
131 );
132
133 let signing_key = Self::get_signature_key(&config.secret_key, &date_stamp, &config.region, "s3");
134 let signature = Self::hex_encode(&Self::hmac_sha256(&signing_key, string_to_sign.as_bytes()));
135
136 url.set_query(Some(&format!("{}&X-Amz-Signature={}", canonical_querystring, signature)));
137
138 Ok(url)
139 }
140
141 fn get_presigned_put_url(&self, key: &str, config: &StorageConfig) -> StorageResult<Url> {
142 let clean_key = key.trim_start_matches('/');
143 let now = Utc::now();
144 let amz_date = now.format("%Y%m%dT%H%M%SZ").to_string();
145 let date_stamp = now.format("%Y%m%d").to_string();
146 let expires = 900;
147
148 let host_str =
149 config.endpoint.clone().unwrap_or_else(|| format!("https://{}.s3.{}.amazonaws.com", config.bucket, config.region));
150
151 let host_url =
152 if host_str.starts_with("http") { Url::parse(&host_str) } else { Url::parse(&format!("https://{}", host_str)) }
153 .map_err(|e| WaeError::invalid_params("host", e.to_string()))?;
154
155 let mut url = host_url.join(clean_key).map_err(|e| WaeError::invalid_params("key", e.to_string()))?;
156
157 let host = url.host_str().unwrap_or("");
158
159 let method = "PUT";
160 let canonical_uri = format!("/{}", clean_key);
161 let canonical_querystring = format!(
162 "X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential={}%2F{}%2F{}%2Fs3%2Faws4_request&X-Amz-Date={}&X-Amz-Expires={}&X-Amz-SignedHeaders=host",
163 Self::uri_encode(&config.secret_id, true),
164 date_stamp,
165 config.region,
166 amz_date,
167 expires
168 );
169 let canonical_headers = format!("host:{}\n", host);
170 let signed_headers = "host";
171 let payload_hash = Self::hex_encode(&Self::sha256_hash(b"UNSIGNED-PAYLOAD"));
172
173 let canonical_request = format!(
174 "{}\n{}\n{}\n{}\n{}\n{}",
175 method, canonical_uri, canonical_querystring, canonical_headers, signed_headers, payload_hash
176 );
177
178 let credential_scope = format!("{}/{}/s3/aws4_request", date_stamp, config.region);
179 let string_to_sign = format!(
180 "AWS4-HMAC-SHA256\n{}\n{}\n{}",
181 amz_date,
182 credential_scope,
183 Self::hex_encode(&Self::sha256_hash(canonical_request.as_bytes()))
184 );
185
186 let signing_key = Self::get_signature_key(&config.secret_key, &date_stamp, &config.region, "s3");
187 let signature = Self::hex_encode(&Self::hmac_sha256(&signing_key, string_to_sign.as_bytes()));
188
189 url.set_query(Some(&format!("{}&X-Amz-Signature={}", canonical_querystring, signature)));
190
191 Ok(url)
192 }
193}