wae_storage/providers/
gcs.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 GcsProvider;
15
16impl GcsProvider {
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!("GOOG4{}", 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"goog4_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 GcsProvider {
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://{}.storage.googleapis.com", config.bucket)
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-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential={}%2F{}%2F{}%2Fstorage%2Fgoog4_request&X-Goog-Date={}&X-Goog-Expires={}&X-Goog-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!("{}/{}/storage/goog4_request", date_stamp, config.region);
126 let string_to_sign = format!(
127 "GOOG4-RSA-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, "storage");
134 let signature = Self::hex_encode(&Self::hmac_sha256(&signing_key, string_to_sign.as_bytes()));
135
136 url.set_query(Some(&format!("{}&X-Goog-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 = config.endpoint.clone().unwrap_or_else(|| format!("https://{}.storage.googleapis.com", config.bucket));
149
150 let host_url =
151 if host_str.starts_with("http") { Url::parse(&host_str) } else { Url::parse(&format!("https://{}", host_str)) }
152 .map_err(|e| WaeError::invalid_params("host", e.to_string()))?;
153
154 let mut url = host_url.join(clean_key).map_err(|e| WaeError::invalid_params("key", e.to_string()))?;
155
156 let host = url.host_str().unwrap_or("");
157
158 let method = "PUT";
159 let canonical_uri = format!("/{}", clean_key);
160 let canonical_querystring = format!(
161 "X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential={}%2F{}%2F{}%2Fstorage%2Fgoog4_request&X-Goog-Date={}&X-Goog-Expires={}&X-Goog-SignedHeaders=host",
162 Self::uri_encode(&config.secret_id, true),
163 date_stamp,
164 config.region,
165 amz_date,
166 expires
167 );
168 let canonical_headers = format!("host:{}\n", host);
169 let signed_headers = "host";
170 let payload_hash = Self::hex_encode(&Self::sha256_hash(b"UNSIGNED-PAYLOAD"));
171
172 let canonical_request = format!(
173 "{}\n{}\n{}\n{}\n{}\n{}",
174 method, canonical_uri, canonical_querystring, canonical_headers, signed_headers, payload_hash
175 );
176
177 let credential_scope = format!("{}/{}/storage/goog4_request", date_stamp, config.region);
178 let string_to_sign = format!(
179 "GOOG4-RSA-SHA256\n{}\n{}\n{}",
180 amz_date,
181 credential_scope,
182 Self::hex_encode(&Self::sha256_hash(canonical_request.as_bytes()))
183 );
184
185 let signing_key = Self::get_signature_key(&config.secret_key, &date_stamp, &config.region, "storage");
186 let signature = Self::hex_encode(&Self::hmac_sha256(&signing_key, string_to_sign.as_bytes()));
187
188 url.set_query(Some(&format!("{}&X-Goog-Signature={}", canonical_querystring, signature)));
189
190 Ok(url)
191 }
192}