Skip to main content

wae_storage/providers/
cos.rs

1//! 腾讯云 COS 存储提供商实现
2
3use crate::{StorageConfig, StorageProvider, StorageResult};
4
5use chrono::Utc;
6use hmac::Hmac;
7use sha1::Sha1;
8use std::collections::HashSet;
9use url::Url;
10use wae_types::{WaeError, hex_encode, url_encode};
11
12/// 腾讯云 COS 存储提供商
13///
14/// 实现腾讯云对象存储服务的签名和预签名 URL 生成
15pub struct CosProvider;
16
17/// 使用 HMAC-SHA1 算法计算消息认证码
18fn hmac_sha1(key: &[u8], msg: &str) -> Vec<u8> {
19    use hmac::Mac;
20    type HmacSha1 = Hmac<Sha1>;
21    let mut mac = HmacSha1::new_from_slice(key).expect("HMAC can take key of any size");
22    mac.update(msg.as_bytes());
23    mac.finalize().into_bytes().to_vec()
24}
25
26/// 使用 HMAC-SHA1 算法计算消息认证码并返回十六进制字符串
27fn hmac_sha1_hex(key: &[u8], msg: &str) -> String {
28    hex_encode(&hmac_sha1(key, msg))
29}
30
31/// 对字符串进行 COS 规范的 URL 编码
32fn cos_encode(s: &str) -> String {
33    let encoded = url_encode(s);
34    ensure_lowercase_hex(&encoded)
35}
36
37/// 确保百分号编码中的十六进制字符为小写
38fn ensure_lowercase_hex(s: &str) -> String {
39    let mut result = String::with_capacity(s.len());
40    let mut chars = s.chars().peekable();
41    while let Some(c) = chars.next() {
42        if c == '%' {
43            result.push(c);
44            if let Some(h1) = chars.next() {
45                result.push(h1.to_ascii_lowercase());
46            }
47            if let Some(h2) = chars.next() {
48                result.push(h2.to_ascii_lowercase());
49            }
50        }
51        else {
52            result.push(c);
53        }
54    }
55    result
56}
57
58/// 计算字符串的 SHA1 哈希值并返回十六进制字符串
59fn sha1_hex(msg: &str) -> String {
60    use sha1::Digest;
61    let mut hasher = Sha1::new();
62    hasher.update(msg.as_bytes());
63    hex_encode(&hasher.finalize())
64}
65
66impl StorageProvider for CosProvider {
67    fn sign_url(&self, path: &str, config: &StorageConfig) -> StorageResult<Url> {
68        if path.is_empty() {
69            return Err(WaeError::invalid_params("path", "Empty path"));
70        }
71
72        if path.starts_with("http://") || path.starts_with("https://") {
73            return Url::parse(path).map_err(|e| WaeError::invalid_params("path", e.to_string()));
74        }
75
76        let base_url_str =
77            config.cdn_url.clone().unwrap_or_else(|| format!("https://{}.cos.{}.myqcloud.com", config.bucket, config.region));
78
79        let base_url = if base_url_str.starts_with("http") {
80            Url::parse(&base_url_str)
81        }
82        else {
83            Url::parse(&format!("https://{}", base_url_str))
84        }
85        .map_err(|e| WaeError::invalid_params("base_url", e.to_string()))?;
86
87        let mut url =
88            base_url.join(path.trim_start_matches('/')).map_err(|e| WaeError::invalid_params("path", e.to_string()))?;
89
90        let http_method = "get";
91        let http_uri = ensure_lowercase_hex(url.path());
92        let host = url.host_str().unwrap_or("").to_lowercase();
93
94        let key_time = {
95            use std::time::{SystemTime, UNIX_EPOCH};
96            let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
97            format!("{};{}", now, now + 7200)
98        };
99
100        let original_params: Vec<(String, String)> = url.query_pairs().map(|(k, v)| (k.to_string(), v.to_string())).collect();
101
102        let mut params_for_sign: Vec<(String, String)> =
103            original_params.iter().map(|(k, v)| (k.to_lowercase(), v.to_string())).collect();
104
105        params_for_sign.sort_by(|a, b| a.0.cmp(&b.0));
106
107        let mut params_list = Vec::new();
108        let mut params_kv = Vec::new();
109        let mut seen_keys = HashSet::new();
110
111        for (k_lower, v) in &params_for_sign {
112            let k_encoded = cos_encode(k_lower);
113
114            if seen_keys.insert(k_lower.clone()) {
115                params_list.push(k_encoded.clone());
116            }
117
118            params_kv.push(format!("{}={}", k_encoded, cos_encode(v)));
119        }
120
121        let http_params = params_kv.join("&");
122        let param_list_str = params_list.join(";");
123        let http_headers = if host.is_empty() { "".to_string() } else { format!("host={}", host) };
124
125        let http_string = format!("{}\n{}\n{}\n{}\n", http_method, http_uri, http_params, http_headers);
126        let http_string_hash = sha1_hex(&http_string);
127        let string_to_sign = format!("sha1\n{}\n{}\n", key_time, http_string_hash);
128
129        let sign_key = hmac_sha1_hex(config.secret_key.as_bytes(), &key_time);
130        let signature = hmac_sha1_hex(sign_key.as_bytes(), &string_to_sign);
131
132        let mut final_query_parts = Vec::new();
133
134        final_query_parts.push("q-sign-algorithm=sha1".to_string());
135        final_query_parts.push(format!("q-ak={}", cos_encode(&config.secret_id)));
136        final_query_parts.push(format!("q-sign-time={}", cos_encode(&key_time)));
137        final_query_parts.push(format!("q-key-time={}", cos_encode(&key_time)));
138        final_query_parts.push(format!("q-header-list={}", if host.is_empty() { "" } else { "host" }));
139        final_query_parts.push(format!("q-url-param-list={}", cos_encode(&param_list_str)));
140        final_query_parts.push(format!("q-signature={}", signature));
141
142        for (k, v) in &original_params {
143            if v.is_empty() {
144                final_query_parts.push(k.to_string());
145            }
146            else {
147                final_query_parts.push(format!("{}={}", cos_encode(k), cos_encode(v)));
148            }
149        }
150
151        let final_query = final_query_parts.join("&");
152        url.set_query(Some(&final_query));
153
154        Ok(url)
155    }
156
157    fn get_presigned_put_url(&self, key: &str, config: &StorageConfig) -> StorageResult<Url> {
158        let host = config.endpoint.clone().unwrap_or_else(|| format!("{}.cos.{}.myqcloud.com", config.bucket, config.region));
159
160        let now = Utc::now().timestamp();
161        let start_time = (now / 3600) * 3600;
162        let end_time = start_time + 7200;
163        let key_time = format!("{};{}", start_time, end_time);
164
165        let base_url = if host.starts_with("http") { Url::parse(&host) } else { Url::parse(&format!("https://{}", host)) }
166            .map_err(|e| WaeError::invalid_params("base_url", e.to_string()))?;
167
168        let mut url = base_url.join(key.trim_start_matches('/')).map_err(|e| WaeError::invalid_params("url", e.to_string()))?;
169
170        let host_only = url.host_str().unwrap_or("").to_lowercase();
171
172        let http_method = "put";
173        let http_uri = ensure_lowercase_hex(url.path());
174        let http_params = "";
175        let http_headers = format!("host={}", host_only);
176        let http_string = format!("{}\n{}\n{}\n{}\n", http_method, http_uri, http_params, http_headers);
177
178        let http_string_hash = sha1_hex(&http_string);
179        let string_to_sign = format!("sha1\n{}\n{}\n", key_time, http_string_hash);
180
181        let sign_key = hmac_sha1_hex(config.secret_key.as_bytes(), &key_time);
182        let signature = hmac_sha1_hex(sign_key.as_bytes(), &string_to_sign);
183
184        let mut query_parts = Vec::new();
185        query_parts.push("q-sign-algorithm=sha1".to_string());
186        query_parts.push(format!("q-ak={}", cos_encode(&config.secret_id)));
187        query_parts.push(format!("q-sign-time={}", cos_encode(&key_time)));
188        query_parts.push(format!("q-key-time={}", cos_encode(&key_time)));
189        query_parts.push("q-header-list=host".to_string());
190        query_parts.push("q-url-param-list=".to_string());
191        query_parts.push(format!("q-signature={}", signature));
192
193        url.set_query(Some(&query_parts.join("&")));
194
195        Ok(url)
196    }
197}