From 7dcaba652524cf9be60cbc67f59a9bec290ddd32 Mon Sep 17 00:00:00 2001 From: Trinity Pointard Date: Sat, 11 Dec 2021 12:53:23 +0100 Subject: [PATCH 1/4] add basic support for website controle through s3 api --- src/api/api_server.rs | 5 ++++ src/api/lib.rs | 1 + src/api/s3_website.rs | 59 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 src/api/s3_website.rs diff --git a/src/api/api_server.rs b/src/api/api_server.rs index 6d6e5b68..2de86233 100644 --- a/src/api/api_server.rs +++ b/src/api/api_server.rs @@ -22,6 +22,7 @@ use crate::s3_get::*; use crate::s3_list::*; use crate::s3_put::*; use crate::s3_router::{Authorization, Endpoint}; +use crate::s3_website::*; /// Run the S3 API server pub async fn run_api_server( @@ -254,6 +255,10 @@ async fn handler_inner(garage: Arc, req: Request) -> Result { handle_delete_objects(garage, &bucket, req, content_sha256).await } + Endpoint::PutBucketWebsite { bucket } => { + handle_put_website(garage, bucket, req, content_sha256).await + } + Endpoint::DeleteBucketWebsite { bucket } => handle_delete_website(garage, bucket).await, endpoint => Err(Error::NotImplemented(endpoint.name().to_owned())), } } diff --git a/src/api/lib.rs b/src/api/lib.rs index 09a55d56..589ffe9f 100644 --- a/src/api/lib.rs +++ b/src/api/lib.rs @@ -20,4 +20,5 @@ pub mod s3_get; mod s3_list; mod s3_put; mod s3_router; +mod s3_website; mod s3_xml; diff --git a/src/api/s3_website.rs b/src/api/s3_website.rs new file mode 100644 index 00000000..1b5c62a5 --- /dev/null +++ b/src/api/s3_website.rs @@ -0,0 +1,59 @@ +use std::sync::Arc; + +use hyper::{Body, Request, Response, StatusCode}; + +use crate::error::*; +use crate::signature::verify_signed_content; +use garage_model::bucket_table::BucketState; +use garage_model::garage::Garage; +use garage_table::*; +use garage_util::data::Hash; + +pub async fn handle_delete_website( + garage: Arc, + bucket: String, +) -> Result, Error> { + let mut bucket = garage + .bucket_table + .get(&EmptyKey, &bucket) + .await? + .ok_or(Error::NotFound)?; + + if let BucketState::Present(state) = bucket.state.get_mut() { + state.website.update(false); + garage.bucket_table.insert(&bucket).await?; + } + + Ok(Response::builder() + .status(StatusCode::NO_CONTENT) + .body(Body::from(vec![])) + .unwrap()) +} + +pub async fn handle_put_website( + garage: Arc, + bucket: String, + req: Request, + content_sha256: Option, +) -> Result, Error> { + let body = hyper::body::to_bytes(req.into_body()).await?; + verify_signed_content(content_sha256, &body[..])?; + + let mut bucket = garage + .bucket_table + .get(&EmptyKey, &bucket) + .await? + .ok_or(Error::NotFound)?; + + // TODO: parse xml + + if let BucketState::Present(state) = bucket.state.get_mut() { + state.website.update(true); + garage.bucket_table.insert(&bucket).await?; + } + + Ok(Response::builder() + .status(StatusCode::OK) + .body(Body::from(vec![])) + .unwrap()) +} -- 2.43.4 From 3999fd4a02fc06fe28dd5fc89c665562413f5683 Mon Sep 17 00:00:00 2001 From: Trinity Pointard Date: Mon, 13 Dec 2021 23:37:01 +0100 Subject: [PATCH 2/4] add website xml parsing and serizalizing --- src/api/s3_website.rs | 117 +++++++++++++++++++++++++++++++++++++++++- src/api/s3_xml.rs | 8 +-- 2 files changed, 120 insertions(+), 5 deletions(-) diff --git a/src/api/s3_website.rs b/src/api/s3_website.rs index 1b5c62a5..6587015b 100644 --- a/src/api/s3_website.rs +++ b/src/api/s3_website.rs @@ -1,8 +1,11 @@ +use quick_xml::de::from_reader; use std::sync::Arc; use hyper::{Body, Request, Response, StatusCode}; +use serde::{Deserialize, Serialize}; use crate::error::*; +use crate::s3_xml::{xmlns_tag, IntValue, Value}; use crate::signature::verify_signed_content; use garage_model::bucket_table::BucketState; use garage_model::garage::Garage; @@ -45,7 +48,7 @@ pub async fn handle_put_website( .await? .ok_or(Error::NotFound)?; - // TODO: parse xml + let _conf: WebsiteConfiguration = from_reader(&body as &[u8])?; if let BucketState::Present(state) = bucket.state.get_mut() { state.website.update(true); @@ -57,3 +60,115 @@ pub async fn handle_put_website( .body(Body::from(vec![])) .unwrap()) } + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub struct WebsiteConfiguration { + #[serde(serialize_with = "xmlns_tag", skip_deserializing)] + pub xmlns: (), + #[serde(rename = "ErrorDocument")] + pub error_document: Option, + #[serde(rename = "IndexDocument")] + pub index_document: Option, + #[serde(rename = "RedirectAllRequestsTo")] + pub redirect_all_requests_to: Option, + #[serde(rename = "RoutingRules")] + pub routing_rules: Option>, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub struct RoutingRule { + #[serde(rename = "RoutingRule")] + pub routing_rule: RoutingRuleInner, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub struct RoutingRuleInner { + #[serde(rename = "Condition")] + pub condition: Option, + #[serde(rename = "Redirect")] + pub redirect: Redirect, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub struct Key { + #[serde(rename = "Key")] + pub key: Value, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub struct Suffix { + #[serde(rename = "Suffix")] + pub suffix: Value, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub struct Target { + #[serde(rename = "HostName")] + pub hostname: Option, + #[serde(rename = "Protocol")] + pub protocol: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub struct Condition { + #[serde(rename = "HttpErrorCodeReturnedEquals")] + pub http_error_code: Option, + #[serde(rename = "KeyPrefixEquals")] + pub prefix: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub struct Redirect { + #[serde(rename = "HostName")] + pub hostname: Option, + #[serde(rename = "Protocol")] + pub protocol: Option, + #[serde(rename = "HttpRedirectCode")] + pub http_redirect_code: Option, + #[serde(rename = "ReplaceKeyPrefixWith")] + pub replace_prefix: Option, + #[serde(rename = "ReplaceKeyWith")] + pub replace_full: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + + use quick_xml::de::from_str; + + #[test] + fn test_deserialize() { + let message = r#" + + + string + + + string + + + string + string + + + + + 404 + string + + + string + string + 303 + string + string + + + +"#; + let _conf: WebsiteConfiguration = from_str(message).unwrap(); + // TODO verify result is ok + // TODO cycle back and verify if ok + } +} diff --git a/src/api/s3_xml.rs b/src/api/s3_xml.rs index f0547961..9b5a0202 100644 --- a/src/api/s3_xml.rs +++ b/src/api/s3_xml.rs @@ -1,5 +1,5 @@ use quick_xml::se::to_string; -use serde::{Serialize, Serializer}; +use serde::{Deserialize, Serialize, Serializer}; use crate::Error as ApiError; @@ -9,14 +9,14 @@ pub fn to_xml_with_header(x: &T) -> Result { Ok(xml) } -fn xmlns_tag(_v: &(), s: S) -> Result { +pub fn xmlns_tag(_v: &(), s: S) -> Result { s.serialize_str("http://s3.amazonaws.com/doc/2006-03-01/") } -#[derive(Debug, Serialize, PartialEq)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct Value(#[serde(rename = "$value")] pub String); -#[derive(Debug, Serialize, PartialEq)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct IntValue(#[serde(rename = "$value")] pub i64); #[derive(Debug, Serialize, PartialEq)] -- 2.43.4 From 6fef0f4cadfca1ab3ae2bb6fe738741bb82bd41b Mon Sep 17 00:00:00 2001 From: Trinity Pointard Date: Tue, 14 Dec 2021 19:27:45 +0100 Subject: [PATCH 3/4] 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 } -- 2.43.4 From fd341177e9c53868a589efd49f7721c84f13ecb1 Mon Sep 17 00:00:00 2001 From: Trinity Pointard Date: Tue, 14 Dec 2021 19:34:01 +0100 Subject: [PATCH 4/4] please clippy --- src/api/s3_website.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/api/s3_website.rs b/src/api/s3_website.rs index 75fabe23..37c8b86c 100644 --- a/src/api/s3_website.rs +++ b/src/api/s3_website.rs @@ -134,15 +134,14 @@ pub struct Redirect { impl WebsiteConfiguration { pub fn validate(&self) -> Result<(), Error> { - if self.redirect_all_requests_to.is_some() { - if self.error_document.is_some() + if self.redirect_all_requests_to.is_some() + && (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(), - )); - } + || 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()?; -- 2.43.4