1use crate::error::S3Error;
30use crate::utils::now_utc;
31use crate::{Bucket, LONG_DATETIME, signing};
32
33use awscreds::Rfc3339OffsetDateTime;
34use awscreds::error::CredentialsError;
35use serde::ser;
36use serde::ser::{Serialize, SerializeMap, SerializeSeq, SerializeTuple, Serializer};
37use std::borrow::Cow;
38use std::collections::HashMap;
39use thiserror::Error;
40use time::{Duration, OffsetDateTime};
41
42#[derive(Clone, Debug)]
43pub struct PostPolicy<'a> {
44 expiration: PostPolicyExpiration,
45 conditions: ConditionsSerializer<'a>,
46}
47
48impl<'a> PostPolicy<'a> {
49 pub fn new<T>(expiration: T) -> Self
50 where
51 T: Into<PostPolicyExpiration>,
52 {
53 Self {
54 expiration: expiration.into(),
55 conditions: ConditionsSerializer(Vec::new()),
56 }
57 }
58
59 #[maybe_async::maybe_async]
61 async fn build(
62 &self,
63 now: &OffsetDateTime,
64 bucket: &Bucket,
65 ) -> Result<PostPolicy<'_>, S3Error> {
66 let access_key = bucket.access_key().await?.ok_or(S3Error::Credentials(
67 CredentialsError::ConfigMissingAccessKeyId,
68 ))?;
69 let credential = format!(
70 "{}/{}",
71 access_key,
72 signing::scope_string(now, &bucket.region)?
73 );
74
75 let mut post_policy = self
76 .clone()
77 .condition(
78 PostPolicyField::Bucket,
79 PostPolicyValue::Exact(Cow::from(bucket.name.clone())),
80 )?
81 .condition(
82 PostPolicyField::AmzAlgorithm,
83 PostPolicyValue::Exact(Cow::from("AWS4-HMAC-SHA256")),
84 )?
85 .condition(
86 PostPolicyField::AmzCredential,
87 PostPolicyValue::Exact(Cow::from(credential)),
88 )?
89 .condition(
90 PostPolicyField::AmzDate,
91 PostPolicyValue::Exact(Cow::from(now.format(LONG_DATETIME)?)),
92 )?;
93
94 if let Some(security_token) = bucket.security_token().await? {
95 post_policy = post_policy.condition(
96 PostPolicyField::AmzSecurityToken,
97 PostPolicyValue::Exact(Cow::from(security_token)),
98 )?;
99 }
100 Ok(post_policy.clone())
101 }
102
103 fn policy_string(&self) -> Result<String, S3Error> {
104 use base64::Engine;
105 use base64::engine::general_purpose;
106
107 let data = serde_json::to_string(self)?;
108
109 Ok(general_purpose::STANDARD.encode(data))
110 }
111
112 #[maybe_async::maybe_async]
113 pub async fn sign(&self, bucket: Box<Bucket>) -> Result<PresignedPost, S3Error> {
114 use hmac::Mac;
115
116 bucket.credentials_refresh().await?;
117 let now = now_utc();
118
119 let policy = self.build(&now, &bucket).await?;
120 let policy_string = policy.policy_string()?;
121
122 let signing_key = signing::signing_key(
123 &now,
124 &bucket.secret_key().await?.ok_or(S3Error::Credentials(
125 CredentialsError::ConfigMissingSecretKey,
126 ))?,
127 &bucket.region,
128 "s3",
129 )?;
130
131 let mut hmac = signing::HmacSha256::new_from_slice(&signing_key)?;
132 hmac.update(policy_string.as_bytes());
133 let signature = hex::encode(hmac.finalize().into_bytes());
134 let mut fields: HashMap<String, String> = HashMap::new();
135 let mut dynamic_fields = HashMap::new();
136 for field in policy.conditions.0.iter() {
137 let f: Cow<str> = field.field.clone().into();
138 match &field.value {
139 PostPolicyValue::Anything => {
140 dynamic_fields.insert(f.to_string(), "".to_string());
141 }
142 PostPolicyValue::StartsWith(e) => {
143 dynamic_fields.insert(f.to_string(), e.clone().into_owned());
144 }
145 PostPolicyValue::Range(b, e) => {
146 dynamic_fields.insert(f.to_string(), format!("{},{}", b, e));
147 }
148 PostPolicyValue::Exact(e) => {
149 fields.insert(f.to_string(), e.clone().into_owned());
150 }
151 }
152 }
153 fields.insert("x-amz-signature".to_string(), signature);
154 fields.insert("Policy".to_string(), policy_string);
155 let url = bucket.url();
156 Ok(PresignedPost {
157 url,
158 fields,
159 dynamic_fields,
160 expiration: policy.expiration.into(),
161 })
162 }
163
164 pub fn condition(
166 mut self,
167 field: PostPolicyField<'a>,
168 value: PostPolicyValue<'a>,
169 ) -> Result<Self, S3Error> {
170 if matches!(field, PostPolicyField::ContentLengthRange)
171 != matches!(value, PostPolicyValue::Range(_, _))
172 {
173 Err(PostPolicyError::MismatchedCondition)?
174 }
175 self.conditions.0.push(PostPolicyCondition { field, value });
176 Ok(self)
177 }
178}
179
180impl Serialize for PostPolicy<'_> {
181 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
182 where
183 S: Serializer,
184 {
185 let mut map = serializer.serialize_map(Some(2))?;
186 map.serialize_entry("expiration", &self.expiration)?;
187 map.serialize_entry("conditions", &self.conditions)?;
188 map.end()
189 }
190}
191
192#[derive(Clone, Debug)]
193struct ConditionsSerializer<'a>(Vec<PostPolicyCondition<'a>>);
194
195impl Serialize for ConditionsSerializer<'_> {
196 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
197 where
198 S: Serializer,
199 {
200 let mut seq = serializer.serialize_seq(None)?;
201 for e in self.0.iter() {
202 if let PostPolicyField::AmzChecksumAlgorithm(checksum) = &e.field {
203 let checksum: Cow<str> = (*checksum).into();
204 seq.serialize_element(&PostPolicyCondition {
205 field: PostPolicyField::Custom(Cow::from("x-amz-checksum-algorithm")),
206 value: PostPolicyValue::Exact(Cow::from(checksum.to_uppercase())),
207 })?;
208 }
209 seq.serialize_element(&e)?;
210 }
211 seq.end()
212 }
213}
214
215#[derive(Clone, Debug)]
216struct PostPolicyCondition<'a> {
217 field: PostPolicyField<'a>,
218 value: PostPolicyValue<'a>,
219}
220
221impl Serialize for PostPolicyCondition<'_> {
222 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
223 where
224 S: Serializer,
225 {
226 let f: Cow<str> = self.field.clone().into();
227
228 match &self.value {
229 PostPolicyValue::Exact(e) => {
230 let mut map = serializer.serialize_map(Some(1))?;
231 map.serialize_entry(&f, e)?;
232 map.end()
233 }
234 PostPolicyValue::StartsWith(e) => {
235 let mut seq = serializer.serialize_tuple(3)?;
236 seq.serialize_element("starts-with")?;
237 let field = format!("${}", f);
238 seq.serialize_element(&field)?;
239 seq.serialize_element(e)?;
240 seq.end()
241 }
242 PostPolicyValue::Anything => {
243 let mut seq = serializer.serialize_tuple(3)?;
244 seq.serialize_element("starts-with")?;
245 let field = format!("${}", f);
246 seq.serialize_element(&field)?;
247 seq.serialize_element("")?;
248 seq.end()
249 }
250 PostPolicyValue::Range(b, e) => {
251 if matches!(self.field, PostPolicyField::ContentLengthRange) {
252 let mut seq = serializer.serialize_tuple(3)?;
253 seq.serialize_element("content-length-range")?;
254 seq.serialize_element(b)?;
255 seq.serialize_element(e)?;
256 seq.end()
257 } else {
258 Err(ser::Error::custom(
259 "Range is only valid for ContentLengthRange",
260 ))
261 }
262 }
263 }
264 }
265}
266
267#[derive(Clone, Debug)]
269#[non_exhaustive]
270pub enum PostPolicyField<'a> {
271 Key,
273 Acl,
275 Tagging,
277 SuccessActionRedirect,
279 SuccessActionStatus,
281
282 CacheControl,
284 ContentLengthRange,
286 ContentType,
288 ContentDisposition,
290 ContentEncoding,
292 Expires,
294
295 AmzServerSideEncryption,
297 AmzServerSideEncryptionKeyId,
299 AmzServerSideEncryptionContext,
301 AmzStorageClass,
303 AmzWebsiteRedirectLocation,
305 AmzChecksumAlgorithm(PostPolicyChecksum),
307 AmzMeta(Cow<'a, str>),
309
310 AmzCredential,
312 AmzAlgorithm,
314 AmzDate,
316 AmzSecurityToken,
318 Bucket,
320
321 Custom(Cow<'a, str>),
323}
324
325#[allow(clippy::from_over_into)]
326impl<'a> Into<Cow<'a, str>> for PostPolicyField<'a> {
327 fn into(self) -> Cow<'a, str> {
328 match self {
329 PostPolicyField::Key => Cow::from("key"),
330 PostPolicyField::Acl => Cow::from("acl"),
331 PostPolicyField::Tagging => Cow::from("tagging"),
332 PostPolicyField::SuccessActionRedirect => Cow::from("success_action_redirect"),
333 PostPolicyField::SuccessActionStatus => Cow::from("success_action_status"),
334 PostPolicyField::CacheControl => Cow::from("Cache-Control"),
335 PostPolicyField::ContentLengthRange => Cow::from("content-length-range"),
336 PostPolicyField::ContentType => Cow::from("Content-Type"),
337 PostPolicyField::ContentDisposition => Cow::from("Content-Disposition"),
338 PostPolicyField::ContentEncoding => Cow::from("Content-Encoding"),
339 PostPolicyField::Expires => Cow::from("Expires"),
340
341 PostPolicyField::AmzServerSideEncryption => Cow::from("x-amz-server-side-encryption"),
342 PostPolicyField::AmzServerSideEncryptionKeyId => {
343 Cow::from("x-amz-server-side-encryption-aws-kms-key-id")
344 }
345 PostPolicyField::AmzServerSideEncryptionContext => {
346 Cow::from("x-amz-server-side-encryption-context")
347 }
348 PostPolicyField::AmzStorageClass => Cow::from("x-amz-storage-class"),
349 PostPolicyField::AmzWebsiteRedirectLocation => {
350 Cow::from("x-amz-website-redirect-location")
351 }
352 PostPolicyField::AmzChecksumAlgorithm(e) => {
353 let e: Cow<str> = e.into();
354 Cow::from(format!("x-amz-checksum-{}", e))
355 }
356 PostPolicyField::AmzMeta(e) => Cow::from(format!("x-amz-meta-{}", e)),
357 PostPolicyField::AmzCredential => Cow::from("x-amz-credential"),
358 PostPolicyField::AmzAlgorithm => Cow::from("x-amz-algorithm"),
359 PostPolicyField::AmzDate => Cow::from("x-amz-date"),
360 PostPolicyField::AmzSecurityToken => Cow::from("x-amz-security-token"),
361 PostPolicyField::Bucket => Cow::from("bucket"),
362 PostPolicyField::Custom(e) => e,
363 }
364 }
365}
366
367#[derive(Clone, Copy, Debug)]
368pub enum PostPolicyChecksum {
369 CRC32,
370 CRC32c,
371 SHA1,
372 SHA256,
373}
374
375#[allow(clippy::from_over_into)]
376impl<'a> Into<Cow<'a, str>> for PostPolicyChecksum {
377 fn into(self) -> Cow<'a, str> {
378 match self {
379 PostPolicyChecksum::CRC32 => Cow::from("crc32"),
380 PostPolicyChecksum::CRC32c => Cow::from("crc32c"),
381 PostPolicyChecksum::SHA1 => Cow::from("sha1"),
382 PostPolicyChecksum::SHA256 => Cow::from("sha256"),
383 }
384 }
385}
386
387#[derive(Clone, Debug)]
388pub enum PostPolicyValue<'a> {
389 Anything,
391 StartsWith(Cow<'a, str>),
393 Range(u32, u32),
395 Exact(Cow<'a, str>),
397}
398
399#[derive(Clone, Debug)]
400pub enum PostPolicyExpiration {
401 ExpiresIn(u32),
403 ExpiresAt(Rfc3339OffsetDateTime),
405}
406
407impl From<u32> for PostPolicyExpiration {
408 fn from(value: u32) -> Self {
409 Self::ExpiresIn(value)
410 }
411}
412
413impl From<Rfc3339OffsetDateTime> for PostPolicyExpiration {
414 fn from(value: Rfc3339OffsetDateTime) -> Self {
415 Self::ExpiresAt(value)
416 }
417}
418
419impl From<PostPolicyExpiration> for Rfc3339OffsetDateTime {
420 fn from(value: PostPolicyExpiration) -> Self {
421 match value {
422 PostPolicyExpiration::ExpiresIn(d) => {
423 Rfc3339OffsetDateTime(now_utc().saturating_add(Duration::seconds(d as i64)))
424 }
425 PostPolicyExpiration::ExpiresAt(t) => t,
426 }
427 }
428}
429
430impl Serialize for PostPolicyExpiration {
431 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
432 where
433 S: Serializer,
434 {
435 Rfc3339OffsetDateTime::from(self.clone()).serialize(serializer)
436 }
437}
438
439#[derive(Debug)]
440pub struct PresignedPost {
441 pub url: String,
442 pub fields: HashMap<String, String>,
443 pub dynamic_fields: HashMap<String, String>,
444 pub expiration: Rfc3339OffsetDateTime,
445}
446
447#[derive(Error, Debug)]
448#[non_exhaustive]
449pub enum PostPolicyError {
450 #[error("This value is not supported for this field")]
451 MismatchedCondition,
452}
453
454#[cfg(test)]
455mod test {
456 use super::*;
457
458 use crate::creds::Credentials;
459 use crate::region::Region;
460 use crate::utils::with_timestamp;
461
462 use serde_json::json;
463
464 fn test_bucket() -> Box<Bucket> {
465 Bucket::new(
466 "rust-s3",
467 Region::UsEast1,
468 Credentials::new(
469 Some("AKIAIOSFODNN7EXAMPLE"),
470 Some("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"),
471 None,
472 None,
473 None,
474 )
475 .unwrap(),
476 )
477 .unwrap()
478 }
479
480 fn test_bucket_with_security_token() -> Box<Bucket> {
481 Bucket::new(
482 "rust-s3",
483 Region::UsEast1,
484 Credentials::new(
485 Some("AKIAIOSFODNN7EXAMPLE"),
486 Some("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"),
487 Some("SomeSecurityToken"),
488 None,
489 None,
490 )
491 .unwrap(),
492 )
493 .unwrap()
494 }
495
496 mod conditions {
497 use super::*;
498
499 #[test]
500 fn starts_with_condition() {
501 let policy = PostPolicy::new(300)
502 .condition(
503 PostPolicyField::Key,
504 PostPolicyValue::StartsWith(Cow::from("users/user1/")),
505 )
506 .unwrap();
507
508 let data = serde_json::to_value(&policy).unwrap();
509
510 assert!(data["expiration"].is_string());
511 assert_eq!(
512 data["conditions"],
513 json!([["starts-with", "$key", "users/user1/"]])
514 );
515 }
516
517 #[test]
518 fn exact_condition() {
519 let policy = PostPolicy::new(300)
520 .condition(
521 PostPolicyField::Acl,
522 PostPolicyValue::Exact(Cow::from("public-read")),
523 )
524 .unwrap();
525
526 let data = serde_json::to_value(&policy).unwrap();
527
528 assert!(data["expiration"].is_string());
529 assert_eq!(data["conditions"], json!([{"acl":"public-read"}]));
530 }
531
532 #[test]
533 fn anything_condition() {
534 let policy = PostPolicy::new(300)
535 .condition(PostPolicyField::Key, PostPolicyValue::Anything)
536 .unwrap();
537
538 let data = serde_json::to_value(&policy).unwrap();
539
540 assert!(data["expiration"].is_string());
541 assert_eq!(data["conditions"], json!([["starts-with", "$key", ""]]));
542 }
543
544 #[test]
545 fn range_condition() {
546 let policy = PostPolicy::new(300)
547 .condition(
548 PostPolicyField::ContentLengthRange,
549 PostPolicyValue::Range(0, 3_000_000),
550 )
551 .unwrap();
552
553 let data = serde_json::to_value(&policy).unwrap();
554
555 assert!(data["expiration"].is_string());
556 assert_eq!(
557 data["conditions"],
558 json!([["content-length-range", 0, 3_000_000]])
559 );
560 }
561
562 #[test]
563 fn range_condition_for_non_content_length_range() -> Result<(), S3Error> {
564 let result = PostPolicy::new(86400)
565 .condition(PostPolicyField::ContentType, PostPolicyValue::Range(0, 100));
566
567 assert!(matches!(
568 result,
569 Err(S3Error::PostPolicyError(
570 PostPolicyError::MismatchedCondition
571 ))
572 ));
573
574 Ok(())
575 }
576
577 #[test]
578 fn starts_with_condition_for_content_length_range() -> Result<(), S3Error> {
579 let result = PostPolicy::new(86400).condition(
580 PostPolicyField::ContentLengthRange,
581 PostPolicyValue::StartsWith(Cow::from("")),
582 );
583
584 assert!(matches!(
585 result,
586 Err(S3Error::PostPolicyError(
587 PostPolicyError::MismatchedCondition
588 ))
589 ));
590
591 Ok(())
592 }
593
594 #[test]
595 fn exact_condition_for_content_length_range() -> Result<(), S3Error> {
596 let result = PostPolicy::new(86400).condition(
597 PostPolicyField::ContentLengthRange,
598 PostPolicyValue::Exact(Cow::from("test")),
599 );
600
601 assert!(matches!(
602 result,
603 Err(S3Error::PostPolicyError(
604 PostPolicyError::MismatchedCondition
605 ))
606 ));
607
608 Ok(())
609 }
610
611 #[test]
612 fn anything_condition_for_content_length_range() -> Result<(), S3Error> {
613 let result = PostPolicy::new(86400).condition(
614 PostPolicyField::ContentLengthRange,
615 PostPolicyValue::Anything,
616 );
617
618 assert!(matches!(
619 result,
620 Err(S3Error::PostPolicyError(
621 PostPolicyError::MismatchedCondition
622 ))
623 ));
624
625 Ok(())
626 }
627
628 #[test]
629 fn checksum_policy() {
630 let policy = PostPolicy::new(300)
631 .condition(
632 PostPolicyField::AmzChecksumAlgorithm(PostPolicyChecksum::SHA256),
633 PostPolicyValue::Exact(Cow::from("abcdef1234567890")),
634 )
635 .unwrap();
636
637 let data = serde_json::to_value(&policy).unwrap();
638
639 assert!(data["expiration"].is_string());
640 assert_eq!(
641 data["conditions"],
642 json!([
643 {"x-amz-checksum-algorithm": "SHA256"},
644 {"x-amz-checksum-sha256": "abcdef1234567890"}
645 ])
646 );
647 }
648 }
649
650 mod build {
651 use super::*;
652
653 #[maybe_async::test(
654 feature = "sync",
655 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
656 async(
657 all(not(feature = "sync"), feature = "with-async-std"),
658 async_std::test
659 )
660 )]
661 async fn adds_credentials() {
662 let policy = PostPolicy::new(86400)
663 .condition(
664 PostPolicyField::Key,
665 PostPolicyValue::StartsWith(Cow::from("user/user1/")),
666 )
667 .unwrap();
668
669 let bucket = test_bucket();
670
671 let _ts = with_timestamp(1_451_347_200);
672 let policy = policy.build(&now_utc(), &bucket).await.unwrap();
673
674 let data = serde_json::to_value(&policy).unwrap();
675
676 assert_eq!(
677 data["conditions"],
678 json!([
679 ["starts-with", "$key", "user/user1/"],
680 {"bucket": "rust-s3"},
681 {"x-amz-algorithm": "AWS4-HMAC-SHA256"},
682 {"x-amz-credential": "AKIAIOSFODNN7EXAMPLE/20151229/us-east-1/s3/aws4_request"},
683 {"x-amz-date": "20151229T000000Z"},
684 ])
685 );
686 }
687
688 #[maybe_async::test(
689 feature = "sync",
690 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
691 async(
692 all(not(feature = "sync"), feature = "with-async-std"),
693 async_std::test
694 )
695 )]
696 async fn with_security_token() {
697 let policy = PostPolicy::new(86400)
698 .condition(
699 PostPolicyField::Key,
700 PostPolicyValue::StartsWith(Cow::from("user/user1/")),
701 )
702 .unwrap();
703
704 let bucket = test_bucket_with_security_token();
705
706 let _ts = with_timestamp(1_451_347_200);
707 let policy = policy.build(&now_utc(), &bucket).await.unwrap();
708
709 let data = serde_json::to_value(&policy).unwrap();
710
711 assert_eq!(
712 data["conditions"],
713 json!([
714 ["starts-with", "$key", "user/user1/"],
715 {"bucket": "rust-s3"},
716 {"x-amz-algorithm": "AWS4-HMAC-SHA256"},
717 {"x-amz-credential": "AKIAIOSFODNN7EXAMPLE/20151229/us-east-1/s3/aws4_request"},
718 {"x-amz-date": "20151229T000000Z"},
719 {"x-amz-security-token": "SomeSecurityToken"},
720 ])
721 );
722 }
723 }
724
725 mod policy_string {
726 use super::*;
727
728 #[test]
729 fn returns_base64_encoded() {
730 let policy = PostPolicy::new(129600)
731 .condition(
732 PostPolicyField::Key,
733 PostPolicyValue::StartsWith(Cow::from("user/user1/")),
734 )
735 .unwrap();
736
737 let _ts = with_timestamp(1_451_347_200);
738
739 let expected = "eyJleHBpcmF0aW9uIjoiMjAxNS0xMi0zMFQxMjowMDowMFoiLCJjb25kaXRpb25zIjpbWyJzdGFydHMtd2l0aCIsIiRrZXkiLCJ1c2VyL3VzZXIxLyJdXX0=";
740
741 assert_eq!(policy.policy_string().unwrap(), expected);
742 }
743 }
744
745 mod sign {
746 use super::*;
747
748 #[maybe_async::test(
749 feature = "sync",
750 async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
751 async(
752 all(not(feature = "sync"), feature = "with-async-std"),
753 async_std::test
754 )
755 )]
756 async fn returns_full_details() {
757 let policy = PostPolicy::new(86400)
758 .condition(
759 PostPolicyField::Key,
760 PostPolicyValue::StartsWith(Cow::from("user/user1/")),
761 )
762 .unwrap()
763 .condition(
764 PostPolicyField::ContentLengthRange,
765 PostPolicyValue::Range(0, 3_000_000),
766 )
767 .unwrap();
768
769 let bucket = test_bucket();
770
771 let _ts = with_timestamp(1_451_347_200);
772 let post = policy.sign(bucket).await.unwrap();
773
774 assert_eq!(post.url, "https://rust-s3.s3.amazonaws.com");
775 assert_eq!(
776 serde_json::to_value(&post.fields).unwrap(),
777 json!({
778 "x-amz-credential": "AKIAIOSFODNN7EXAMPLE/20151229/us-east-1/s3/aws4_request",
779 "bucket": "rust-s3",
780 "Policy": "eyJleHBpcmF0aW9uIjoiMjAxNS0xMi0zMFQwMDowMDowMFoiLCJjb25kaXRpb25zIjpbWyJzdGFydHMtd2l0aCIsIiRrZXkiLCJ1c2VyL3VzZXIxLyJdLFsiY29udGVudC1sZW5ndGgtcmFuZ2UiLDAsMzAwMDAwMF0seyJidWNrZXQiOiJydXN0LXMzIn0seyJ4LWFtei1hbGdvcml0aG0iOiJBV1M0LUhNQUMtU0hBMjU2In0seyJ4LWFtei1jcmVkZW50aWFsIjoiQUtJQUlPU0ZPRE5ON0VYQU1QTEUvMjAxNTEyMjkvdXMtZWFzdC0xL3MzL2F3czRfcmVxdWVzdCJ9LHsieC1hbXotZGF0ZSI6IjIwMTUxMjI5VDAwMDAwMFoifV19",
781 "x-amz-date": "20151229T000000Z",
782 "x-amz-signature": "0ff9c50ab7e543a841e91e5c663fd32117c5243e56e7a69db88f94ee95c4706f",
783 "x-amz-algorithm": "AWS4-HMAC-SHA256"
784 })
785 );
786 assert_eq!(
787 serde_json::to_value(&post.dynamic_fields).unwrap(),
788 json!({
789 "key": "user/user1/",
790 "content-length-range": "0,3000000",
791 })
792 );
793 }
794 }
795}