Skip to main content

ali_oss_rs/
presign.rs

1//! Trait and implementation for pre-signing URL for OSS object
2
3use std::collections::HashMap;
4
5use crate::{
6    presign_common::{build_presign_get_request, PresignGetOptions},
7    request::OssRequest,
8    util::{self, get_iso8601_date_time_string},
9    Client,
10};
11
12/// All data for sending request to aliyun oss api after signature calculated
13#[derive(Debug, Clone)]
14#[cfg_attr(feature = "serde-support", derive(serde::Serialize, serde::Deserialize))]
15#[cfg_attr(feature = "serde-camelcase", serde(rename_all = "camelCase"))]
16pub struct SignedOssRequest {
17    /// The full url
18    pub url: String,
19
20    /// The request headers with calculated authorization value.
21    pub headers: HashMap<String, String>,
22}
23
24impl Client {
25    /// Presign URL for GET request without any additional headers supported, for browser mostly
26    pub fn presign_url<S1, S2>(&self, bucket_name: S1, object_key: S2, options: PresignGetOptions) -> String
27    where
28        S1: AsRef<str>,
29        S2: AsRef<str>,
30    {
31        let mut request = build_presign_get_request(bucket_name.as_ref(), object_key.as_ref(), &options);
32
33        let date_time_string = request.query.get("x-oss-date").unwrap().clone();
34        let date_string = &date_time_string[..8];
35
36        let credential = format!("{}/{}/{}/oss/aliyun_v4_request", self.access_key_id, date_string, self.region);
37
38        request = request.add_query("x-oss-credential", &credential);
39
40        if let Some(s) = &self.sts_token {
41            request = request.add_query("x-oss-security-token", s);
42        }
43
44        let canonical_request = request.build_canonical_request();
45
46        let canonical_request_hash = util::sha256(canonical_request.as_bytes());
47
48        let string_to_sign = format!(
49            "OSS4-HMAC-SHA256\n{}\n{}/{}/oss/aliyun_v4_request\n{}",
50            date_time_string,
51            date_string,
52            self.region,
53            hex::encode(&canonical_request_hash)
54        );
55
56        let sig = self.calculate_signature(&string_to_sign, date_string);
57
58        request = request.add_query("x-oss-signature", &sig);
59
60        let uri = request.build_request_uri();
61        let query_string = request.build_canonical_query_string();
62
63        let domain_name = if request.bucket_name.is_empty() {
64            format!("{}://{}{}", self.scheme, self.endpoint, uri)
65        } else {
66            format!("{}://{}.{}{}", self.scheme, request.bucket_name, self.endpoint, uri)
67        };
68
69        if query_string.is_empty() {
70            domain_name
71        } else {
72            format!("{}?{}", domain_name, query_string)
73        }
74    }
75
76    /// Presign a raw request, get the url and headers which contain calculated signature.
77    /// So you can use the url and headers in other applications, frameworks or languages to complete the request.
78    ///
79    /// # Examples
80    ///
81    /// Get the presigned url and headers using `ali-oss-rs` crate.
82    ///
83    /// ```rust
84    /// let client = Client::from_env();
85    ///
86    /// let object = format!("rust-sdk-test/{}.webp", Uuid::new_v4());
87    ///
88    /// let request = OssRequest::new()
89    ///     .method(RequestMethod::Put)
90    ///     .bucket("yuanyq")
91    ///     .object(&object)
92    ///     .add_header("content-type", "image/webp")
93    ///     .add_header("content-length", "36958");
94    ///
95    /// let SignedOssRequest {url, headers} = client.presign_raw_request(request);
96    /// log::debug!("{} {:#?}", url, headers);
97    /// ```
98    ///
99    /// You will get the headers includes calculated authorization string.
100    /// Then copy the url and headers to your javascript code which sending HTTP request using Axios:
101    ///
102    /// ```javascript
103    /// const Axios = require("axios");
104    /// const fs = require("fs");
105    ///
106    /// const axios = Axios.create();
107    ///
108    /// (async function() {
109    ///     const filePath = "/home/yuanyq/Pictures/test-8.webp";
110    ///     const fileStream = fs.createReadStream(filePath);
111    ///
112    ///     const response = await axios.request({
113    ///         method: "PUT",
114    ///         url: "https://yuanyq.oss-cn-beijing.aliyuncs.com/rust-sdk-test/xxxxx.webp",
115    ///         headers: {
116    ///             "content-type": "image/webp",
117    ///             "authorization": "OSS4-HMAC-SHA256 Credential=LTAIxxxxxxpeA/20250228/cn-beijing/oss/aliyun_v4_request,Signature=xxxxx",
118    ///             "x-oss-content-sha256": "UNSIGNED-PAYLOAD",
119    ///             "x-sdk-client": "ali-oss-rs/0.1.3",
120    ///             "x-oss-date": "20250228T074254Z",
121    ///             "content-length": "36958",
122    ///         },
123    ///         data: fileStream
124    ///     });
125    ///     console.log(response);
126    /// })();
127    /// ```
128    ///
129    pub fn presign_raw_request(&self, mut oss_request: OssRequest) -> SignedOssRequest {
130        let date_header = "x-oss-date".to_string();
131        {
132            oss_request.headers_mut().entry(date_header.clone()).or_insert(get_iso8601_date_time_string());
133        }
134
135        let date_time_string = oss_request.headers.get(&date_header).unwrap().to_string();
136        let date_string = &date_time_string[..8];
137
138        if let Some(s) = &self.sts_token {
139            if !oss_request.headers.contains_key("x-oss-security-token") {
140                oss_request = oss_request.add_header("x-oss-security-token", s);
141            }
142        }
143        let additional_headers = oss_request.build_additional_headers();
144        let string_to_sign = oss_request.build_string_to_sign(&self.region);
145
146        log::debug!("string to sign: \n--------\n{}\n--------", string_to_sign);
147
148        let sig = self.calculate_signature(&string_to_sign, date_string);
149
150        log::debug!("signature: {}", sig);
151
152        let auth_string = format!(
153            "OSS4-HMAC-SHA256 Credential={}/{}/{}/oss/aliyun_v4_request,{}Signature={}",
154            self.access_key_id,
155            date_string,
156            self.region,
157            if additional_headers.is_empty() {
158                "".to_string()
159            } else {
160                format!("{},", additional_headers)
161            },
162            sig
163        );
164
165        oss_request = oss_request.add_header("authorization", &auth_string);
166
167        let uri = oss_request.build_request_uri();
168        let query_string = oss_request.build_canonical_query_string();
169
170        let url = if oss_request.bucket_name.is_empty() {
171            format!("{}://{}{}", self.scheme, self.endpoint, uri)
172        } else {
173            format!("{}://{}.{}{}", self.scheme, oss_request.bucket_name, self.endpoint, uri)
174        };
175
176        let url = if query_string.is_empty() { url } else { format!("{}?{}", url, query_string) };
177
178        SignedOssRequest {
179            url,
180            headers: oss_request.headers,
181        }
182    }
183}
184
185#[cfg(all(test, feature = "blocking"))]
186mod test_presign {
187    use std::{str::FromStr, sync::Once};
188
189    use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
190    use uuid::Uuid;
191
192    use crate::{
193        presign::SignedOssRequest,
194        presign_common::PresignGetOptionsBuilder,
195        request::{OssRequest, RequestMethod},
196        util::debug_blocking_request,
197        Client,
198    };
199
200    static INIT: Once = Once::new();
201
202    fn setup() {
203        INIT.call_once(|| {
204            simple_logger::init_with_level(log::Level::Debug).unwrap();
205            dotenvy::dotenv().unwrap();
206        });
207    }
208
209    #[test]
210    fn test_presign_get_with_options() {
211        setup();
212        let client = Client::from_env();
213
214        let bucket = "yuanyq";
215        let object = "rust-sdk-test/test-1.webp";
216
217        let options = PresignGetOptionsBuilder::new(3600).process("style/test-img-process").build();
218
219        let url = client.presign_url(bucket, object, options);
220
221        log::debug!("{}", url);
222
223        let response = reqwest::blocking::get(url);
224        assert!(response.is_ok());
225        assert_eq!(reqwest::StatusCode::OK, response.unwrap().status());
226    }
227
228    #[test]
229    fn test_presign_raw_request() {
230        setup();
231        let client = Client::from_env();
232
233        let request = OssRequest::new()
234            .method(RequestMethod::Get)
235            .bucket("yuanyq")
236            .object("rust-sdk-test/20dcd5da-804f-406d-a921-3c3f08de04e3.jpg")
237            .add_header("x-oss-expires", "3600")
238            .add_query("x-oss-process", "style/test-img-process");
239
240        let SignedOssRequest { url, headers } = client.presign_raw_request(request);
241        log::debug!("{} {:#?}", url, headers);
242
243        let mut req_headers = HeaderMap::new();
244        headers.into_iter().for_each(|(k, v)| {
245            req_headers.append(HeaderName::from_str(k.as_str()).unwrap(), HeaderValue::from_str(v.as_str()).unwrap());
246        });
247
248        let http_client = reqwest::blocking::Client::new();
249        let req = http_client
250            .request(reqwest::Method::GET, reqwest::Url::parse(url.as_str()).unwrap())
251            .headers(req_headers)
252            .build()
253            .unwrap();
254
255        debug_blocking_request(&req);
256
257        let response = http_client.execute(req);
258        assert!(response.is_ok());
259        let response = response.unwrap();
260        assert_eq!(reqwest::StatusCode::OK, response.status());
261    }
262
263    #[test]
264    fn test_presign_raw_request_2() {
265        setup();
266        let client = Client::from_env();
267
268        let object = format!("rust-sdk-test/{}.webp", Uuid::new_v4());
269
270        let request = OssRequest::new()
271            .method(RequestMethod::Put)
272            .bucket("yuanyq")
273            .object(&object)
274            .add_header("content-type", "image/webp")
275            .add_header("content-length", "36958");
276
277        let SignedOssRequest { url, headers } = client.presign_raw_request(request);
278        log::debug!("{} {:#?}", url, headers);
279    }
280}