From dcfc32cf85bc6276fdff2492898c1cbb527e9b9d Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Wed, 28 Apr 2021 01:05:40 +0200 Subject: [PATCH] Many S3 compatibility improvements: - return XML errors - implement AuthorizationHeaderMalformed error to redirect clients to correct location (used by minio client) - implement GetBucketLocation - fix DeleteObjects XML parsing and response --- src/api/api_server.rs | 26 ++++++++++++++++++-------- src/api/error.rs | 31 +++++++++++++++++++++++++++++++ src/api/lib.rs | 1 + src/api/s3_bucket.rs | 24 ++++++++++++++++++++++++ src/api/s3_delete.rs | 23 ++++++++++++++++++++--- src/api/signature.rs | 9 ++++----- 6 files changed, 98 insertions(+), 16 deletions(-) create mode 100644 src/api/s3_bucket.rs diff --git a/src/api/api_server.rs b/src/api/api_server.rs index dcc9f478..ab8bd736 100644 --- a/src/api/api_server.rs +++ b/src/api/api_server.rs @@ -14,6 +14,7 @@ use garage_model::garage::Garage; use crate::error::*; use crate::signature::check_signature; +use crate::s3_bucket::*; use crate::s3_copy::*; use crate::s3_delete::*; use crate::s3_get::*; @@ -52,17 +53,21 @@ async fn handler( req: Request, addr: SocketAddr, ) -> Result, GarageError> { - info!("{} {} {}", addr, req.method(), req.uri()); + let uri = req.uri().clone(); + info!("{} {} {}", addr, req.method(), uri); debug!("{:?}", req); - match handler_inner(garage, req).await { + match handler_inner(garage.clone(), req).await { Ok(x) => { debug!("{} {:?}", x.status(), x.headers()); Ok(x) } Err(e) => { - let body: Body = Body::from(format!("{}\n", e)); - let mut http_error = Response::new(body); - *http_error.status_mut() = e.http_status_code(); + let body: Body = Body::from(e.aws_xml(&garage.config.s3_api.s3_region, uri.path())); + let http_error = Response::builder() + .status(e.http_status_code()) + .header("Content-Type", "application/xml") + .body(body)?; + if e.http_status_code().is_server_error() { warn!("Response: error {}, {}", e.http_status_code(), e); } else { @@ -211,9 +216,14 @@ async fn handler_inner(garage: Arc, req: Request) -> Result { - // ListObjects or ListObjectsV2 query - let q = parse_list_objects_query(bucket, ¶ms)?; - Ok(handle_list(garage, &q).await?) + if params.contains_key("location") { + // GetBucketLocation call + Ok(handle_get_bucket_location(garage)?) + } else { + // ListObjects or ListObjectsV2 query + let q = parse_list_objects_query(bucket, ¶ms)?; + Ok(handle_list(garage, &q).await?) + } } &Method::POST => { if params.contains_key(&"delete".to_string()) { diff --git a/src/api/error.rs b/src/api/error.rs index ad0174ad..a3cdfdbd 100644 --- a/src/api/error.rs +++ b/src/api/error.rs @@ -1,8 +1,12 @@ +use std::fmt::Write; + use err_derive::Error; use hyper::StatusCode; use garage_util::error::Error as GarageError; +use crate::encoding::*; + /// Errors of this crate #[derive(Debug, Error)] pub enum Error { @@ -24,6 +28,10 @@ pub enum Error { #[error(display = "Forbidden: {}", _0)] Forbidden(String), + /// Authorization Header Malformed + #[error(display = "Authorization header malformed, expected scope: {}", _0)] + AuthorizationHeaderMalformed(String), + /// The object requested don't exists #[error(display = "Not found")] NotFound, @@ -77,6 +85,29 @@ impl Error { _ => StatusCode::BAD_REQUEST, } } + + pub fn aws_code(&self) -> &'static str { + match self { + Error::NotFound => "NoSuchKey", + Error::Forbidden(_) => "AccessDenied", + Error::AuthorizationHeaderMalformed(_) => "AuthorizationHeaderMalformed", + Error::InternalError(GarageError::RPC(_)) => "ServiceUnavailable", + Error::InternalError(_) | Error::Hyper(_) | Error::HTTP(_) => "InternalError", + _ => "InvalidRequest", + } + } + + pub fn aws_xml(&self, garage_region: &str, path: &str) -> String { + let mut xml = String::new(); + writeln!(&mut xml, r#""#).unwrap(); + writeln!(&mut xml, "").unwrap(); + writeln!(&mut xml, "\t{}", self.aws_code()).unwrap(); + writeln!(&mut xml, "\t{}", self).unwrap(); + writeln!(&mut xml, "\t{}", xml_escape(path)).unwrap(); + writeln!(&mut xml, "\t{}", garage_region).unwrap(); + writeln!(&mut xml, "").unwrap(); + xml + } } /// Trait to map error to the Bad Request error code diff --git a/src/api/lib.rs b/src/api/lib.rs index be7e37c8..6c6447da 100644 --- a/src/api/lib.rs +++ b/src/api/lib.rs @@ -12,6 +12,7 @@ pub use api_server::run_api_server; mod signature; +mod s3_bucket; mod s3_copy; mod s3_delete; pub mod s3_get; diff --git a/src/api/s3_bucket.rs b/src/api/s3_bucket.rs new file mode 100644 index 00000000..cbefd005 --- /dev/null +++ b/src/api/s3_bucket.rs @@ -0,0 +1,24 @@ +use std::fmt::Write; +use std::sync::Arc; + +use hyper::{Body, Response}; + +use garage_model::garage::Garage; + +use crate::error::*; + +pub fn handle_get_bucket_location(garage: Arc) -> Result, Error> { + let mut xml = String::new(); + + writeln!(&mut xml, r#""#).unwrap(); + writeln!( + &mut xml, + r#"{}"#, + garage.config.s3_api.s3_region + ) + .unwrap(); + + Ok(Response::builder() + .header("Content-Type", "application/xml") + .body(Body::from(xml.into_bytes()))?) +} diff --git a/src/api/s3_delete.rs b/src/api/s3_delete.rs index 6abbfc48..05387403 100644 --- a/src/api/s3_delete.rs +++ b/src/api/s3_delete.rs @@ -85,11 +85,18 @@ pub async fn handle_delete_objects( let mut retxml = String::new(); writeln!(&mut retxml, r#""#).unwrap(); - writeln!(&mut retxml, "").unwrap(); + writeln!( + &mut retxml, + r#""# + ) + .unwrap(); for obj in cmd.objects.iter() { match handle_delete_internal(&garage, bucket, &obj.key).await { Ok((deleted_version, delete_marker_version)) => { + if cmd.quiet { + continue; + } writeln!(&mut retxml, "\t").unwrap(); writeln!(&mut retxml, "\t\t{}", xml_escape(&obj.key)).unwrap(); writeln!( @@ -121,7 +128,7 @@ pub async fn handle_delete_objects( } } - writeln!(&mut retxml, "").unwrap(); + writeln!(&mut retxml, "").unwrap(); Ok(Response::builder() .header("Content-Type", "application/xml") @@ -129,6 +136,7 @@ pub async fn handle_delete_objects( } struct DeleteRequest { + quiet: bool, objects: Vec, } @@ -137,7 +145,10 @@ struct DeleteObject { } fn parse_delete_objects_xml(xml: &roxmltree::Document) -> Option { - let mut ret = DeleteRequest { objects: vec![] }; + let mut ret = DeleteRequest { + quiet: false, + objects: vec![], + }; let root = xml.root(); let delete = root.first_child()?; @@ -153,6 +164,12 @@ fn parse_delete_objects_xml(xml: &roxmltree::Document) -> Option ret.objects.push(DeleteObject { key: key_str.to_string(), }); + } else if item.has_tag_name("Quiet") { + if item.text()? == "true" { + ret.quiet = true; + } else { + ret.quiet = false; + } } else { return None; } diff --git a/src/api/signature.rs b/src/api/signature.rs index 6dc69afa..7fcab0f9 100644 --- a/src/api/signature.rs +++ b/src/api/signature.rs @@ -58,10 +58,7 @@ pub async fn check_signature( garage.config.s3_api.s3_region ); if authorization.scope != scope { - return Err(Error::BadRequest(format!( - "Invalid scope in authorization field, expected: {}", - scope - ))); + return Err(Error::AuthorizationHeaderMalformed(scope.to_string())); } let key = garage @@ -101,7 +98,9 @@ pub async fn check_signature( return Err(Error::Forbidden(format!("Invalid signature"))); } - let content_sha256 = if authorization.content_sha256 == "UNSIGNED-PAYLOAD" { + let content_sha256 = if authorization.content_sha256 == "UNSIGNED-PAYLOAD" + || authorization.content_sha256 == "STREAMING-AWS4-HMAC-SHA256-PAYLOAD" + { None } else { let bytes = hex::decode(authorization.content_sha256)