Skip to main content

ali_oss_rs/
request.rs

1use std::{
2    collections::{HashMap, HashSet},
3    fmt::Display,
4    ops::Range,
5    path::PathBuf,
6};
7
8use crate::{common, util};
9
10/// Request body
11#[derive(Debug, Default, Clone)]
12pub enum RequestBody {
13    #[default]
14    Empty,
15    Text(String),
16    Bytes(Vec<u8>),
17
18    /// `.1` is used when doing multipart uploads from file.
19    File(PathBuf, Option<Range<u64>>),
20}
21
22/// Request method
23#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub enum RequestMethod {
25    Get,
26    Put,
27    Post,
28    Delete,
29    Head,
30}
31
32impl Display for RequestMethod {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        match self {
35            RequestMethod::Get => write!(f, "GET"),
36            RequestMethod::Put => write!(f, "PUT"),
37            RequestMethod::Post => write!(f, "POST"),
38            RequestMethod::Delete => write!(f, "DELETE"),
39            RequestMethod::Head => write!(f, "HEAD"),
40        }
41    }
42}
43
44impl From<RequestMethod> for reqwest::Method {
45    fn from(value: RequestMethod) -> Self {
46        match value {
47            RequestMethod::Get => reqwest::Method::GET,
48            RequestMethod::Put => reqwest::Method::PUT,
49            RequestMethod::Post => reqwest::Method::POST,
50            RequestMethod::Delete => reqwest::Method::DELETE,
51            RequestMethod::Head => reqwest::Method::HEAD,
52        }
53    }
54}
55
56/// Raw oss request
57pub struct OssRequest {
58    pub bucket_name: String,
59    pub object_key: String,
60    pub method: RequestMethod,
61    pub headers: HashMap<String, String>,
62
63    // 这个是根据官方文档的名字来取的属性名。
64    // 实际上这个表示除了 Content-Type、Content-MD5 之外还需要需要参与签名的请求头的名字
65    // 由于构建 Canonical Headers 的时候,请求头都是小写的,所以这里在存入 Set 的时候就转换小写。
66    pub additional_headers: HashSet<String>,
67    pub query: HashMap<String, String>,
68
69    pub body: RequestBody,
70}
71
72impl Default for OssRequest {
73    fn default() -> Self {
74        Self::new()
75    }
76}
77
78impl OssRequest {
79    /// Create a new instance with following headers are set:
80    ///
81    /// - `x-sdk-client`: `ali-oss-rs/{version_no}`
82    /// - `x-oss-content-sha256` is set to literal string `UNSIGNED-PAYLOAD`
83    /// - `x-oss-date` is set to current time. e.g. `20231203T121212Z`
84    pub fn new() -> Self {
85        let date_time_string = util::get_iso8601_date_time_string();
86        Self {
87            bucket_name: "".to_string(),
88            object_key: "".to_string(),
89            method: RequestMethod::Get,
90            headers: HashMap::from([
91                ("x-sdk-client".to_string(), format!("ali-oss-rs/{}", common::VERSION)),
92                ("x-oss-content-sha256".to_string(), "UNSIGNED-PAYLOAD".to_string()),
93                ("x-oss-date".to_string(), date_time_string),
94            ]),
95            additional_headers: HashSet::new(),
96            query: HashMap::new(),
97            body: RequestBody::Empty,
98        }
99    }
100
101    /// Set request method.
102    pub fn method(mut self, m: RequestMethod) -> Self {
103        self.method = m;
104        self
105    }
106
107    /// Set bucket name.
108    pub fn bucket<S: Into<String>>(mut self, bucket_name: S) -> Self {
109        self.bucket_name = bucket_name.into();
110        self
111    }
112
113    /// Set object key.
114    pub fn object<S: Into<String>>(mut self, object_key: S) -> Self {
115        self.object_key = object_key.into();
116        self
117    }
118
119    /// Add header to the builder and **DO NOT** treat this header as additional header.
120    /// The header name should be lowercase string. e.g. `content-type`, `x-oss-meta-my-key`.
121    pub fn add_header<S1, S2>(self, k: S1, v: S2) -> Self
122    where
123        S1: AsRef<str>,
124        S2: AsRef<str>,
125    {
126        self.add_header_ext(k, v, false)
127    }
128
129    /// Add header to the builder. The header name should be lowercase string. e.g. `content-type`, `x-oss-meta-my-key`.
130    ///
131    /// `addtional_header` identifies if the header name should be added to additional header,
132    /// and being used when calculating canonical request and signature
133    pub fn add_header_ext<S1, S2>(mut self, k: S1, v: S2, addtional_header: bool) -> Self
134    where
135        S1: AsRef<str>,
136        S2: AsRef<str>,
137    {
138        self.headers.insert(k.as_ref().to_string(), v.as_ref().to_string());
139        if addtional_header {
140            self.additional_headers.insert(k.as_ref().to_lowercase());
141        }
142
143        self
144    }
145
146    /// Add query parameters. Parameter names and values do not need to be URL encoded,
147    /// as they will be automatically encoded when calculating the signature.
148    /// For example: `add_query("userName", "张三")`.
149    pub fn add_query<S1, S2>(mut self, k: S1, v: S2) -> Self
150    where
151        S1: AsRef<str>,
152        S2: AsRef<str>,
153    {
154        self.query.insert(k.as_ref().to_string(), v.as_ref().to_string());
155        self
156    }
157
158    /// Add the header name to additional headers which will be used in signature calculating.
159    #[allow(dead_code)]
160    pub fn add_additional_header_name<S>(mut self, name: S) -> Self
161    where
162        S: AsRef<str>,
163    {
164        self.additional_headers.insert(name.as_ref().to_lowercase());
165        self
166    }
167
168    /// Set request body.
169    pub fn body(mut self, body: RequestBody) -> Self {
170        self.body = body;
171        self
172    }
173
174    /// helper method for [`Self::body`]. only the body is set and left `content-length`, `content-type` untouched.
175    pub fn text_body(self, text: impl Into<String>) -> Self {
176        self.body(RequestBody::Text(text.into()))
177    }
178
179    #[allow(dead_code)]
180    /// helper method for [`Self::body`]. only the body is set and left `content-length`, `content-type` untouched.
181    pub fn bytes_body(self, bytes: impl Into<Vec<u8>>) -> Self {
182        self.body(RequestBody::Bytes(bytes.into()))
183    }
184
185    #[allow(dead_code)]
186    /// helper method for [`Self::body`]. only the body is set and left `content-length`, `content-type` untouched.
187    pub fn file_body(self, file_path: impl Into<PathBuf>) -> Self {
188        self.body(RequestBody::File(file_path.into(), None))
189    }
190
191    /// Set `content-type` header.
192    pub fn content_type(mut self, content_type: &str) -> Self {
193        self.headers.insert("content-type".to_string(), content_type.to_string());
194        self
195    }
196
197    /// Set `content-length` header.
198    pub fn content_length(mut self, len: u64) -> Self {
199        self.headers.insert("content-length".to_string(), len.to_string());
200        self
201    }
202
203    #[allow(dead_code)]
204    pub fn headers_mut(&mut self) -> &mut HashMap<String, String> {
205        &mut self.headers
206    }
207
208    #[allow(dead_code)]
209    pub fn additional_headers_mut(&mut self) -> &mut HashSet<String> {
210        &mut self.additional_headers
211    }
212
213    #[allow(dead_code)]
214    pub fn query_mut(&mut self) -> &mut HashMap<String, String> {
215        &mut self.query
216    }
217
218    ///
219    /// 官方文档:<https://help.aliyun.com/zh/oss/developer-reference/recommend-to-use-signature-version-4>
220    ///
221    /// - 如果请求的 URI 中既包含 Bucket 也包含 Object,则 Canonical URI 填写示例为: `/examplebucket/exampleobject`
222    /// - 如果请求的 URI 中只包含 Bucket 不包含 Object,则 Canonical URI 填写示例为: `/examplebucket/`
223    /// - 如果请求的 URI 中不包含 Bucket 且不包含 Object,则 Canonical URI 填写示例为: `/`
224    ///
225    pub(crate) fn build_canonical_uri(&self) -> String {
226        match (self.bucket_name.is_empty(), self.object_key.is_empty()) {
227            (true, true) => "/".to_string(),
228            (true, false) => format!("/{}/", urlencoding::encode(&self.bucket_name)),
229            (_, _) => {
230                format!(
231                    "/{}/{}",
232                    urlencoding::encode(&self.bucket_name),
233                    self.object_key.split("/").map(|s| urlencoding::encode(s)).collect::<Vec<_>>().join("/")
234                )
235            }
236        }
237    }
238
239    /// Build the uri part of real http request
240    /// The returned string starts with "/"
241    pub(crate) fn build_request_uri(&self) -> String {
242        if self.object_key.is_empty() {
243            return "/".to_string();
244        }
245
246        let s = self.object_key.split("/").collect::<Vec<_>>().join("/");
247        format!("/{}", s)
248    }
249
250    /// 按 QueryString 的 key 进行排序。
251    ///
252    /// - 先编码,再排序
253    /// - 如果有多个相同的 key,按照原来添加的顺序放置即可
254    /// - 中间使用 `&`进行连接。
255    /// - 只有 key 没有 value 的情况下,只添加 key即可
256    /// - 如果没有 QueryString,则只需要放置空字符串 “”,末尾仍然需要有换行符。
257    ///
258    pub(crate) fn build_canonical_query_string(&self) -> String {
259        if self.query.is_empty() {
260            return "".to_string();
261        }
262
263        let mut pairs = self
264            .query
265            .iter()
266            .map(|(k, v)| (urlencoding::encode(k), urlencoding::encode(v)))
267            .collect::<Vec<(_, _)>>();
268
269        pairs.sort_by(|e1, e2| e1.0.cmp(&e2.0));
270
271        pairs
272            .iter()
273            .map(|(k, v)| if v.is_empty() { k.to_string() } else { format!("{}={}", k, v) })
274            .collect::<Vec<_>>()
275            .join("&")
276    }
277
278    /// 对请求 Header 的列表格式化后的字符串,各个 Header 之间需要添加换行符分隔。
279    ///
280    /// - 单个 Header 中的 key 和 value 通过冒号 `:` 分隔, Header 与 Header 之间通过换行符分隔。
281    /// - Header 的key必须小写,value 必须经过Trim(去除头尾的空格)。
282    /// - 按 Header 中 key 的字典序进行排列。
283    /// - 请求时间通过 `x-oss-date` 来描述,要求格式必须是 ISO8601 标准时间格式(示例值为 `20231203T121212Z`)。
284    ///
285    /// Canonical  Headers 包含以下两类:
286    ///
287    /// - 必须存在且参与签名的 Header 包括:
288    ///   - `x-oss-content-sha256`(其值为 `UNSIGNED-PAYLOAD`)
289    ///   - Additional Header 指定必须存在且参与签名的 Header
290    /// - 如果存在则加入签名的 Header 包括:
291    ///   - `Content-Type`
292    ///   - `Content-MD5`
293    ///   - `x-oss-*`
294    pub(crate) fn build_canonical_headers(&self) -> String {
295        // If no header are set, just return empty string without line break
296        if self.headers.is_empty() {
297            return "".to_string();
298        }
299
300        let mut pairs = self
301            .headers
302            .iter()
303            .map(|(k, v)| (k.to_lowercase(), v))
304            .filter(|(k, _)| k == "content-type" || k == "content-md5" || k.starts_with("x-oss-") || self.additional_headers.contains(k))
305            .collect::<Vec<_>>();
306
307        pairs.sort_by(|a, b| a.0.cmp(&b.0));
308
309        let s = pairs.iter().map(|(k, v)| format!("{}:{}", k, v.trim())).collect::<Vec<_>>().join("\n");
310
311        // 不知道为什么这里要多一个空行
312        // 参考 Java SDK 的代码:
313        //   // Canonical Headers + "\n" +
314        //   orderMap = buildSortedHeadersMap(request.getHeaders());
315        //   canonicalPart = new StringBuilder();
316        //   for (Map.Entry<String, String> param : orderMap.entrySet()) {
317        //       canonicalPart.append(param.getKey()).append(":").append(param.getValue().trim()).append(SignParameters.NEW_LINE);
318        //   }
319        //   canonicalString.append(canonicalPart).append(SignParameters.NEW_LINE);
320        //
321        // 看起来是每对儿 header k:v 后面都跟了一个换行符,我们这里使用 `join` 的话,实际上缺少一个换行符,所以在最后返回的时候不冲上去
322        format!("{}\n", s)
323    }
324
325    pub(crate) fn build_additional_headers(&self) -> String {
326        if self.additional_headers.is_empty() {
327            return "".to_string();
328        }
329
330        let mut keys = self.additional_headers.iter().map(|k| k.to_lowercase()).collect::<Vec<_>>();
331
332        keys.sort();
333
334        keys.join(";")
335    }
336
337    pub(crate) fn build_canonical_request(&self) -> String {
338        let canonical_uri = self.build_canonical_uri();
339        let canonical_query = self.build_canonical_query_string();
340        let canonical_headers = self.build_canonical_headers();
341        let additional_headers = self.build_additional_headers();
342        let method = self.method.to_string();
343
344        format!("{method}\n{canonical_uri}\n{canonical_query}\n{canonical_headers}\n{additional_headers}\nUNSIGNED-PAYLOAD")
345    }
346
347    /// 在构建要签名的认证字符串的时候,才生成 `x-oss-date` 头,并放到 headers 里面
348    pub(crate) fn build_string_to_sign(&self, region: &str) -> String {
349        let date_time_string = self.headers.get("x-oss-date").unwrap();
350        let date_string = &date_time_string[..8];
351
352        let canonical_request = self.build_canonical_request();
353
354        log::debug!("canonical request: \n--------\n{}\n--------", canonical_request);
355
356        let canonical_request_hash = util::sha256(canonical_request.as_bytes());
357
358        format!(
359            "OSS4-HMAC-SHA256\n{}\n{}/{}/oss/aliyun_v4_request\n{}",
360            date_time_string,
361            date_string,
362            region,
363            hex::encode(&canonical_request_hash)
364        )
365    }
366}