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 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 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 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 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 200 | 404 => {
288 if self.hostname.is_some() || self.protocol.is_some() {
289 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}