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("x-cos-security-token".to_string(), session_token.to_string());
85 }
86 let (format_parameters, signed_param_list) = format_parameters(&url_params);
87
88 let mut headers = BTreeMap::new();
89 headers.insert("host".to_string(), host.clone());
90 let (format_headers, signed_header_list) = format_headers(&headers);
91
92 let http_string = format!(
93 "{}\n{}\n{}\n{}\n",
94 method.to_lowercase(),
95 uri_path,
96 format_parameters,
97 format_headers
98 );
99 let string_to_sign = format!("sha1\n{key_time}\n{}\n", sha1_hex(&http_string));
100 let signature = hmac_sha1_hex(sign_key.as_bytes(), &string_to_sign);
101
102 let encoded_path = request_path;
103 let auth_query = format!(
104 "q-sign-algorithm=sha1&q-ak={secret_id}&q-sign-time={key_time}&q-key-time={key_time}\
105 &q-header-list={}&q-url-param-list={}&q-signature={signature}",
106 signed_header_list.join(";"),
107 signed_param_list.join(";")
108 );
109
110 let base = format!("https://{host}{encoded_path}");
111 if url_params.is_empty() {
112 Ok(format!("{base}?{auth_query}"))
113 } else {
114 let token_qs = url_params
115 .iter()
116 .map(|(k, v)| format!("{}={}", safe_url_encode(k), safe_url_encode(v)))
117 .collect::<Vec<_>>()
118 .join("&");
119 Ok(format!("{base}?{token_qs}&{auth_query}"))
120 }
121}
122
123fn sign_uri_path(object_key: &str) -> String {
125 let trimmed = object_key.trim_start_matches('/');
126 if trimmed.is_empty() {
127 "/".into()
128 } else {
129 format!("/{trimmed}")
130 }
131}
132
133fn encode_uri_path(object_key: &str) -> String {
135 let trimmed = object_key.trim_start_matches('/');
136 if trimmed.is_empty() {
137 return "/".into();
138 }
139 format!(
140 "/{}",
141 trimmed
142 .split('/')
143 .map(|seg| safe_url_encode(seg))
144 .collect::<Vec<_>>()
145 .join("/")
146 )
147}
148
149fn encode_object_path(object_key: &str) -> String {
151 encode_uri_path(object_key)
152}
153
154fn format_parameters(params: &BTreeMap<String, String>) -> (String, Vec<String>) {
156 let mut signed_list = Vec::new();
157 let mut pairs = Vec::new();
158 for (k, v) in params {
159 let lk = safe_url_encode(&k.to_lowercase());
160 signed_list.push(lk.clone());
161 pairs.push(format!("{lk}={}", safe_url_encode(v)));
162 }
163 signed_list.sort();
164 (pairs.join("&"), signed_list)
165}
166
167fn format_headers(headers: &BTreeMap<String, String>) -> (String, Vec<String>) {
169 let mut signed_list = Vec::new();
170 let mut pairs = Vec::new();
171 for (k, v) in headers {
172 let lk = safe_url_encode(&k.to_lowercase());
173 signed_list.push(lk.clone());
174 pairs.push(format!("{lk}={}", safe_url_encode(v)));
175 }
176 signed_list.sort();
177 (pairs.join("&"), signed_list)
178}
179
180fn safe_url_encode(s: &str) -> String {
182 urlencoding::encode(s)
183 .replace('!', "%21")
184 .replace('\'', "%27")
185 .replace('(', "%28")
186 .replace(')', "%29")
187 .replace('*', "%2A")
188}
189
190fn hmac_sha1_hex(key: &[u8], data: &str) -> String {
192 let mut mac = HmacSha1::new_from_slice(key).expect("hmac key");
193 mac.update(data.as_bytes());
194 hex::encode(mac.finalize().into_bytes())
195}
196
197fn sha1_hex(data: &str) -> String {
199 let mut hasher = Sha1::new();
200 hasher.update(data.as_bytes());
201 hex::encode(hasher.finalize())
202}