common_s3_headers/
s3.rs

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/// Used to specify the datetime to use when building the headers. Defaults to
9/// `S3DateTime::Now` which will use the current time when the headers are built.
10///
11/// Note: This is designed for future expansion or variations of plain timestamps.
12#[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/// Builder for S3 headers. Main entry point for this crate. Used to build
31/// the headers necessary to make a request to a AWS compatible service.
32///
33/// The returned headers are just strings and can be used with any HTTP client.
34///
35/// # Example
36///
37/// ```
38/// use common_s3_headers::S3HeadersBuilder;
39/// use url::Url;
40///
41/// let url = Url::parse("https://jsonlog.s3.amazonaws.com/test/test.json").unwrap();
42/// let headers: Vec<(&str, String)> = S3HeadersBuilder::new(&url)
43///  .set_access_key("access_key")
44///  .set_secret_key("secret_key")
45///  .set_region("us-east-1")
46///  .set_method("GET")
47///  .set_service("s3")
48///  .build();
49/// ```
50#[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
140/// Gets all the headers necessary to make a request to a AWS compatible service. Consumes the builder.
141fn 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
168/// Only gets the authorirzation header. Consumes the builder.
169fn 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)) // 20130524T000000Z
257      .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}