Skip to main content

garage_api_common/xml/
website.rs

1use serde::{Deserialize, Serialize};
2use utoipa::ToSchema;
3
4use garage_model::bucket_table::{self, RoutingRule as GarageRoutingRule, WebsiteConfig};
5
6use crate::common_error::CommonError as Error;
7use crate::xml::{xmlns_tag, IntValue, Value};
8
9#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
10pub struct WebsiteConfiguration {
11	#[serde(rename = "@xmlns", serialize_with = "xmlns_tag", skip_deserializing)]
12	pub xmlns: (),
13	#[serde(rename = "ErrorDocument", skip_serializing_if = "Option::is_none")]
14	pub error_document: Option<Key>,
15	#[serde(rename = "IndexDocument", skip_serializing_if = "Option::is_none")]
16	pub index_document: Option<Suffix>,
17	#[serde(
18		rename = "RedirectAllRequestsTo",
19		skip_serializing_if = "Option::is_none"
20	)]
21	pub redirect_all_requests_to: Option<Target>,
22	#[serde(
23		rename = "RoutingRules",
24		default,
25		skip_serializing_if = "RoutingRules::is_empty"
26	)]
27	pub routing_rules: RoutingRules,
28}
29
30#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Default)]
31pub struct RoutingRules {
32	#[serde(rename = "RoutingRule")]
33	pub rules: Vec<RoutingRule>,
34}
35
36impl RoutingRules {
37	fn is_empty(&self) -> bool {
38		self.rules.is_empty()
39	}
40}
41
42#[derive(Debug, ToSchema, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
43#[schema(as = website::RoutingRule)]
44pub struct RoutingRule {
45	#[serde(rename = "Condition")]
46	pub condition: Option<Condition>,
47	#[serde(rename = "Redirect")]
48	pub redirect: Redirect,
49}
50
51#[derive(Debug, ToSchema, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
52#[schema(as = website::Key)]
53pub struct Key {
54	#[serde(rename = "Key")]
55	pub key: Value,
56}
57
58#[derive(Debug, ToSchema, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
59#[schema(as = website::Suffix)]
60pub struct Suffix {
61	#[serde(rename = "Suffix")]
62	pub suffix: Value,
63}
64
65#[derive(Debug, ToSchema, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
66#[schema(as = website::Target)]
67pub struct Target {
68	#[serde(rename = "HostName")]
69	pub hostname: Value,
70	#[serde(rename = "Protocol")]
71	pub protocol: Option<Value>,
72}
73
74#[derive(Debug, ToSchema, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
75#[schema(as = website::Condition)]
76pub struct Condition {
77	#[serde(
78		rename = "HttpErrorCodeReturnedEquals",
79		skip_serializing_if = "Option::is_none"
80	)]
81	pub http_error_code: Option<IntValue>,
82	#[serde(rename = "KeyPrefixEquals", skip_serializing_if = "Option::is_none")]
83	pub prefix: Option<Value>,
84}
85
86#[derive(Debug, ToSchema, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
87#[schema(as = website::Redirect)]
88pub struct Redirect {
89	#[serde(rename = "HostName", skip_serializing_if = "Option::is_none")]
90	pub hostname: Option<Value>,
91	#[serde(rename = "Protocol", skip_serializing_if = "Option::is_none")]
92	pub protocol: Option<Value>,
93	#[serde(rename = "HttpRedirectCode", skip_serializing_if = "Option::is_none")]
94	pub http_redirect_code: Option<IntValue>,
95	#[serde(
96		rename = "ReplaceKeyPrefixWith",
97		skip_serializing_if = "Option::is_none"
98	)]
99	pub replace_prefix: Option<Value>,
100	#[serde(rename = "ReplaceKeyWith", skip_serializing_if = "Option::is_none")]
101	pub replace_full: Option<Value>,
102}
103
104impl WebsiteConfiguration {
105	pub fn validate(&self) -> Result<(), Error> {
106		if self.redirect_all_requests_to.is_some()
107			&& (self.error_document.is_some()
108				|| self.index_document.is_some()
109				|| !self.routing_rules.is_empty())
110		{
111			return Err(Error::bad_request(
112				"Bad XML: can't have RedirectAllRequestsTo and other fields",
113			));
114		}
115		if let Some(ref ed) = self.error_document {
116			ed.validate()?;
117		}
118		if let Some(ref id) = self.index_document {
119			id.validate()?;
120		}
121		if let Some(ref rart) = self.redirect_all_requests_to {
122			rart.validate()?;
123		}
124		for rr in &self.routing_rules.rules {
125			rr.validate()?;
126		}
127		if self.routing_rules.rules.len() > 1000 {
128			// we will do linear scans, best to avoid overly long configuration. The
129			// limit was chosen arbitrarily
130			return Err(Error::bad_request(
131				"Bad XML: RoutingRules can't have more than 1000 child elements",
132			));
133		}
134
135		Ok(())
136	}
137
138	pub fn into_garage_website_config(self) -> Result<WebsiteConfig, Error> {
139		if self.redirect_all_requests_to.is_some() {
140			Err(Error::NotImplemented(
141				"RedirectAllRequestsTo is not currently implemented in Garage, however its effect can be emulated using a single unconditional RoutingRule.".into(),
142			))
143		} else {
144			Ok(WebsiteConfig {
145				index_document: self
146					.index_document
147					.map(|x| x.suffix.0)
148					.unwrap_or_else(|| "index.html".to_string()),
149				error_document: self.error_document.map(|x| x.key.0),
150				redirect_all: None,
151				routing_rules: self
152					.routing_rules
153					.rules
154					.into_iter()
155					.map(RoutingRule::into_garage_routing_rule)
156					.collect(),
157			})
158		}
159	}
160}
161
162impl Key {
163	pub fn validate(&self) -> Result<(), Error> {
164		if self.key.0.is_empty() {
165			Err(Error::bad_request(
166				"Bad XML: error document specified but empty",
167			))
168		} else {
169			Ok(())
170		}
171	}
172}
173
174impl Suffix {
175	pub fn validate(&self) -> Result<(), Error> {
176		if self.suffix.0.is_empty() | self.suffix.0.contains('/') {
177			Err(Error::bad_request(
178				"Bad XML: index document is empty or contains /",
179			))
180		} else {
181			Ok(())
182		}
183	}
184}
185
186impl Target {
187	pub fn validate(&self) -> Result<(), Error> {
188		if let Some(ref protocol) = self.protocol {
189			if protocol.0 != "http" && protocol.0 != "https" {
190				return Err(Error::bad_request("Bad XML: invalid protocol"));
191			}
192		}
193		Ok(())
194	}
195}
196
197impl RoutingRule {
198	pub fn validate(&self) -> Result<(), Error> {
199		if let Some(condition) = &self.condition {
200			condition.validate()?;
201		}
202		self.redirect.validate()
203	}
204
205	pub fn from_garage_routing_rule(rule: GarageRoutingRule) -> Self {
206		RoutingRule {
207			condition: rule.condition.map(|cond| Condition {
208				http_error_code: cond.http_error_code.map(|c| IntValue(c as i64)),
209				prefix: cond.prefix.map(Value),
210			}),
211			redirect: Redirect {
212				hostname: rule.redirect.hostname.map(Value),
213				http_redirect_code: Some(IntValue(rule.redirect.http_redirect_code as i64)),
214				protocol: rule.redirect.protocol.map(Value),
215				replace_full: rule.redirect.replace_key.map(Value),
216				replace_prefix: rule.redirect.replace_key_prefix.map(Value),
217			},
218		}
219	}
220
221	pub fn into_garage_routing_rule(self) -> bucket_table::RoutingRule {
222		bucket_table::RoutingRule {
223			condition: self
224				.condition
225				.map(|condition| bucket_table::RedirectCondition {
226					http_error_code: condition.http_error_code.map(|c| c.0 as u16),
227					prefix: condition.prefix.map(|p| p.0),
228				}),
229			redirect: bucket_table::Redirect {
230				hostname: self.redirect.hostname.map(|h| h.0),
231				protocol: self.redirect.protocol.map(|p| p.0),
232				// aws default to 301, which i find punitive in case of
233				// misconfiguration (can be permanently cached on the
234				// user agent)
235				http_redirect_code: self
236					.redirect
237					.http_redirect_code
238					.map(|c| c.0 as u16)
239					.unwrap_or(302),
240				replace_key_prefix: self.redirect.replace_prefix.map(|k| k.0),
241				replace_key: self.redirect.replace_full.map(|k| k.0),
242			},
243		}
244	}
245}
246
247impl Condition {
248	pub fn validate(&self) -> Result<bool, Error> {
249		if let Some(ref error_code) = self.http_error_code {
250			// TODO do other error codes make sense? Aws only allows 4xx and 5xx
251			if error_code.0 != 404 {
252				return Err(Error::bad_request(
253					"Bad XML: HttpErrorCodeReturnedEquals must be 404 or absent",
254				));
255			}
256		}
257		Ok(self.prefix.is_some())
258	}
259}
260
261impl Redirect {
262	pub fn validate(&self) -> Result<(), Error> {
263		if self.replace_prefix.is_some() && self.replace_full.is_some() {
264			return Err(Error::bad_request(
265				"Bad XML: both ReplaceKeyPrefixWith and ReplaceKeyWith are set",
266			));
267		}
268		if let Some(ref protocol) = self.protocol {
269			if protocol.0 != "http" && protocol.0 != "https" {
270				return Err(Error::bad_request("Bad XML: invalid protocol"));
271			}
272		}
273		if let Some(ref http_redirect_code) = self.http_redirect_code {
274			match http_redirect_code.0 {
275				// aws allows all 3xx except 300, but some are non-sensical (not modified,
276				// use proxy...)
277				301 | 302 | 303 | 307 | 308 => {
278					if self.hostname.is_none() && self.protocol.is_some() {
279						return Err(Error::bad_request(
280							"Bad XML: HostName must be set if Protocol is set",
281						));
282					}
283				}
284				// aws doesn't allow these codes, but netlify does, and it seems like a
285				// cool feature (change the page seen without changing the url shown by the
286				// user agent)
287				200 | 404 => {
288					if self.hostname.is_some() || self.protocol.is_some() {
289						// hostname would mean different bucket, protocol doesn't make
290						// sense
291						return Err(Error::bad_request(
292                                        "Bad XML: an HttpRedirectCode of 200 is not acceptable alongside HostName or Protocol",
293                                ));
294					}
295				}
296				_ => {
297					return Err(Error::bad_request("Bad XML: invalid HttpRedirectCode"));
298				}
299			}
300		}
301		Ok(())
302	}
303}
304
305#[cfg(test)]
306mod tests {
307	use crate::xml::{to_xml_with_header, unprettify_xml};
308
309	use super::*;
310
311	use quick_xml::de::from_str;
312
313	#[test]
314	fn test_deserialize() {
315		let message = r#"<?xml version="1.0" encoding="UTF-8"?>
316<WebsiteConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
317   <ErrorDocument>
318         <Key>my-error-doc</Key>
319   </ErrorDocument>
320   <IndexDocument>
321      <Suffix>my-index</Suffix>
322   </IndexDocument>
323   <RedirectAllRequestsTo>
324      <HostName>garage.tld</HostName>
325      <Protocol>https</Protocol>
326   </RedirectAllRequestsTo>
327   <RoutingRules>
328      <RoutingRule>
329         <Condition>
330            <HttpErrorCodeReturnedEquals>404</HttpErrorCodeReturnedEquals>
331            <KeyPrefixEquals>prefix1</KeyPrefixEquals>
332         </Condition>
333         <Redirect>
334            <HostName>gara.ge</HostName>
335            <Protocol>http</Protocol>
336            <HttpRedirectCode>303</HttpRedirectCode>
337            <ReplaceKeyPrefixWith>prefix2</ReplaceKeyPrefixWith>
338            <ReplaceKeyWith>fullkey</ReplaceKeyWith>
339         </Redirect>
340      </RoutingRule>
341      <RoutingRule>
342         <Condition>
343            <KeyPrefixEquals></KeyPrefixEquals>
344         </Condition>
345         <Redirect>
346            <HttpRedirectCode>404</HttpRedirectCode>
347            <ReplaceKeyWith>missing</ReplaceKeyWith>
348         </Redirect>
349      </RoutingRule>
350   </RoutingRules>
351</WebsiteConfiguration>"#;
352		let conf: WebsiteConfiguration =
353			from_str(message).expect("failed to deserialize xml in `WebsiteConfiguration`");
354		let ref_value = WebsiteConfiguration {
355			xmlns: (),
356			error_document: Some(Key {
357				key: Value("my-error-doc".to_owned()),
358			}),
359			index_document: Some(Suffix {
360				suffix: Value("my-index".to_owned()),
361			}),
362			redirect_all_requests_to: Some(Target {
363				hostname: Value("garage.tld".to_owned()),
364				protocol: Some(Value("https".to_owned())),
365			}),
366			routing_rules: RoutingRules {
367				rules: vec![
368					RoutingRule {
369						condition: Some(Condition {
370							http_error_code: Some(IntValue(404)),
371							prefix: Some(Value("prefix1".to_owned())),
372						}),
373						redirect: Redirect {
374							hostname: Some(Value("gara.ge".to_owned())),
375							protocol: Some(Value("http".to_owned())),
376							http_redirect_code: Some(IntValue(303)),
377							replace_prefix: Some(Value("prefix2".to_owned())),
378							replace_full: Some(Value("fullkey".to_owned())),
379						},
380					},
381					RoutingRule {
382						condition: Some(Condition {
383							http_error_code: None,
384							prefix: Some(Value("".to_owned())),
385						}),
386						redirect: Redirect {
387							hostname: None,
388							protocol: None,
389							http_redirect_code: Some(IntValue(404)),
390							replace_prefix: None,
391							replace_full: Some(Value("missing".to_owned())),
392						},
393					},
394				],
395			},
396		};
397		assert_eq! {
398			ref_value,
399			conf
400		}
401
402		let message2 = to_xml_with_header(&ref_value).expect("xml serialization");
403
404		assert_eq!(unprettify_xml(message), unprettify_xml(&message2));
405	}
406
407	#[test]
408	fn test_serialize_empty() {
409		let conf = WebsiteConfiguration {
410			xmlns: (),
411			error_document: None,
412			index_document: None,
413			redirect_all_requests_to: None,
414			routing_rules: RoutingRules { rules: vec![] },
415		};
416		let serialized_ref = r#"<?xml version="1.0" encoding="UTF-8"?>
417<WebsiteConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
418</WebsiteConfiguration>"#;
419
420		let serialized = to_xml_with_header(&conf).expect("xml serialization");
421		assert_eq!(unprettify_xml(&serialized), unprettify_xml(&serialized_ref));
422	}
423}