use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use garage_model::bucket_table::{self, RoutingRule as GarageRoutingRule, WebsiteConfig};
use crate::common_error::CommonError as Error;
use crate::xml::{xmlns_tag, IntValue, Value};
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct WebsiteConfiguration {
#[serde(rename = "@xmlns", serialize_with = "xmlns_tag", skip_deserializing)]
pub xmlns: (),
#[serde(rename = "ErrorDocument", skip_serializing_if = "Option::is_none")]
pub error_document: Option<Key>,
#[serde(rename = "IndexDocument", skip_serializing_if = "Option::is_none")]
pub index_document: Option<Suffix>,
#[serde(
rename = "RedirectAllRequestsTo",
skip_serializing_if = "Option::is_none"
)]
pub redirect_all_requests_to: Option<Target>,
#[serde(
rename = "RoutingRules",
default,
skip_serializing_if = "RoutingRules::is_empty"
)]
pub routing_rules: RoutingRules,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Default)]
pub struct RoutingRules {
#[serde(rename = "RoutingRule")]
pub rules: Vec<RoutingRule>,
}
impl RoutingRules {
fn is_empty(&self) -> bool {
self.rules.is_empty()
}
}
#[derive(Debug, ToSchema, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
#[schema(as = website::RoutingRule)]
pub struct RoutingRule {
#[serde(rename = "Condition")]
pub condition: Option<Condition>,
#[serde(rename = "Redirect")]
pub redirect: Redirect,
}
#[derive(Debug, ToSchema, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
#[schema(as = website::Key)]
pub struct Key {
#[serde(rename = "Key")]
pub key: Value,
}
#[derive(Debug, ToSchema, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
#[schema(as = website::Suffix)]
pub struct Suffix {
#[serde(rename = "Suffix")]
pub suffix: Value,
}
#[derive(Debug, ToSchema, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
#[schema(as = website::Target)]
pub struct Target {
#[serde(rename = "HostName")]
pub hostname: Value,
#[serde(rename = "Protocol")]
pub protocol: Option<Value>,
}
#[derive(Debug, ToSchema, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
#[schema(as = website::Condition)]
pub struct Condition {
#[serde(
rename = "HttpErrorCodeReturnedEquals",
skip_serializing_if = "Option::is_none"
)]
pub http_error_code: Option<IntValue>,
#[serde(rename = "KeyPrefixEquals", skip_serializing_if = "Option::is_none")]
pub prefix: Option<Value>,
}
#[derive(Debug, ToSchema, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
#[schema(as = website::Redirect)]
pub struct Redirect {
#[serde(rename = "HostName", skip_serializing_if = "Option::is_none")]
pub hostname: Option<Value>,
#[serde(rename = "Protocol", skip_serializing_if = "Option::is_none")]
pub protocol: Option<Value>,
#[serde(rename = "HttpRedirectCode", skip_serializing_if = "Option::is_none")]
pub http_redirect_code: Option<IntValue>,
#[serde(
rename = "ReplaceKeyPrefixWith",
skip_serializing_if = "Option::is_none"
)]
pub replace_prefix: Option<Value>,
#[serde(rename = "ReplaceKeyWith", skip_serializing_if = "Option::is_none")]
pub replace_full: Option<Value>,
}
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_empty())
{
return Err(Error::bad_request(
"Bad XML: can't have RedirectAllRequestsTo and other fields",
));
}
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()?;
}
for rr in &self.routing_rules.rules {
rr.validate()?;
}
if self.routing_rules.rules.len() > 1000 {
return Err(Error::bad_request(
"Bad XML: RoutingRules can't have more than 1000 child elements",
));
}
Ok(())
}
pub fn into_garage_website_config(self) -> Result<WebsiteConfig, Error> {
if self.redirect_all_requests_to.is_some() {
Err(Error::NotImplemented(
"RedirectAllRequestsTo is not currently implemented in Garage, however its effect can be emulated using a single unconditional RoutingRule.".into(),
))
} else {
Ok(WebsiteConfig {
index_document: self
.index_document
.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(RoutingRule::into_garage_routing_rule)
.collect(),
})
}
}
}
impl Key {
pub fn validate(&self) -> Result<(), Error> {
if self.key.0.is_empty() {
Err(Error::bad_request(
"Bad XML: error document specified but empty",
))
} else {
Ok(())
}
}
}
impl Suffix {
pub fn validate(&self) -> Result<(), Error> {
if self.suffix.0.is_empty() | self.suffix.0.contains('/') {
Err(Error::bad_request(
"Bad XML: index document is empty or contains /",
))
} 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::bad_request("Bad XML: invalid protocol"));
}
}
Ok(())
}
}
impl RoutingRule {
pub fn validate(&self) -> Result<(), Error> {
if let Some(condition) = &self.condition {
condition.validate()?;
}
self.redirect.validate()
}
pub fn from_garage_routing_rule(rule: GarageRoutingRule) -> Self {
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),
},
}
}
pub fn into_garage_routing_rule(self) -> bucket_table::RoutingRule {
bucket_table::RoutingRule {
condition: self
.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: self.redirect.hostname.map(|h| h.0),
protocol: self.redirect.protocol.map(|p| p.0),
http_redirect_code: self
.redirect
.http_redirect_code
.map(|c| c.0 as u16)
.unwrap_or(302),
replace_key_prefix: self.redirect.replace_prefix.map(|k| k.0),
replace_key: self.redirect.replace_full.map(|k| k.0),
},
}
}
}
impl Condition {
pub fn validate(&self) -> Result<bool, Error> {
if let Some(ref error_code) = self.http_error_code {
if error_code.0 != 404 {
return Err(Error::bad_request(
"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"));
}
}
if let Some(ref http_redirect_code) = self.http_redirect_code {
match http_redirect_code.0 {
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",
));
}
}
200 | 404 => {
if self.hostname.is_some() || self.protocol.is_some() {
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(())
}
}
#[cfg(test)]
mod tests {
use crate::xml::{to_xml_with_header, unprettify_xml};
use super::*;
use quick_xml::de::from_str;
#[test]
fn test_deserialize() {
let message = r#"<?xml version="1.0" encoding="UTF-8"?>
<WebsiteConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<ErrorDocument>
<Key>my-error-doc</Key>
</ErrorDocument>
<IndexDocument>
<Suffix>my-index</Suffix>
</IndexDocument>
<RedirectAllRequestsTo>
<HostName>garage.tld</HostName>
<Protocol>https</Protocol>
</RedirectAllRequestsTo>
<RoutingRules>
<RoutingRule>
<Condition>
<HttpErrorCodeReturnedEquals>404</HttpErrorCodeReturnedEquals>
<KeyPrefixEquals>prefix1</KeyPrefixEquals>
</Condition>
<Redirect>
<HostName>gara.ge</HostName>
<Protocol>http</Protocol>
<HttpRedirectCode>303</HttpRedirectCode>
<ReplaceKeyPrefixWith>prefix2</ReplaceKeyPrefixWith>
<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).expect("failed to deserialize xml in `WebsiteConfiguration`");
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: 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,
conf
}
let message2 = to_xml_with_header(&ref_value).expect("xml serialization");
assert_eq!(unprettify_xml(message), unprettify_xml(&message2));
}
#[test]
fn test_serialize_empty() {
let conf = WebsiteConfiguration {
xmlns: (),
error_document: None,
index_document: None,
redirect_all_requests_to: None,
routing_rules: RoutingRules { rules: vec![] },
};
let serialized_ref = r#"<?xml version="1.0" encoding="UTF-8"?>
<WebsiteConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
</WebsiteConfiguration>"#;
let serialized = to_xml_with_header(&conf).expect("xml serialization");
assert_eq!(unprettify_xml(&serialized), unprettify_xml(&serialized_ref));
}
}