aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Auvolat <alex@adnab.me>2021-04-28 01:05:40 +0200
committerAlex Auvolat <alex@adnab.me>2021-04-28 01:05:40 +0200
commitdcfc32cf85bc6276fdff2492898c1cbb527e9b9d (patch)
tree01ec3f7476733dab8c74ecb1c72b5061f6e5c8e1
parent368eb354846790e9fc616d9a26ddc414748d847f (diff)
downloadgarage-dcfc32cf85bc6276fdff2492898c1cbb527e9b9d.tar.gz
garage-dcfc32cf85bc6276fdff2492898c1cbb527e9b9d.zip
Many S3 compatibility improvements:v0.2.1.5
- return XML errors - implement AuthorizationHeaderMalformed error to redirect clients to correct location (used by minio client) - implement GetBucketLocation - fix DeleteObjects XML parsing and response
-rw-r--r--src/api/api_server.rs26
-rw-r--r--src/api/error.rs31
-rw-r--r--src/api/lib.rs1
-rw-r--r--src/api/s3_bucket.rs24
-rw-r--r--src/api/s3_delete.rs23
-rw-r--r--src/api/signature.rs9
6 files changed, 98 insertions, 16 deletions
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<Body>,
addr: SocketAddr,
) -> Result<Response<Body>, 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<Garage>, req: Request<Body>) -> Result<Respon
))
}
&Method::GET => {
- // ListObjects or ListObjectsV2 query
- let q = parse_list_objects_query(bucket, &params)?;
- 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, &params)?;
+ 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#"<?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
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<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()))?)
+}
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#"<?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() {
match handle_delete_internal(&garage, bucket, &obj.key).await {
Ok((deleted_version, delete_marker_version)) => {
+ if cmd.quiet {
+ continue;
+ }
writeln!(&mut retxml, "\t<Deleted>").unwrap();
writeln!(&mut retxml, "\t\t<Key>{}</Key>", xml_escape(&obj.key)).unwrap();
writeln!(
@@ -121,7 +128,7 @@ pub async fn handle_delete_objects(
}
}
- writeln!(&mut retxml, "</DeleteObjectsOutput>").unwrap();
+ writeln!(&mut retxml, "</DeleteResult>").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<DeleteObject>,
}
@@ -137,7 +145,10 @@ struct DeleteObject {
}
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 delete = root.first_child()?;
@@ -153,6 +164,12 @@ fn parse_delete_objects_xml(xml: &roxmltree::Document) -> Option<DeleteRequest>
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)