1use crate::{aws_canonical, aws_format, aws_math};
2use hmac::Mac;
3use std::borrow::Cow;
4use std::fmt;
5use url::Url;
6
7pub const EMPTY_PAYLOAD_SHA: &str =
8 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
9
10pub const UNSIGNED_PAYLOAD: &str = "UNSIGNED-PAYLOAD";
13
14#[derive(Debug)]
17pub enum S3HeadersError {
18 InvalidUrl(url::ParseError),
20 MissingHost,
23}
24
25impl fmt::Display for S3HeadersError {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27 match self {
28 S3HeadersError::InvalidUrl(e) => write!(f, "invalid URL: {e}"),
29 S3HeadersError::MissingHost => write!(f, "URL must have a host"),
30 }
31 }
32}
33
34impl std::error::Error for S3HeadersError {
35 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
36 match self {
37 S3HeadersError::InvalidUrl(e) => Some(e),
38 S3HeadersError::MissingHost => None,
39 }
40 }
41}
42
43#[derive(Debug)]
49pub struct ValidS3Url(Url);
50
51impl ValidS3Url {
52 pub fn parse(input: &str) -> Result<Self, S3HeadersError> {
55 let url = Url::parse(input).map_err(S3HeadersError::InvalidUrl)?;
56 if url.host_str().is_none() {
57 return Err(S3HeadersError::MissingHost);
58 }
59 Ok(Self(url))
60 }
61
62 pub fn as_url(&self) -> &Url {
63 &self.0
64 }
65}
66
67#[derive(Debug, Default, Clone, Copy)]
75pub enum S3DateTime {
76 #[default]
77 Now,
78 UnixTimestamp(i64),
79 Resolved(time::OffsetDateTime),
81}
82
83impl S3DateTime {
84 pub fn get_offset_datetime(&self) -> time::OffsetDateTime {
85 match self {
86 S3DateTime::Now => time::OffsetDateTime::now_utc(),
87 S3DateTime::UnixTimestamp(timestamp) => {
88 time::OffsetDateTime::from_unix_timestamp(*timestamp).expect("Always valid")
89 }
90 S3DateTime::Resolved(dt) => *dt,
91 }
92 }
93}
94
95#[derive(Debug, Clone)]
96pub struct S3HeadersBuilder<'a> {
97 pub(crate) datetime: S3DateTime,
98 pub(crate) access_key: &'a str,
99 pub(crate) secret_key: &'a str,
100 pub(crate) region: &'a str,
101 pub(crate) service: &'a str,
102 pub(crate) url: &'a ValidS3Url,
103 pub(crate) method: &'a str,
104 pub(crate) headers: &'a [(&'static str, std::string::String)],
105 pub(crate) payload_hash: Cow<'a, str>,
106}
107
108impl<'a> S3HeadersBuilder<'a> {
109 pub fn new(url: &'a ValidS3Url) -> Self {
110 Self {
111 datetime: Default::default(),
112 access_key: Default::default(),
113 secret_key: Default::default(),
114 region: Default::default(),
115 service: "s3",
116 url,
117 method: Default::default(),
118 headers: Default::default(),
119 payload_hash: Cow::Borrowed(EMPTY_PAYLOAD_SHA),
120 }
121 }
122
123 pub fn set_access_key(mut self, value: &'a str) -> Self {
124 self.access_key = value;
125 self
126 }
127
128 pub fn set_secret_key(mut self, value: &'a str) -> Self {
129 self.secret_key = value;
130 self
131 }
132
133 pub fn set_region(mut self, value: &'a str) -> Self {
134 self.region = value;
135 self
136 }
137 pub fn set_datetime(mut self, value: S3DateTime) -> Self {
138 self.datetime = value;
139 self
140 }
141
142 pub fn set_payload_hash(mut self, value: &'a str) -> Self {
143 self.payload_hash = Cow::Borrowed(value);
144 self
145 }
146
147 pub fn set_payload_hash_with_content(mut self, content: &[u8]) -> Self {
148 let sha = aws_math::get_sha256(content);
149 self.payload_hash = Cow::Owned(sha);
150 self
151 }
152
153 pub fn set_method(mut self, value: &'a str) -> Self {
154 self.method = value;
155 self
156 }
157
158 pub fn set_service(mut self, value: &'a str) -> Self {
159 self.service = value;
160 self
161 }
162
163 pub fn set_url(mut self, url: &'a ValidS3Url) -> Self {
164 self.url = url;
165 self
166 }
167
168 pub fn set_headers(mut self, headers: &'a [(&'static str, std::string::String)]) -> Self {
169 self.headers = headers;
170 self
171 }
172
173 pub fn build(self) -> Vec<(&'static str, String)> {
174 get_headers(self)
175 }
176}
177
178pub fn get_headers(options: S3HeadersBuilder) -> Vec<(&'static str, String)> {
180 let url = options.url;
181 let payload_hash = &options.payload_hash;
182 let datetime = options.datetime.get_offset_datetime();
185 let amz_date = aws_format::to_long_datetime(&datetime);
186
187 let mut headers: Vec<(&'static str, String)> = [
188 options.headers,
189 &[
190 ("Host", url.0.host_str().unwrap().to_owned()),
191 ("x-amz-content-sha256", payload_hash.to_string()),
192 ("x-amz-date", amz_date),
193 ],
194 ]
195 .concat();
196
197 let auth_header = get_authorization_header(
200 options
201 .set_datetime(S3DateTime::Resolved(datetime))
202 .set_headers(&headers),
203 );
204 headers.push(("Authorization", auth_header));
205 headers
206}
207
208pub fn get_range_headers(start: u64, end: Option<u64>) -> Vec<(&'static str, String)> {
210 let mut range = format!("bytes={}-", start);
211
212 if let Some(end) = end {
213 range.push_str(&end.to_string());
214 }
215
216 let headers: Vec<(&'static str, String)> = vec![
217 ("Accept", "application/octet-stream".to_string()),
218 ("Range", range),
219 ];
220 headers
221}
222
223pub fn get_authorization_header(options: S3HeadersBuilder) -> String {
225 let datetime = options.datetime.get_offset_datetime();
226 let region = options.region;
227 let access_key = options.access_key;
228 let secret_key = options.secret_key;
229 let service = options.service;
230 let url = options.url;
231 let method = options.method;
232 let payload_hash = options.payload_hash;
233 let canonical_headers = aws_canonical::to_canonical_headers(options.headers);
234 let canonical_request =
235 aws_format::canonical_request_string(method, &url.0, &canonical_headers, &payload_hash);
236
237 let string_to_sign = aws_format::string_to_sign(&datetime, region, service, &canonical_request);
238 let signing_key = aws_math::get_signature_key(&datetime, secret_key, region, service);
239
240 let hmac: aws_math::HmacSha256 = aws_math::sign(&signing_key, string_to_sign.as_bytes());
241 let signature = hex::encode(hmac.finalize().into_bytes());
242 let signed_headers = aws_format::get_keys(&canonical_headers).join(";");
243
244 aws_format::authorization_header_string(
245 access_key,
246 &datetime,
247 region,
248 service,
249 &signed_headers,
250 &signature,
251 )
252}
253
254pub fn presign_get(
269 url: &ValidS3Url,
270 access_key: &str,
271 secret_key: &str,
272 region: &str,
273 service: &str,
274 datetime: S3DateTime,
275 expires: u32,
276) -> String {
277 let datetime = datetime.get_offset_datetime();
278 let signing_key = aws_math::get_signature_key(&datetime, secret_key, region, service);
279 presign_get_with_key(
280 url,
281 access_key,
282 region,
283 service,
284 datetime,
285 expires,
286 &signing_key,
287 )
288}
289
290pub fn presign_get_with_key(
297 url: &ValidS3Url,
298 access_key: &str,
299 region: &str,
300 service: &str,
301 datetime: time::OffsetDateTime,
302 expires: u32,
303 signing_key: &[u8],
304) -> String {
305 let host = host_with_port(url);
306 let canonical_uri = aws_format::canonical_uri_string(&url.0);
307
308 let presign_query = aws_math::authorization_query_params_no_sig(
312 access_key, &datetime, region, service, expires, None, None,
313 );
314 let canonical_query = presign_query.strip_prefix('?').unwrap_or(&presign_query);
315
316 let canonical_request = format!(
321 "GET\n{canonical_uri}\n{canonical_query}\nhost:{host}\n\nhost\n{UNSIGNED_PAYLOAD}",
322 );
323
324 let string_to_sign = aws_format::string_to_sign(&datetime, region, service, &canonical_request);
325 let hmac = aws_math::sign(signing_key, string_to_sign.as_bytes());
326 let signature = hex::encode(hmac.finalize().into_bytes());
327
328 format!(
329 "{}://{host}{canonical_uri}?{canonical_query}&X-Amz-Signature={signature}",
330 url.0.scheme(),
331 )
332}
333
334fn host_with_port(url: &ValidS3Url) -> String {
338 let host = url.0.host_str().expect("url must have a host");
339 match url.0.port() {
340 Some(port) => format!("{host}:{port}"),
341 None => host.to_string(),
342 }
343}