diff options
Diffstat (limited to 'src/api')
-rw-r--r-- | src/api/api_server.rs | 6 | ||||
-rw-r--r-- | src/api/lib.rs | 1 | ||||
-rw-r--r-- | src/api/s3_cors.rs | 339 | ||||
-rw-r--r-- | src/api/s3_router.rs | 10 | ||||
-rw-r--r-- | src/api/s3_website.rs | 15 | ||||
-rw-r--r-- | src/api/s3_xml.rs | 6 |
6 files changed, 368 insertions, 9 deletions
diff --git a/src/api/api_server.rs b/src/api/api_server.rs index ea1990d9..2563d375 100644 --- a/src/api/api_server.rs +++ b/src/api/api_server.rs @@ -20,6 +20,7 @@ use crate::signature::check_signature; use crate::helpers::*; use crate::s3_bucket::*; use crate::s3_copy::*; +use crate::s3_cors::*; use crate::s3_delete::*; use crate::s3_get::*; use crate::s3_list::*; @@ -310,6 +311,11 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon handle_put_website(garage, bucket_id, req, content_sha256).await } Endpoint::DeleteBucketWebsite { .. } => handle_delete_website(garage, bucket_id).await, + Endpoint::GetBucketCors { .. } => handle_get_cors(garage, bucket_id).await, + Endpoint::PutBucketCors { .. } => { + handle_put_cors(garage, bucket_id, req, content_sha256).await + } + Endpoint::DeleteBucketCors { .. } => handle_delete_cors(garage, bucket_id).await, endpoint => Err(Error::NotImplemented(endpoint.name().to_owned())), } } diff --git a/src/api/lib.rs b/src/api/lib.rs index 725cd9d1..bb5a8265 100644 --- a/src/api/lib.rs +++ b/src/api/lib.rs @@ -15,6 +15,7 @@ mod signature; pub mod helpers; mod s3_bucket; mod s3_copy; +pub mod s3_cors; mod s3_delete; pub mod s3_get; mod s3_list; diff --git a/src/api/s3_cors.rs b/src/api/s3_cors.rs new file mode 100644 index 00000000..c3329085 --- /dev/null +++ b/src/api/s3_cors.rs @@ -0,0 +1,339 @@ +use quick_xml::de::from_reader; +use std::sync::Arc; + +use http::header::{ + ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, + ACCESS_CONTROL_EXPOSE_HEADERS, +}; +use hyper::{header::HeaderName, Body, Method, Request, Response, StatusCode}; + +use serde::{Deserialize, Serialize}; + +use crate::error::*; +use crate::s3_xml::{to_xml_with_header, xmlns_tag, IntValue, Value}; +use crate::signature::verify_signed_content; + +use garage_model::bucket_table::CorsRule as GarageCorsRule; +use garage_model::garage::Garage; +use garage_table::*; +use garage_util::data::*; + +pub async fn handle_get_cors( + garage: Arc<Garage>, + bucket_id: Uuid, +) -> Result<Response<Body>, Error> { + let bucket = garage + .bucket_table + .get(&EmptyKey, &bucket_id) + .await? + .ok_or(Error::NoSuchBucket)?; + + let param = bucket + .params() + .ok_or_internal_error("Bucket should not be deleted at this point")?; + + if let Some(cors) = param.cors_config.get() { + let wc = CorsConfiguration { + xmlns: (), + cors_rules: cors + .iter() + .map(CorsRule::from_garage_cors_rule) + .collect::<Vec<_>>(), + }; + let xml = to_xml_with_header(&wc)?; + Ok(Response::builder() + .status(StatusCode::OK) + .header(http::header::CONTENT_TYPE, "application/xml") + .body(Body::from(xml))?) + } else { + Ok(Response::builder() + .status(StatusCode::NO_CONTENT) + .body(Body::empty())?) + } +} + +pub async fn handle_delete_cors( + garage: Arc<Garage>, + bucket_id: Uuid, +) -> Result<Response<Body>, Error> { + let mut bucket = garage + .bucket_table + .get(&EmptyKey, &bucket_id) + .await? + .ok_or(Error::NoSuchBucket)?; + + let param = bucket + .params_mut() + .ok_or_internal_error("Bucket should not be deleted at this point")?; + + param.cors_config.update(None); + garage.bucket_table.insert(&bucket).await?; + + Ok(Response::builder() + .status(StatusCode::NO_CONTENT) + .body(Body::empty())?) +} + +pub async fn handle_put_cors( + garage: Arc<Garage>, + bucket_id: Uuid, + req: Request<Body>, + content_sha256: Option<Hash>, +) -> Result<Response<Body>, Error> { + let body = hyper::body::to_bytes(req.into_body()).await?; + verify_signed_content(content_sha256, &body[..])?; + + let mut bucket = garage + .bucket_table + .get(&EmptyKey, &bucket_id) + .await? + .ok_or(Error::NoSuchBucket)?; + + let param = bucket + .params_mut() + .ok_or_internal_error("Bucket should not be deleted at this point")?; + + let conf: CorsConfiguration = from_reader(&body as &[u8])?; + conf.validate()?; + + param + .cors_config + .update(Some(conf.into_garage_cors_config()?)); + garage.bucket_table.insert(&bucket).await?; + + Ok(Response::builder() + .status(StatusCode::OK) + .body(Body::empty())?) +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +#[serde(rename = "CORSConfiguration")] +pub struct CorsConfiguration { + #[serde(serialize_with = "xmlns_tag", skip_deserializing)] + pub xmlns: (), + #[serde(rename = "CORSRule")] + pub cors_rules: Vec<CorsRule>, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub struct CorsRule { + #[serde(rename = "ID")] + pub id: Option<Value>, + #[serde(rename = "MaxAgeSeconds")] + pub max_age_seconds: Option<IntValue>, + #[serde(rename = "AllowedOrigin")] + pub allowed_origins: Vec<Value>, + #[serde(rename = "AllowedMethod")] + pub allowed_methods: Vec<Value>, + #[serde(rename = "AllowedHeader", default)] + pub allowed_headers: Vec<Value>, + #[serde(rename = "ExposeHeader", default)] + pub expose_headers: Vec<Value>, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub struct AllowedMethod { + #[serde(rename = "AllowedMethod")] + pub allowed_method: Value, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub struct AllowedHeader { + #[serde(rename = "AllowedHeader")] + pub allowed_header: Value, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub struct ExposeHeader { + #[serde(rename = "ExposeHeader")] + pub expose_header: Value, +} + +impl CorsConfiguration { + pub fn validate(&self) -> Result<(), Error> { + for r in self.cors_rules.iter() { + r.validate()?; + } + Ok(()) + } + + pub fn into_garage_cors_config(self) -> Result<Vec<GarageCorsRule>, Error> { + Ok(self + .cors_rules + .iter() + .map(CorsRule::to_garage_cors_rule) + .collect()) + } +} + +impl CorsRule { + pub fn validate(&self) -> Result<(), Error> { + for method in self.allowed_methods.iter() { + method + .0 + .parse::<Method>() + .ok_or_bad_request("Invalid CORSRule method")?; + } + for header in self + .allowed_headers + .iter() + .chain(self.expose_headers.iter()) + { + header + .0 + .parse::<HeaderName>() + .ok_or_bad_request("Invalid HTTP header name")?; + } + Ok(()) + } + + pub fn to_garage_cors_rule(&self) -> GarageCorsRule { + let convert_vec = + |vval: &[Value]| vval.iter().map(|x| x.0.to_owned()).collect::<Vec<String>>(); + GarageCorsRule { + id: self.id.as_ref().map(|x| x.0.to_owned()), + max_age_seconds: self.max_age_seconds.as_ref().map(|x| x.0 as u64), + allow_origins: convert_vec(&self.allowed_origins), + allow_methods: convert_vec(&self.allowed_methods), + allow_headers: convert_vec(&self.allowed_headers), + expose_headers: convert_vec(&self.expose_headers), + } + } + + pub fn from_garage_cors_rule(rule: &GarageCorsRule) -> Self { + let convert_vec = |vval: &[String]| { + vval.iter() + .map(|x| Value(x.clone())) + .collect::<Vec<Value>>() + }; + Self { + id: rule.id.as_ref().map(|x| Value(x.clone())), + max_age_seconds: rule.max_age_seconds.map(|x| IntValue(x as i64)), + allowed_origins: convert_vec(&rule.allow_origins), + allowed_methods: convert_vec(&rule.allow_methods), + allowed_headers: convert_vec(&rule.allow_headers), + expose_headers: convert_vec(&rule.expose_headers), + } + } +} + +pub fn cors_rule_matches<'a, HI, S>( + rule: &GarageCorsRule, + origin: &'a str, + method: &'a str, + mut request_headers: HI, +) -> bool +where + HI: Iterator<Item = S>, + S: AsRef<str>, +{ + rule.allow_origins.iter().any(|x| x == "*" || x == origin) + && rule.allow_methods.iter().any(|x| x == "*" || x == method) + && request_headers.all(|h| { + rule.allow_headers + .iter() + .any(|x| x == "*" || x == h.as_ref()) + }) +} + +pub fn add_cors_headers( + resp: &mut Response<Body>, + rule: &GarageCorsRule, +) -> Result<(), http::header::InvalidHeaderValue> { + let h = resp.headers_mut(); + h.insert( + ACCESS_CONTROL_ALLOW_ORIGIN, + rule.allow_origins.join(", ").parse()?, + ); + h.insert( + ACCESS_CONTROL_ALLOW_METHODS, + rule.allow_methods.join(", ").parse()?, + ); + h.insert( + ACCESS_CONTROL_ALLOW_HEADERS, + rule.allow_headers.join(", ").parse()?, + ); + h.insert( + ACCESS_CONTROL_EXPOSE_HEADERS, + rule.expose_headers.join(", ").parse()?, + ); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + use quick_xml::de::from_str; + + #[test] + fn test_deserialize() -> Result<(), Error> { + let message = r#"<?xml version="1.0" encoding="UTF-8"?> +<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> + <CORSRule> + <AllowedOrigin>http://www.example.com</AllowedOrigin> + + <AllowedMethod>PUT</AllowedMethod> + <AllowedMethod>POST</AllowedMethod> + <AllowedMethod>DELETE</AllowedMethod> + + <AllowedHeader>*</AllowedHeader> + </CORSRule> + <CORSRule> + <AllowedOrigin>*</AllowedOrigin> + <AllowedMethod>GET</AllowedMethod> + </CORSRule> + <CORSRule> + <ID>qsdfjklm</ID> + <MaxAgeSeconds>12345</MaxAgeSeconds> + <AllowedOrigin>https://perdu.com</AllowedOrigin> + + <AllowedMethod>GET</AllowedMethod> + <AllowedMethod>DELETE</AllowedMethod> + <AllowedHeader>*</AllowedHeader> + <ExposeHeader>*</ExposeHeader> + </CORSRule> +</CORSConfiguration>"#; + let conf: CorsConfiguration = from_str(message).unwrap(); + let ref_value = CorsConfiguration { + xmlns: (), + cors_rules: vec![ + CorsRule { + id: None, + max_age_seconds: None, + allowed_origins: vec!["http://www.example.com".into()], + allowed_methods: vec!["PUT".into(), "POST".into(), "DELETE".into()], + allowed_headers: vec!["*".into()], + expose_headers: vec![], + }, + CorsRule { + id: None, + max_age_seconds: None, + allowed_origins: vec!["*".into()], + allowed_methods: vec!["GET".into()], + allowed_headers: vec![], + expose_headers: vec![], + }, + CorsRule { + id: Some("qsdfjklm".into()), + max_age_seconds: Some(IntValue(12345)), + allowed_origins: vec!["https://perdu.com".into()], + allowed_methods: vec!["GET".into(), "DELETE".into()], + allowed_headers: vec!["*".into()], + expose_headers: vec!["*".into()], + }, + ], + }; + assert_eq! { + ref_value, + conf + }; + + let message2 = to_xml_with_header(&ref_value)?; + + let cleanup = |c: &str| c.replace(char::is_whitespace, ""); + assert_eq!(cleanup(message), cleanup(&message2)); + + Ok(()) + } +} diff --git a/src/api/s3_router.rs b/src/api/s3_router.rs index a8ac0086..24a3fbfd 100644 --- a/src/api/s3_router.rs +++ b/src/api/s3_router.rs @@ -773,7 +773,6 @@ impl Endpoint { GetBucketAccelerateConfiguration, GetBucketAcl, GetBucketAnalyticsConfiguration, - GetBucketCors, GetBucketEncryption, GetBucketIntelligentTieringConfiguration, GetBucketInventoryConfiguration, @@ -821,6 +820,9 @@ impl Endpoint { GetBucketWebsite, PutBucketWebsite, DeleteBucketWebsite, + GetBucketCors, + PutBucketCors, + DeleteBucketCors, ] } .is_some(); @@ -1134,7 +1136,7 @@ mod tests { OWNER_DELETE "/" => DeleteBucket DELETE "/?analytics&id=list1" => DeleteBucketAnalyticsConfiguration DELETE "/?analytics&id=Id" => DeleteBucketAnalyticsConfiguration - DELETE "/?cors" => DeleteBucketCors + OWNER_DELETE "/?cors" => DeleteBucketCors DELETE "/?encryption" => DeleteBucketEncryption DELETE "/?intelligent-tiering&id=Id" => DeleteBucketIntelligentTieringConfiguration DELETE "/?inventory&id=list1" => DeleteBucketInventoryConfiguration @@ -1157,7 +1159,7 @@ mod tests { GET "/?accelerate" => GetBucketAccelerateConfiguration GET "/?acl" => GetBucketAcl GET "/?analytics&id=Id" => GetBucketAnalyticsConfiguration - GET "/?cors" => GetBucketCors + OWNER_GET "/?cors" => GetBucketCors GET "/?encryption" => GetBucketEncryption GET "/?intelligent-tiering&id=Id" => GetBucketIntelligentTieringConfiguration GET "/?inventory&id=list1" => GetBucketInventoryConfiguration @@ -1233,7 +1235,7 @@ mod tests { PUT "/?acl" => PutBucketAcl PUT "/?analytics&id=report1" => PutBucketAnalyticsConfiguration PUT "/?analytics&id=Id" => PutBucketAnalyticsConfiguration - PUT "/?cors" => PutBucketCors + OWNER_PUT "/?cors" => PutBucketCors PUT "/?encryption" => PutBucketEncryption PUT "/?intelligent-tiering&id=Id" => PutBucketIntelligentTieringConfiguration PUT "/?inventory&id=report1" => PutBucketInventoryConfiguration diff --git a/src/api/s3_website.rs b/src/api/s3_website.rs index fcf8cba3..6dcb59c8 100644 --- a/src/api/s3_website.rs +++ b/src/api/s3_website.rs @@ -5,7 +5,7 @@ use hyper::{Body, Request, Response, StatusCode}; use serde::{Deserialize, Serialize}; use crate::error::*; -use crate::s3_xml::{xmlns_tag, IntValue, Value}; +use crate::s3_xml::{to_xml_with_header, xmlns_tag, IntValue, Value}; use crate::signature::verify_signed_content; use garage_model::bucket_table::*; @@ -39,7 +39,7 @@ pub async fn handle_get_website( redirect_all_requests_to: None, routing_rules: None, }; - let xml = quick_xml::se::to_string(&wc)?; + let xml = to_xml_with_header(&wc)?; Ok(Response::builder() .status(StatusCode::OK) .header(http::header::CONTENT_TYPE, "application/xml") @@ -303,7 +303,7 @@ mod tests { use quick_xml::de::from_str; #[test] - fn test_deserialize() { + fn test_deserialize() -> Result<(), Error> { let message = r#"<?xml version="1.0" encoding="UTF-8"?> <WebsiteConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> <ErrorDocument> @@ -365,7 +365,12 @@ mod tests { ref_value, conf } - // TODO verify result is ok - // TODO cycle back and verify if ok + + let message2 = to_xml_with_header(&ref_value)?; + + let cleanup = |c: &str| c.replace(char::is_whitespace, ""); + assert_eq!(cleanup(message), cleanup(&message2)); + + Ok(()) } } diff --git a/src/api/s3_xml.rs b/src/api/s3_xml.rs index 1df4ed60..0532f3d1 100644 --- a/src/api/s3_xml.rs +++ b/src/api/s3_xml.rs @@ -16,6 +16,12 @@ pub fn xmlns_tag<S: Serializer>(_v: &(), s: S) -> Result<S::Ok, S::Error> { #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct Value(#[serde(rename = "$value")] pub String); +impl From<&str> for Value { + fn from(s: &str) -> Value { + Value(s.to_string()) + } +} + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct IntValue(#[serde(rename = "$value")] pub i64); |