1use base64::Engine;
2use base64::engine::general_purpose;
3use hmac::Mac;
4use quick_xml::se::to_string;
5use std::collections::HashMap;
6#[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
7use std::pin::Pin;
8use time::OffsetDateTime;
9use time::format_description::well_known::Rfc2822;
10use url::Url;
11
12use crate::LONG_DATETIME;
13use crate::bucket::Bucket;
14use crate::command::Command;
15use crate::error::S3Error;
16use crate::signing;
17use bytes::Bytes;
18use http::HeaderMap;
19use http::header::{
20 ACCEPT, AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE, DATE, HOST, HeaderName, RANGE,
21};
22use std::fmt::Write as _;
23
24#[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
25use futures_util::Stream;
26#[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
27use futures_util::stream::{self, StreamExt};
28
29#[derive(Debug)]
30
31pub struct ResponseData {
32 bytes: Bytes,
33 status_code: u16,
34 headers: HashMap<String, String>,
35}
36
37#[cfg(all(
38 test,
39 any(feature = "sync", feature = "with-tokio", feature = "with-async-std")
40))]
41mod generated_bodyless_header_tests {
42 use super::Request;
43 use crate::bucket::Bucket;
44 use crate::command::Command;
45 use crate::error::S3Error;
46 use crate::region::Region;
47 use awscreds::Credentials;
48 use http::header::{AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE};
49
50 #[cfg(feature = "sync")]
51 type BackendRequest<'a> = crate::request::blocking::AttoRequest<'a>;
52 #[cfg(all(not(feature = "sync"), feature = "with-tokio"))]
53 type BackendRequest<'a> = crate::request::tokio_backend::ReqwestRequest<'a>;
54 #[cfg(all(
55 not(feature = "sync"),
56 not(feature = "with-tokio"),
57 feature = "with-async-std"
58 ))]
59 type BackendRequest<'a> = crate::request::async_std_backend::SurfRequest<'a>;
60
61 #[maybe_async::maybe_async]
62 async fn make_request<'a>(
63 bucket: &'a Bucket,
64 command: Command<'a>,
65 ) -> Result<BackendRequest<'a>, S3Error> {
66 BackendRequest::new(bucket, "key", command).await
67 }
68
69 #[maybe_async::test(
70 feature = "sync",
71 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
72 async(
73 all(not(feature = "sync"), feature = "with-async-std"),
74 async_std::test
75 )
76 )]
77 async fn custom_endpoint_path_stays_in_url_but_not_host_authority() {
78 use http::header::{AUTHORIZATION, HOST};
79 use url::Url;
80
81 let credentials =
82 Credentials::new(Some("test-key"), Some("test-secret"), None, None, None).unwrap();
83 let bucket = Bucket::new(
84 "bucket",
85 Region::Custom {
86 region: "us-east-1".to_owned(),
87 endpoint: "https://project.example:8443/storage/v1/s3".to_owned(),
88 },
89 credentials,
90 )
91 .unwrap()
92 .with_path_style();
93
94 let request = make_request(&bucket, Command::GetObject).await.unwrap();
95 let request_url = request.url().unwrap();
96 assert_eq!(
97 request_url.as_str(),
98 "https://project.example:8443/storage/v1/s3/bucket/key"
99 );
100
101 let headers = request.headers().await.unwrap();
102 assert_eq!(headers[HOST], "project.example:8443");
103 let mut signing_headers = headers.clone();
104 signing_headers.remove(AUTHORIZATION);
105 signing_headers.remove(http::header::DATE);
106 let canonical_request = request.canonical_request(&signing_headers).unwrap();
107 let canonical_lines = canonical_request.lines().collect::<Vec<_>>();
108 assert_eq!(canonical_lines[0], "GET");
109 assert_eq!(canonical_lines[1], "/storage/v1/s3/bucket/key");
110 assert!(canonical_request.contains("\nhost:project.example:8443\n"));
111 assert!(!canonical_request.contains("\nhost:project.example:8443/storage"));
112 let signed_headers = headers[AUTHORIZATION]
113 .to_str()
114 .unwrap()
115 .split("SignedHeaders=")
116 .nth(1)
117 .unwrap()
118 .split(',')
119 .next()
120 .unwrap();
121 assert!(signed_headers.split(';').any(|header| header == "host"));
122
123 let presigned = bucket.presign_get("/key", 3600, None).await.unwrap();
124 let presigned_url = Url::parse(&presigned).unwrap();
125 assert_eq!(presigned_url.host_str(), Some("project.example"));
126 assert_eq!(presigned_url.port(), Some(8443));
127 assert_eq!(presigned_url.path(), "/storage/v1/s3/bucket/key");
128 assert!(
129 presigned_url
130 .query_pairs()
131 .any(|(key, _)| key == "X-Amz-Signature")
132 );
133 }
134
135 #[maybe_async::test(
136 feature = "sync",
137 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
138 async(
139 all(not(feature = "sync"), feature = "with-async-std"),
140 async_std::test
141 )
142 )]
143 async fn delete_object_does_not_sign_generated_body_headers() {
144 let credentials =
145 Credentials::new(Some("test-key"), Some("test-secret"), None, None, None).unwrap();
146 let mut bucket = Bucket::new(
147 "bucket",
148 "us-east-1".parse::<Region>().unwrap(),
149 credentials,
150 )
151 .unwrap();
152 bucket.add_header("x-test-caller-extra", "kept");
153 let request = make_request(&bucket, Command::DeleteObject).await.unwrap();
154 let headers = request.headers().await.unwrap();
155
156 assert!(!headers.contains_key(CONTENT_LENGTH));
157 assert!(!headers.contains_key(CONTENT_TYPE));
158 assert_eq!(headers["x-test-caller-extra"], "kept");
159 let authorization = headers[AUTHORIZATION].to_str().unwrap();
160 assert!(!authorization.contains("content-length"));
161 assert!(!authorization.contains("content-type"));
162 assert!(authorization.contains("x-test-caller-extra"));
163
164 for command in [Command::GetObject, Command::HeadObject] {
165 let request = make_request(&bucket, command).await.unwrap();
166 let headers = request.headers().await.unwrap();
167 assert!(!headers.contains_key(CONTENT_LENGTH));
168 assert!(!headers.contains_key(CONTENT_TYPE));
169 }
170
171 bucket.add_header("content-length", "0");
172 bucket.add_header("content-type", "application/caller");
173 let request = make_request(&bucket, Command::DeleteObject).await.unwrap();
174 let headers = request.headers().await.unwrap();
175 assert_eq!(headers[CONTENT_LENGTH], "0");
176 assert_eq!(headers[CONTENT_TYPE], "application/caller");
177 }
178
179 #[maybe_async::test(
180 feature = "sync",
181 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
182 async(
183 all(not(feature = "sync"), feature = "with-async-std"),
184 async_std::test
185 )
186 )]
187 async fn generated_body_headers_remain_for_put_and_xml_post() {
188 let credentials =
189 Credentials::new(Some("test-key"), Some("test-secret"), None, None, None).unwrap();
190 let bucket = Bucket::new(
191 "bucket",
192 "us-east-1".parse::<Region>().unwrap(),
193 credentials,
194 )
195 .unwrap();
196
197 let empty_put = Command::PutObject {
198 content: b"",
199 content_type: "application/test",
200 custom_headers: None,
201 multipart: None,
202 };
203 let request = make_request(&bucket, empty_put).await.unwrap();
204 let headers = request.headers().await.unwrap();
205 assert_eq!(headers[CONTENT_LENGTH], "0");
206 assert_eq!(headers[CONTENT_TYPE], "application/test");
207
208 let put = Command::PutObject {
209 content: b"abc",
210 content_type: "application/test",
211 custom_headers: None,
212 multipart: None,
213 };
214 let request = make_request(&bucket, put).await.unwrap();
215 let headers = request.headers().await.unwrap();
216 assert_eq!(headers[CONTENT_LENGTH], "3");
217 assert_eq!(headers[CONTENT_TYPE], "application/test");
218 assert!(
219 headers[AUTHORIZATION]
220 .to_str()
221 .unwrap()
222 .contains("content-length")
223 );
224 assert!(
225 headers[AUTHORIZATION]
226 .to_str()
227 .unwrap()
228 .contains("content-type")
229 );
230
231 let request = make_request(&bucket, Command::CopyObject { from: "/source" })
232 .await
233 .unwrap();
234 let headers = request.headers().await.unwrap();
235 assert!(!headers.contains_key(CONTENT_LENGTH));
236 assert!(!headers.contains_key(CONTENT_TYPE));
237 assert_eq!(headers["x-amz-copy-source"], "/source");
238
239 let post = Command::CompleteMultipartUpload {
240 upload_id: "upload-id",
241 data: crate::serde_types::CompleteMultipartUploadData {
242 parts: vec![crate::serde_types::Part {
243 part_number: 1,
244 etag: "etag".into(),
245 }],
246 },
247 };
248 let expected_length = post.content_length().unwrap();
249 let request = make_request(&bucket, post).await.unwrap();
250 let headers = request.headers().await.unwrap();
251 assert_eq!(headers[CONTENT_LENGTH], expected_length.to_string());
252 assert_eq!(headers[CONTENT_TYPE], "application/xml");
253 assert!(
254 headers[AUTHORIZATION]
255 .to_str()
256 .unwrap()
257 .contains("content-length")
258 );
259 assert!(
260 headers[AUTHORIZATION]
261 .to_str()
262 .unwrap()
263 .contains("content-type")
264 );
265 }
266}
267
268#[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
269pub type DataStream = Pin<Box<dyn Stream<Item = StreamItem> + Send>>;
270#[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
271pub type StreamItem = Result<Bytes, S3Error>;
272
273#[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
274pub struct ResponseDataStream {
275 pub bytes: DataStream,
276 pub status_code: u16,
277}
278
279#[cfg(any(feature = "with-tokio", feature = "with-async-std"))]
280impl ResponseDataStream {
281 pub fn bytes(&mut self) -> &mut DataStream {
282 &mut self.bytes
283 }
284}
285
286impl From<ResponseData> for Vec<u8> {
287 fn from(data: ResponseData) -> Vec<u8> {
288 data.to_vec()
289 }
290}
291
292impl ResponseData {
293 pub fn new(bytes: Bytes, status_code: u16, headers: HashMap<String, String>) -> ResponseData {
294 ResponseData {
295 bytes,
296 status_code,
297 headers,
298 }
299 }
300
301 pub fn as_slice(&self) -> &[u8] {
302 &self.bytes
303 }
304
305 pub fn to_vec(self) -> Vec<u8> {
306 self.bytes.to_vec()
307 }
308
309 pub fn bytes(&self) -> &Bytes {
310 &self.bytes
311 }
312
313 pub fn bytes_mut(&mut self) -> &mut Bytes {
314 &mut self.bytes
315 }
316
317 pub fn into_bytes(self) -> Bytes {
318 self.bytes
319 }
320
321 pub fn status_code(&self) -> u16 {
322 self.status_code
323 }
324
325 pub fn as_str(&self) -> Result<&str, std::str::Utf8Error> {
326 std::str::from_utf8(self.as_slice())
327 }
328
329 pub fn to_string(&self) -> Result<String, std::str::Utf8Error> {
330 std::str::from_utf8(self.as_slice()).map(|s| s.to_string())
331 }
332
333 pub fn headers(&self) -> HashMap<String, String> {
334 self.headers.clone()
335 }
336}
337
338use std::fmt;
339
340impl fmt::Display for ResponseData {
341 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
342 write!(
343 f,
344 "Status code: {}\n Data: {}",
345 self.status_code(),
346 self.to_string()
347 .unwrap_or_else(|_| "Data could not be cast to UTF string".to_string())
348 )
349 }
350}
351
352#[cfg(feature = "with-tokio")]
353impl tokio::io::AsyncRead for ResponseDataStream {
354 fn poll_read(
355 mut self: Pin<&mut Self>,
356 cx: &mut std::task::Context<'_>,
357 buf: &mut tokio::io::ReadBuf<'_>,
358 ) -> std::task::Poll<std::io::Result<()>> {
359 if buf.remaining() == 0 {
360 return std::task::Poll::Ready(Ok(()));
361 }
362
363 loop {
364 match Stream::poll_next(self.bytes.as_mut(), cx) {
365 std::task::Poll::Ready(Some(Ok(chunk))) => {
366 if chunk.is_empty() {
367 continue;
368 }
369
370 let amt = std::cmp::min(chunk.len(), buf.remaining());
371 buf.put_slice(&chunk[..amt]);
372
373 if amt < chunk.len() {
374 let remainder = chunk.slice(amt..);
375 let previous_stream =
376 std::mem::replace(&mut self.bytes, Box::pin(stream::empty()));
377 self.bytes = Box::pin(
378 stream::once(async move { Ok(remainder) }).chain(previous_stream),
379 );
380 }
381
382 return std::task::Poll::Ready(Ok(()));
383 }
384 std::task::Poll::Ready(Some(Err(error))) => {
385 return std::task::Poll::Ready(Err(std::io::Error::other(error)));
386 }
387 std::task::Poll::Ready(None) => {
388 return std::task::Poll::Ready(Ok(()));
389 }
390 std::task::Poll::Pending => return std::task::Poll::Pending,
391 }
392 }
393 }
394}
395
396#[cfg(feature = "with-async-std")]
397impl async_std::io::Read for ResponseDataStream {
398 fn poll_read(
399 mut self: Pin<&mut Self>,
400 cx: &mut std::task::Context<'_>,
401 buf: &mut [u8],
402 ) -> std::task::Poll<std::io::Result<usize>> {
403 if buf.is_empty() {
404 return std::task::Poll::Ready(Ok(0));
405 }
406
407 loop {
408 match Stream::poll_next(self.bytes.as_mut(), cx) {
409 std::task::Poll::Ready(Some(Ok(chunk))) => {
410 if chunk.is_empty() {
411 continue;
412 }
413
414 let amt = std::cmp::min(chunk.len(), buf.len());
415 buf[..amt].copy_from_slice(&chunk[..amt]);
416
417 if amt < chunk.len() {
418 let remainder = chunk.slice(amt..);
419 let previous_stream =
420 std::mem::replace(&mut self.bytes, Box::pin(stream::empty()));
421 self.bytes = Box::pin(
422 stream::once(async move { Ok(remainder) }).chain(previous_stream),
423 );
424 }
425
426 return std::task::Poll::Ready(Ok(amt));
427 }
428 std::task::Poll::Ready(Some(Err(error))) => {
429 return std::task::Poll::Ready(Err(std::io::Error::other(error)));
430 }
431 std::task::Poll::Ready(None) => {
432 return std::task::Poll::Ready(Ok(0));
433 }
434 std::task::Poll::Pending => return std::task::Poll::Pending,
435 }
436 }
437 }
438}
439
440fn authority_of(host: &str) -> &str {
448 match host.find('/') {
449 Some(position) => &host[..position],
450 None => host,
451 }
452}
453
454#[maybe_async::maybe_async]
455pub trait Request {
456 type Response;
457 type HeaderMap;
458
459 async fn response(&self) -> Result<Self::Response, S3Error>;
460 async fn response_data(&self, etag: bool) -> Result<ResponseData, S3Error>;
461 #[cfg(feature = "with-tokio")]
462 async fn response_data_to_writer<T: tokio::io::AsyncWrite + Send + Unpin + ?Sized>(
463 &self,
464 writer: &mut T,
465 ) -> Result<u16, S3Error>;
466 #[cfg(feature = "with-async-std")]
467 async fn response_data_to_writer<T: async_std::io::Write + Send + Unpin + ?Sized>(
468 &self,
469 writer: &mut T,
470 ) -> Result<u16, S3Error>;
471 #[cfg(feature = "sync")]
472 fn response_data_to_writer<T: std::io::Write + Send + ?Sized>(
473 &self,
474 writer: &mut T,
475 ) -> Result<u16, S3Error>;
476 #[cfg(any(feature = "with-async-std", feature = "with-tokio"))]
477 async fn response_data_to_stream(&self) -> Result<ResponseDataStream, S3Error>;
478 async fn response_header(&self) -> Result<(Self::HeaderMap, u16), S3Error>;
479 async fn response_status(&self) -> Result<u16, S3Error> {
480 let (_, status_code) = self.response_header().await?;
481 Ok(status_code)
482 }
483 fn datetime(&self) -> OffsetDateTime;
484 fn bucket(&self) -> Bucket;
485 fn command(&self) -> Command<'_>;
486 fn path(&self) -> String;
487
488 async fn signing_key(&self) -> Result<Vec<u8>, S3Error> {
489 signing::signing_key(
490 &self.datetime(),
491 &self
492 .bucket()
493 .secret_key()
494 .await?
495 .expect("Secret key must be provided to sign headers, found None"),
496 &self.bucket().region(),
497 "s3",
498 )
499 }
500
501 fn request_body(&self) -> Result<Vec<u8>, S3Error> {
502 let result = if let Command::PutObject { content, .. } = self.command() {
503 Vec::from(content)
504 } else if let Command::PutObjectTagging { tags } = self.command() {
505 Vec::from(tags)
506 } else if let Command::UploadPart { content, .. } = self.command() {
507 Vec::from(content)
508 } else if let Command::CompleteMultipartUpload { data, .. } = &self.command() {
509 let body = data.to_string();
510 body.as_bytes().to_vec()
511 } else if let Command::CreateBucket { config } = &self.command() {
512 if let Some(payload) = config.location_constraint_payload() {
513 Vec::from(payload)
514 } else {
515 Vec::new()
516 }
517 } else if let Command::PutBucketLifecycle { configuration, .. } = &self.command() {
518 quick_xml::se::to_string(configuration)?.as_bytes().to_vec()
519 } else if let Command::PutBucketCors { configuration, .. } = &self.command() {
520 let cors = configuration.to_string();
521 cors.as_bytes().to_vec()
522 } else if let Command::PutBucketPolicy { policy } = &self.command() {
523 policy.as_bytes().to_vec()
524 } else if let Command::DeleteObjects { data } = &self.command() {
525 data.to_string().as_bytes().to_vec()
526 } else {
527 Vec::new()
528 };
529 Ok(result)
530 }
531
532 fn long_date(&self) -> Result<String, S3Error> {
533 Ok(self.datetime().format(LONG_DATETIME)?)
534 }
535
536 fn string_to_sign(&self, request: &str) -> Result<String, S3Error> {
537 signing::string_to_sign(&self.datetime(), &self.bucket().region(), request)
538 }
539
540 fn host_header(&self) -> String {
541 authority_of(&self.bucket().host()).to_string()
542 }
543
544 #[maybe_async::async_impl]
545 async fn presigned(&self) -> Result<String, S3Error> {
546 let (expiry, custom_headers, custom_queries) = match self.command() {
547 Command::PresignGet {
548 expiry_secs,
549 custom_queries,
550 } => (expiry_secs, None, custom_queries),
551 Command::PresignPut {
552 expiry_secs,
553 custom_headers,
554 custom_queries,
555 } => (expiry_secs, custom_headers, custom_queries),
556 Command::PresignDelete { expiry_secs } => (expiry_secs, None, None),
557 _ => unreachable!(),
558 };
559
560 let url = self
561 .presigned_url_no_sig(expiry, custom_headers.as_ref(), custom_queries.as_ref())
562 .await?;
563
564 let url_str = if let awsregion::Region::Custom { ref endpoint, .. } = self.bucket().region()
568 {
569 if (endpoint.contains(":80") && url.scheme() == "http" && url.port().is_none())
571 || (endpoint.contains(":443") && url.scheme() == "https" && url.port().is_none())
572 {
573 let host = self.bucket().host();
575 format!(
576 "{}://{}{}{}",
577 url.scheme(),
578 host,
579 url.path(),
580 url.query().map(|q| format!("?{}", q)).unwrap_or_default()
581 )
582 } else {
583 url.to_string()
584 }
585 } else {
586 url.to_string()
587 };
588
589 Ok(format!(
590 "{}&X-Amz-Signature={}",
591 url_str,
592 self.presigned_authorization(custom_headers.as_ref())
593 .await?
594 ))
595 }
596
597 #[maybe_async::sync_impl]
598 async fn presigned(&self) -> Result<String, S3Error> {
599 let (expiry, custom_headers, custom_queries) = match self.command() {
600 Command::PresignGet {
601 expiry_secs,
602 custom_queries,
603 } => (expiry_secs, None, custom_queries),
604 Command::PresignPut {
605 expiry_secs,
606 custom_headers,
607 ..
608 } => (expiry_secs, custom_headers, None),
609 Command::PresignDelete { expiry_secs } => (expiry_secs, None, None),
610 _ => unreachable!(),
611 };
612
613 let url =
614 self.presigned_url_no_sig(expiry, custom_headers.as_ref(), custom_queries.as_ref())?;
615
616 let url_str = if let awsregion::Region::Custom { ref endpoint, .. } = self.bucket().region()
620 {
621 if (endpoint.contains(":80") && url.scheme() == "http" && url.port().is_none())
623 || (endpoint.contains(":443") && url.scheme() == "https" && url.port().is_none())
624 {
625 let host = self.bucket().host();
627 format!(
628 "{}://{}{}{}",
629 url.scheme(),
630 host,
631 url.path(),
632 url.query().map(|q| format!("?{}", q)).unwrap_or_default()
633 )
634 } else {
635 url.to_string()
636 }
637 } else {
638 url.to_string()
639 };
640
641 Ok(format!(
642 "{}&X-Amz-Signature={}",
643 url_str,
644 self.presigned_authorization(custom_headers.as_ref())?
645 ))
646 }
647
648 async fn presigned_authorization(
649 &self,
650 custom_headers: Option<&HeaderMap>,
651 ) -> Result<String, S3Error> {
652 let mut headers = HeaderMap::new();
653 let host_header = self.host_header();
654 headers.insert(HOST, host_header.parse()?);
655 if let Some(custom_headers) = custom_headers {
656 for (k, v) in custom_headers.iter() {
657 headers.insert(k.clone(), v.clone());
658 }
659 }
660 let canonical_request = self.presigned_canonical_request(&headers).await?;
661 let string_to_sign = self.string_to_sign(&canonical_request)?;
662 let mut hmac = signing::HmacSha256::new_from_slice(&self.signing_key().await?)?;
663 hmac.update(string_to_sign.as_bytes());
664 let signature = hex::encode(hmac.finalize().into_bytes());
665 Ok(signature)
667 }
668
669 async fn presigned_canonical_request(&self, headers: &HeaderMap) -> Result<String, S3Error> {
670 let (expiry, custom_headers, custom_queries) = match self.command() {
671 Command::PresignGet {
672 expiry_secs,
673 custom_queries,
674 } => (expiry_secs, None, custom_queries),
675 Command::PresignPut {
676 expiry_secs,
677 custom_headers,
678 custom_queries,
679 } => (expiry_secs, custom_headers, custom_queries),
680 Command::PresignDelete { expiry_secs } => (expiry_secs, None, None),
681 _ => unreachable!(),
682 };
683
684 signing::canonical_request(
685 &self.command().http_verb().to_string(),
686 &self
687 .presigned_url_no_sig(expiry, custom_headers.as_ref(), custom_queries.as_ref())
688 .await?,
689 headers,
690 "UNSIGNED-PAYLOAD",
691 )
692 }
693
694 #[maybe_async::async_impl]
695 async fn presigned_url_no_sig(
696 &self,
697 expiry: u32,
698 custom_headers: Option<&HeaderMap>,
699 custom_queries: Option<&HashMap<String, String>>,
700 ) -> Result<Url, S3Error> {
701 let bucket = self.bucket();
702 let token = if let Some(security_token) = bucket.security_token().await? {
703 Some(security_token)
704 } else {
705 bucket.session_token().await?
706 };
707 let url = Url::parse(&format!(
708 "{}{}{}",
709 self.url()?,
710 signing::authorization_query_params_no_sig(
711 &self.bucket().access_key().await?.unwrap_or_default(),
712 &self.datetime(),
713 &self.bucket().region(),
714 expiry,
715 custom_headers,
716 token.as_ref()
717 )?,
718 signing::flatten_queries(custom_queries)?,
719 ))?;
720
721 Ok(url)
722 }
723
724 #[maybe_async::sync_impl]
725 fn presigned_url_no_sig(
726 &self,
727 expiry: u32,
728 custom_headers: Option<&HeaderMap>,
729 custom_queries: Option<&HashMap<String, String>>,
730 ) -> Result<Url, S3Error> {
731 let bucket = self.bucket();
732 let token = if let Some(security_token) = bucket.security_token()? {
733 Some(security_token)
734 } else {
735 bucket.session_token()?
736 };
737 let url = Url::parse(&format!(
738 "{}{}{}",
739 self.url()?,
740 signing::authorization_query_params_no_sig(
741 &self.bucket().access_key()?.unwrap_or_default(),
742 &self.datetime(),
743 &self.bucket().region(),
744 expiry,
745 custom_headers,
746 token.as_ref()
747 )?,
748 signing::flatten_queries(custom_queries)?,
749 ))?;
750
751 Ok(url)
752 }
753
754 fn url(&self) -> Result<Url, S3Error> {
755 let mut url_str = self.bucket().url();
756
757 if let Command::ListBuckets { .. } = self.command() {
758 return Ok(Url::parse(&url_str)?);
759 }
760
761 if let Command::CreateBucket { .. } = self.command() {
762 return Ok(Url::parse(&url_str)?);
763 }
764
765 let path = if self.path().starts_with('/') {
766 self.path()[1..].to_string()
767 } else {
768 self.path()[..].to_string()
769 };
770
771 url_str.push('/');
772 url_str.push_str(&signing::uri_encode(&path, false));
773
774 #[allow(clippy::collapsible_match)]
776 match self.command() {
777 Command::InitiateMultipartUpload { .. } | Command::ListMultipartUploads { .. } => {
778 url_str.push_str("?uploads")
779 }
780 Command::AbortMultipartUpload { upload_id } => {
781 write!(url_str, "?uploadId={}", upload_id).expect("Could not write to url_str");
782 }
783 Command::CompleteMultipartUpload { upload_id, .. } => {
784 write!(url_str, "?uploadId={}", upload_id).expect("Could not write to url_str");
785 }
786 Command::GetObjectTorrent => url_str.push_str("?torrent"),
787 Command::PutObject { multipart, .. } => {
788 if let Some(multipart) = multipart {
789 url_str.push_str(&multipart.query_string())
790 }
791 }
792 Command::GetBucketLifecycle
793 | Command::PutBucketLifecycle { .. }
794 | Command::DeleteBucketLifecycle => {
795 url_str.push_str("?lifecycle");
796 }
797 Command::GetBucketCors { .. }
798 | Command::PutBucketCors { .. }
799 | Command::DeleteBucketCors { .. } => {
800 url_str.push_str("?cors");
801 }
802 Command::GetBucketPolicy
803 | Command::PutBucketPolicy { .. }
804 | Command::DeleteBucketPolicy => {
805 url_str.push_str("?policy");
806 }
807 Command::GetObjectAttributes { version_id, .. } => {
808 if let Some(version_id) = version_id {
809 url_str.push_str(&format!("?attributes&versionId={}", version_id));
810 } else {
811 url_str.push_str("?attributes&versionId=null");
812 }
813 }
814 Command::HeadObject => {}
815 Command::DeleteObject => {}
816 Command::DeleteObjectTagging => {}
817 Command::GetObject => {}
818 Command::GetObjectRange { .. } => {}
819 Command::GetObjectTagging => {}
820 Command::ListObjects { .. } => {}
821 Command::ListObjectsV2 { .. } => {}
822 Command::GetBucketLocation => {}
823 Command::PresignGet { .. } => {}
824 Command::PresignPut { .. } => {}
825 Command::PresignDelete { .. } => {}
826 Command::DeleteBucket => {}
827 Command::ListBuckets => {}
828 Command::CopyObject { .. } => {}
829 Command::PutObjectTagging { .. } => {}
830 Command::UploadPart { .. } => {}
831 Command::CreateBucket { .. } => {}
832 Command::DeleteObjects { .. } => {
833 url_str.push_str("?delete");
834 }
835 }
836
837 let mut url = Url::parse(&url_str)?;
838
839 for (key, value) in &self.bucket().extra_query {
840 url.query_pairs_mut().append_pair(key, value);
841 }
842
843 if let Command::ListObjectsV2 {
844 prefix,
845 delimiter,
846 continuation_token,
847 start_after,
848 max_keys,
849 } = self.command().clone()
850 {
851 let mut query_pairs = url.query_pairs_mut();
852 delimiter.map(|d| query_pairs.append_pair("delimiter", &d));
853
854 query_pairs.append_pair("prefix", &prefix);
855 query_pairs.append_pair("list-type", "2");
856 if let Some(token) = continuation_token {
857 query_pairs.append_pair("continuation-token", &token);
858 }
859 if let Some(start_after) = start_after {
860 query_pairs.append_pair("start-after", &start_after);
861 }
862 if let Some(max_keys) = max_keys {
863 query_pairs.append_pair("max-keys", &max_keys.to_string());
864 }
865 }
866
867 if let Command::ListObjects {
868 prefix,
869 delimiter,
870 marker,
871 max_keys,
872 } = self.command().clone()
873 {
874 let mut query_pairs = url.query_pairs_mut();
875 delimiter.map(|d| query_pairs.append_pair("delimiter", &d));
876
877 query_pairs.append_pair("prefix", &prefix);
878 if let Some(marker) = marker {
879 query_pairs.append_pair("marker", &marker);
880 }
881 if let Some(max_keys) = max_keys {
882 query_pairs.append_pair("max-keys", &max_keys.to_string());
883 }
884 }
885
886 match self.command() {
887 Command::ListMultipartUploads {
888 prefix,
889 delimiter,
890 key_marker,
891 upload_id_marker,
892 max_uploads,
893 } => {
894 let mut query_pairs = url.query_pairs_mut();
895 delimiter.map(|d| query_pairs.append_pair("delimiter", d));
896 if let Some(prefix) = prefix {
897 query_pairs.append_pair("prefix", prefix);
898 }
899 if let Some(key_marker) = key_marker {
900 query_pairs.append_pair("key-marker", &key_marker);
901 }
902 if let Some(upload_id_marker) = upload_id_marker {
903 query_pairs.append_pair("upload-id-marker", &upload_id_marker);
904 }
905 if let Some(max_uploads) = max_uploads {
906 query_pairs.append_pair("max-uploads", max_uploads.to_string().as_str());
907 }
908 }
909 Command::PutObjectTagging { .. }
910 | Command::GetObjectTagging
911 | Command::DeleteObjectTagging => {
912 url.query_pairs_mut().append_pair("tagging", "");
913 }
914 _ => {}
915 }
916
917 Ok(url)
918 }
919
920 fn canonical_request(&self, headers: &HeaderMap) -> Result<String, S3Error> {
921 signing::canonical_request(
922 &self.command().http_verb().to_string(),
923 &self.url()?,
924 headers,
925 &self.command().sha256()?,
926 )
927 }
928
929 #[maybe_async::maybe_async]
930 async fn authorization(&self, headers: &HeaderMap) -> Result<String, S3Error> {
931 let canonical_request = self.canonical_request(headers)?;
932 let string_to_sign = self.string_to_sign(&canonical_request)?;
933 let mut hmac = signing::HmacSha256::new_from_slice(&self.signing_key().await?)?;
934 hmac.update(string_to_sign.as_bytes());
935 let signature = hex::encode(hmac.finalize().into_bytes());
936 let signed_header = signing::signed_header_string(headers);
937 signing::authorization_header(
938 &self
939 .bucket()
940 .access_key()
941 .await?
942 .expect("No access_key provided"),
943 &self.datetime(),
944 &self.bucket().region(),
945 &signed_header,
946 &signature,
947 )
948 }
949
950 #[maybe_async::maybe_async]
951 async fn headers(&self) -> Result<HeaderMap, S3Error> {
952 let sha256 = self.command().sha256()?;
954
955 let mut headers = HeaderMap::new();
959
960 for (k, v) in self.bucket().extra_headers.iter() {
961 if k.as_str().starts_with("x-amz-meta-") {
962 match self.command() {
964 Command::UploadPart { .. }
965 | Command::AbortMultipartUpload { .. }
966 | Command::CompleteMultipartUpload { .. }
967 | Command::PutObject {
968 multipart: Some(_), ..
969 } => continue,
970 _ => (),
971 }
972 }
973 headers.insert(k.clone(), v.clone());
974 }
975
976 if let Command::PutObject { custom_headers, .. } = self.command()
978 && let Some(custom_headers) = custom_headers
979 {
980 for (k, v) in custom_headers.iter() {
981 headers.insert(k.clone(), v.clone());
982 }
983 }
984
985 let host_header = self.host_header();
986
987 headers.insert(HOST, host_header.parse()?);
988
989 if let Command::CopyObject { from } = self.command() {
990 headers.insert(HeaderName::from_static("x-amz-copy-source"), from.parse()?);
991 } else if !matches!(
992 self.command().http_verb(),
993 crate::command::HttpMethod::Get
994 | crate::command::HttpMethod::Head
995 | crate::command::HttpMethod::Delete
996 ) {
997 headers.insert(
998 CONTENT_LENGTH,
999 self.command().content_length()?.to_string().parse()?,
1000 );
1001 headers.insert(CONTENT_TYPE, self.command().content_type().parse()?);
1002 }
1003 headers.insert(
1004 HeaderName::from_static("x-amz-content-sha256"),
1005 sha256.parse()?,
1006 );
1007 headers.insert(
1008 HeaderName::from_static("x-amz-date"),
1009 self.long_date()?.parse()?,
1010 );
1011
1012 if let Some(session_token) = self.bucket().session_token().await? {
1013 headers.insert(
1014 HeaderName::from_static("x-amz-security-token"),
1015 session_token.parse()?,
1016 );
1017 } else if let Some(security_token) = self.bucket().security_token().await? {
1018 headers.insert(
1019 HeaderName::from_static("x-amz-security-token"),
1020 security_token.parse()?,
1021 );
1022 }
1023
1024 if let Command::PutObjectTagging { tags } = self.command() {
1025 let digest = md5::compute(tags);
1026 let hash = general_purpose::STANDARD.encode(digest.as_ref());
1027 headers.insert(HeaderName::from_static("content-md5"), hash.parse()?);
1028 } else if let Command::PutObject { content, .. } = self.command() {
1029 let digest = md5::compute(content);
1030 let hash = general_purpose::STANDARD.encode(digest.as_ref());
1031 headers.insert(HeaderName::from_static("content-md5"), hash.parse()?);
1032 } else if let Command::UploadPart { content, .. } = self.command() {
1033 let digest = md5::compute(content);
1034 let hash = general_purpose::STANDARD.encode(digest.as_ref());
1035 headers.insert(HeaderName::from_static("content-md5"), hash.parse()?);
1036 } else if let Command::GetObject {} = self.command() {
1037 headers.insert(ACCEPT, "application/octet-stream".to_string().parse()?);
1038 } else if let Command::GetObjectRange { start, end } = self.command() {
1040 headers.insert(ACCEPT, "application/octet-stream".to_string().parse()?);
1041
1042 let mut range = format!("bytes={}-", start);
1043
1044 if let Some(end) = end {
1045 range.push_str(&end.to_string());
1046 }
1047
1048 headers.insert(RANGE, range.parse()?);
1049 } else if let Command::CreateBucket { ref config } = self.command() {
1050 config.add_headers(&mut headers)?;
1051 } else if let Command::PutBucketLifecycle { ref configuration } = self.command() {
1052 let digest = md5::compute(to_string(configuration)?.as_bytes());
1053 let hash = general_purpose::STANDARD.encode(digest.as_ref());
1054 headers.insert(HeaderName::from_static("content-md5"), hash.parse()?);
1055 } else if let Command::PutBucketCors {
1056 expected_bucket_owner,
1057 configuration,
1058 ..
1059 } = self.command()
1060 {
1061 let digest = md5::compute(configuration.to_string().as_bytes());
1062 let hash = general_purpose::STANDARD.encode(digest.as_ref());
1063 headers.insert(HeaderName::from_static("content-md5"), hash.parse()?);
1064
1065 headers.insert(
1066 HeaderName::from_static("x-amz-expected-bucket-owner"),
1067 expected_bucket_owner.parse()?,
1068 );
1069 } else if let Command::PutBucketPolicy { policy } = self.command() {
1070 let digest = md5::compute(policy.as_bytes());
1071 let hash = general_purpose::STANDARD.encode(digest.as_ref());
1072 headers.insert(HeaderName::from_static("content-md5"), hash.parse()?);
1073 } else if let Command::GetBucketCors {
1074 expected_bucket_owner,
1075 } = self.command()
1076 {
1077 headers.insert(
1078 HeaderName::from_static("x-amz-expected-bucket-owner"),
1079 expected_bucket_owner.parse()?,
1080 );
1081 } else if let Command::DeleteBucketCors {
1082 expected_bucket_owner,
1083 } = self.command()
1084 {
1085 headers.insert(
1086 HeaderName::from_static("x-amz-expected-bucket-owner"),
1087 expected_bucket_owner.parse()?,
1088 );
1089 } else if let Command::GetObjectAttributes {
1090 expected_bucket_owner,
1091 ..
1092 } = self.command()
1093 {
1094 headers.insert(
1095 HeaderName::from_static("x-amz-expected-bucket-owner"),
1096 expected_bucket_owner.parse()?,
1097 );
1098 headers.insert(
1099 HeaderName::from_static("x-amz-object-attributes"),
1100 "ETag".parse()?,
1101 );
1102 } else if let Command::DeleteObjects { ref data } = self.command() {
1103 let body = data.to_string();
1104 let digest = md5::compute(body.as_bytes());
1105 let hash = general_purpose::STANDARD.encode(digest.as_ref());
1106 headers.insert(HeaderName::from_static("content-md5"), hash.parse()?);
1107 }
1108
1109 if self.bucket().secret_key().await?.is_some() {
1111 let authorization = self.authorization(&headers).await?;
1112 headers.insert(AUTHORIZATION, authorization.parse()?);
1113 }
1114
1115 headers.insert(DATE, self.datetime().format(&Rfc2822)?.parse()?);
1122
1123 Ok(headers)
1124 }
1125}
1126
1127#[cfg(all(test, feature = "with-tokio"))]
1128mod tests {
1129 use super::*;
1130 use bytes::Bytes;
1131 use futures_util::stream;
1132 use tokio::io::AsyncReadExt;
1133
1134 #[test]
1135 fn test_authority_of_keeps_a_plain_host() {
1136 assert_eq!(
1137 authority_of("s3.eu-central-1.amazonaws.com"),
1138 "s3.eu-central-1.amazonaws.com"
1139 );
1140 assert_eq!(authority_of("127.0.0.1:9000"), "127.0.0.1:9000");
1141 }
1142
1143 #[test]
1144 fn test_authority_of_drops_an_endpoint_path() {
1145 assert_eq!(
1146 authority_of("project.supabase.co/storage/v1/s3"),
1147 "project.supabase.co"
1148 );
1149 assert_eq!(
1150 authority_of("127.0.0.1:54321/storage/v1/s3"),
1151 "127.0.0.1:54321"
1152 );
1153 }
1154
1155 #[tokio::test]
1156 async fn test_async_read_implementation() {
1157 let chunks = vec![
1159 Ok(Bytes::from("Hello, ")),
1160 Ok(Bytes::from("World!")),
1161 Ok(Bytes::from(" This is a test.")),
1162 ];
1163
1164 let stream = stream::iter(chunks);
1165 let data_stream: DataStream = Box::pin(stream);
1166
1167 let mut response_stream = ResponseDataStream {
1168 bytes: data_stream,
1169 status_code: 200,
1170 };
1171
1172 let mut buffer = Vec::new();
1174 response_stream.read_to_end(&mut buffer).await.unwrap();
1175
1176 assert_eq!(buffer, b"Hello, World! This is a test.");
1177 }
1178
1179 #[tokio::test]
1180 async fn test_async_read_with_small_buffer_preserves_large_chunk() {
1181 let expected = b"This is a much longer string that won't fit in a small buffer";
1182 let chunks = vec![Ok(Bytes::copy_from_slice(expected))];
1183
1184 let stream = stream::iter(chunks);
1185 let data_stream: DataStream = Box::pin(stream);
1186
1187 let mut response_stream = ResponseDataStream {
1188 bytes: data_stream,
1189 status_code: 200,
1190 };
1191
1192 let mut output = Vec::new();
1193 let mut buffer = [0u8; 10];
1194
1195 loop {
1196 let n = response_stream.read(&mut buffer).await.unwrap();
1197 if n == 0 {
1198 break;
1199 }
1200 output.extend_from_slice(&buffer[..n]);
1201 }
1202
1203 assert_eq!(output, expected);
1204 }
1205
1206 #[tokio::test]
1207 async fn test_async_read_with_error() {
1208 use crate::error::S3Error;
1209
1210 let chunks: Vec<Result<Bytes, S3Error>> = vec![
1212 Ok(Bytes::from("Some data")),
1213 Err(S3Error::Io(std::io::Error::new(
1214 std::io::ErrorKind::Other,
1215 "Test error",
1216 ))),
1217 ];
1218
1219 let stream = stream::iter(chunks);
1220 let data_stream: DataStream = Box::pin(stream);
1221
1222 let mut response_stream = ResponseDataStream {
1223 bytes: data_stream,
1224 status_code: 200,
1225 };
1226
1227 let mut buffer = [0u8; 20];
1229 let n = response_stream.read(&mut buffer).await.unwrap();
1230 assert_eq!(n, 9);
1231 assert_eq!(&buffer[..n], b"Some data");
1232
1233 let result = response_stream.read(&mut buffer).await;
1235 assert!(result.is_err());
1236 }
1237
1238 #[tokio::test]
1239 async fn test_async_read_copy() {
1240 let chunks = vec![
1242 Ok(Bytes::from("First chunk\n")),
1243 Ok(Bytes::from("Second chunk\n")),
1244 Ok(Bytes::from("Third chunk\n")),
1245 ];
1246
1247 let stream = stream::iter(chunks);
1248 let data_stream: DataStream = Box::pin(stream);
1249
1250 let mut response_stream = ResponseDataStream {
1251 bytes: data_stream,
1252 status_code: 200,
1253 };
1254
1255 let mut output = Vec::new();
1256 tokio::io::copy(&mut response_stream, &mut output)
1257 .await
1258 .unwrap();
1259
1260 assert_eq!(output, b"First chunk\nSecond chunk\nThird chunk\n");
1261 }
1262}
1263
1264#[cfg(all(test, feature = "with-async-std"))]
1265mod async_std_tests {
1266 use super::*;
1267 use async_std::io::ReadExt;
1268 use bytes::Bytes;
1269 use futures_util::stream;
1270
1271 #[async_std::test]
1272 async fn test_async_read_implementation() {
1273 let chunks = vec![
1275 Ok(Bytes::from("Hello, ")),
1276 Ok(Bytes::from("World!")),
1277 Ok(Bytes::from(" This is a test.")),
1278 ];
1279
1280 let stream = stream::iter(chunks);
1281 let data_stream: DataStream = Box::pin(stream);
1282
1283 let mut response_stream = ResponseDataStream {
1284 bytes: data_stream,
1285 status_code: 200,
1286 };
1287
1288 let mut buffer = Vec::new();
1290 response_stream.read_to_end(&mut buffer).await.unwrap();
1291
1292 assert_eq!(buffer, b"Hello, World! This is a test.");
1293 }
1294
1295 #[async_std::test]
1296 async fn test_async_read_with_small_buffer_preserves_large_chunk() {
1297 let expected = b"This is a much longer string that won't fit in a small buffer";
1298 let chunks = vec![Ok(Bytes::copy_from_slice(expected))];
1299
1300 let stream = stream::iter(chunks);
1301 let data_stream: DataStream = Box::pin(stream);
1302
1303 let mut response_stream = ResponseDataStream {
1304 bytes: data_stream,
1305 status_code: 200,
1306 };
1307
1308 let mut output = Vec::new();
1309 let mut buffer = [0u8; 10];
1310
1311 loop {
1312 let n = response_stream.read(&mut buffer).await.unwrap();
1313 if n == 0 {
1314 break;
1315 }
1316 output.extend_from_slice(&buffer[..n]);
1317 }
1318
1319 assert_eq!(output, expected);
1320 }
1321
1322 #[async_std::test]
1323 async fn test_async_read_with_error() {
1324 use crate::error::S3Error;
1325
1326 let chunks: Vec<Result<Bytes, S3Error>> = vec![
1328 Ok(Bytes::from("Some data")),
1329 Err(S3Error::Io(std::io::Error::new(
1330 std::io::ErrorKind::Other,
1331 "Test error",
1332 ))),
1333 ];
1334
1335 let stream = stream::iter(chunks);
1336 let data_stream: DataStream = Box::pin(stream);
1337
1338 let mut response_stream = ResponseDataStream {
1339 bytes: data_stream,
1340 status_code: 200,
1341 };
1342
1343 let mut buffer = [0u8; 20];
1345 let n = response_stream.read(&mut buffer).await.unwrap();
1346 assert_eq!(n, 9);
1347 assert_eq!(&buffer[..n], b"Some data");
1348
1349 let result = response_stream.read(&mut buffer).await;
1351 assert!(result.is_err());
1352 }
1353
1354 #[async_std::test]
1355 async fn test_async_read_copy() {
1356 let chunks = vec![
1358 Ok(Bytes::from("First chunk\n")),
1359 Ok(Bytes::from("Second chunk\n")),
1360 Ok(Bytes::from("Third chunk\n")),
1361 ];
1362
1363 let stream = stream::iter(chunks);
1364 let data_stream: DataStream = Box::pin(stream);
1365
1366 let mut response_stream = ResponseDataStream {
1367 bytes: data_stream,
1368 status_code: 200,
1369 };
1370
1371 let mut output = Vec::new();
1372 async_std::io::copy(&mut response_stream, &mut output)
1373 .await
1374 .unwrap();
1375
1376 assert_eq!(output, b"First chunk\nSecond chunk\nThird chunk\n");
1377 }
1378}