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
This commit is contained in:
Alex 2021-04-28 01:05:40 +02:00
parent 368eb35484
commit dcfc32cf85
No known key found for this signature in database
GPG key ID: EDABF9711E244EB1
6 changed files with 98 additions and 16 deletions

View file

@ -14,6 +14,7 @@ use garage_model::garage::Garage;
use crate::error::*; use crate::error::*;
use crate::signature::check_signature; use crate::signature::check_signature;
use crate::s3_bucket::*;
use crate::s3_copy::*; use crate::s3_copy::*;
use crate::s3_delete::*; use crate::s3_delete::*;
use crate::s3_get::*; use crate::s3_get::*;
@ -52,17 +53,21 @@ async fn handler(
req: Request<Body>, req: Request<Body>,
addr: SocketAddr, addr: SocketAddr,
) -> Result<Response<Body>, GarageError> { ) -> Result<Response<Body>, GarageError> {
info!("{} {} {}", addr, req.method(), req.uri()); let uri = req.uri().clone();
info!("{} {} {}", addr, req.method(), uri);
debug!("{:?}", req); debug!("{:?}", req);
match handler_inner(garage, req).await { match handler_inner(garage.clone(), req).await {
Ok(x) => { Ok(x) => {
debug!("{} {:?}", x.status(), x.headers()); debug!("{} {:?}", x.status(), x.headers());
Ok(x) Ok(x)
} }
Err(e) => { Err(e) => {
let body: Body = Body::from(format!("{}\n", e)); let body: Body = Body::from(e.aws_xml(&garage.config.s3_api.s3_region, uri.path()));
let mut http_error = Response::new(body); let http_error = Response::builder()
*http_error.status_mut() = e.http_status_code(); .status(e.http_status_code())
.header("Content-Type", "application/xml")
.body(body)?;
if e.http_status_code().is_server_error() { if e.http_status_code().is_server_error() {
warn!("Response: error {}, {}", e.http_status_code(), e); warn!("Response: error {}, {}", e.http_status_code(), e);
} else { } else {
@ -211,9 +216,14 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
)) ))
} }
&Method::GET => { &Method::GET => {
// ListObjects or ListObjectsV2 query if params.contains_key("location") {
let q = parse_list_objects_query(bucket, &params)?; // GetBucketLocation call
Ok(handle_list(garage, &q).await?) Ok(handle_get_bucket_location(garage)?)
} else {
// ListObjects or ListObjectsV2 query
let q = parse_list_objects_query(bucket, &params)?;
Ok(handle_list(garage, &q).await?)
}
} }
&Method::POST => { &Method::POST => {
if params.contains_key(&"delete".to_string()) { if params.contains_key(&"delete".to_string()) {

View file

@ -1,8 +1,12 @@
use std::fmt::Write;
use err_derive::Error; use err_derive::Error;
use hyper::StatusCode; use hyper::StatusCode;
use garage_util::error::Error as GarageError; use garage_util::error::Error as GarageError;
use crate::encoding::*;
/// Errors of this crate /// Errors of this crate
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
@ -24,6 +28,10 @@ pub enum Error {
#[error(display = "Forbidden: {}", _0)] #[error(display = "Forbidden: {}", _0)]
Forbidden(String), Forbidden(String),
/// Authorization Header Malformed
#[error(display = "Authorization header malformed, expected scope: {}", _0)]
AuthorizationHeaderMalformed(String),
/// The object requested don't exists /// The object requested don't exists
#[error(display = "Not found")] #[error(display = "Not found")]
NotFound, NotFound,
@ -77,6 +85,29 @@ impl Error {
_ => StatusCode::BAD_REQUEST, _ => 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#"<?xml version="1.0" encoding="UTF-8"?>"#).unwrap();
writeln!(&mut xml, "<Error>").unwrap();
writeln!(&mut xml, "\t<Code>{}</Code>", self.aws_code()).unwrap();
writeln!(&mut xml, "\t<Message>{}</Message>", self).unwrap();
writeln!(&mut xml, "\t<Resource>{}</Resource>", xml_escape(path)).unwrap();
writeln!(&mut xml, "\t<Region>{}</Region>", garage_region).unwrap();
writeln!(&mut xml, "</Error>").unwrap();
xml
}
} }
/// Trait to map error to the Bad Request error code /// Trait to map error to the Bad Request error code

View file

@ -12,6 +12,7 @@ pub use api_server::run_api_server;
mod signature; mod signature;
mod s3_bucket;
mod s3_copy; mod s3_copy;
mod s3_delete; mod s3_delete;
pub mod s3_get; pub mod s3_get;

24
src/api/s3_bucket.rs Normal file
View file

@ -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<Garage>) -> Result<Response<Body>, Error> {
let mut xml = String::new();
writeln!(&mut xml, r#"<?xml version="1.0" encoding="UTF-8"?>"#).unwrap();
writeln!(
&mut xml,
r#"<LocationConstraint xmlns="http://s3.amazonaws.com/doc/2006-03-01/">{}</LocationConstraint>"#,
garage.config.s3_api.s3_region
)
.unwrap();
Ok(Response::builder()
.header("Content-Type", "application/xml")
.body(Body::from(xml.into_bytes()))?)
}

View file

@ -85,11 +85,18 @@ pub async fn handle_delete_objects(
let mut retxml = String::new(); let mut retxml = String::new();
writeln!(&mut retxml, r#"<?xml version="1.0" encoding="UTF-8"?>"#).unwrap(); writeln!(&mut retxml, r#"<?xml version="1.0" encoding="UTF-8"?>"#).unwrap();
writeln!(&mut retxml, "<DeleteObjectsOutput>").unwrap(); writeln!(
&mut retxml,
r#"<DeleteResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">"#
)
.unwrap();
for obj in cmd.objects.iter() { for obj in cmd.objects.iter() {
match handle_delete_internal(&garage, bucket, &obj.key).await { match handle_delete_internal(&garage, bucket, &obj.key).await {
Ok((deleted_version, delete_marker_version)) => { Ok((deleted_version, delete_marker_version)) => {
if cmd.quiet {
continue;
}
writeln!(&mut retxml, "\t<Deleted>").unwrap(); writeln!(&mut retxml, "\t<Deleted>").unwrap();
writeln!(&mut retxml, "\t\t<Key>{}</Key>", xml_escape(&obj.key)).unwrap(); writeln!(&mut retxml, "\t\t<Key>{}</Key>", xml_escape(&obj.key)).unwrap();
writeln!( writeln!(
@ -121,7 +128,7 @@ pub async fn handle_delete_objects(
} }
} }
writeln!(&mut retxml, "</DeleteObjectsOutput>").unwrap(); writeln!(&mut retxml, "</DeleteResult>").unwrap();
Ok(Response::builder() Ok(Response::builder()
.header("Content-Type", "application/xml") .header("Content-Type", "application/xml")
@ -129,6 +136,7 @@ pub async fn handle_delete_objects(
} }
struct DeleteRequest { struct DeleteRequest {
quiet: bool,
objects: Vec<DeleteObject>, objects: Vec<DeleteObject>,
} }
@ -137,7 +145,10 @@ struct DeleteObject {
} }
fn parse_delete_objects_xml(xml: &roxmltree::Document) -> Option<DeleteRequest> { fn parse_delete_objects_xml(xml: &roxmltree::Document) -> Option<DeleteRequest> {
let mut ret = DeleteRequest { objects: vec![] }; let mut ret = DeleteRequest {
quiet: false,
objects: vec![],
};
let root = xml.root(); let root = xml.root();
let delete = root.first_child()?; let delete = root.first_child()?;
@ -153,6 +164,12 @@ fn parse_delete_objects_xml(xml: &roxmltree::Document) -> Option<DeleteRequest>
ret.objects.push(DeleteObject { ret.objects.push(DeleteObject {
key: key_str.to_string(), key: key_str.to_string(),
}); });
} else if item.has_tag_name("Quiet") {
if item.text()? == "true" {
ret.quiet = true;
} else {
ret.quiet = false;
}
} else { } else {
return None; return None;
} }

View file

@ -58,10 +58,7 @@ pub async fn check_signature(
garage.config.s3_api.s3_region garage.config.s3_api.s3_region
); );
if authorization.scope != scope { if authorization.scope != scope {
return Err(Error::BadRequest(format!( return Err(Error::AuthorizationHeaderMalformed(scope.to_string()));
"Invalid scope in authorization field, expected: {}",
scope
)));
} }
let key = garage let key = garage
@ -101,7 +98,9 @@ pub async fn check_signature(
return Err(Error::Forbidden(format!("Invalid 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 None
} else { } else {
let bytes = hex::decode(authorization.content_sha256) let bytes = hex::decode(authorization.content_sha256)