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