Compare commits

...

4 Commits

Author SHA1 Message Date
Alex Auvolat dcfc32cf85 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
2021-04-28 01:05:40 +02:00
Alex 368eb35484 Merge pull request 'Correctly encode ampersand' (#61) from bug/ampersand into main
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/61
2021-04-27 23:15:01 +02:00
Alex Auvolat 642186c530 Fix #59 (& issue) 2021-04-27 23:10:43 +02:00
Quentin Dufour e01f74e763 Introduce test case that demonstrates #59 (the & problem) 2021-04-27 23:09:26 +02:00
8 changed files with 123 additions and 34 deletions
+23 -17
View File
@@ -3,6 +3,8 @@
set -ex set -ex
shopt -s expand_aliases shopt -s expand_aliases
export LC_ALL=C.UTF-8
export LANG=C.UTF-8
SCRIPT_FOLDER="`dirname \"$0\"`" SCRIPT_FOLDER="`dirname \"$0\"`"
REPO_FOLDER="${SCRIPT_FOLDER}/../" REPO_FOLDER="${SCRIPT_FOLDER}/../"
@@ -19,41 +21,45 @@ garage status
garage key list garage key list
garage bucket list garage bucket list
dd if=/dev/urandom of=/tmp/garage.1.rnd bs=1k count=2 # < INLINE_THRESHOLD = 3072 bytes dd if=/dev/urandom of=/tmp/garage.1.rnd bs=1k count=2 # No multipart, inline storage (< INLINE_THRESHOLD = 3072 bytes)
dd if=/dev/urandom of=/tmp/garage.2.rnd bs=1M count=5 dd if=/dev/urandom of=/tmp/garage.2.rnd bs=1M count=5 # No multipart but file will be chunked
dd if=/dev/urandom of=/tmp/garage.3.rnd bs=1M count=10 dd if=/dev/urandom of=/tmp/garage.3.rnd bs=1M count=10 # by default, AWS starts using multipart at 8MB
echo "s3 api testing..." echo "s3 api testing..."
awsgrg --version
s3cmd --version
python3 --version
for idx in $(seq 1 3); do for idx in $(seq 1 3); do
# AWS sends # AWS sends
awsgrg cp /tmp/garage.$idx.rnd s3://eprouvette/garage.$idx.aws awsgrg cp "/tmp/garage.$idx.rnd" "s3://eprouvette/&+-é\"/garage.$idx.aws"
awsgrg ls s3://eprouvette awsgrg ls s3://eprouvette
awsgrg cp s3://eprouvette/garage.$idx.aws /tmp/garage.$idx.dl awsgrg cp "s3://eprouvette/&+-é\"/garage.$idx.aws" "/tmp/garage.$idx.dl"
diff /tmp/garage.$idx.rnd /tmp/garage.$idx.dl diff /tmp/garage.$idx.rnd /tmp/garage.$idx.dl
rm /tmp/garage.$idx.dl rm /tmp/garage.$idx.dl
s3grg get s3://eprouvette/garage.$idx.aws /tmp/garage.$idx.dl s3grg get "s3://eprouvette/&+-é\"/garage.$idx.aws" "/tmp/garage.$idx.dl"
diff /tmp/garage.$idx.rnd /tmp/garage.$idx.dl diff /tmp/garage.$idx.rnd /tmp/garage.$idx.dl
rm /tmp/garage.$idx.dl rm /tmp/garage.$idx.dl
awsgrg rm s3://eprouvette/garage.$idx.aws awsgrg rm "s3://eprouvette/&+-é\"/garage.$idx.aws"
# S3CMD sends # S3CMD sends
s3grg put /tmp/garage.$idx.rnd s3://eprouvette/garage.$idx.s3cmd s3grg put "/tmp/garage.$idx.rnd" "s3://eprouvette/&+-é\"/garage.$idx.s3cmd"
s3grg ls s3://eprouvette s3grg ls s3://eprouvette
s3grg get s3://eprouvette/garage.$idx.s3cmd /tmp/garage.$idx.dl s3grg get "s3://eprouvette/&+-é\"/garage.$idx.s3cmd" "/tmp/garage.$idx.dl"
diff /tmp/garage.$idx.rnd /tmp/garage.$idx.dl diff /tmp/garage.$idx.rnd /tmp/garage.$idx.dl
rm /tmp/garage.$idx.dl rm /tmp/garage.$idx.dl
awsgrg cp s3://eprouvette/garage.$idx.s3cmd /tmp/garage.$idx.dl awsgrg cp "s3://eprouvette/&+-é\"/garage.$idx.s3cmd" "/tmp/garage.$idx.dl"
diff /tmp/garage.$idx.rnd /tmp/garage.$idx.dl diff /tmp/garage.$idx.rnd /tmp/garage.$idx.dl
rm /tmp/garage.$idx.dl rm /tmp/garage.$idx.dl
s3grg rm s3://eprouvette/garage.$idx.s3cmd s3grg rm "s3://eprouvette/&+-é\"/garage.$idx.s3cmd"
done done
rm /tmp/garage.{1,2,3}.rnd rm /tmp/garage.{1,2,3}.rnd
+18 -8
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()) {
+2 -1
View File
@@ -2,7 +2,8 @@
/// Escape &str for xml inclusion /// Escape &str for xml inclusion
pub fn xml_escape(s: &str) -> String { pub fn xml_escape(s: &str) -> String {
s.replace("<", "&lt;") s.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;") .replace(">", "&gt;")
.replace("\"", "&quot;") .replace("\"", "&quot;")
} }
+31
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
+1
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
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()))?)
}
+20 -3
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;
} }
+4 -5
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)