Add verification of part numbers in CompleteMultipartUpload (WIP #30)
This commit is contained in:
parent
1de96248e0
commit
10b983b8e7
4 changed files with 77 additions and 26 deletions
|
@ -157,7 +157,7 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
|
|||
// CompleteMultipartUpload call
|
||||
let upload_id = params.get("uploadid").unwrap();
|
||||
Ok(
|
||||
handle_complete_multipart_upload(garage, req, &bucket, &key, upload_id)
|
||||
handle_complete_multipart_upload(garage, req, &bucket, &key, upload_id, content_sha256)
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
|
@ -205,7 +205,7 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
|
|||
&Method::POST => {
|
||||
if params.contains_key(&"delete".to_string()) {
|
||||
// DeleteObjects
|
||||
Ok(handle_delete_objects(garage, bucket, req).await?)
|
||||
Ok(handle_delete_objects(garage, bucket, req, content_sha256).await?)
|
||||
} else {
|
||||
debug!(
|
||||
"Body: {}",
|
||||
|
|
|
@ -10,6 +10,7 @@ use garage_model::object_table::*;
|
|||
|
||||
use crate::encoding::*;
|
||||
use crate::error::*;
|
||||
use crate::signature::verify_signed_content;
|
||||
|
||||
async fn handle_delete_internal(
|
||||
garage: &Garage,
|
||||
|
@ -73,8 +74,11 @@ pub async fn handle_delete_objects(
|
|||
garage: Arc<Garage>,
|
||||
bucket: &str,
|
||||
req: Request<Body>,
|
||||
content_sha256: Option<Hash>,
|
||||
) -> Result<Response<Body>, Error> {
|
||||
let body = hyper::body::to_bytes(req.into_body()).await?;
|
||||
verify_signed_content(content_sha256, &body[..])?;
|
||||
|
||||
let cmd_xml = roxmltree::Document::parse(&std::str::from_utf8(&body)?)?;
|
||||
let cmd = parse_delete_objects_xml(&cmd_xml).ok_or_bad_request("Invalid delete XML query")?;
|
||||
|
||||
|
@ -131,33 +135,27 @@ struct DeleteObject {
|
|||
key: String,
|
||||
}
|
||||
|
||||
fn parse_delete_objects_xml(xml: &roxmltree::Document) -> Result<DeleteRequest, String> {
|
||||
fn parse_delete_objects_xml(xml: &roxmltree::Document) -> Option<DeleteRequest> {
|
||||
let mut ret = DeleteRequest { objects: vec![] };
|
||||
|
||||
let root = xml.root();
|
||||
let delete = root.first_child().ok_or(format!("Delete tag not found"))?;
|
||||
let delete = root.first_child()?;
|
||||
|
||||
if !delete.has_tag_name("Delete") {
|
||||
return Err(format!("Invalid root tag: {:?}", root));
|
||||
return None;
|
||||
}
|
||||
|
||||
for item in delete.children() {
|
||||
if item.has_tag_name("Object") {
|
||||
if let Some(key) = item.children().find(|e| e.has_tag_name("Key")) {
|
||||
if let Some(key_str) = key.text() {
|
||||
ret.objects.push(DeleteObject {
|
||||
key: key_str.to_string(),
|
||||
});
|
||||
} else {
|
||||
return Err(format!("No text for key: {:?}", key));
|
||||
}
|
||||
} else {
|
||||
return Err(format!("No delete key for item: {:?}", item));
|
||||
}
|
||||
let key = item.children().find(|e| e.has_tag_name("Key"))?;
|
||||
let key_str = key.text()?;
|
||||
ret.objects.push(DeleteObject {
|
||||
key: key_str.to_string(),
|
||||
});
|
||||
} else {
|
||||
return Err(format!("Invalid delete item: {:?}", item));
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
Some(ret)
|
||||
}
|
||||
|
|
|
@ -11,14 +11,15 @@ use garage_table::*;
|
|||
use garage_util::data::*;
|
||||
use garage_util::error::Error as GarageError;
|
||||
|
||||
use crate::error::*;
|
||||
use garage_model::block::INLINE_THRESHOLD;
|
||||
use garage_model::block_ref_table::*;
|
||||
use garage_model::garage::Garage;
|
||||
use garage_model::object_table::*;
|
||||
use garage_model::version_table::*;
|
||||
|
||||
use crate::error::*;
|
||||
use crate::encoding::*;
|
||||
use crate::signature::verify_signed_content;
|
||||
|
||||
pub async fn handle_put(
|
||||
garage: Arc<Garage>,
|
||||
|
@ -416,11 +417,19 @@ pub async fn handle_put_part(
|
|||
|
||||
pub async fn handle_complete_multipart_upload(
|
||||
garage: Arc<Garage>,
|
||||
_req: Request<Body>,
|
||||
req: Request<Body>,
|
||||
bucket: &str,
|
||||
key: &str,
|
||||
upload_id: &str,
|
||||
content_sha256: Option<Hash>,
|
||||
) -> Result<Response<Body>, Error> {
|
||||
let body = hyper::body::to_bytes(req.into_body()).await?;
|
||||
verify_signed_content(content_sha256, &body[..])?;
|
||||
|
||||
let body_xml = roxmltree::Document::parse(&std::str::from_utf8(&body)?)?;
|
||||
let body_list_of_parts = parse_complete_multpart_upload_body(&body_xml).ok_or_bad_request("Invalid CompleteMultipartUpload XML")?;
|
||||
debug!("CompleteMultipartUpload list of parts: {:?}", body_list_of_parts);
|
||||
|
||||
let version_uuid = decode_upload_id(upload_id)?;
|
||||
|
||||
let bucket = bucket.to_string();
|
||||
|
@ -450,6 +459,16 @@ pub async fn handle_complete_multipart_upload(
|
|||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
// Check that the list of parts they gave us corresponds to the parts we have here
|
||||
// TODO: check MD5 sum of all uploaded parts? but that would mean we have to store them somewhere...
|
||||
let mut parts = version.blocks().iter().map(|x| x.part_number)
|
||||
.collect::<Vec<_>>();
|
||||
parts.dedup();
|
||||
let same_parts = body_list_of_parts.iter().map(|x| &x.part_number).eq(parts.iter());
|
||||
if !same_parts {
|
||||
return Err(Error::BadRequest(format!("We don't have the same parts")));
|
||||
}
|
||||
|
||||
// ETag calculation: we produce ETags that have the same form as
|
||||
// those of S3 multipart uploads, but we don't use their actual
|
||||
// calculation for the first part (we use random bytes). This
|
||||
|
@ -465,11 +484,6 @@ pub async fn handle_complete_multipart_upload(
|
|||
num_parts
|
||||
);
|
||||
|
||||
// TODO: check that all the parts that they pretend they gave us are indeed there
|
||||
// TODO: when we read the XML from _req, remember to check the sha256 sum of the payload
|
||||
// against the signed x-amz-content-sha256
|
||||
// TODO: check MD5 sum of all uploaded parts? but that would mean we have to store them somewhere...
|
||||
|
||||
let total_size = version
|
||||
.blocks()
|
||||
.iter()
|
||||
|
@ -583,3 +597,34 @@ fn decode_upload_id(id: &str) -> Result<UUID, Error> {
|
|||
uuid.copy_from_slice(&id_bin[..]);
|
||||
Ok(UUID::from(uuid))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CompleteMultipartUploadPart {
|
||||
etag: String,
|
||||
part_number: u64,
|
||||
}
|
||||
|
||||
fn parse_complete_multpart_upload_body(xml: &roxmltree::Document) -> Option<Vec<CompleteMultipartUploadPart>> {
|
||||
let mut parts = vec![];
|
||||
|
||||
let root = xml.root();
|
||||
let cmu = root.first_child()?;
|
||||
if !cmu.has_tag_name("CompleteMultipartUpload") {
|
||||
return None;
|
||||
}
|
||||
|
||||
for item in cmu.children() {
|
||||
if item.has_tag_name("Part") {
|
||||
let etag = item.children().find(|e| e.has_tag_name("ETag"))?.text()?;
|
||||
let part_number = item.children().find(|e| e.has_tag_name("PartNumber"))?.text()?;
|
||||
parts.push(CompleteMultipartUploadPart{
|
||||
etag: etag.trim_matches('"').to_string(),
|
||||
part_number: part_number.parse().ok()?,
|
||||
});
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Some(parts)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ use hyper::{Body, Method, Request};
|
|||
use sha2::{Digest, Sha256};
|
||||
|
||||
use garage_table::*;
|
||||
use garage_util::data::Hash;
|
||||
use garage_util::data::{hash, Hash};
|
||||
|
||||
use garage_model::garage::Garage;
|
||||
use garage_model::key_table::*;
|
||||
|
@ -293,3 +293,11 @@ fn canonical_query_string(uri: &hyper::Uri) -> String {
|
|||
"".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_signed_content(content_sha256: Option<Hash>, body: &[u8]) -> Result<(), Error> {
|
||||
let expected_sha256 = content_sha256.ok_or_bad_request("Request content hash not signed, aborting.")?;
|
||||
if expected_sha256 != hash(body) {
|
||||
return Err(Error::BadRequest(format!("Request content hash does not match signed hash")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue