wae_storage/providers/
cos.rs1use 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
12pub struct CosProvider;
16
17fn 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
26fn hmac_sha1_hex(key: &[u8], msg: &str) -> String {
28 hex_encode(&hmac_sha1(key, msg))
29}
30
31fn cos_encode(s: &str) -> String {
33 let encoded = url_encode(s);
34 ensure_lowercase_hex(&encoded)
35}
36
37fn 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
58fn 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 ¶ms_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(¶m_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}