zino_auth/
authentication.rs1use super::{AccessKeyId, SecretAccessKey};
2use hmac::{
3 Mac,
4 digest::{FixedOutput, KeyInit, MacMarker, Update},
5};
6use http::HeaderMap;
7use std::time::Duration;
8use zino_core::{Map, datetime::DateTime, encoding::base64, error::Error, validation::Validation};
9
10pub struct Authentication {
12 service_name: String,
14 access_key_id: AccessKeyId,
16 signature: String,
18 method: String,
20 accept: Option<String>,
22 content_md5: Option<String>,
24 content_type: Option<String>,
26 date_header: (&'static str, DateTime),
28 expires: Option<DateTime>,
30 headers: Vec<(String, String)>,
32 resource: String,
34}
35
36impl Authentication {
37 #[inline]
39 pub fn new(method: &str) -> Self {
40 Self {
41 service_name: String::new(),
42 access_key_id: AccessKeyId::default(),
43 signature: String::new(),
44 method: method.to_ascii_uppercase(),
45 accept: None,
46 content_md5: None,
47 content_type: None,
48 date_header: ("date", DateTime::now()),
49 expires: None,
50 headers: Vec::new(),
51 resource: String::new(),
52 }
53 }
54
55 #[inline]
57 pub fn set_service_name(&mut self, service_name: &str) {
58 self.service_name = service_name.to_ascii_uppercase();
59 }
60
61 #[inline]
63 pub fn set_access_key_id(&mut self, access_key_id: impl Into<AccessKeyId>) {
64 self.access_key_id = access_key_id.into();
65 }
66
67 #[inline]
69 pub fn set_signature(&mut self, signature: String) {
70 self.signature = signature;
71 }
72
73 #[inline]
75 pub fn set_accept(&mut self, accept: Option<String>) {
76 self.accept = accept;
77 }
78
79 #[inline]
81 pub fn set_content_md5(&mut self, content_md5: String) {
82 self.content_md5 = Some(content_md5);
83 }
84
85 #[inline]
87 pub fn set_content_type(&mut self, content_type: Option<String>) {
88 self.content_type = content_type;
89 }
90
91 #[inline]
93 pub fn set_date_header(&mut self, header_name: &'static str, date: DateTime) {
94 self.date_header = (header_name, date);
95 }
96
97 #[inline]
99 pub fn set_expires(&mut self, expires: Option<DateTime>) {
100 self.expires = expires;
101 }
102
103 #[inline]
106 pub fn set_headers(&mut self, headers: HeaderMap, filters: &[&'static str]) {
107 let mut headers = headers
108 .into_iter()
109 .filter_map(|(name, value)| {
110 name.and_then(|name| {
111 let key = name.as_str();
112 if filters.iter().any(|&s| key.starts_with(s)) {
113 value
114 .to_str()
115 .inspect_err(|err| tracing::warn!("invalid header value: {err}"))
116 .ok()
117 .map(|value| (key.to_ascii_lowercase(), value.to_owned()))
118 } else {
119 None
120 }
121 })
122 })
123 .collect::<Vec<_>>();
124 headers.sort_by(|a, b| a.0.cmp(&b.0));
125 self.headers = headers;
126 }
127
128 #[inline]
130 pub fn set_resource(&mut self, path: String, query: Option<&Map>) {
131 if let Some(query) = query {
132 if query.is_empty() {
133 self.resource = path;
134 } else {
135 let mut query_pairs = query.iter().collect::<Vec<_>>();
136 query_pairs.sort_by(|a, b| a.0.cmp(b.0));
137
138 let query = query_pairs
139 .iter()
140 .map(|(key, value)| format!("{key}={value}"))
141 .collect::<Vec<_>>();
142 self.resource = path + "?" + &query.join("&");
143 }
144 } else {
145 self.resource = path;
146 }
147 }
148
149 #[inline]
151 pub fn service_name(&self) -> &str {
152 self.service_name.as_str()
153 }
154
155 #[inline]
157 pub fn access_key_id(&self) -> &str {
158 self.access_key_id.as_str()
159 }
160
161 #[inline]
163 pub fn signature(&self) -> &str {
164 self.signature.as_str()
165 }
166
167 #[inline]
169 pub fn authorization(&self) -> String {
170 let service_name = self.service_name();
171 let access_key_id = self.access_key_id();
172 let signature = self.signature();
173 if service_name.is_empty() {
174 format!("{access_key_id}:{signature}")
175 } else {
176 format!("{service_name} {access_key_id}:{signature}")
177 }
178 }
179
180 pub fn string_to_sign(&self) -> String {
182 let mut sign_parts = Vec::new();
183
184 sign_parts.push(self.method.clone());
186
187 if let Some(accept) = self.accept.as_ref() {
189 sign_parts.push(accept.to_owned());
190 }
191
192 let content_md5 = self
194 .content_md5
195 .as_ref()
196 .map(|s| s.to_owned())
197 .unwrap_or_default();
198 sign_parts.push(content_md5);
199
200 let content_type = self
202 .content_type
203 .as_ref()
204 .map(|s| s.to_owned())
205 .unwrap_or_default();
206 sign_parts.push(content_type);
207
208 if let Some(expires) = self.expires.as_ref() {
210 sign_parts.push(expires.timestamp().to_string());
211 } else {
212 let date_header = &self.date_header;
214 let date = if date_header.0.eq_ignore_ascii_case("date") {
215 date_header.1.to_utc_string()
216 } else {
217 "".to_owned()
218 };
219 sign_parts.push(date);
220 }
221
222 let headers = self
224 .headers
225 .iter()
226 .map(|(name, values)| format!("{}:{}", name, values.trim()))
227 .collect::<Vec<_>>();
228 sign_parts.extend(headers);
229
230 sign_parts.push(self.resource.clone());
232
233 sign_parts.join("\n")
234 }
235
236 pub fn sign_with<H>(&self, secret_access_key: &SecretAccessKey) -> Result<String, Error>
238 where
239 H: FixedOutput + KeyInit + MacMarker + Update,
240 {
241 let string_to_sign = self.string_to_sign();
242 let mut mac = H::new_from_slice(secret_access_key.as_ref())?;
243 mac.update(string_to_sign.as_ref());
244 Ok(base64::encode(mac.finalize().into_bytes()))
245 }
246
247 pub fn validate_with<H>(&self, secret_access_key: &SecretAccessKey) -> Validation
249 where
250 H: FixedOutput + KeyInit + MacMarker + Update,
251 {
252 let mut validation = Validation::new();
253 let current = DateTime::now();
254 let date = self.date_header.1;
255 let max_tolerance = Duration::from_secs(900);
256 if date < current && date < current - max_tolerance
257 || date > current && date > current + max_tolerance
258 {
259 validation.record("date", "untrusted date");
260 }
261 if let Some(expires) = self.expires
262 && current > expires
263 {
264 validation.record("expires", "valid period has expired");
265 }
266
267 let signature = self.signature();
268 if signature.is_empty() {
269 validation.record("signature", "should be nonempty");
270 } else if self
271 .sign_with::<H>(secret_access_key)
272 .is_ok_and(|s| s != signature)
273 {
274 validation.record("signature", "invalid signature");
275 }
276 validation
277 }
278}