1use crate::client::{Calculator, Client};
2use crate::error::Error;
3use crate::util::date::{acs_short_date, date};
4use crate::util::request::{canonical, hash, hex, sign, sign_string};
5use crate::{Result, __setter};
6use reqwest::header::HeaderMap;
7use reqwest::Method;
8use serde::Serialize;
9use std::collections::HashMap;
10const SIGN_ALGORITHM: &'static str = "OSS4-HMAC-SHA256";
11
12#[derive(Debug, Default)]
13pub struct PresignUrlReqeust {
14 pub expire: i64,
15 pub method: Method,
16 pub bucket: String,
17 pub host: Option<String>,
18 pub ssl: bool,
19 pub uri: String,
20 pub queries: Option<HashMap<String, String>>,
21 pub headers: Option<HashMap<String, String>>,
22}
23
24impl PresignUrlReqeust {
25 __setter!(expire: i64);
26 __setter!(method: Method);
27 __setter!(bucket: String);
28 __setter!(host: Option<String>);
29 __setter!(ssl: bool);
30 __setter!(uri: String);
31 __setter!(queries: Option<HashMap<String, String>>);
32 __setter!(headers: Option<HashMap<String, String>>);
33
34 pub fn get() -> Self {
35 Self {
36 method: Method::GET,
37 ..Default::default()
38 }
39 }
40 pub fn post() -> Self {
41 Self {
42 method: Method::POST,
43 ..Default::default()
44 }
45 }
46
47
48 pub fn put() -> Self {
49 Self {
50 method: Method::PUT,
51 ..Default::default()
52 }
53 }
54
55 fn credential(&self, access_key_id: &str, region: &str) -> String {
56 let credential = format!(
57 "{}/{}/{}/oss/aliyun_v4_request",
58 access_key_id,
59 date(),
60 region
61 );
62 credential
63 }
64
65 pub fn signature(&self, key: &str, data: &str, date: &str, region: &str) -> Result<String> {
66 let key1 = sign(format!("aliyun_v4{}", key).as_bytes(), date)?;
67 let key2 = sign(&key1, region)?;
68 let key3 = sign(&key2, "oss")?;
69 let key = sign(&key3, "aliyun_v4_request")?;
70 Ok(sign_string(&key, data)?)
71 }
72 fn canonical_uri(&self) -> String {
73 format!(
74 "/{}/{}",
75 self.bucket,
76 self.uri
77 .split("/")
78 .map(|s| canonical(s))
79 .collect::<Vec<String>>()
80 .join("/")
81 )
82 }
83
84 fn canonical_headers(&self, headers: &HashMap<String, String>) -> Result<(String, String)> {
85 let mut canonical_headers = headers
86 .iter()
87 .map(|item| (item.0.to_string().to_lowercase(), item.1))
88 .filter(|item| {
89 item.0 == "host"
90 || item.0 == "content-type"
91 || item.0 == "content-md5"
92 || item.0.starts_with("x-acs")
93 })
94 .collect::<Vec<(_, _)>>();
95
96 canonical_headers.sort_by(|a, b| a.0.cmp(&b.0));
97
98 let mut header_string = canonical_headers
99 .iter()
100 .map(|item| format!("{}:{}\n", item.0, item.1.trim()))
101 .fold(String::new(), |acc, item| acc + &item);
102
103 let canonical_header_name_string = canonical_headers
104 .iter()
105 .map(|item| item.0.clone())
106 .collect::<Vec<_>>()
107 .join(";");
108 Ok((header_string, canonical_header_name_string))
109 }
110
111 fn canonical_queries(&self, queries: &HashMap<String, String>) -> Result<String> {
112 let mut canonical_queries = queries
113 .iter()
114 .map(|item| (item.0.to_string().to_lowercase(), item.1))
115 .collect::<Vec<(_, _)>>();
116 canonical_queries.sort_by(|a, b| a.0.cmp(&b.0));
117 Ok(serde_urlencoded::to_string(canonical_queries)?)
118 }
119
120 fn build_queries(&self, client: &Client) -> Result<HashMap<String, String>> {
121 let Client {
122 access_key_id,
123 region,
124 ..
125 } = client;
126
127 let region = if let Some(region) = region {
128 region
129 } else {
130 return Err(Error::CommonError("region should be set".to_string()));
131 };
132
133 let mut queries = if let Some(queries) = &self.queries {
134 queries.clone()
135 } else {
136 HashMap::new()
137 };
138
139 queries.insert(
140 "x-oss-signature-version".to_string(),
141 SIGN_ALGORITHM.to_string(),
142 );
143 queries.insert(
144 "x-oss-credential".to_string(),
145 self.credential(access_key_id, region),
146 );
147 queries.insert("x-oss-date".to_string(), acs_short_date());
148 queries.insert("x-oss-expires".to_string(), format!("{}", self.expire));
149
150 let (header_string, header_name_string) = if let Some(headers) = &self.headers {
151 self.canonical_headers(&headers)?
152 } else {
153 (String::new(), String::new())
154 };
155
156 if !header_name_string.is_empty() {
157 queries.insert(
158 "x-oss-additional-headers".to_string(),
159 header_name_string.clone(),
160 );
161 }
162
163 let canonical_request = format!(
164 "{}\n{}\n{}\n{}\n{}\nUNSIGNED-PAYLOAD",
165 self.method,
166 self.canonical_uri(),
167 self.canonical_queries(&queries)?,
168 header_string,
169 header_name_string
170 );
171
172 log::debug!("canonical request: {}", canonical_request);
173 let date_str = date();
174 let pre_sign = format!(
175 "{}\n{}\n{}/{}/oss/aliyun_v4_request\n{}",
176 SIGN_ALGORITHM,
177 acs_short_date(),
178 date_str,
179 region,
180 hex(&canonical_request)
181 );
182 log::debug!("pre-sign: {}", pre_sign);
183 let sign = self.signature(&client.access_key_secret, &pre_sign, &date_str, region)?;
184 queries.insert("x-oss-signature".to_string(), sign);
185 Ok(queries)
186 }
187}
188
189impl Calculator<String> for PresignUrlReqeust {
190 fn calculate(&self, client: &Client) -> Result<String> {
191 let queries = self.build_queries(client)?;
192 let host = if let Some(host) = &self.host {
193 host
194 } else {
195 &format!("{}.{}", self.bucket, client.endpoint)
196 };
197 Ok(format!(
198 "{}{}/{}?{}",
199 if self.ssl { "https://" } else { "http://" },
200 host,
201 self.uri,
202 serde_urlencoded::to_string(&queries)?
203 ))
204 }
205}