From 6fef0f4cadfca1ab3ae2bb6fe738741bb82bd41b Mon Sep 17 00:00:00 2001 From: Trinity Pointard Date: Tue, 14 Dec 2021 19:27:45 +0100 Subject: [PATCH] add xml validation and basic deserialization test --- src/api/s3_website.rs | 161 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 148 insertions(+), 13 deletions(-) diff --git a/src/api/s3_website.rs b/src/api/s3_website.rs index 6587015b..75fabe23 100644 --- a/src/api/s3_website.rs +++ b/src/api/s3_website.rs @@ -48,7 +48,8 @@ pub async fn handle_put_website( .await? .ok_or(Error::NotFound)?; - let _conf: WebsiteConfiguration = from_reader(&body as &[u8])?; + let conf: WebsiteConfiguration = from_reader(&body as &[u8])?; + conf.validate()?; if let BucketState::Present(state) = bucket.state.get_mut() { state.website.update(true); @@ -78,7 +79,7 @@ pub struct WebsiteConfiguration { #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct RoutingRule { #[serde(rename = "RoutingRule")] - pub routing_rule: RoutingRuleInner, + pub inner: RoutingRuleInner, } #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] @@ -104,7 +105,7 @@ pub struct Suffix { #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct Target { #[serde(rename = "HostName")] - pub hostname: Option, + pub hostname: Value, #[serde(rename = "Protocol")] pub protocol: Option, } @@ -131,6 +132,108 @@ pub struct Redirect { pub replace_full: Option, } +impl WebsiteConfiguration { + pub fn validate(&self) -> Result<(), Error> { + if self.redirect_all_requests_to.is_some() { + if self.error_document.is_some() + || self.index_document.is_some() + || self.routing_rules.is_some() + { + return Err(Error::BadRequest( + "Bad XML: can't have RedirectAllRequestsTo and other fields".to_owned(), + )); + } + } + if let Some(ref ed) = self.error_document { + ed.validate()?; + } + if let Some(ref id) = self.index_document { + id.validate()?; + } + if let Some(ref rart) = self.redirect_all_requests_to { + rart.validate()?; + } + if let Some(ref rrs) = self.routing_rules { + for rr in rrs { + rr.inner.validate()?; + } + } + + Ok(()) + } +} + +impl Key { + pub fn validate(&self) -> Result<(), Error> { + if self.key.0.is_empty() { + Err(Error::BadRequest( + "Bad XML: error document specified but empty".to_owned(), + )) + } else { + Ok(()) + } + } +} + +impl Suffix { + pub fn validate(&self) -> Result<(), Error> { + if self.suffix.0.is_empty() | self.suffix.0.contains('/') { + Err(Error::BadRequest( + "Bad XML: index document is empty or contains /".to_owned(), + )) + } else { + Ok(()) + } + } +} + +impl Target { + pub fn validate(&self) -> Result<(), Error> { + if let Some(ref protocol) = self.protocol { + if protocol.0 != "http" && protocol.0 != "https" { + return Err(Error::BadRequest("Bad XML: invalid protocol".to_owned())); + } + } + Ok(()) + } +} + +impl RoutingRuleInner { + pub fn validate(&self) -> Result<(), Error> { + let has_prefix = self + .condition + .as_ref() + .map(|c| c.prefix.as_ref()) + .flatten() + .is_some(); + self.redirect.validate(has_prefix) + } +} + +impl Redirect { + pub fn validate(&self, has_prefix: bool) -> Result<(), Error> { + if self.replace_prefix.is_some() { + if self.replace_full.is_some() { + return Err(Error::BadRequest( + "Bad XML: both ReplaceKeyPrefixWith and ReplaceKeyWith are set".to_owned(), + )); + } + if !has_prefix { + return Err(Error::BadRequest( + "Bad XML: ReplaceKeyPrefixWith is set, but KeyPrefixEquals isn't".to_owned(), + )); + } + } + if let Some(ref protocol) = self.protocol { + if protocol.0 != "http" && protocol.0 != "https" { + return Err(Error::BadRequest("Bad XML: invalid protocol".to_owned())); + } + } + // TODO there are probably more invalide cases, but which ones? + Ok(()) + } +} + #[cfg(test)] mod tests { use super::*; @@ -142,32 +245,64 @@ mod tests { let message = r#" - string + my-error-doc - string + my-index - string - string + garage.tld + https 404 - string + prefix1 - string - string + gara.ge + http 303 - string - string + prefix2 + fullkey "#; - let _conf: WebsiteConfiguration = from_str(message).unwrap(); + let conf: WebsiteConfiguration = from_str(message).unwrap(); + let ref_value = WebsiteConfiguration { + xmlns: (), + error_document: Some(Key { + key: Value("my-error-doc".to_owned()), + }), + index_document: Some(Suffix { + suffix: Value("my-index".to_owned()), + }), + redirect_all_requests_to: Some(Target { + hostname: Value("garage.tld".to_owned()), + protocol: Some(Value("https".to_owned())), + }), + routing_rules: Some(vec![RoutingRule { + inner: RoutingRuleInner { + condition: Some(Condition { + http_error_code: Some(IntValue(404)), + prefix: Some(Value("prefix1".to_owned())), + }), + redirect: Redirect { + hostname: Some(Value("gara.ge".to_owned())), + protocol: Some(Value("http".to_owned())), + http_redirect_code: Some(IntValue(303)), + replace_prefix: Some(Value("prefix2".to_owned())), + replace_full: Some(Value("fullkey".to_owned())), + }, + }, + }]), + }; + assert_eq! { + ref_value, + conf + } // TODO verify result is ok // TODO cycle back and verify if ok }