1use crate::Credential;
19use crate::constants::{
20 AWS_QUERY_ENCODE_SET, X_AMZ_CONTENT_SHA_256, X_AMZ_DATE, X_AMZ_S3_SESSION_TOKEN,
21 X_AMZ_SECURITY_TOKEN,
22};
23use async_trait::async_trait;
24use http::request::Parts;
25use http::{HeaderValue, header};
26use log::debug;
27use percent_encoding::{percent_decode_str, utf8_percent_encode};
28use reqsign_core::hash::{hex_hmac_sha256, hex_sha256, hmac_sha256};
29use reqsign_core::time::Timestamp;
30use reqsign_core::{Context, Result, SignRequest, SigningRequest};
31use std::fmt::Write;
32use std::time::Duration;
33
34#[derive(Debug)]
38pub struct RequestSigner {
39 service: String,
40 region: String,
41
42 time: Option<Timestamp>,
43}
44
45impl RequestSigner {
46 pub fn new(service: &str, region: &str) -> Self {
48 Self {
49 service: service.into(),
50 region: region.into(),
51
52 time: None,
53 }
54 }
55
56 #[cfg(test)]
63 pub fn with_time(mut self, time: Timestamp) -> Self {
64 self.time = Some(time);
65 self
66 }
67}
68
69#[async_trait]
70impl SignRequest for RequestSigner {
71 type Credential = Credential;
72
73 async fn sign_request(
74 &self,
75 _: &Context,
76 req: &mut Parts,
77 credential: Option<&Self::Credential>,
78 expires_in: Option<Duration>,
79 ) -> Result<()> {
80 let Some(cred) = credential else {
81 return Ok(());
82 };
83
84 let now = self.time.unwrap_or_else(Timestamp::now);
85 let mut signed_req = SigningRequest::build(req)?;
86
87 canonicalize_header(&mut signed_req, cred, expires_in, now)?;
89 canonicalize_query(
90 &mut signed_req,
91 cred,
92 expires_in,
93 now,
94 &self.service,
95 &self.region,
96 )?;
97
98 let creq = canonical_request_string(&mut signed_req)?;
100 let encoded_req = hex_sha256(creq.as_bytes());
101
102 let scope = format!(
104 "{}/{}/{}/aws4_request",
105 now.format_date(),
106 self.region,
107 self.service
108 );
109 debug!("calculated scope: {scope}");
110
111 let string_to_sign = {
118 let mut f = String::new();
119 writeln!(f, "AWS4-HMAC-SHA256").map_err(|e| {
120 reqsign_core::Error::unexpected(format!("failed to write algorithm: {e}"))
121 })?;
122 writeln!(f, "{}", now.format_iso8601()).map_err(|e| {
123 reqsign_core::Error::unexpected(format!("failed to write timestamp: {e}"))
124 })?;
125 writeln!(f, "{}", &scope).map_err(|e| {
126 reqsign_core::Error::unexpected(format!("failed to write scope: {e}"))
127 })?;
128 write!(f, "{}", &encoded_req).map_err(|e| {
129 reqsign_core::Error::unexpected(format!("failed to write encoded request: {e}"))
130 })?;
131 f
132 };
133 debug!("calculated string to sign: {string_to_sign}");
134
135 let signing_key =
136 generate_signing_key(&cred.secret_access_key, now, &self.region, &self.service);
137 let signature = hex_hmac_sha256(&signing_key, string_to_sign.as_bytes());
138
139 if expires_in.is_some() {
140 signed_req.query.push(("X-Amz-Signature".into(), signature));
141 } else {
142 let mut authorization = HeaderValue::from_str(&format!(
143 "AWS4-HMAC-SHA256 Credential={}/{}, SignedHeaders={}, Signature={}",
144 cred.access_key_id,
145 scope,
146 signed_req.header_name_to_vec_sorted().join(";"),
147 signature
148 ))
149 .map_err(|e| {
150 reqsign_core::Error::unexpected(format!(
151 "failed to create authorization header: {e}"
152 ))
153 })?;
154 authorization.set_sensitive(true);
155
156 signed_req
157 .headers
158 .insert(header::AUTHORIZATION, authorization);
159 }
160
161 signed_req.apply(req)
163 }
164}
165
166fn canonical_request_string(ctx: &mut SigningRequest) -> Result<String> {
167 let mut f = String::with_capacity(256);
169
170 writeln!(f, "{}", ctx.method)
172 .map_err(|e| reqsign_core::Error::unexpected(format!("failed to write method: {e}")))?;
173 let path = percent_decode_str(&ctx.path)
175 .decode_utf8()
176 .map_err(|e| reqsign_core::Error::unexpected(format!("failed to decode path: {e}")))?;
177 writeln!(
178 f,
179 "{}",
180 utf8_percent_encode(&path, &super::constants::AWS_URI_ENCODE_SET)
181 )
182 .map_err(|e| reqsign_core::Error::unexpected(format!("failed to write encoded path: {e}")))?;
183 writeln!(
185 f,
186 "{}",
187 ctx.query
188 .iter()
189 .map(|(k, v)| { format!("{k}={v}") })
190 .collect::<Vec<_>>()
191 .join("&")
192 )
193 .map_err(|e| reqsign_core::Error::unexpected(format!("failed to write query: {e}")))?;
194 let signed_headers = ctx.header_name_to_vec_sorted();
196 for header in signed_headers.iter() {
197 let value = &ctx.headers[*header];
198 writeln!(
199 f,
200 "{}:{}",
201 header,
202 value.to_str().expect("header value must be valid")
203 )
204 .map_err(|e| reqsign_core::Error::unexpected(format!("failed to write header: {e}")))?;
205 }
206 writeln!(f)
207 .map_err(|e| reqsign_core::Error::unexpected(format!("failed to write newline: {e}")))?;
208 writeln!(f, "{}", signed_headers.join(";")).map_err(|e| {
209 reqsign_core::Error::unexpected(format!("failed to write signed headers: {e}"))
210 })?;
211
212 if ctx.headers.get(X_AMZ_CONTENT_SHA_256).is_none() {
213 write!(f, "UNSIGNED-PAYLOAD").map_err(|e| {
214 reqsign_core::Error::unexpected(format!("failed to write unsigned payload: {e}"))
215 })?;
216 } else {
217 write!(
218 f,
219 "{}",
220 ctx.headers[X_AMZ_CONTENT_SHA_256].to_str().map_err(|e| {
221 reqsign_core::Error::unexpected(format!("invalid header value: {e}"))
222 })?
223 )
224 .map_err(|e| {
225 reqsign_core::Error::unexpected(format!("failed to write content sha256: {e}"))
226 })?;
227 }
228
229 Ok(f)
230}
231
232fn canonicalize_header(
233 ctx: &mut SigningRequest,
234 cred: &Credential,
235 expires_in: Option<Duration>,
236 now: Timestamp,
237) -> Result<()> {
238 for (_, value) in ctx.headers.iter_mut() {
240 SigningRequest::header_value_normalize(value)
241 }
242
243 if ctx.headers.get(header::HOST).is_none() {
245 ctx.headers.insert(
246 header::HOST,
247 ctx.authority.as_str().parse().map_err(|e| {
248 reqsign_core::Error::unexpected(format!(
249 "failed to parse authority as header value: {e}"
250 ))
251 })?,
252 );
253 }
254
255 if expires_in.is_none() {
256 if ctx.headers.get(X_AMZ_DATE).is_none() {
258 let date_header = HeaderValue::try_from(now.format_iso8601()).map_err(|e| {
259 reqsign_core::Error::unexpected(format!("failed to create date header: {e}"))
260 })?;
261 ctx.headers.insert(X_AMZ_DATE, date_header);
262 }
263
264 if ctx.headers.get(X_AMZ_CONTENT_SHA_256).is_none() {
266 ctx.headers.insert(
267 X_AMZ_CONTENT_SHA_256,
268 HeaderValue::from_static("UNSIGNED-PAYLOAD"),
269 );
270 }
271
272 if let Some(token) = &cred.session_token {
274 let mut value = HeaderValue::from_str(token).map_err(|e| {
275 reqsign_core::Error::unexpected(format!(
276 "failed to create security token header: {e}"
277 ))
278 })?;
279 value.set_sensitive(true);
281
282 let is_s3_express = ctx.authority.as_str().contains("s3express")
284 || ctx.authority.as_str().contains("--x-s3");
285
286 if is_s3_express {
287 ctx.headers.insert(X_AMZ_S3_SESSION_TOKEN, value);
288 } else {
289 ctx.headers.insert(X_AMZ_SECURITY_TOKEN, value);
290 }
291 }
292 }
293
294 Ok(())
295}
296
297fn canonicalize_query(
298 ctx: &mut SigningRequest,
299 cred: &Credential,
300 expires_in: Option<Duration>,
301 now: Timestamp,
302 service: &str,
303 region: &str,
304) -> Result<()> {
305 if let Some(expire) = expires_in {
306 ctx.query
307 .push(("X-Amz-Algorithm".into(), "AWS4-HMAC-SHA256".into()));
308 ctx.query.push((
309 "X-Amz-Credential".into(),
310 format!(
311 "{}/{}/{}/{}/aws4_request",
312 cred.access_key_id,
313 now.format_date(),
314 region,
315 service
316 ),
317 ));
318 ctx.query.push(("X-Amz-Date".into(), now.format_iso8601()));
319 ctx.query
320 .push(("X-Amz-Expires".into(), expire.as_secs().to_string()));
321 ctx.query.push((
322 "X-Amz-SignedHeaders".into(),
323 ctx.header_name_to_vec_sorted().join(";"),
324 ));
325
326 if let Some(token) = &cred.session_token {
327 ctx.query
328 .push(("X-Amz-Security-Token".into(), token.into()));
329 }
330 }
331
332 if ctx.query.is_empty() {
334 return Ok(());
335 }
336
337 ctx.query.sort();
339
340 ctx.query = ctx
341 .query
342 .iter()
343 .map(|(k, v)| {
344 (
345 utf8_percent_encode(k, &AWS_QUERY_ENCODE_SET).to_string(),
346 utf8_percent_encode(v, &AWS_QUERY_ENCODE_SET).to_string(),
347 )
348 })
349 .collect();
350
351 Ok(())
352}
353
354fn generate_signing_key(secret: &str, time: Timestamp, region: &str, service: &str) -> Vec<u8> {
355 let secret = format!("AWS4{secret}");
357 let sign_date = hmac_sha256(secret.as_bytes(), time.format_date().as_bytes());
359 let sign_region = hmac_sha256(sign_date.as_slice(), region.as_bytes());
361 let sign_service = hmac_sha256(sign_region.as_slice(), service.as_bytes());
363 hmac_sha256(sign_service.as_slice(), "aws4_request".as_bytes())
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370 use crate::provide_credential::StaticCredentialProvider;
371 use anyhow::Result;
372 use aws_credential_types::Credentials;
373 use aws_sigv4::http_request::PayloadChecksumKind;
374 use aws_sigv4::http_request::PercentEncodingMode;
375 use aws_sigv4::http_request::SignableBody;
376 use aws_sigv4::http_request::SignableRequest;
377 use aws_sigv4::http_request::SignatureLocation;
378 use aws_sigv4::http_request::SigningSettings;
379 use aws_sigv4::sign::v4;
380 use http::Request;
381 use http::header;
382 use reqsign_core::ProvideCredential;
383 use reqsign_file_read_tokio::TokioFileRead;
384 use reqsign_http_send_reqwest::ReqwestHttpSend;
385
386 type TestCase = (&'static str, fn() -> Request<&'static str>);
388
389 fn test_cases() -> Vec<TestCase> {
390 vec![
391 ("get_request", test_get_request),
392 ("get_request_with_sse", test_get_request_with_sse),
393 ("get_request_with_query", test_get_request_with_query),
394 ("get_request_virtual_host", test_get_request_virtual_host),
395 (
396 "get_request_with_query_virtual_host",
397 test_get_request_with_query_virtual_host,
398 ),
399 ("put_request", test_put_request),
400 (
401 "put_request_with_body_digest",
402 test_put_request_with_body_digest,
403 ),
404 ("put_request_virtual_host", test_put_request_virtual_host),
405 ]
406 }
407
408 fn test_get_request() -> Request<&'static str> {
409 let mut req = Request::new("");
410 *req.method_mut() = http::Method::GET;
411 *req.uri_mut() = "http://127.0.0.1:9000/hello"
412 .parse()
413 .expect("url must be valid");
414
415 req
416 }
417
418 fn test_get_request_with_sse() -> Request<&'static str> {
419 let mut req = Request::new("");
420 *req.method_mut() = http::Method::GET;
421 *req.uri_mut() = "http://127.0.0.1:9000/hello"
422 .parse()
423 .expect("url must be valid");
424 req.headers_mut().insert(
425 "x-amz-server-side-encryption",
426 "a".parse().expect("must be valid"),
427 );
428 req.headers_mut().insert(
429 "x-amz-server-side-encryption-customer-algorithm",
430 "b".parse().expect("must be valid"),
431 );
432 req.headers_mut().insert(
433 "x-amz-server-side-encryption-customer-key",
434 "c".parse().expect("must be valid"),
435 );
436 req.headers_mut().insert(
437 "x-amz-server-side-encryption-customer-key-md5",
438 "d".parse().expect("must be valid"),
439 );
440 req.headers_mut().insert(
441 "x-amz-server-side-encryption-aws-kms-key-id",
442 "e".parse().expect("must be valid"),
443 );
444
445 req
446 }
447
448 fn test_get_request_with_query() -> Request<&'static str> {
449 let mut req = Request::new("");
450 *req.method_mut() = http::Method::GET;
451 *req.uri_mut() = "http://127.0.0.1:9000/hello?list-type=2&max-keys=3&prefix=CI/&start-after=ExampleGuide.pdf"
452 .parse()
453 .expect("url must be valid");
454
455 req
456 }
457
458 fn test_get_request_virtual_host() -> Request<&'static str> {
459 let mut req = Request::new("");
460 *req.method_mut() = http::Method::GET;
461 *req.uri_mut() = "http://hello.s3.test.example.com"
462 .parse()
463 .expect("url must be valid");
464
465 req
466 }
467
468 fn test_get_request_with_query_virtual_host() -> Request<&'static str> {
469 let mut req = Request::new("");
470 *req.method_mut() = http::Method::GET;
471 *req.uri_mut() = "http://hello.s3.test.example.com?list-type=2&max-keys=3&prefix=CI/&start-after=ExampleGuide.pdf"
472 .parse()
473 .expect("url must be valid");
474
475 req
476 }
477
478 fn test_put_request() -> Request<&'static str> {
479 let content = "Hello,World!";
480 let mut req = Request::new(content);
481 *req.method_mut() = http::Method::PUT;
482 *req.uri_mut() = "http://127.0.0.1:9000/hello"
483 .parse()
484 .expect("url must be valid");
485
486 req.headers_mut().insert(
487 header::CONTENT_LENGTH,
488 HeaderValue::from_str(&content.len().to_string()).expect("must be valid"),
489 );
490
491 req
492 }
493
494 fn test_put_request_with_body_digest() -> Request<&'static str> {
495 let content = "Hello,World!";
496 let mut req = Request::new(content);
497 *req.method_mut() = http::Method::PUT;
498 *req.uri_mut() = "http://127.0.0.1:9000/hello"
499 .parse()
500 .expect("url must be valid");
501
502 req.headers_mut().insert(
503 header::CONTENT_LENGTH,
504 HeaderValue::from_str(&content.len().to_string()).expect("must be valid"),
505 );
506
507 let body = hex_sha256(content.as_bytes());
508 req.headers_mut().insert(
509 "x-amz-content-sha256",
510 HeaderValue::from_str(&body).expect("must be valid"),
511 );
512
513 req
514 }
515
516 fn test_put_request_virtual_host() -> Request<&'static str> {
517 let content = "Hello,World!";
518 let mut req = Request::new(content);
519 *req.method_mut() = http::Method::PUT;
520 *req.uri_mut() = "http://hello.s3.test.example.com"
521 .parse()
522 .expect("url must be valid");
523
524 req.headers_mut().insert(
525 header::CONTENT_LENGTH,
526 HeaderValue::from_str(&content.len().to_string()).expect("must be valid"),
527 );
528
529 req
530 }
531
532 #[track_caller]
533 fn compare_request(name: &str, l: &Request<&str>, r: &Request<&str>) {
534 fn format_headers(req: &Request<&str>) -> Vec<String> {
535 let mut hs = req
536 .headers()
537 .iter()
538 .map(|(k, v)| format!("{}:{}", k, v.to_str().expect("must be valid")))
539 .collect::<Vec<_>>();
540
541 if !hs.contains(&format!("host:{}", req.uri().authority().unwrap())) {
543 hs.push(format!("host:{}", req.uri().authority().unwrap()))
544 }
545
546 hs.sort();
547 hs
548 }
549
550 assert_eq!(
551 format_headers(l),
552 format_headers(r),
553 "{name} header mismatch"
554 );
555
556 fn format_query(req: &Request<&str>) -> Vec<String> {
557 let query = req.uri().query().unwrap_or_default();
558 let mut query = form_urlencoded::parse(query.as_bytes())
559 .map(|(k, v)| format!("{}={}", &k, &v))
560 .collect::<Vec<_>>();
561 query.sort();
562 query
563 }
564
565 assert_eq!(format_query(l), format_query(r), "{name} query mismatch");
566 }
567
568 #[tokio::test]
569 async fn test() -> Result<()> {
570 for (name, req) in test_cases() {
571 calculate(req)
572 .await
573 .unwrap_or_else(|err| panic!("calculate {name} should pass: {err:?}"));
574 calculate_in_query(req)
575 .await
576 .unwrap_or_else(|err| panic!("calculate_in_query {name} should pass: {err:?}"));
577 test_calculate_with_token(req).await.unwrap_or_else(|err| {
578 panic!("test_calculate_with_token {name} should pass: {err:?}")
579 });
580 test_calculate_with_token_in_query(req)
581 .await
582 .unwrap_or_else(|err| {
583 panic!("test_calculate_with_token_in_query {name} should pass: {err:?}")
584 });
585 }
586 Ok(())
587 }
588
589 async fn calculate(req_fn: fn() -> Request<&'static str>) -> Result<()> {
590 let _ = env_logger::builder().is_test(true).try_init();
591
592 let mut req = req_fn();
593 let name = format!(
594 "{} {} {:?}",
595 req.method(),
596 req.uri().path(),
597 req.uri().query(),
598 );
599 let now = Timestamp::now();
600
601 let mut ss = SigningSettings::default();
602 ss.percent_encoding_mode = PercentEncodingMode::Double;
603 ss.payload_checksum_kind = PayloadChecksumKind::XAmzSha256;
604 let id = Credentials::new(
605 "access_key_id",
606 "secret_access_key",
607 None,
608 None,
609 "hardcoded-credentials",
610 )
611 .into();
612 let sp = v4::SigningParams::builder()
613 .identity(&id)
614 .region("test")
615 .name("s3")
616 .time(now.as_system_time())
617 .settings(ss)
618 .build()
619 .expect("signing params must be valid");
620
621 let mut body = SignableBody::UnsignedPayload;
622 if req.headers().get(X_AMZ_CONTENT_SHA_256).is_some() {
623 body = SignableBody::Bytes(req.body().as_bytes());
624 }
625
626 let output = aws_sigv4::http_request::sign(
627 SignableRequest::new(
628 req.method().as_str(),
629 req.uri().to_string(),
630 req.headers()
631 .iter()
632 .map(|(k, v)| (k.as_str(), std::str::from_utf8(v.as_bytes()).unwrap())),
633 body,
634 )
635 .unwrap(),
636 &sp.into(),
637 )?;
638 let (aws_sig, _) = output.into_parts();
639 aws_sig.apply_to_request_http1x(&mut req);
640 let expected_req = req;
641
642 let req = req_fn();
643 let (mut parts, body) = req.into_parts();
644
645 let ctx = Context::new()
646 .with_file_read(TokioFileRead)
647 .with_http_send(ReqwestHttpSend::default());
648 let loader = StaticCredentialProvider::new("access_key_id", "secret_access_key");
649 let cred = loader.provide_credential(&ctx).await?.unwrap();
650
651 let builder = RequestSigner::new("s3", "test").with_time(now);
652 builder
653 .sign_request(&ctx, &mut parts, Some(&cred), None)
654 .await
655 .expect("must apply success");
656
657 let actual_req = Request::from_parts(parts, body);
658
659 compare_request(&name, &expected_req, &actual_req);
660
661 Ok(())
662 }
663
664 async fn calculate_in_query(req_fn: fn() -> Request<&'static str>) -> Result<()> {
665 let _ = env_logger::builder().is_test(true).try_init();
666
667 let mut req = req_fn();
668 let name = format!(
669 "{} {} {:?}",
670 req.method(),
671 req.uri().path(),
672 req.uri().query(),
673 );
674 let now = Timestamp::now();
675
676 let mut ss = SigningSettings::default();
677 ss.percent_encoding_mode = PercentEncodingMode::Double;
678 ss.payload_checksum_kind = PayloadChecksumKind::XAmzSha256;
679 ss.signature_location = SignatureLocation::QueryParams;
680 ss.expires_in = Some(Duration::from_secs(3600));
681 let id = Credentials::new(
682 "access_key_id",
683 "secret_access_key",
684 None,
685 None,
686 "hardcoded-credentials",
687 )
688 .into();
689 let sp = v4::SigningParams::builder()
690 .identity(&id)
691 .region("test")
692 .name("s3")
693 .time(now.as_system_time())
694 .settings(ss)
695 .build()
696 .expect("signing params must be valid");
697
698 let mut body = SignableBody::UnsignedPayload;
699 if req.headers().get(X_AMZ_CONTENT_SHA_256).is_some() {
700 body = SignableBody::Bytes(req.body().as_bytes());
701 }
702
703 let output = aws_sigv4::http_request::sign(
704 SignableRequest::new(
705 req.method().as_str(),
706 req.uri().to_string(),
707 req.headers()
708 .iter()
709 .map(|(k, v)| (k.as_str(), std::str::from_utf8(v.as_bytes()).unwrap())),
710 body,
711 )
712 .unwrap(),
713 &sp.into(),
714 )
715 .expect("signing must succeed");
716 let (aws_sig, _) = output.into_parts();
717 aws_sig.apply_to_request_http1x(&mut req);
718 let expected_req = req;
719
720 let req = req_fn();
721 let (mut parts, body) = req.into_parts();
722
723 let ctx = Context::new()
724 .with_file_read(TokioFileRead)
725 .with_http_send(ReqwestHttpSend::default());
726 let loader = StaticCredentialProvider::new("access_key_id", "secret_access_key");
727 let cred = loader.provide_credential(&ctx).await?.unwrap();
728
729 let builder = RequestSigner::new("s3", "test").with_time(now);
730
731 builder
732 .sign_request(
733 &ctx,
734 &mut parts,
735 Some(&cred),
736 Some(Duration::from_secs(3600)),
737 )
738 .await?;
739 let actual_req = Request::from_parts(parts, body);
740
741 compare_request(&name, &expected_req, &actual_req);
742
743 Ok(())
744 }
745
746 async fn test_calculate_with_token(req_fn: fn() -> Request<&'static str>) -> Result<()> {
747 let _ = env_logger::builder().is_test(true).try_init();
748
749 let mut req = req_fn();
750 let name = format!(
751 "{} {} {:?}",
752 req.method(),
753 req.uri().path(),
754 req.uri().query(),
755 );
756 let now = Timestamp::now();
757
758 let mut ss = SigningSettings::default();
759 ss.percent_encoding_mode = PercentEncodingMode::Double;
760 ss.payload_checksum_kind = PayloadChecksumKind::XAmzSha256;
761 let id = Credentials::new(
762 "access_key_id",
763 "secret_access_key",
764 Some("security_token".to_string()),
765 None,
766 "hardcoded-credentials",
767 )
768 .into();
769 let sp = v4::SigningParams::builder()
770 .identity(&id)
771 .region("test")
772 .name("s3")
773 .time(now.as_system_time())
774 .settings(ss)
775 .build()
776 .expect("signing params must be valid");
777
778 let mut body = SignableBody::UnsignedPayload;
779 if req.headers().get(X_AMZ_CONTENT_SHA_256).is_some() {
780 body = SignableBody::Bytes(req.body().as_bytes());
781 }
782
783 let output = aws_sigv4::http_request::sign(
784 SignableRequest::new(
785 req.method().as_str(),
786 req.uri().to_string(),
787 req.headers()
788 .iter()
789 .map(|(k, v)| (k.as_str(), std::str::from_utf8(v.as_bytes()).unwrap())),
790 body,
791 )
792 .unwrap(),
793 &sp.into(),
794 )
795 .expect("signing must succeed");
796 let (aws_sig, _) = output.into_parts();
797 aws_sig.apply_to_request_http1x(&mut req);
798 let expected_req = req;
799
800 let req = req_fn();
801 let (mut parts, body) = req.into_parts();
802
803 let ctx = Context::new()
804 .with_file_read(TokioFileRead)
805 .with_http_send(ReqwestHttpSend::default());
806 let loader = StaticCredentialProvider::new("access_key_id", "secret_access_key")
807 .with_session_token("security_token");
808 let cred = loader.provide_credential(&ctx).await?.unwrap();
809
810 let builder = RequestSigner::new("s3", "test").with_time(now);
811 builder
812 .sign_request(&ctx, &mut parts, Some(&cred), None)
813 .await
814 .expect("must apply success");
815 let actual_req = Request::from_parts(parts, body);
816
817 compare_request(&name, &expected_req, &actual_req);
818
819 Ok(())
820 }
821
822 async fn test_calculate_with_token_in_query(
823 req_fn: fn() -> Request<&'static str>,
824 ) -> Result<()> {
825 let _ = env_logger::builder().is_test(true).try_init();
826
827 let mut req = req_fn();
828 let name = format!(
829 "{} {} {:?}",
830 req.method(),
831 req.uri().path(),
832 req.uri().query(),
833 );
834 let now = Timestamp::now();
835
836 let mut ss = SigningSettings::default();
837 ss.percent_encoding_mode = PercentEncodingMode::Double;
838 ss.payload_checksum_kind = PayloadChecksumKind::XAmzSha256;
839 ss.signature_location = SignatureLocation::QueryParams;
840 ss.expires_in = Some(Duration::from_secs(3600));
841 let id = Credentials::new(
842 "access_key_id",
843 "secret_access_key",
844 Some("security_token".to_string()),
845 None,
846 "hardcoded-credentials",
847 )
848 .into();
849 let sp = v4::SigningParams::builder()
850 .identity(&id)
851 .region("test")
852 .name("s3")
854 .time(now.as_system_time())
855 .settings(ss)
856 .build()
857 .expect("signing params must be valid");
858
859 let mut body = SignableBody::UnsignedPayload;
860 if req.headers().get(X_AMZ_CONTENT_SHA_256).is_some() {
861 body = SignableBody::Bytes(req.body().as_bytes());
862 }
863
864 let output = aws_sigv4::http_request::sign(
865 SignableRequest::new(
866 req.method().as_str(),
867 req.uri().to_string(),
868 req.headers()
869 .iter()
870 .map(|(k, v)| (k.as_str(), std::str::from_utf8(v.as_bytes()).unwrap())),
871 body,
872 )
873 .unwrap(),
874 &sp.into(),
875 )
876 .expect("signing must succeed");
877 let (aws_sig, _) = output.into_parts();
878 aws_sig.apply_to_request_http1x(&mut req);
879 let expected_req = req;
880
881 let req = req_fn();
882 let (mut parts, body) = req.into_parts();
883
884 let ctx = Context::new()
885 .with_file_read(TokioFileRead)
886 .with_http_send(ReqwestHttpSend::default());
887 let loader = StaticCredentialProvider::new("access_key_id", "secret_access_key")
888 .with_session_token("security_token");
889 let cred = loader.provide_credential(&ctx).await?.unwrap();
890
891 let builder = RequestSigner::new("s3", "test").with_time(now);
892 builder
893 .sign_request(
894 &ctx,
895 &mut parts,
896 Some(&cred),
897 Some(Duration::from_secs(3600)),
898 )
899 .await
900 .expect("must apply success");
901 let actual_req = Request::from_parts(parts, body);
902
903 compare_request(&name, &expected_req, &actual_req);
904
905 Ok(())
906 }
907}