1use crate::{aws_canonical, aws_format, aws_math};
2use hmac::Mac;
3use std::borrow::Cow;
4use url::Url;
5
6pub const EMPTY_PAYLOAD_SHA: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
7
8#[derive(Debug, Default, Clone, Copy)]
13pub enum S3DateTime {
14 #[default]
15 Now,
16 UnixTimestamp(i64),
17}
18
19impl S3DateTime {
20 pub fn get_offset_datetime(&self) -> time::OffsetDateTime {
21 match self {
22 S3DateTime::Now => time::OffsetDateTime::now_utc(),
23 S3DateTime::UnixTimestamp(timestamp) => {
24 time::OffsetDateTime::from_unix_timestamp(*timestamp).expect("Always valid")
25 }
26 }
27 }
28}
29
30#[derive(Debug, Clone)]
51pub struct S3HeadersBuilder<'a> {
52 pub datetime: S3DateTime,
53 pub access_key: &'a str,
54 pub secret_key: &'a str,
55 pub region: &'a str,
56 pub service: &'a str,
57 pub url: &'a Url,
58 pub method: &'a str,
59 pub headers: &'a [(&'static str, std::string::String)],
60 pub payload_hash: Cow<'a, str>,
61 pub range: Option<(u64, Option<u64>)>,
62}
63
64impl<'a> S3HeadersBuilder<'a> {
65 pub fn new(url: &'a Url) -> Self {
66 Self {
67 datetime: Default::default(),
68 access_key: Default::default(),
69 secret_key: Default::default(),
70 region: Default::default(),
71 service: Default::default(),
72 url,
73 method: Default::default(),
74 headers: Default::default(),
75 payload_hash: Cow::Borrowed(EMPTY_PAYLOAD_SHA),
76 range: Default::default(),
77 }
78 }
79
80 pub fn set_access_key(mut self, value: &'a str) -> Self {
81 self.access_key = value;
82 self
83 }
84
85 pub fn set_secret_key(mut self, value: &'a str) -> Self {
86 self.secret_key = value;
87 self
88 }
89
90 pub fn set_region(mut self, value: &'a str) -> Self {
91 self.region = value;
92 self
93 }
94 pub fn set_datetime(mut self, value: S3DateTime) -> Self {
95 self.datetime = value;
96 self
97 }
98
99 pub fn set_payload_hash(mut self, value: &'a str) -> Self {
100 self.payload_hash = Cow::Borrowed(value);
101 self
102 }
103
104 pub fn set_payload_hash_with_content(mut self, content: &[u8]) -> Self {
105 let sha = aws_math::get_sha256(content);
106 self.payload_hash = Cow::Owned(sha);
107 self
108 }
109
110 pub fn set_method(mut self, value: &'a str) -> Self {
111 self.method = value;
112 self
113 }
114
115 pub fn set_service(mut self, value: &'a str) -> Self {
116 self.service = value;
117 self
118 }
119
120 pub fn set_url(mut self, url: &'a Url) -> Self {
121 self.url = url;
122 self
123 }
124
125 pub fn set_range(mut self, start: u64, end: Option<u64>) -> Self {
126 self.range = Some((start, end));
127 self
128 }
129
130 pub fn set_headers(mut self, headers: &'a [(&'static str, std::string::String)]) -> Self {
131 self.headers = headers;
132 self
133 }
134
135 pub fn build(self) -> Vec<(&'static str, String)> {
136 get_headers(self)
137 }
138}
139
140fn get_headers(options: S3HeadersBuilder) -> Vec<(&'static str, String)> {
142 let url = options.url;
143 let payload_hash = &options.payload_hash;
144 let datetime = options.datetime.get_offset_datetime();
145 let amz_date = aws_format::to_long_datetime(&datetime);
146
147 let mut headers: Vec<(&'static str, String)> = [
148 options.headers,
149 &[
150 ("Host", url.host_str().unwrap().to_owned()),
151 ("x-amz-content-sha256", payload_hash.to_string()),
152 ("x-amz-date", amz_date),
153 ],
154 ]
155 .concat();
156
157 if let Some((start, end)) = options.range {
158 let range_headers = aws_canonical::get_range_headers(start, end);
159 headers.extend(range_headers);
160 }
161
162 let auth_header = get_authorization_header(options.set_headers(&headers));
163
164 headers.push(("Authorization", auth_header));
165 headers
166}
167
168fn get_authorization_header(options: S3HeadersBuilder) -> String {
170 let datetime = options.datetime.get_offset_datetime();
171 let region = options.region;
172 let access_key = options.access_key;
173 let secret_key = options.secret_key;
174 let service = options.service;
175 let url = options.url;
176 let method = options.method;
177 let payload_hash = options.payload_hash;
178 let canonical_headers = aws_canonical::to_canonical_headers(options.headers);
179 let canonical_request = aws_format::canonical_request_string(method, url, &canonical_headers, &payload_hash);
180 let string_to_sign = aws_format::string_to_sign(&datetime, region, service, &canonical_request);
181 let signing_key = aws_math::get_signature_key(&datetime, secret_key, region, service);
182 let hmac: aws_math::HmacSha256 = aws_math::sign(&signing_key, string_to_sign.as_bytes());
183 let signature = hex::encode(hmac.finalize().into_bytes());
184 let signed_headers = aws_format::get_keys(&canonical_headers).join(";");
185
186 aws_format::authorization_header_string(access_key, &datetime, region, service, &signed_headers, &signature)
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use common_testing::assert;
193 use std::str::FromStr;
194
195 #[test]
196 fn test_get_object() {
197 let url = Url::from_str("https://jsonlog.s3.amazonaws.com/test/test.json").unwrap();
198 let headers = S3HeadersBuilder::new(&url)
199 .set_access_key("some_access_key")
200 .set_secret_key("some_secret_key")
201 .set_region("some_place")
202 .set_datetime(S3DateTime::UnixTimestamp(0))
203 .set_method("GET")
204 .set_service("s3")
205 .build();
206
207 assert::equal(
208 headers,
209 vec![
210 ("Host", "jsonlog.s3.amazonaws.com".to_owned()),
211 (
212 "x-amz-content-sha256",
213 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".to_owned(),
214 ),
215 ("x-amz-date", "19700101T000000Z".to_owned()),
216 (
217 "Authorization",
218 "AWS4-HMAC-SHA256 Credential=some_access_key/19700101/some_place/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=ac9a3c846f7368e934f31980d9df58d14cec3863a1a8be60bdeea708972b5a7b".to_owned(),
219 ),
220 ],
221 )
222 }
223
224 #[test]
225 fn test_get_object_2() {
226 let url = Url::from_str("https://jsonlog.s3.amazonaws.com/test.json").unwrap();
227 let headers = S3HeadersBuilder::new(&url)
228 .set_access_key("some_access_key")
229 .set_secret_key("some_secret_key")
230 .set_region("some_place")
231 .set_datetime(S3DateTime::UnixTimestamp(0))
232 .set_method("GET")
233 .set_service("s3")
234 .build();
235
236 assert::equal(headers, vec![
237 ("Host", "jsonlog.s3.amazonaws.com".to_owned()),
238 ("x-amz-content-sha256", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".to_owned()),
239 ("x-amz-date", "19700101T000000Z".to_owned()),
240 (
241 "Authorization",
242 "AWS4-HMAC-SHA256 Credential=some_access_key/19700101/some_place/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=521595a9eeee7092d3b2cc49d4db7cb828a5db5c7ad5136c149db0b0e7277f83".to_owned()
243 )
244 ])
245 }
246
247 #[test]
248 fn test_put_object() {
249 let url = Url::from_str("https://examplebucket.s3.amazonaws.com/test$file.text").unwrap();
250 let headers = &[("x-amz-storage-class", "REDUCED_REDUNDANCY".to_owned())];
251 let content = b"".as_slice();
252 let result = S3HeadersBuilder::new(&url)
253 .set_access_key("some_access_key")
254 .set_secret_key("some_secret_key")
255 .set_region("some_place")
256 .set_datetime(S3DateTime::UnixTimestamp(1369324800)) .set_headers(headers)
258 .set_method("PUT")
259 .set_service("s3")
260 .set_payload_hash_with_content(content)
261 .build();
262
263 assert::equal(result, vec![
264 ("x-amz-storage-class", "REDUCED_REDUNDANCY".to_owned()),
265 ("Host", "examplebucket.s3.amazonaws.com".to_owned()),
266 ("x-amz-content-sha256", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".to_owned()),
267 ("x-amz-date", "20130523T160000Z".to_owned()),
268 (
269 "Authorization",
270 "AWS4-HMAC-SHA256 Credential=some_access_key/20130523/some_place/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-storage-class,Signature=7e2911c8225f7591609bcbdc2faf8c443a898d8c83fc35b6a23f0b0e8084da60".to_owned()
271 )
272 ])
273 }
274}