tdm_server_rust/utils/
cos_presign.rs1use hmac::{Hmac, Mac};
19use sha1::{Digest, Sha1};
20use std::collections::BTreeMap;
21use std::time::{SystemTime, UNIX_EPOCH};
22
23type HmacSha1 = Hmac<Sha1>;
24
25pub fn presigned_url(
55 secret_id: &str,
56 secret_key: &str,
57 bucket: &str,
58 region: &str,
59 object_key: &str,
60 method: &str,
61 duration_secs: u64,
62 session_token: &str,
63) -> crate::error::ApiResult<String> {
64 if secret_id.is_empty() || secret_key.is_empty() {
65 return Err(crate::error::AppError::Oss {
66 code: None,
67 msg: "腾讯云 SecretId/SecretKey 未配置".into(),
68 });
69 }
70 let now = SystemTime::now()
71 .duration_since(UNIX_EPOCH)
72 .map_err(|e| crate::error::AppError::Internal(e.to_string()))?
73 .as_secs();
74 let end = now + duration_secs;
75 let key_time = format!("{now};{end}");
76 let sign_key = hmac_sha1_hex(secret_key.as_bytes(), &key_time);
77
78 let host = format!("{bucket}.cos.{region}.myqcloud.com");
79 let uri_path = sign_uri_path(object_key);
80 let request_path = encode_object_path(object_key);
81
82 let mut url_params = BTreeMap::new();
83 if !session_token.is_empty() {
84 url_params.insert(
85 "x-cos-security-token".to_string(),
86 session_token.to_string(),
87 );
88 }
89 let (format_parameters, signed_param_list) = format_parameters(&url_params);
90
91 let mut headers = BTreeMap::new();
92 headers.insert("host".to_string(), host.clone());
93 let (format_headers, signed_header_list) = format_headers(&headers);
94
95 let http_string = format!(
96 "{}\n{}\n{}\n{}\n",
97 method.to_lowercase(),
98 uri_path,
99 format_parameters,
100 format_headers
101 );
102 let string_to_sign = format!("sha1\n{key_time}\n{}\n", sha1_hex(&http_string));
103 let signature = hmac_sha1_hex(sign_key.as_bytes(), &string_to_sign);
104
105 let encoded_path = request_path;
106 let auth_query = format!(
107 "q-sign-algorithm=sha1&q-ak={secret_id}&q-sign-time={key_time}&q-key-time={key_time}\
108 &q-header-list={}&q-url-param-list={}&q-signature={signature}",
109 signed_header_list.join(";"),
110 signed_param_list.join(";")
111 );
112
113 let base = format!("https://{host}{encoded_path}");
114 if url_params.is_empty() {
115 Ok(format!("{base}?{auth_query}"))
116 } else {
117 let token_qs = url_params
118 .iter()
119 .map(|(k, v)| format!("{}={}", safe_url_encode(k), safe_url_encode(v)))
120 .collect::<Vec<_>>()
121 .join("&");
122 Ok(format!("{base}?{token_qs}&{auth_query}"))
123 }
124}
125
126fn sign_uri_path(object_key: &str) -> String {
128 let trimmed = object_key.trim_start_matches('/');
129 if trimmed.is_empty() {
130 "/".into()
131 } else {
132 format!("/{trimmed}")
133 }
134}
135
136fn encode_uri_path(object_key: &str) -> String {
138 let trimmed = object_key.trim_start_matches('/');
139 if trimmed.is_empty() {
140 return "/".into();
141 }
142 format!(
143 "/{}",
144 trimmed
145 .split('/')
146 .map(|seg| safe_url_encode(seg))
147 .collect::<Vec<_>>()
148 .join("/")
149 )
150}
151
152fn encode_object_path(object_key: &str) -> String {
154 encode_uri_path(object_key)
155}
156
157fn format_parameters(params: &BTreeMap<String, String>) -> (String, Vec<String>) {
159 let mut signed_list = Vec::new();
160 let mut pairs = Vec::new();
161 for (k, v) in params {
162 let lk = safe_url_encode(&k.to_lowercase());
163 signed_list.push(lk.clone());
164 pairs.push(format!("{lk}={}", safe_url_encode(v)));
165 }
166 signed_list.sort();
167 (pairs.join("&"), signed_list)
168}
169
170fn format_headers(headers: &BTreeMap<String, String>) -> (String, Vec<String>) {
172 let mut signed_list = Vec::new();
173 let mut pairs = Vec::new();
174 for (k, v) in headers {
175 let lk = safe_url_encode(&k.to_lowercase());
176 signed_list.push(lk.clone());
177 pairs.push(format!("{lk}={}", safe_url_encode(v)));
178 }
179 signed_list.sort();
180 (pairs.join("&"), signed_list)
181}
182
183fn safe_url_encode(s: &str) -> String {
185 urlencoding::encode(s)
186 .replace('!', "%21")
187 .replace('\'', "%27")
188 .replace('(', "%28")
189 .replace(')', "%29")
190 .replace('*', "%2A")
191}
192
193fn hmac_sha1_hex(key: &[u8], data: &str) -> String {
195 let mut mac = HmacSha1::new_from_slice(key).expect("hmac key");
196 mac.update(data.as_bytes());
197 hex::encode(mac.finalize().into_bytes())
198}
199
200fn sha1_hex(data: &str) -> String {
202 let mut hasher = Sha1::new();
203 hasher.update(data.as_bytes());
204 hex::encode(hasher.finalize())
205}