From 631c36b3ff76f9b09a5aed54dc343469a89f9989 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Sun, 2 May 2021 22:30:56 +0200 Subject: [PATCH] S3 API: support ListBuckets --- Cargo.lock | 12 ++++++ script/test-smoke.sh | 4 ++ src/api/Cargo.toml | 2 + src/api/api_server.rs | 6 ++- src/api/error.rs | 6 +++ src/api/s3_bucket.rs | 87 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 115 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5dc83df..1ea1371 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -397,7 +397,9 @@ dependencies = [ "log", "md-5", "percent-encoding", + "quick-xml", "roxmltree", + "serde", "sha2", "tokio", "url", @@ -1034,6 +1036,16 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quick-xml" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0452695941410a58c8ce4391707ba9bad26a247173bd9886a05a5e8a8babec75" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quote" version = "1.0.9" diff --git a/script/test-smoke.sh b/script/test-smoke.sh index e8d41ed..9fe06de 100755 --- a/script/test-smoke.sh +++ b/script/test-smoke.sh @@ -32,6 +32,7 @@ echo "🧪 S3 API testing..." if [ -z "$SKIP_AWS" ]; then echo "🛠️ Testing with awscli" source ${SCRIPT_FOLDER}/dev-env-aws.sh + aws s3 ls for idx in $(seq 1 3); do aws s3 cp "/tmp/garage.$idx.rnd" "s3://eprouvette/&+-é\"/garage.$idx.aws" aws s3 ls s3://eprouvette @@ -46,6 +47,7 @@ fi if [ -z "$SKIP_S3CMD" ]; then echo "🛠️ Testing with s3cmd" source ${SCRIPT_FOLDER}/dev-env-s3cmd.sh + s3cmd ls for idx in $(seq 1 3); do s3cmd put "/tmp/garage.$idx.rnd" "s3://eprouvette/&+-é\"/garage.$idx.s3cmd" s3cmd ls s3://eprouvette @@ -60,6 +62,7 @@ fi if [ -z "$SKIP_MC" ]; then echo "🛠️ Testing with mc (minio client)" source ${SCRIPT_FOLDER}/dev-env-mc.sh + mc ls garage/ for idx in $(seq 1 3); do mc cp "/tmp/garage.$idx.rnd" "garage/eprouvette/&+-é\"/garage.$idx.mc" mc ls garage/eprouvette @@ -74,6 +77,7 @@ fi if [ -z "$SKIP_RCLONE" ]; then echo "🛠️ Testing with rclone" source ${SCRIPT_FOLDER}/dev-env-rclone.sh + rclone lsd garage: for idx in $(seq 1 3); do cp /tmp/garage.$idx.rnd /tmp/garage.$idx.dl rclone copy "/tmp/garage.$idx.dl" "garage:eprouvette/&+-é\"/" diff --git a/src/api/Cargo.toml b/src/api/Cargo.toml index 0b824ca..b9fc4bf 100644 --- a/src/api/Cargo.toml +++ b/src/api/Cargo.toml @@ -38,4 +38,6 @@ http-range = "0.1" hyper = "0.14" percent-encoding = "2.1.0" roxmltree = "0.14" +serde = { version = "1.0", features = ["derive"] } +quick-xml = { version = "0.21", features = [ "serialize" ] } url = "2.1" diff --git a/src/api/api_server.rs b/src/api/api_server.rs index ab8bd73..8a51b85 100644 --- a/src/api/api_server.rs +++ b/src/api/api_server.rs @@ -81,10 +81,12 @@ async fn handler( async fn handler_inner(garage: Arc, req: Request) -> Result, Error> { let path = req.uri().path().to_string(); let path = percent_encoding::percent_decode_str(&path).decode_utf8()?; + let (api_key, content_sha256) = check_signature(&garage, &req).await?; + if path == "/" { + return handle_list_buckets(&api_key); + } let (bucket, key) = parse_bucket_key(&path)?; - - let (api_key, content_sha256) = check_signature(&garage, &req).await?; let allowed = match req.method() { &Method::HEAD | &Method::GET => api_key.allow_read(&bucket), _ => api_key.allow_write(&bucket), diff --git a/src/api/error.rs b/src/api/error.rs index a3cdfdb..bd340fa 100644 --- a/src/api/error.rs +++ b/src/api/error.rs @@ -72,6 +72,12 @@ impl From for Error { } } +impl From for Error { + fn from(err: quick_xml::de::DeError) -> Self { + Self::InvalidXML(format!("{}", err)) + } +} + impl Error { /// Get the HTTP status code that best represents the meaning of the error for the client pub fn http_status_code(&self) -> StatusCode { diff --git a/src/api/s3_bucket.rs b/src/api/s3_bucket.rs index cbefd00..d1a4425 100644 --- a/src/api/s3_bucket.rs +++ b/src/api/s3_bucket.rs @@ -2,11 +2,62 @@ use std::fmt::Write; use std::sync::Arc; use hyper::{Body, Response}; +use quick_xml::se::to_string; +use serde::Serialize; use garage_model::garage::Garage; +use garage_model::key_table::Key; +use garage_util::time::*; use crate::error::*; +#[derive(Debug, Serialize, PartialEq)] +struct CreationDate { + #[serde(rename = "$value")] + pub body: String, +} +#[derive(Debug, Serialize, PartialEq)] +struct Name { + #[serde(rename = "$value")] + pub body: String, +} +#[derive(Debug, Serialize, PartialEq)] +struct Bucket { + #[serde(rename = "CreationDate")] + pub creation_date: CreationDate, + #[serde(rename = "Name")] + pub name: Name, +} +#[derive(Debug, Serialize, PartialEq)] +struct DisplayName { + #[serde(rename = "$value")] + pub body: String, +} +#[derive(Debug, Serialize, PartialEq)] +struct ID { + #[serde(rename = "$value")] + pub body: String, +} +#[derive(Debug, Serialize, PartialEq)] +struct Owner { + #[serde(rename = "DisplayName")] + display_name: DisplayName, + #[serde(rename = "ID")] + id: ID, +} +#[derive(Debug, Serialize, PartialEq)] +struct BucketList { + #[serde(rename = "Bucket")] + pub entries: Vec, +} +#[derive(Debug, Serialize, PartialEq)] +struct ListAllMyBucketsResult { + #[serde(rename = "Buckets")] + buckets: BucketList, + #[serde(rename = "Owner")] + owner: Owner, +} + pub fn handle_get_bucket_location(garage: Arc) -> Result, Error> { let mut xml = String::new(); @@ -22,3 +73,39 @@ pub fn handle_get_bucket_location(garage: Arc) -> Result, .header("Content-Type", "application/xml") .body(Body::from(xml.into_bytes()))?) } + +pub fn handle_list_buckets(api_key: &Key) -> Result, Error> { + let list_buckets = ListAllMyBucketsResult { + owner: Owner { + display_name: DisplayName { + body: api_key.name.get().to_string(), + }, + id: ID { + body: api_key.key_id.to_string(), + }, + }, + buckets: BucketList { + entries: api_key + .authorized_buckets + .items() + .iter() + .map(|(name, ts, _)| Bucket { + creation_date: CreationDate { + body: msec_to_rfc3339(*ts), + }, + name: Name { + body: name.to_string(), + }, + }) + .collect(), + }, + }; + + let mut xml = r#""#.to_string(); + xml.push_str(&to_string(&list_buckets)?); + trace!("xml: {}", xml); + + Ok(Response::builder() + .header("Content-Type", "application/xml") + .body(Body::from(xml))?) +}