From 60d445992632a4962f709f9c9a19f698caf0e4af Mon Sep 17 00:00:00 2001 From: trinity-1686a Date: Wed, 15 Dec 2021 10:41:39 +0100 Subject: [PATCH] BucketWebsite (#174) fix #77 this does not store anything but a on/off switch for website, and does not implement GetBucketWebsite as it would require storing more. GetBucketWebsite should be pretty easy to implement once data is stored though. Co-authored-by: Trinity Pointard Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/174 Co-authored-by: trinity-1686a Co-committed-by: trinity-1686a --- src/api/api_server.rs | 5 + src/api/lib.rs | 1 + src/api/s3_website.rs | 308 ++++++++++++++++++++++++++++++++++++++++++ src/api/s3_xml.rs | 8 +- 4 files changed, 318 insertions(+), 4 deletions(-) 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..37c8b86c --- /dev/null +++ b/src/api/s3_website.rs @@ -0,0 +1,308 @@ +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; +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)?; + + let conf: WebsiteConfiguration = from_reader(&body as &[u8])?; + conf.validate()?; + + 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()) +} + +#[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 inner: 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: Value, + #[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, +} + +impl WebsiteConfiguration { + pub fn validate(&self) -> Result<(), Error> { + 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(), + )); + } + 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::*; + + use quick_xml::de::from_str; + + #[test] + fn test_deserialize() { + let message = r#" + + + my-error-doc + + + my-index + + + garage.tld + https + + + + + 404 + prefix1 + + + gara.ge + http + 303 + prefix2 + fullkey + + + +"#; + 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 + } +} 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)]