diff options
Diffstat (limited to 'src/api')
-rw-r--r-- | src/api/admin/bucket.rs | 2 | ||||
-rw-r--r-- | src/api/s3/website.rs | 218 |
2 files changed, 170 insertions, 50 deletions
diff --git a/src/api/admin/bucket.rs b/src/api/admin/bucket.rs index d2bb62e0..ca5b2d86 100644 --- a/src/api/admin/bucket.rs +++ b/src/api/admin/bucket.rs @@ -376,6 +376,8 @@ impl RequestHandler for UpdateBucketRequest { "Please specify indexDocument when enabling website access.", )?, error_document: wa.error_document, + redirect_all: None, + routing_rules: Vec::new(), })); } else { if wa.index_document.is_some() || wa.error_document.is_some() { diff --git a/src/api/s3/website.rs b/src/api/s3/website.rs index b55bb345..b714ea23 100644 --- a/src/api/s3/website.rs +++ b/src/api/s3/website.rs @@ -4,7 +4,7 @@ use http_body_util::BodyExt; use hyper::{Request, Response, StatusCode}; use serde::{Deserialize, Serialize}; -use garage_model::bucket_table::*; +use garage_model::bucket_table::{self, *}; use garage_util::data::*; use garage_api_common::helpers::*; @@ -26,7 +26,28 @@ pub async fn handle_get_website(ctx: ReqCtx) -> Result<Response<ResBody>, Error> suffix: Value(website.index_document.to_string()), }), redirect_all_requests_to: None, - routing_rules: None, + routing_rules: RoutingRules { + rules: website + .routing_rules + .clone() + .into_iter() + .map(|rule| RoutingRule { + condition: rule.condition.map(|cond| Condition { + http_error_code: cond.http_error_code.map(|c| IntValue(c as i64)), + prefix: cond.prefix.map(Value), + }), + redirect: Redirect { + hostname: rule.redirect.hostname.map(Value), + http_redirect_code: Some(IntValue( + rule.redirect.http_redirect_code as i64, + )), + protocol: rule.redirect.protocol.map(Value), + replace_full: rule.redirect.replace_key.map(Value), + replace_prefix: rule.redirect.replace_key_prefix.map(Value), + }, + }) + .collect(), + }, }; let xml = to_xml_with_header(&wc)?; Ok(Response::builder() @@ -102,18 +123,28 @@ pub struct WebsiteConfiguration { pub index_document: Option<Suffix>, #[serde(rename = "RedirectAllRequestsTo")] pub redirect_all_requests_to: Option<Target>, - #[serde(rename = "RoutingRules")] - pub routing_rules: Option<Vec<RoutingRule>>, + #[serde( + rename = "RoutingRules", + default, + skip_serializing_if = "RoutingRules::is_empty" + )] + pub routing_rules: RoutingRules, } -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] -pub struct RoutingRule { +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Default)] +pub struct RoutingRules { #[serde(rename = "RoutingRule")] - pub inner: RoutingRuleInner, + pub rules: Vec<RoutingRule>, +} + +impl RoutingRules { + fn is_empty(&self) -> bool { + self.rules.is_empty() + } } #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] -pub struct RoutingRuleInner { +pub struct RoutingRule { #[serde(rename = "Condition")] pub condition: Option<Condition>, #[serde(rename = "Redirect")] @@ -167,7 +198,7 @@ impl WebsiteConfiguration { if self.redirect_all_requests_to.is_some() && (self.error_document.is_some() || self.index_document.is_some() - || self.routing_rules.is_some()) + || !self.routing_rules.is_empty()) { return Err(Error::bad_request( "Bad XML: can't have RedirectAllRequestsTo and other fields", @@ -182,10 +213,15 @@ impl WebsiteConfiguration { 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()?; - } + for rr in &self.routing_rules.rules { + rr.validate()?; + } + if self.routing_rules.rules.len() > 1000 { + // we will do linear scans, best to avoid overly long configuration. The + // limit was choosen arbitrarily + return Err(Error::bad_request( + "Bad XML: RoutingRules can't have more than 1000 child elements", + )); } Ok(()) @@ -194,11 +230,7 @@ impl WebsiteConfiguration { pub fn into_garage_website_config(self) -> Result<WebsiteConfig, Error> { if self.redirect_all_requests_to.is_some() { Err(Error::NotImplemented( - "S3 website redirects are not currently implemented in Garage.".into(), - )) - } else if self.routing_rules.map(|x| !x.is_empty()).unwrap_or(false) { - Err(Error::NotImplemented( - "S3 routing rules are not currently implemented in Garage.".into(), + "RedirectAllRequestsTo is not currently implemented in Garage, however its effect can be emulated using a single inconditional RoutingRule.".into(), )) } else { Ok(WebsiteConfig { @@ -207,6 +239,36 @@ impl WebsiteConfiguration { .map(|x| x.suffix.0) .unwrap_or_else(|| "index.html".to_string()), error_document: self.error_document.map(|x| x.key.0), + redirect_all: None, + routing_rules: self + .routing_rules + .rules + .into_iter() + .map(|rule| { + bucket_table::RoutingRule { + condition: rule.condition.map(|condition| { + bucket_table::RedirectCondition { + http_error_code: condition.http_error_code.map(|c| c.0 as u16), + prefix: condition.prefix.map(|p| p.0), + } + }), + redirect: bucket_table::Redirect { + hostname: rule.redirect.hostname.map(|h| h.0), + protocol: rule.redirect.protocol.map(|p| p.0), + // aws default to 301, which i find punitive in case of + // missconfiguration (can be permanently cached on the + // user agent) + http_redirect_code: rule + .redirect + .http_redirect_code + .map(|c| c.0 as u16) + .unwrap_or(302), + replace_key_prefix: rule.redirect.replace_prefix.map(|k| k.0), + replace_key: rule.redirect.replace_full.map(|k| k.0), + }, + } + }) + .collect(), }) } } @@ -247,37 +309,69 @@ impl Target { } } -impl RoutingRuleInner { +impl RoutingRule { pub fn validate(&self) -> Result<(), Error> { - let has_prefix = self - .condition - .as_ref() - .and_then(|c| c.prefix.as_ref()) - .is_some(); - self.redirect.validate(has_prefix) + if let Some(condition) = &self.condition { + condition.validate()?; + } + self.redirect.validate() } } -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::bad_request( - "Bad XML: both ReplaceKeyPrefixWith and ReplaceKeyWith are set", - )); - } - if !has_prefix { +impl Condition { + pub fn validate(&self) -> Result<bool, Error> { + if let Some(ref error_code) = self.http_error_code { + // TODO do other error codes make sense? Aws only allows 4xx and 5xx + if error_code.0 != 404 { return Err(Error::bad_request( - "Bad XML: ReplaceKeyPrefixWith is set, but KeyPrefixEquals isn't", + "Bad XML: HttpErrorCodeReturnedEquals must be 404 or absent", )); } } + Ok(self.prefix.is_some()) + } +} + +impl Redirect { + pub fn validate(&self) -> Result<(), Error> { + if self.replace_prefix.is_some() && self.replace_full.is_some() { + return Err(Error::bad_request( + "Bad XML: both ReplaceKeyPrefixWith and ReplaceKeyWith are set", + )); + } if let Some(ref protocol) = self.protocol { if protocol.0 != "http" && protocol.0 != "https" { return Err(Error::bad_request("Bad XML: invalid protocol")); } } - // TODO there are probably more invalid cases, but which ones? + if let Some(ref http_redirect_code) = self.http_redirect_code { + match http_redirect_code.0 { + // aws allows all 3xx except 300, but some are non-sensical (not modified, + // use proxy...) + 301 | 302 | 303 | 307 | 308 => { + if self.hostname.is_none() && self.protocol.is_some() { + return Err(Error::bad_request( + "Bad XML: HostName must be set if Protocol is set", + )); + } + } + // aws doesn't allow these codes, but netlify does, and it seems like a + // cool feature (change the page seen without changing the url shown by the + // user agent) + 200 | 404 => { + if self.hostname.is_some() || self.protocol.is_some() { + // hostname would mean different bucket, protocol doesn't make + // sense + return Err(Error::bad_request( + "Bad XML: an HttpRedirectCode of 200 is not acceptable alongside HostName or Protocol", + )); + } + } + _ => { + return Err(Error::bad_request("Bad XML: invalid HttpRedirectCode")); + } + } + } Ok(()) } } @@ -316,6 +410,15 @@ mod tests { <ReplaceKeyWith>fullkey</ReplaceKeyWith> </Redirect> </RoutingRule> + <RoutingRule> + <Condition> + <KeyPrefixEquals></KeyPrefixEquals> + </Condition> + <Redirect> + <HttpRedirectCode>404</HttpRedirectCode> + <ReplaceKeyWith>missing</ReplaceKeyWith> + </Redirect> + </RoutingRule> </RoutingRules> </WebsiteConfiguration>"#; let conf: WebsiteConfiguration = from_str(message).unwrap(); @@ -331,21 +434,36 @@ mod tests { 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())), + routing_rules: RoutingRules { + rules: vec![ + RoutingRule { + 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())), + }, + }, + RoutingRule { + condition: Some(Condition { + http_error_code: None, + prefix: Some(Value("".to_owned())), + }), + redirect: Redirect { + hostname: None, + protocol: None, + http_redirect_code: Some(IntValue(404)), + replace_prefix: None, + replace_full: Some(Value("missing".to_owned())), + }, }, - }, - }]), + ], + }, }; assert_eq! { ref_value, |