diff options
author | Alex <alex@adnab.me> | 2024-03-04 14:39:06 +0000 |
---|---|---|
committer | Alex <alex@adnab.me> | 2024-03-04 14:39:06 +0000 |
commit | a08ac4a3fd402c47013c3631505b0ec424cbb795 (patch) | |
tree | a1c7ae17e5100624349cf59fc6c86a6f6f234821 /src | |
parent | e28599497702ab541f652d9d5ae097049b3b804d (diff) | |
parent | 2eb114f42225faec3f70d498fdd51a6e8952a396 (diff) | |
download | garage-main-0.8.x.tar.gz garage-main-0.8.x.zip |
Merge pull request 'Garage v0.8.7' (#758) from fix-presigned-0.8 into main-0.8.xmain-0.8.x
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/758
Diffstat (limited to 'src')
-rw-r--r-- | src/api/Cargo.toml | 2 | ||||
-rw-r--r-- | src/api/signature/payload.rs | 57 | ||||
-rw-r--r-- | src/block/Cargo.toml | 2 | ||||
-rw-r--r-- | src/db/Cargo.toml | 2 | ||||
-rw-r--r-- | src/garage/Cargo.toml | 2 | ||||
-rw-r--r-- | src/garage/tests/common/custom_requester.rs | 4 | ||||
-rw-r--r-- | src/garage/tests/s3/mod.rs | 1 | ||||
-rw-r--r-- | src/garage/tests/s3/presigned.rs | 71 | ||||
-rw-r--r-- | src/model/Cargo.toml | 2 | ||||
-rw-r--r-- | src/rpc/Cargo.toml | 2 | ||||
-rw-r--r-- | src/table/Cargo.toml | 2 | ||||
-rw-r--r-- | src/util/Cargo.toml | 2 | ||||
-rw-r--r-- | src/web/Cargo.toml | 2 |
13 files changed, 120 insertions, 31 deletions
diff --git a/src/api/Cargo.toml b/src/api/Cargo.toml index cd24de3b..379274b3 100644 --- a/src/api/Cargo.toml +++ b/src/api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "garage_api" -version = "0.8.6" +version = "0.8.7" authors = ["Alex Auvolat <alex@adnab.me>"] edition = "2018" license = "AGPL-3.0" diff --git a/src/api/signature/payload.rs b/src/api/signature/payload.rs index 03884186..1164f74a 100644 --- a/src/api/signature/payload.rs +++ b/src/api/signature/payload.rs @@ -31,7 +31,13 @@ pub const AWS4_HMAC_SHA256: &str = "AWS4-HMAC-SHA256"; pub const UNSIGNED_PAYLOAD: &str = "UNSIGNED-PAYLOAD"; pub const STREAMING_AWS4_HMAC_SHA256_PAYLOAD: &str = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"; -pub type QueryMap = HashMap<String, String>; +pub type QueryMap = HeaderMap<QueryValue>; +pub struct QueryValue { + /// Original key with potential uppercase characters, + /// for use in signature calculation + key: String, + value: String, +} pub async fn check_payload_signature( garage: &Garage, @@ -40,7 +46,7 @@ pub async fn check_payload_signature( ) -> Result<(Option<Key>, Option<Hash>), Error> { let query = parse_query_map(request.uri())?; - if query.contains_key(X_AMZ_ALGORITHM.as_str()) { + if query.contains_key(&X_AMZ_ALGORITHM) { // We check for presigned-URL-style authentification first, because // the browser or someting else could inject an Authorization header // that is totally unrelated to AWS signatures. @@ -121,8 +127,8 @@ async fn check_presigned_signature( request: &mut Request<Body>, mut query: QueryMap, ) -> Result<(Option<Key>, Option<Hash>), Error> { - let algorithm = query.get(X_AMZ_ALGORITHM.as_str()).unwrap(); - let authorization = Authorization::parse_presigned(algorithm, &query)?; + let algorithm = query.get(&X_AMZ_ALGORITHM).unwrap(); + let authorization = Authorization::parse_presigned(&algorithm.value, &query)?; // Verify that all necessary request headers are included in signed_headers // For AWSv4 pre-signed URLs, the following must be incldued: @@ -135,7 +141,7 @@ async fn check_presigned_signature( // but the signature cannot be computed from a string that contains itself. // AWS specifies that all query params except X-Amz-Signature are included // in the canonical request. - query.remove(X_AMZ_SIGNATURE.as_str()); + query.remove(&X_AMZ_SIGNATURE); let canonical_request = canonical_request( service, request.method(), @@ -161,10 +167,8 @@ async fn check_presigned_signature( // then an InvalidRequest error is raised. let headers_mut = request.headers_mut(); for (name, value) in query.iter() { - let name = - HeaderName::from_bytes(name.as_bytes()).ok_or_bad_request("Invalid header name")?; - if let Some(existing) = headers_mut.get(&name) { - if signed_headers.contains(&name) && existing.as_bytes() != value.as_bytes() { + if let Some(existing) = headers_mut.get(name) { + if signed_headers.contains(&name) && existing.as_bytes() != value.value.as_bytes() { return Err(Error::bad_request(format!( "Conflicting values for `{}` in query parameters and request headers", name @@ -180,7 +184,7 @@ async fn check_presigned_signature( // that are not signed, however there is not much reason that this would happen) headers_mut.insert( name, - HeaderValue::from_bytes(value.as_bytes()) + HeaderValue::from_bytes(value.value.as_bytes()) .ok_or_bad_request("invalid query parameter value")?, ); } @@ -192,11 +196,19 @@ async fn check_presigned_signature( } pub fn parse_query_map(uri: &http::uri::Uri) -> Result<QueryMap, Error> { - let mut query = QueryMap::new(); + let mut query = QueryMap::with_capacity(0); if let Some(query_str) = uri.query() { let query_pairs = url::form_urlencoded::parse(query_str.as_bytes()); for (key, val) in query_pairs { - if query.insert(key.to_string(), val.into_owned()).is_some() { + let name = + HeaderName::from_bytes(key.as_bytes()).ok_or_bad_request("Invalid header name")?; + + let value = QueryValue { + key: key.to_string(), + value: val.into_owned(), + }; + + if query.insert(name, value).is_some() { return Err(Error::bad_request(format!( "duplicate query parameter: `{}`", key @@ -305,7 +317,7 @@ pub fn canonical_request( // Canonical query string from passed HeaderMap let canonical_query_string = { let mut items = Vec::with_capacity(query.len()); - for (key, value) in query.iter() { + for (_, QueryValue { key, value }) in query.iter() { items.push(uri_encode(&key, true) + "=" + &uri_encode(&value, true)); } items.sort(); @@ -463,18 +475,19 @@ impl Authorization { } let cred = query - .get(X_AMZ_CREDENTIAL.as_str()) + .get(&X_AMZ_CREDENTIAL) .ok_or_bad_request("X-Amz-Credential not found in query parameters")?; let signed_headers = query - .get(X_AMZ_SIGNEDHEADERS.as_str()) + .get(&X_AMZ_SIGNEDHEADERS) .ok_or_bad_request("X-Amz-SignedHeaders not found in query parameters")?; let signature = query - .get(X_AMZ_SIGNATURE.as_str()) + .get(&X_AMZ_SIGNATURE) .ok_or_bad_request("X-Amz-Signature not found in query parameters")?; let duration = query - .get(X_AMZ_EXPIRES.as_str()) + .get(&X_AMZ_EXPIRES) .ok_or_bad_request("X-Amz-Expires not found in query parameters")? + .value .parse() .map_err(|_| Error::bad_request("X-Amz-Expires is not a number".to_string()))?; @@ -485,20 +498,20 @@ impl Authorization { } let date = query - .get(X_AMZ_DATE.as_str()) + .get(&X_AMZ_DATE) .ok_or_bad_request("Missing X-Amz-Date field")?; - let date = parse_date(date)?; + let date = parse_date(&date.value)?; if Utc::now() - date > Duration::seconds(duration) { return Err(Error::bad_request("Date is too old".to_string())); } - let (key_id, scope) = parse_credential(cred)?; + let (key_id, scope) = parse_credential(&cred.value)?; Ok(Authorization { key_id, scope, - signed_headers: signed_headers.to_string(), - signature: signature.to_string(), + signed_headers: signed_headers.value.clone(), + signature: signature.value.clone(), content_sha256: UNSIGNED_PAYLOAD.to_string(), date, }) diff --git a/src/block/Cargo.toml b/src/block/Cargo.toml index cd9685e4..efc4cba3 100644 --- a/src/block/Cargo.toml +++ b/src/block/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "garage_block" -version = "0.8.6" +version = "0.8.7" authors = ["Alex Auvolat <alex@adnab.me>"] edition = "2018" license = "AGPL-3.0" diff --git a/src/db/Cargo.toml b/src/db/Cargo.toml index e39825d4..8beb7b22 100644 --- a/src/db/Cargo.toml +++ b/src/db/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "garage_db" -version = "0.8.6" +version = "0.8.7" authors = ["Alex Auvolat <alex@adnab.me>"] edition = "2018" license = "AGPL-3.0" diff --git a/src/garage/Cargo.toml b/src/garage/Cargo.toml index cf214686..fb5d458b 100644 --- a/src/garage/Cargo.toml +++ b/src/garage/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "garage" -version = "0.8.6" +version = "0.8.7" authors = ["Alex Auvolat <alex@adnab.me>"] edition = "2018" license = "AGPL-3.0" diff --git a/src/garage/tests/common/custom_requester.rs b/src/garage/tests/common/custom_requester.rs index c931f6ef..f527557f 100644 --- a/src/garage/tests/common/custom_requester.rs +++ b/src/garage/tests/common/custom_requester.rs @@ -59,6 +59,10 @@ impl CustomRequester { vhost_style: false, } } + + pub fn client(&self) -> &Client<HttpConnector, Body> { + &self.client + } } pub struct RequestBuilder<'a> { diff --git a/src/garage/tests/s3/mod.rs b/src/garage/tests/s3/mod.rs index 623eb665..4ebc4914 100644 --- a/src/garage/tests/s3/mod.rs +++ b/src/garage/tests/s3/mod.rs @@ -1,6 +1,7 @@ mod list; mod multipart; mod objects; +mod presigned; mod simple; mod streaming_signature; mod website; diff --git a/src/garage/tests/s3/presigned.rs b/src/garage/tests/s3/presigned.rs new file mode 100644 index 00000000..cd720b3b --- /dev/null +++ b/src/garage/tests/s3/presigned.rs @@ -0,0 +1,71 @@ +use std::time::{Duration, SystemTime}; + +use crate::common; +use aws_sdk_s3::presigning::PresigningConfig; +use bytes::Bytes; +use hyper::{Body, Request}; + +const STD_KEY: &str = "hello world"; +const BODY: &[u8; 62] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + +#[tokio::test] +async fn test_presigned_url() { + let ctx = common::context(); + let bucket = ctx.create_bucket("presigned"); + + let etag = "\"46cf18a9b447991b450cad3facf5937e\""; + let body = Bytes::from(BODY.to_vec()); + + let psc = PresigningConfig::builder() + .start_time(SystemTime::now() - Duration::from_secs(60)) + .expires_in(Duration::from_secs(3600)) + .build() + .unwrap(); + + { + // PutObject + let req = ctx + .client + .put_object() + .bucket(&bucket) + .key(STD_KEY) + .presigned(psc.clone()) + .await + .unwrap(); + + let client = ctx.custom_request.client(); + let req = Request::builder() + .method("PUT") + .uri(req.uri()) + .body(body.clone().into()) + .unwrap(); + let res = client.request(req).await.unwrap(); + assert_eq!(res.status(), 200); + assert_eq!(res.headers().get("etag").unwrap(), etag); + } + + { + // GetObject + let req = ctx + .client + .get_object() + .bucket(&bucket) + .key(STD_KEY) + .presigned(psc) + .await + .unwrap(); + + let client = ctx.custom_request.client(); + let req = Request::builder() + .method("GET") + .uri(req.uri()) + .body(Body::empty()) + .unwrap(); + let res = client.request(req).await.unwrap(); + assert_eq!(res.status(), 200); + assert_eq!(res.headers().get("etag").unwrap(), etag); + + let body2 = hyper::body::to_bytes(res.into_body()).await.unwrap(); + assert_eq!(body, body2); + } +} diff --git a/src/model/Cargo.toml b/src/model/Cargo.toml index 0a342b01..0c09c4c2 100644 --- a/src/model/Cargo.toml +++ b/src/model/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "garage_model" -version = "0.8.6" +version = "0.8.7" authors = ["Alex Auvolat <alex@adnab.me>"] edition = "2018" license = "AGPL-3.0" diff --git a/src/rpc/Cargo.toml b/src/rpc/Cargo.toml index 87cead5e..087ade71 100644 --- a/src/rpc/Cargo.toml +++ b/src/rpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "garage_rpc" -version = "0.8.6" +version = "0.8.7" authors = ["Alex Auvolat <alex@adnab.me>"] edition = "2018" license = "AGPL-3.0" diff --git a/src/table/Cargo.toml b/src/table/Cargo.toml index 12e11e0d..6bfca25d 100644 --- a/src/table/Cargo.toml +++ b/src/table/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "garage_table" -version = "0.8.6" +version = "0.8.7" authors = ["Alex Auvolat <alex@adnab.me>"] edition = "2018" license = "AGPL-3.0" diff --git a/src/util/Cargo.toml b/src/util/Cargo.toml index f859e0fa..27f15170 100644 --- a/src/util/Cargo.toml +++ b/src/util/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "garage_util" -version = "0.8.6" +version = "0.8.7" authors = ["Alex Auvolat <alex@adnab.me>"] edition = "2018" license = "AGPL-3.0" diff --git a/src/web/Cargo.toml b/src/web/Cargo.toml index 5e146b6f..64f8048d 100644 --- a/src/web/Cargo.toml +++ b/src/web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "garage_web" -version = "0.8.6" +version = "0.8.7" authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"] edition = "2018" license = "AGPL-3.0" |