cloud_storage_signature/
html_form_data.rs

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/// HTML Form Data.
42///
43/// <https://cloud.google.com/storage/docs/xml-api/post-object-forms>
44///
45/// # Example (full)
46///
47/// ```rust
48/// # async fn example_for_html_form_data_full() -> anyhow::Result<()> {
49/// #     let service_account_client_email = "test@example.com";
50/// #     let service_account_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";
51/// use cloud_storage_signature::HtmlFormData;
52/// use cloud_storage_signature::PolicyDocumentSigningOptions;
53/// use cloud_storage_signature::SigningKey;
54/// let form_data = HtmlFormData::builder()
55///     .acl("public-read")
56///     .bucket("example-bucket")
57///     .cache_control("max-age=3600")
58///     .content_disposition("attachment")
59///     .content_encoding("gzip")
60///     .content_length(1024)
61///     .content_type("application/octet-stream")
62///     .expires("2022-01-04T00:00:00Z")
63///     .key("example-object")
64///     .success_action_redirect("https://example.com/success")
65///     .success_action_status(201)
66///     .x_goog_custom_time("2022-01-03T00:00:00Z")
67///     .x_goog_meta("reviewer", "jane")
68///     .x_goog_meta("project-manager", "john")
69///     .policy_document_signing_options(PolicyDocumentSigningOptions {
70///         accessible_at: None,
71///         expiration: std::time::SystemTime::now() + std::time::Duration::from_secs(60 * 60),
72///         region: None,
73///         signing_key: SigningKey::service_account(
74///             service_account_client_email.to_string(),
75///             service_account_private_key.to_string(),
76///         ),
77///         use_sign_blob: false,
78///     })
79///     .build()
80///     .await?;
81/// assert_eq!(
82///   form_data.into_vec().into_iter().map(|(n, _)| n).collect::<Vec<String>>(),
83///   [
84///     "acl",
85///     "bucket",
86///     "Cache-Control",
87///     "Content-Disposition",
88///     "Content-Encoding",
89///     "Content-Length",
90///     "Content-Type",
91///     "Expires",
92///     "key",
93///     "policy",
94///     "success_action_redirect",
95///     "success_action_status",
96///     "x-goog-algorithm",
97///     "x-goog-credential",
98///     "x-goog-custom-time",
99///     "x-goog-date",
100///     "x-goog-signature",
101///     "x-goog-meta-reviewer",
102///     "x-goog-meta-project-manager",
103///   ].into_iter().map(|n| n.to_string()).collect::<Vec<String>>()
104/// );
105/// #     Ok(())
106/// # }
107/// ```
108///
109/// # Example (minimal)
110///
111/// ```rust
112/// # async fn example_for_html_form_data_minimal() -> Result<(), cloud_storage_signature::html_form_data::Error>
113/// # {
114/// use cloud_storage_signature::HtmlFormData;
115/// assert_eq!(
116///     HtmlFormData::builder()
117///         .key("object_name1")
118///         .build()
119///         .await?
120///         .into_vec(),
121///     vec![("key".to_string(), "object_name1".to_string())]
122/// );
123/// #     Ok(())
124/// # }
125/// ```
126///
127/// # Example (policy document minimal)
128///
129/// ```rust
130/// # async fn example_for_html_form_data_with_policy_document() -> anyhow::Result<()> {
131/// #     let service_account_client_email = "test@example.com";
132/// #     let service_account_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";
133/// use cloud_storage_signature::HtmlFormData;
134/// use cloud_storage_signature::PolicyDocumentSigningOptions;
135/// use cloud_storage_signature::SigningKey;
136/// let form_data = HtmlFormData::builder()
137///     .bucket("example-bucket")
138///     .key("example-object")
139///     .policy_document_signing_options(PolicyDocumentSigningOptions {
140///         accessible_at: None,
141///         expiration: std::time::SystemTime::now() + std::time::Duration::from_secs(60 * 60),
142///         region: None,
143///         signing_key: SigningKey::service_account(
144///             service_account_client_email.to_string(),
145///             service_account_private_key.to_string()
146///         ),
147///         use_sign_blob: false,
148///     })
149///     .build()
150///     .await?;
151/// assert_eq!(
152///   form_data.into_vec().into_iter().map(|(n, _)| n).collect::<Vec<String>>(),
153///   [
154///     "bucket",
155///     "key",
156///     "policy",
157///     "x-goog-algorithm",
158///     "x-goog-credential",
159///     "x-goog-date",
160///     "x-goog-signature",
161///   ].into_iter().map(|n| n.to_string()).collect::<Vec<String>>()
162/// );
163/// #     Ok(())
164/// # }
165/// ```
166#[derive(Clone, Debug)]
167pub struct HtmlFormData(Vec<(String, String)>);
168
169impl HtmlFormData {
170    /// Returns a new `HtmlFormDataBuilder`.
171    pub fn builder() -> HtmlFormDataBuilder {
172        HtmlFormDataBuilder::default()
173    }
174
175    /// Converts `self` into a `Vec<(String, String)>`.
176    /// The first element of each tuple is the field name, and the second
177    /// element is the field value.
178    pub fn into_vec(self) -> Vec<(String, String)> {
179        self.0
180    }
181}
182
183/// Policy Document Signing Options.
184///
185/// See [`HtmlFormData`] examples.
186pub 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/// HTML Form Data Builder.
195///
196/// See [`HtmlFormData`] examples.
197#[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    // max content-length size is 5 TiB
205    // <https://cloud.google.com/storage/quotas#objects>
206    content_length: Option<u64>,
207    content_type: Option<String>,
208    expires: Option<String>,
209    // `file` field is not included.
210    key: Option<String>,
211    // `policy_document_sining_options` is not HTML form data field.
212    policy_document_signing_options: Option<PolicyDocumentSigningOptions>,
213    success_action_redirect: Option<String>,
214    success_action_status: Option<u16>,
215    // `x-goog-algorithm` field is not included. It is calculated from the policy_document_signing_options.
216    // `x-goog-credential` field is not included. It is calculated from the policy_document_signing_options.
217    x_goog_custom_time: Option<String>,
218    // `x-goog-date` field is not included. It is calculated from the policy_document_signing_options.
219    // `x-goog-signature` field is not included. It is calculated from the policy_document_signing_options.
220    x_goog_meta: Vec<(String, String)>,
221}
222
223impl HtmlFormDataBuilder {
224    /// Sets the `acl` field.
225    pub fn acl(mut self, acl: impl Into<String>) -> Self {
226        self.acl = Some(acl.into());
227        self
228    }
229
230    /// Sets the `bucket` field.
231    pub fn bucket(mut self, bucket: impl Into<String>) -> Self {
232        self.bucket = Some(bucket.into());
233        self
234    }
235
236    /// Builds the `HtmlFormData`.
237    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    /// Sets the `Cache-Control` field.
307    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    /// Sets the `Content-Disposition` field.
313    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    /// Sets the `Content-Encoding` field.
319    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    /// Sets the `Content-Length` field.
325    pub fn content_length(mut self, content_length: u64) -> Self {
326        self.content_length = Some(content_length);
327        self
328    }
329
330    /// Sets the `Content-Type` field.
331    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    /// Sets the `Expires` field.
337    pub fn expires(mut self, expires: impl Into<String>) -> Self {
338        self.expires = Some(expires.into());
339        self
340    }
341
342    /// Sets the `key` field.
343    pub fn key(mut self, key: impl Into<String>) -> Self {
344        self.key = Some(key.into());
345        self
346    }
347
348    /// Sets the `policy` field, `x-goog-algorithm` field, `x-goog-credential` field, `x-goog-date` field, and `x-goog-signature` field.
349    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    /// Sets the `success_action_redirect` field.
358    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    /// Sets the `success_action_status` field.
364    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    /// Sets the `x-goog-custom-time` field.
370    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    /// Sets the `x-goog-meta-*` field.
376    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    // .0: policy
382    // .1: x-goog-algorithm
383    // .2: x-goog-credential
384    // .3: x-goog-date
385    // .4: x-goog-signature
386    #[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                // TODO: x_goog_algorithm "GOOG4-HMAC-SHA256" is not supported yet
422                if x_goog_algorithm != SigningAlgorithm::Goog4RsaSha256 {
423                    // TODO: return an error if hmac and use_sign_blob
424                    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                // `policy` field is not included in the policy document
502                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                // `x-goog-signature` field is not included in the policy document
539                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                // `file` field is not included in the policy document
550                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}