Skip to main content

wae_storage/providers/
gcs.rs

1//! Google Cloud Storage 存储提供商实现
2
3use crate::{StorageConfig, StorageProvider, StorageResult};
4
5use chrono::Utc;
6use hmac::{Hmac, Mac};
7use sha2::{Digest, Sha256};
8use url::Url;
9use wae_types::WaeError;
10
11/// Google Cloud Storage 存储提供商
12///
13/// 实现 Google Cloud Storage 对象存储服务的签名和预签名 URL 生成
14pub struct GcsProvider;
15
16impl GcsProvider {
17    /// 将哈希值转换为十六进制字符串
18    fn hex_encode(data: &[u8]) -> String {
19        data.iter().map(|b| format!("{:02x}", b)).collect()
20    }
21
22    /// 计算 SHA256 哈希值
23    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    /// 使用 HMAC-SHA256 算法计算消息认证码
30    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    /// 获取签名密钥
37    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    /// URI 编码
45    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}