mirror of
https://github.com/deuxfleurs-org/garage.git
synced 2026-08-16 00:43:11 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dcfc32cf85 | |||
| 368eb35484 | |||
| 642186c530 | |||
| e01f74e763 |
+23
-17
@@ -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
@@ -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, ¶ms)?;
|
// 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, ¶ms)?;
|
||||||
|
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
@@ -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("<", "<")
|
s.replace("&", "&")
|
||||||
|
.replace("<", "<")
|
||||||
.replace(">", ">")
|
.replace(">", ">")
|
||||||
.replace("\"", """)
|
.replace("\"", """)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user