Skip to content

Commit 3cdb3fe

Browse files
authored
fix:improve S3 error response and parsing compatibility (s3s-project#462)
* fix:improve S3 error response and parsing compatibility * fix:cr * fix:add unit test * fix:fmt * fix:cr
1 parent 34a7b53 commit 3cdb3fe

File tree

4 files changed

+79
-15
lines changed

4 files changed

+79
-15
lines changed

crates/s3s/src/dto/copy_source.rs

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -54,31 +54,30 @@ impl CopySource {
5454
/// # Errors
5555
/// Returns an error if the header is invalid
5656
pub fn parse(header: &str) -> Result<Self, ParseCopySourceError> {
57-
let header = urlencoding::decode(header).map_err(|_| ParseCopySourceError::InvalidEncoding)?;
57+
let (path_part, version_id) = if let Some(idx) = header.find("?versionId=") {
58+
let (path, version_part) = header.split_at(idx);
59+
let version_id_raw = version_part.strip_prefix("?versionId=");
60+
let version_id = version_id_raw
61+
.map(urlencoding::decode)
62+
.transpose()
63+
.map_err(|_| ParseCopySourceError::InvalidEncoding)?;
64+
(path, version_id)
65+
} else {
66+
(header, None)
67+
};
68+
let header = urlencoding::decode(path_part).map_err(|_| ParseCopySourceError::InvalidEncoding)?;
5869
let header = header.strip_prefix('/').unwrap_or(&header);
5970

6071
// FIXME: support access point
6172
match header.split_once('/') {
6273
None => Err(ParseCopySourceError::PatternMismatch),
63-
Some((bucket, remaining)) => {
64-
let (key, version_id) = match remaining.split_once('?') {
65-
Some((key, remaining)) => {
66-
let version_id = remaining
67-
.split_once('=')
68-
.and_then(|(name, val)| (name == "versionId").then_some(val));
69-
(key, version_id)
70-
}
71-
None => (remaining, None),
72-
};
73-
74+
Some((bucket, key)) => {
7475
if !path::check_bucket_name(bucket) {
7576
return Err(ParseCopySourceError::InvalidBucketName);
7677
}
77-
7878
if !path::check_key(key) {
7979
return Err(ParseCopySourceError::InvalidKey);
8080
}
81-
8281
Ok(Self::Bucket {
8382
bucket: bucket.into(),
8483
key: key.into(),
@@ -119,6 +118,20 @@ impl http::TryFromHeaderValue for CopySource {
119118
mod tests {
120119
use super::*;
121120

121+
#[test]
122+
fn leading_slash_and_percent_decoding() {
123+
let header = "/awsexamplebucket/reports/file%3Fversion.txt?versionId=abc";
124+
let val = CopySource::parse(header).unwrap();
125+
match val {
126+
CopySource::Bucket { bucket, key, version_id } => {
127+
assert_eq!(&*bucket, "awsexamplebucket");
128+
assert_eq!(&*key, "reports/file?version.txt");
129+
assert_eq!(version_id.as_deref().unwrap(), "abc");
130+
}
131+
CopySource::AccessPoint { .. } => panic!(),
132+
}
133+
}
134+
122135
#[test]
123136
fn path_style() {
124137
{

crates/s3s/src/ops/signature.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ pub struct SignatureContext<'a> {
8484
pub trailing_headers: Option<TrailingHeaders>,
8585
}
8686

87+
#[derive(Debug)]
8788
pub struct CredentialsExt {
8889
pub access_key: String,
8990
pub secret_key: SecretKey,

crates/s3s/src/ops/tests.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,53 @@ fn extract_host_from_uri() {
128128
let host = extract_host(&req).unwrap();
129129
assert_eq!(host, None);
130130
}
131+
132+
#[tokio::test]
133+
async fn presigned_url_expires_0_should_be_expired() {
134+
use crate::S3ErrorCode;
135+
use crate::config::{S3ConfigProvider, StaticConfigProvider};
136+
use crate::http::{Body, OrderedHeaders, OrderedQs};
137+
use crate::ops::signature::SignatureContext;
138+
use hyper::{Method, Uri};
139+
use std::sync::Arc;
140+
141+
let qs = OrderedQs::parse(concat!(
142+
"X-Amz-Algorithm=AWS4-HMAC-SHA256",
143+
"&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request",
144+
"&X-Amz-Date=20130524T000000Z",
145+
"&X-Amz-Expires=0",
146+
"&X-Amz-SignedHeaders=host",
147+
"&X-Amz-Signature=aeeed9bbccd4d02ee5c0109b86d86835f995330da4c265957d157751f604d404"
148+
))
149+
.unwrap();
150+
151+
let config: Arc<dyn S3ConfigProvider> = Arc::new(StaticConfigProvider::default());
152+
153+
let method = Method::GET;
154+
let uri = Uri::from_static("https://s3.amazonaws.com/test.txt");
155+
let mut body = Body::empty();
156+
157+
let mut cx = SignatureContext {
158+
auth: None,
159+
config: &config,
160+
req_version: ::http::Version::HTTP_11,
161+
req_method: &method,
162+
req_uri: &uri,
163+
req_body: &mut body,
164+
qs: Some(&qs),
165+
hs: OrderedHeaders::from_slice_unchecked(&[]),
166+
decoded_uri_path: "/test.txt".to_owned(),
167+
vh_bucket: None,
168+
content_length: None,
169+
mime: None,
170+
decoded_content_length: None,
171+
transformed_body: None,
172+
multipart: None,
173+
trailing_headers: None,
174+
};
175+
176+
let result = cx.v4_check_presigned_url().await;
177+
assert!(result.is_err());
178+
let err = result.unwrap_err();
179+
assert_eq!(err.code(), &S3ErrorCode::AccessDenied);
180+
}

crates/s3s/src/sig_v4/presigned_url_v4.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,6 @@ impl<'a> PresignedUrlV4<'a> {
103103
}
104104

105105
fn parse_expires(s: &str) -> Option<time::Duration> {
106-
let x = s.parse::<u32>().ok().filter(|&x| x > 0)?;
106+
let x = s.parse::<u32>().ok()?;
107107
Some(time::Duration::new(i64::from(x), 0))
108108
}

0 commit comments

Comments
 (0)