aboutsummaryrefslogtreecommitdiff
path: root/src/api
diff options
context:
space:
mode:
Diffstat (limited to 'src/api')
-rw-r--r--src/api/admin/bucket.rs2
-rw-r--r--src/api/s3/website.rs218
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,