1use std::time::SystemTime;
2
3use crate::{
4 private::{
5 hex_encode, policy_document, utils::UnixTimestamp, CredentialScope, Date, Location,
6 RequestType, Service, SigningAlgorithm,
7 },
8 signing_key::SigningKey,
9};
10
11#[derive(Debug, thiserror::Error)]
12#[error(transparent)]
13pub struct Error(#[from] ErrorKind);
14
15#[derive(Debug, thiserror::Error)]
16enum ErrorKind {
17 #[error("accessible_at out of range")]
18 AccessibleAtOutOfRange,
19 #[error("bound token authorizer: {0}")]
20 BoundTokenAuthorizer(#[source] crate::private::signing_key_inner::Error),
21 #[error("bucket not found")]
22 BucketNotFound,
23 #[error("expiration before accessible_at")]
24 ExpirationBeforeAccessibleAt,
25 #[error("expiration out of range")]
26 ExpirationOutOfRange,
27 #[error("key not found")]
28 KeyNotFound,
29 #[error("policy document serialization")]
30 PolicyDocumentSerialization,
31 #[error(transparent)]
32 Signing(crate::private::signing_key_inner::Error),
33 #[error("x-goog-algorithm not supported")]
34 XGoogAlgorithmNotSupported,
35 #[error("x-goog-credential invalid")]
36 XGoogCredentialInvalid(crate::private::credential_scope::Error),
37 #[error("x-goog-meta-* field name is empty")]
38 XGoogMetaNameEmpty,
39}
40
41#[derive(Clone, Debug)]
167pub struct HtmlFormData(Vec<(String, String)>);
168
169impl HtmlFormData {
170 pub fn builder() -> HtmlFormDataBuilder {
172 HtmlFormDataBuilder::default()
173 }
174
175 pub fn into_vec(self) -> Vec<(String, String)> {
179 self.0
180 }
181}
182
183pub struct PolicyDocumentSigningOptions {
187 pub accessible_at: Option<SystemTime>,
188 pub expiration: SystemTime,
189 pub region: Option<String>,
190 pub signing_key: SigningKey,
191 pub use_sign_blob: bool,
192}
193
194#[derive(Default)]
198pub struct HtmlFormDataBuilder {
199 acl: Option<String>,
200 bucket: Option<String>,
201 cache_control: Option<String>,
202 content_disposition: Option<String>,
203 content_encoding: Option<String>,
204 content_length: Option<u64>,
207 content_type: Option<String>,
208 expires: Option<String>,
209 key: Option<String>,
211 policy_document_signing_options: Option<PolicyDocumentSigningOptions>,
213 success_action_redirect: Option<String>,
214 success_action_status: Option<u16>,
215 x_goog_custom_time: Option<String>,
218 x_goog_meta: Vec<(String, String)>,
221}
222
223impl HtmlFormDataBuilder {
224 pub fn acl(mut self, acl: impl Into<String>) -> Self {
226 self.acl = Some(acl.into());
227 self
228 }
229
230 pub fn bucket(mut self, bucket: impl Into<String>) -> Self {
232 self.bucket = Some(bucket.into());
233 self
234 }
235
236 pub async fn build(self) -> Result<HtmlFormData, Error> {
238 let (policy, x_goog_algorithm, x_goog_credential, x_goog_date, x_goog_signature) =
239 self.build_policy_and_x_goog_signature().await?;
240
241 let mut vec = vec![];
242 if let Some(acl) = self.acl {
243 vec.push(("acl".to_string(), acl));
244 }
245 if let Some(bucket) = self.bucket {
246 vec.push(("bucket".to_string(), bucket));
247 }
248 if let Some(cache_control) = self.cache_control {
249 vec.push(("Cache-Control".to_string(), cache_control));
250 }
251 if let Some(content_disposition) = self.content_disposition {
252 vec.push(("Content-Disposition".to_string(), content_disposition));
253 }
254 if let Some(content_encoding) = self.content_encoding {
255 vec.push(("Content-Encoding".to_string(), content_encoding));
256 }
257 if let Some(content_length) = self.content_length {
258 vec.push(("Content-Length".to_string(), content_length.to_string()));
259 }
260 if let Some(content_type) = self.content_type {
261 vec.push(("Content-Type".to_string(), content_type));
262 }
263 if let Some(expires) = self.expires {
264 vec.push(("Expires".to_string(), expires));
265 }
266 vec.push(("key".to_string(), self.key.ok_or(ErrorKind::KeyNotFound)?));
267 if let Some(policy) = policy {
268 vec.push(("policy".to_string(), policy));
269 }
270 if let Some(success_action_redirect) = self.success_action_redirect {
271 vec.push((
272 "success_action_redirect".to_string(),
273 success_action_redirect,
274 ));
275 }
276 if let Some(success_action_status) = self.success_action_status {
277 vec.push((
278 "success_action_status".to_string(),
279 success_action_status.to_string(),
280 ));
281 }
282 if let Some(x_goog_algorithm) = x_goog_algorithm {
283 vec.push(("x-goog-algorithm".to_string(), x_goog_algorithm));
284 }
285 if let Some(x_goog_credential) = x_goog_credential {
286 vec.push(("x-goog-credential".to_string(), x_goog_credential));
287 }
288 if let Some(x_goog_custom_time) = self.x_goog_custom_time {
289 vec.push(("x-goog-custom-time".to_string(), x_goog_custom_time));
290 }
291 if let Some(x_goog_date) = x_goog_date {
292 vec.push(("x-goog-date".to_string(), x_goog_date));
293 }
294 if let Some(x_goog_signature) = x_goog_signature {
295 vec.push(("x-goog-signature".to_string(), x_goog_signature));
296 }
297 for (key, value) in self.x_goog_meta {
298 if key.is_empty() {
299 return Err(Error::from(ErrorKind::XGoogMetaNameEmpty));
300 }
301 vec.push((format!("x-goog-meta-{key}"), value));
302 }
303 Ok(HtmlFormData(vec))
304 }
305
306 pub fn cache_control(mut self, cache_control: impl Into<String>) -> Self {
308 self.cache_control = Some(cache_control.into());
309 self
310 }
311
312 pub fn content_disposition(mut self, content_disposition: impl Into<String>) -> Self {
314 self.content_disposition = Some(content_disposition.into());
315 self
316 }
317
318 pub fn content_encoding(mut self, content_encoding: impl Into<String>) -> Self {
320 self.content_encoding = Some(content_encoding.into());
321 self
322 }
323
324 pub fn content_length(mut self, content_length: u64) -> Self {
326 self.content_length = Some(content_length);
327 self
328 }
329
330 pub fn content_type(mut self, content_type: impl Into<String>) -> Self {
332 self.content_type = Some(content_type.into());
333 self
334 }
335
336 pub fn expires(mut self, expires: impl Into<String>) -> Self {
338 self.expires = Some(expires.into());
339 self
340 }
341
342 pub fn key(mut self, key: impl Into<String>) -> Self {
344 self.key = Some(key.into());
345 self
346 }
347
348 pub fn policy_document_signing_options(
350 mut self,
351 policy_document_signing_options: PolicyDocumentSigningOptions,
352 ) -> Self {
353 self.policy_document_signing_options = Some(policy_document_signing_options);
354 self
355 }
356
357 pub fn success_action_redirect(mut self, success_action_redirect: impl Into<String>) -> Self {
359 self.success_action_redirect = Some(success_action_redirect.into());
360 self
361 }
362
363 pub fn success_action_status(mut self, success_action_status: u16) -> Self {
365 self.success_action_status = Some(success_action_status);
366 self
367 }
368
369 pub fn x_goog_custom_time(mut self, x_goog_custom_time: impl Into<String>) -> Self {
371 self.x_goog_custom_time = Some(x_goog_custom_time.into());
372 self
373 }
374
375 pub fn x_goog_meta(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
377 self.x_goog_meta.push((name.into(), value.into()));
378 self
379 }
380
381 #[allow(clippy::type_complexity)]
387 async fn build_policy_and_x_goog_signature(
388 &self,
389 ) -> Result<
390 (
391 Option<String>,
392 Option<String>,
393 Option<String>,
394 Option<String>,
395 Option<String>,
396 ),
397 Error,
398 > {
399 match self.policy_document_signing_options.as_ref() {
400 None => Ok((None, None, None, None, None)),
401 Some(PolicyDocumentSigningOptions {
402 accessible_at,
403 expiration,
404 region,
405 signing_key,
406 use_sign_blob,
407 }) => {
408 let accessible_at = accessible_at.unwrap_or_else(SystemTime::now);
409 let expiration = *expiration;
410 if expiration <= accessible_at {
411 return Err(Error::from(ErrorKind::ExpirationBeforeAccessibleAt));
412 }
413 let accessible_at = UnixTimestamp::from_system_time(accessible_at)
414 .map_err(|_| ErrorKind::AccessibleAtOutOfRange)?;
415 let expiration = UnixTimestamp::from_system_time(expiration)
416 .map_err(|_| ErrorKind::ExpirationOutOfRange)?;
417 let region = region.as_deref().unwrap_or("auto");
418 let bucket = self.bucket.as_deref().ok_or(ErrorKind::BucketNotFound)?;
419 let key = self.key.as_deref().ok_or(ErrorKind::KeyNotFound)?;
420 let x_goog_algorithm = signing_key.0.x_goog_algorithm();
421 if x_goog_algorithm != SigningAlgorithm::Goog4RsaSha256 {
423 return Err(Error::from(ErrorKind::XGoogAlgorithmNotSupported));
425 }
426
427 let service_account_client_email = signing_key
428 .0
429 .authorizer()
430 .await
431 .map_err(ErrorKind::BoundTokenAuthorizer)?;
432
433 let credential_scope = CredentialScope::new(
434 Date::from_unix_timestamp_obj(accessible_at),
435 Location::try_from(region).expect("region to be valid location"),
436 Service::Storage,
437 RequestType::Goog4Request,
438 )
439 .map_err(ErrorKind::XGoogCredentialInvalid)?;
440 let x_goog_credential =
441 format!("{service_account_client_email}/{credential_scope}");
442 let x_goog_date = accessible_at.to_iso8601_basic_format_date_time();
443 let expiration = policy_document::Expiration::from_unix_timestamp_obj(expiration);
444
445 let mut conditions = vec![];
446 if let Some(acl) = self.acl.as_ref() {
447 conditions.push(policy_document::Condition::ExactMatching(
448 policy_document::Field::new("acl").expect("acl to be valid field name"),
449 policy_document::Value::new(acl),
450 ));
451 }
452 conditions.push(policy_document::Condition::ExactMatching(
453 policy_document::Field::new("bucket").expect("bucket to be valid field name"),
454 policy_document::Value::new(bucket),
455 ));
456 if let Some(cache_control) = self.cache_control.as_ref() {
457 conditions.push(policy_document::Condition::ExactMatching(
458 policy_document::Field::new("Cache-Control")
459 .expect("Cache-Control to be valid field name"),
460 policy_document::Value::new(cache_control),
461 ));
462 }
463 if let Some(content_disposition) = self.content_disposition.as_ref() {
464 conditions.push(policy_document::Condition::ExactMatching(
465 policy_document::Field::new("Content-Disposition")
466 .expect("Content-Disposition to be valid field name"),
467 policy_document::Value::new(content_disposition),
468 ));
469 }
470 if let Some(content_encoding) = self.content_encoding.as_ref() {
471 conditions.push(policy_document::Condition::ExactMatching(
472 policy_document::Field::new("Content-Encoding")
473 .expect("Content-Encoding to be valid field name"),
474 policy_document::Value::new(content_encoding),
475 ));
476 }
477 if let Some(content_length) = self.content_length {
478 conditions.push(policy_document::Condition::ContentLengthRange(
479 content_length,
480 content_length,
481 ));
482 }
483 if let Some(content_type) = self.content_type.as_ref() {
484 conditions.push(policy_document::Condition::ExactMatching(
485 policy_document::Field::new("Content-Type")
486 .expect("Content-Type to be valid field name"),
487 policy_document::Value::new(content_type),
488 ));
489 }
490 if let Some(expires) = self.expires.as_ref() {
491 conditions.push(policy_document::Condition::ExactMatching(
492 policy_document::Field::new("Expires")
493 .expect("Expires to be valid field name"),
494 policy_document::Value::new(expires),
495 ));
496 }
497 conditions.push(policy_document::Condition::ExactMatching(
498 policy_document::Field::new("key").expect("key to be valid field name"),
499 policy_document::Value::new(key),
500 ));
501 if let Some(success_action_redirect) = self.success_action_redirect.as_ref() {
503 conditions.push(policy_document::Condition::ExactMatching(
504 policy_document::Field::new("success_action_redirect")
505 .expect("success_action_redirect to be valid field name"),
506 policy_document::Value::new(success_action_redirect),
507 ));
508 }
509 if let Some(success_action_status) = self.success_action_status {
510 conditions.push(policy_document::Condition::ExactMatching(
511 policy_document::Field::new("success_action_status")
512 .expect("success_action_status to be valid field name"),
513 policy_document::Value::new(success_action_status.to_string()),
514 ));
515 }
516 conditions.push(policy_document::Condition::ExactMatching(
517 policy_document::Field::new("x-goog-algorithm")
518 .expect("x-goog-algorithm to be valid field name"),
519 policy_document::Value::new(x_goog_algorithm.as_str()),
520 ));
521 conditions.push(policy_document::Condition::ExactMatching(
522 policy_document::Field::new("x-goog-credential")
523 .expect("x-goog-credential to be valid field name"),
524 policy_document::Value::new(x_goog_credential.clone()),
525 ));
526 if let Some(x_goog_custom_time) = self.x_goog_custom_time.as_ref() {
527 conditions.push(policy_document::Condition::ExactMatching(
528 policy_document::Field::new("x-goog-custom-time")
529 .expect("x-goog-custom-time to be valid field name"),
530 policy_document::Value::new(x_goog_custom_time),
531 ));
532 }
533 conditions.push(policy_document::Condition::ExactMatching(
534 policy_document::Field::new("x-goog-date")
535 .expect("x-goog-date to be valid field name"),
536 policy_document::Value::new(x_goog_date.clone()),
537 ));
538 for (name, value) in &self.x_goog_meta {
540 if name.is_empty() {
541 return Err(Error::from(ErrorKind::XGoogMetaNameEmpty));
542 }
543 conditions.push(policy_document::Condition::ExactMatching(
544 policy_document::Field::new(format!("x-goog-meta-{name}"))
545 .expect("x-goog-meta-* to be valid field name"),
546 policy_document::Value::new(value),
547 ));
548 }
549 let policy_document = policy_document::PolicyDocument {
551 conditions,
552 expiration,
553 };
554
555 let policy = serde_json::to_string(&policy_document)
556 .map_err(|_| ErrorKind::PolicyDocumentSerialization)?;
557 let encoded_policy = base64::Engine::encode(
558 &base64::engine::general_purpose::STANDARD,
559 policy.as_bytes(),
560 );
561 let message = encoded_policy.as_str();
562 let message_digest = signing_key
563 .0
564 .sign(*use_sign_blob, message.as_bytes())
565 .await
566 .map_err(ErrorKind::Signing)?;
567 let x_goog_signature = hex_encode(&message_digest);
568 Ok((
569 Some(encoded_policy),
570 Some(x_goog_algorithm.as_str().to_string()),
571 Some(x_goog_credential),
572 Some(x_goog_date),
573 Some(x_goog_signature),
574 ))
575 }
576 }
577 }
578}
579
580#[cfg(test)]
581mod tests {
582 use super::*;
583
584 #[tokio::test]
585 async fn test_key_only() -> Result<(), Error> {
586 let form_data = HtmlFormData::builder()
587 .key("example-object")
588 .build()
589 .await?;
590 assert_eq!(
591 form_data.into_vec(),
592 vec![("key".to_string(), "example-object".to_string())]
593 );
594 Ok(())
595 }
596
597 #[tokio::test]
598 async fn test_key_not_found() {
599 assert_eq!(
600 HtmlFormData::builder()
601 .build()
602 .await
603 .unwrap_err()
604 .to_string(),
605 "key not found"
606 );
607 }
608
609 #[tokio::test]
610 async fn test_x_goog_meta_name_empty() {
611 assert_eq!(
612 HtmlFormData::builder()
613 .key("example-object")
614 .x_goog_meta("", "value")
615 .build()
616 .await
617 .unwrap_err()
618 .to_string(),
619 "x-goog-meta-* field name is empty"
620 );
621 }
622
623 #[tokio::test]
624 async fn test_when_policy_is_false() -> anyhow::Result<()> {
625 let form_data = HtmlFormData::builder()
626 .acl("public-read")
627 .bucket("example-bucket")
628 .cache_control("max-age=3600")
629 .content_disposition("attachment")
630 .content_encoding("gzip")
631 .content_length(1024)
632 .content_type("application/octet-stream")
633 .expires("2022-01-01T00:00:00Z")
634 .key("example-object")
635 .success_action_redirect("https://example.com/success")
636 .success_action_status(201)
637 .x_goog_custom_time("2022-01-01T00:00:00Z")
638 .x_goog_meta("reviewer", "jane")
639 .x_goog_meta("project-manager", "john")
640 .build()
641 .await?;
642 assert_eq!(
643 form_data.into_vec(),
644 vec![
645 ("acl".to_string(), "public-read".to_string()),
646 ("bucket".to_string(), "example-bucket".to_string()),
647 ("Cache-Control".to_string(), "max-age=3600".to_string()),
648 ("Content-Disposition".to_string(), "attachment".to_string()),
649 ("Content-Encoding".to_string(), "gzip".to_string()),
650 ("Content-Length".to_string(), "1024".to_string()),
651 (
652 "Content-Type".to_string(),
653 "application/octet-stream".to_string()
654 ),
655 ("Expires".to_string(), "2022-01-01T00:00:00Z".to_string()),
656 ("key".to_string(), "example-object".to_string()),
657 (
658 "success_action_redirect".to_string(),
659 "https://example.com/success".to_string()
660 ),
661 ("success_action_status".to_string(), "201".to_string()),
662 (
663 "x-goog-custom-time".to_string(),
664 "2022-01-01T00:00:00Z".to_string()
665 ),
666 ("x-goog-meta-reviewer".to_string(), "jane".to_string()),
667 (
668 "x-goog-meta-project-manager".to_string(),
669 "john".to_string()
670 )
671 ]
672 );
673 Ok(())
674 }
675
676 #[tokio::test]
677 async fn test_when_policy_is_true() -> anyhow::Result<()> {
678 let client_email = "test@example.com".to_string();
679 let private_key = "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQChK9QyIk4mpcaO\nxXY+DIb8xsJKXfqAgzsboG/Ho8W9C6NZwM0+7kuV39QrP+UGo5GTpKfe3gZYQMoP\nHIAirNMa/K3/8oczucts+ZueWzCdElZ0+E04BLRkWNUM86hQ0TIL+jCi83JZHaGY\npjgMSUUDj+vJc5QjYmu2zGHAsWvBUfIQc6/Am+shtQG1gPpxTKlXS117pIzlAz8Z\nahRKJHn33fpBudYYDKm1fsyCFPS05rBBvjrvGNsGJn/6rRb8+ixVr6LOipSZ5KbS\n+/kvxQSTa7nKKqCoK1fUp+k489IL0XWW4z5PrBNNBjE0oQ3yfnkVW/LBNsrFjT6x\nAOKis8QZAgMBAAECggEABVuCHbqDM4iuLX/F2vEqqYtn2PX/xjbWh6gRHydEAvE4\nmFqu1+ku7Qf4MwnYMJzOUYSXKfLibhuVO+RcJArvp4V/uTLUKLWD3Bb+A8kPOCFs\na033ryWE45MKXfhZf3o8uiYyaLBD/E9eWEcqNMpYt3IYyeUEJxr17qkjlLaxGMd1\nixQdDSS8d48EyMg8RaA2q5l5sG5CoxeEFX7BR3SCjqNS8lzZcQ70mdJjtbmRd7st\nggbcZzd8C2XlT5QFSAEge0uRHEo2d48o09PkTAT4AfsjlYmAhAL1ph0fVPdnXSVk\ng/8u8BGM3WwBIL3jmV/uy5dDmLCv7XwsWxBEnmbwKQKBgQDTbq6QiA+lvLIlpUpA\nmRgWvpHRNv5axSmN77RDcrm96GUrXyLakDmZ/NiAp727RRMcsDkxhTnav/gcQwUC\nl9wCT8ItT32e23HxyQ4kkejrMGtsQyxqd3gN0QzkgAwWQPJMf4vgXOL50lB9Dos1\n5G2p7aUHTLVHqK602S5LbntFhQKBgQDDJPQrlpUhV6zb+B8ZhAJ6SyZUhQ0+81qk\nDxzXdMpUR6gYxzvB5thUqxP9dXuSW7b+L8Pa7ayOxXQqyS+HYKnFJfGkSG2kZMWB\n+zbZgPq1Nq6QyELGFQd3t7g6AOmTL6q7K/D2ghfIGwL2R3TuDrVOW/EQ8mMBAbZP\nLT1FKRvuhQKBgEnBnKfSrxK0BrlXNdXfEiYtCJUhSA3GJb7b1diJlv4GqfQ9Vd1E\n3rM3HxeSbH99kzM4zlrWDN6ghR7mykKjUx6DUEuaJUpbZx5fcs2TENuqom676Cyj\nzH+VY5f6izzgHyZMgDEedheMJIPbpPiB3TegLSekvMBoublg4eNygRI5AoGBALKo\nQmMlmaLNAhThNJfHo/0SkCURKu9XHMTWkTEwW4yNjfghbzQ2hBgACG0kAd4c2Ywd\nbtIghrqvS4tgZYMrnEJCWths9vRqzegSdkTrMJx3U5p5vahb2FpieOehrjZyjXyO\n3izRLbSmBjAze3n3PUZgJnO9daaWSrJyWIXY/RmBAoGBAJasPa2BUV5dg/huiLDE\nnjhWxr2ezceoSxNyhLgmpS2vrBtJWWE4pRVZgJPqbXwMsSfjQqSGp0QWWJ1KHpIv\nn32eCAbgj/9wrwoU9u3cEA4BhYHjg3p9empYdLMJgeLAvKpUbvKbEkZITDFtkWis\njI3VAsh2OHCsO8ToNwX3Kgku\n-----END PRIVATE KEY-----\n".to_string();
680 let form_data = HtmlFormData::builder()
681 .acl("public-read")
682 .bucket("example-bucket")
683 .cache_control("max-age=3600")
684 .content_disposition("attachment")
685 .content_encoding("gzip")
686 .content_length(1024)
687 .content_type("application/octet-stream")
688 .expires("2022-01-04T00:00:00Z")
689 .key("example-object")
690 .success_action_redirect("https://example.com/success")
691 .success_action_status(201)
692 .x_goog_custom_time("2022-01-03T00:00:00Z")
693 .x_goog_meta("reviewer", "jane")
694 .x_goog_meta("project-manager", "john")
695 .policy_document_signing_options(PolicyDocumentSigningOptions {
696 accessible_at: Some(
697 UnixTimestamp::from_rfc3339("2022-01-01T00:00:00Z")?.to_system_time(),
698 ),
699 expiration: UnixTimestamp::from_rfc3339("2022-01-02T00:00:00Z")?.to_system_time(),
700 region: None,
701 signing_key: SigningKey::service_account(client_email, private_key),
702 use_sign_blob: false,
703 })
704 .build()
705 .await?;
706 assert_eq!(
707 form_data.into_vec(),
708 [
709 ("acl", "public-read"),
710 ("bucket", "example-bucket"),
711 ("Cache-Control", "max-age=3600"),
712 ("Content-Disposition", "attachment"),
713 ("Content-Encoding", "gzip"),
714 ("Content-Length", "1024"),
715 ("Content-Type", "application/octet-stream"),
716 ("Expires", "2022-01-04T00:00:00Z"),
717 ("key", "example-object"),
718 ("policy", "eyJjb25kaXRpb25zIjpbWyJlcSIsIiRhY2wiLCJwdWJsaWMtcmVhZCJdLFsiZXEiLCIkYnVja2V0IiwiZXhhbXBsZS1idWNrZXQiXSxbImVxIiwiJENhY2hlLUNvbnRyb2wiLCJtYXgtYWdlPTM2MDAiXSxbImVxIiwiJENvbnRlbnQtRGlzcG9zaXRpb24iLCJhdHRhY2htZW50Il0sWyJlcSIsIiRDb250ZW50LUVuY29kaW5nIiwiZ3ppcCJdLFsiY29udGVudC1sZW5ndGgtcmFuZ2UiLDEwMjQsMTAyNF0sWyJlcSIsIiRDb250ZW50LVR5cGUiLCJhcHBsaWNhdGlvbi9vY3RldC1zdHJlYW0iXSxbImVxIiwiJEV4cGlyZXMiLCIyMDIyLTAxLTA0VDAwOjAwOjAwWiJdLFsiZXEiLCIka2V5IiwiZXhhbXBsZS1vYmplY3QiXSxbImVxIiwiJHN1Y2Nlc3NfYWN0aW9uX3JlZGlyZWN0IiwiaHR0cHM6Ly9leGFtcGxlLmNvbS9zdWNjZXNzIl0sWyJlcSIsIiRzdWNjZXNzX2FjdGlvbl9zdGF0dXMiLCIyMDEiXSxbImVxIiwiJHgtZ29vZy1hbGdvcml0aG0iLCJHT09HNC1SU0EtU0hBMjU2Il0sWyJlcSIsIiR4LWdvb2ctY3JlZGVudGlhbCIsInRlc3RAZXhhbXBsZS5jb20vMjAyMjAxMDEvYXV0by9zdG9yYWdlL2dvb2c0X3JlcXVlc3QiXSxbImVxIiwiJHgtZ29vZy1jdXN0b20tdGltZSIsIjIwMjItMDEtMDNUMDA6MDA6MDBaIl0sWyJlcSIsIiR4LWdvb2ctZGF0ZSIsIjIwMjIwMTAxVDAwMDAwMFoiXSxbImVxIiwiJHgtZ29vZy1tZXRhLXJldmlld2VyIiwiamFuZSJdLFsiZXEiLCIkeC1nb29nLW1ldGEtcHJvamVjdC1tYW5hZ2VyIiwiam9obiJdXSwiZXhwaXJhdGlvbiI6IjIwMjItMDEtMDJUMDA6MDA6MDBaIn0="),
719 ("success_action_redirect", "https://example.com/success"),
720 ("success_action_status", "201"),
721 ("x-goog-algorithm", "GOOG4-RSA-SHA256"),
722 ("x-goog-credential", "test@example.com/20220101/auto/storage/goog4_request"),
723 ("x-goog-custom-time", "2022-01-03T00:00:00Z"),
724 ("x-goog-date", "20220101T000000Z"),
725 ("x-goog-signature", "0ac0d149c7385dddd8abb7e002ccf35479409b858bedaed2378a87d714d4e68e28eb683c73a5a661372d834a658784a4ccc16f49a0cea8dbfb89e56dd53a98db6d9af0294bfde4a205c8686a3acd35d39ed1f35598ea61f4d339f9f0e6b10a5c26f64094f137e368e78739c837dbfbf6940ce55cf26b86f195bb7277292682cf2ad5b9a1dd452036f969e9c4260c5b0371439ef57f2cc4433354edccb7a71cee29ebfb4ead8b1fddc4faf598bde637f70936052fd37cd3bc1317e39a54381c8f65fb17493130abf3d98323a4c55cdc7f29b1d9a2f1eed08017c36202c8c48550c011a243c22724054ae32b82c86d426fe6db0053fe437fe0af0be44c2869f17f"),
726 ("x-goog-meta-reviewer", "jane"),
727 ("x-goog-meta-project-manager", "john")
728 ].into_iter().map(|(n, v)| (n.to_string(), v.to_string())).collect::<Vec<(String, String)>>()
729 );
730 Ok(())
731 }
732}