Skip to content

Commit 944425c

Browse files
committed
feat(presign): support bare query params in canonicalization
1 parent cc6aa0f commit 944425c

3 files changed

Lines changed: 38 additions & 12 deletions

File tree

clients/python/src/objectstore_client/presign.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def _canonical_presigned_request(path: str, query: str) -> str:
9090
- Method is always ``GET`` (HEAD maps to GET).
9191
- Path uses the encoded URI path as received by the service.
9292
- Percent-encoded octets are normalized to uppercase hex digits.
93-
- Query params use the encoded key/value pairs from the URI, excluding
93+
- Query params use the encoded keys and optional values from the URI, excluding
9494
``X-Os-Signature``, sorted by encoded key.
9595
"""
9696
canonical_path = _normalize_percent_encoding(path)
@@ -99,17 +99,20 @@ def _canonical_presigned_request(path: str, query: str) -> str:
9999
for pair in query.split("&"):
100100
if not pair:
101101
continue
102-
if "=" not in pair:
103-
continue
104-
k, _, v = pair.partition("=")
102+
if "=" in pair:
103+
k, _, v = pair.partition("=")
104+
value = _normalize_percent_encoding(v)
105+
else:
106+
k = pair
107+
value = None
105108
k = _normalize_percent_encoding(k)
106109
if k == PARAM_SIGNATURE:
107110
continue
108-
params.append((k, _normalize_percent_encoding(v)))
111+
params.append((k, value))
109112

110113
params.sort(key=lambda x: (x[0], x[1]))
111114

112-
query_str = "&".join(f"{k}={v}" for k, v in params)
115+
query_str = "&".join(k if v is None else f"{k}={v}" for k, v in params)
113116
return f"GET\n{canonical_path}\n{query_str}"
114117

115118

clients/python/tests/test_presign.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ def test_reordered_query_params(self) -> None:
7575
)
7676
assert c1 == c2
7777

78+
def test_bare_query_params(self) -> None:
79+
canonical = _canonical_presigned_request(
80+
"/path",
81+
"y&X-Os-KeyId=test&x=1&X-Os-Expires=1000&z",
82+
)
83+
assert canonical == ("GET\n/path\nX-Os-Expires=1000&X-Os-KeyId=test&x=1&y&z")
84+
7885

7986
class TestPresignUrl:
8087
def test_produces_valid_format(self) -> None:

objectstore-shared/src/presign.rs

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,30 +15,36 @@ pub const PARAM_SIGNATURE: &str = "X-Os-Signature";
1515
/// - Method is always `GET` (HEAD maps to GET).
1616
/// - Path uses the encoded URI path as received by the service.
1717
/// - Percent-encoded octets are normalized to uppercase hex digits.
18-
/// - Query params use the encoded key/value pairs from the URI, excluding
19-
/// `X-Os-Signature`, sorted by encoded key.
18+
/// - Query params use the encoded keys and optional values from the URI,
19+
/// excluding `X-Os-Signature`, sorted by encoded key.
2020
pub fn canonical_presigned_request(path: &str, query: Option<&str>) -> String {
2121
let canonical_path = normalize_percent_encoding(path);
2222

23-
let mut params: Vec<(String, String)> = query
23+
let mut params: Vec<(String, Option<String>)> = query
2424
.unwrap_or("")
2525
.split('&')
2626
.filter(|s| !s.is_empty())
2727
.filter_map(|pair| {
28-
let (k, v) = pair.split_once('=')?;
28+
let (k, v) = match pair.split_once('=') {
29+
Some((k, v)) => (k, Some(normalize_percent_encoding(v))),
30+
None => (pair, None),
31+
};
2932
let canonical_key = normalize_percent_encoding(k);
3033
if canonical_key == PARAM_SIGNATURE {
3134
return None;
3235
}
33-
Some((canonical_key, normalize_percent_encoding(v)))
36+
Some((canonical_key, v))
3437
})
3538
.collect();
3639

3740
params.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
3841

3942
let query_str = params
4043
.iter()
41-
.map(|(k, v)| format!("{k}={v}"))
44+
.map(|(k, v)| match v {
45+
Some(v) => format!("{k}={v}"),
46+
None => k.clone(),
47+
})
4248
.collect::<Vec<_>>()
4349
.join("&");
4450

@@ -114,4 +120,14 @@ mod tests {
114120
let c2 = canonical_presigned_request("/path", Some("X-Os-Expires=1000&X-Os-KeyId=test"));
115121
assert_eq!(c1, c2);
116122
}
123+
124+
#[test]
125+
fn test_canonical_form_bare_query_params() {
126+
let canonical =
127+
canonical_presigned_request("/path", Some("y&X-Os-KeyId=test&x=1&X-Os-Expires=1000&z"));
128+
assert_eq!(
129+
canonical,
130+
"GET\n/path\nX-Os-Expires=1000&X-Os-KeyId=test&x=1&y&z"
131+
);
132+
}
117133
}

0 commit comments

Comments
 (0)