object-storage-proxy 0.2.16__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {object_storage_proxy-0.2.16 → object_storage_proxy-0.3.1}/Cargo.lock +2 -1
- {object_storage_proxy-0.2.16 → object_storage_proxy-0.3.1}/Cargo.toml +2 -1
- {object_storage_proxy-0.2.16 → object_storage_proxy-0.3.1}/PKG-INFO +1 -1
- {object_storage_proxy-0.2.16 → object_storage_proxy-0.3.1}/src/credentials/signer.rs +235 -74
- {object_storage_proxy-0.2.16 → object_storage_proxy-0.3.1}/src/lib.rs +133 -25
- object_storage_proxy-0.3.1/src/parsers/credentials.rs +234 -0
- {object_storage_proxy-0.2.16 → object_storage_proxy-0.3.1}/src/parsers/keystore.rs +1 -1
- object_storage_proxy-0.3.1/test_integration.sh +57 -0
- object_storage_proxy-0.2.16/src/parsers/credentials.rs +0 -82
- {object_storage_proxy-0.2.16 → object_storage_proxy-0.3.1}/.cargo/config.toml +0 -0
- {object_storage_proxy-0.2.16 → object_storage_proxy-0.3.1}/.github/workflows/ci.yml +0 -0
- {object_storage_proxy-0.2.16 → object_storage_proxy-0.3.1}/.gitignore +0 -0
- {object_storage_proxy-0.2.16 → object_storage_proxy-0.3.1}/LICENSE +0 -0
- {object_storage_proxy-0.2.16 → object_storage_proxy-0.3.1}/README.md +0 -0
- {object_storage_proxy-0.2.16 → object_storage_proxy-0.3.1}/img/logo.svg +0 -0
- {object_storage_proxy-0.2.16 → object_storage_proxy-0.3.1}/img/request_lifecycle.svg +0 -0
- {object_storage_proxy-0.2.16 → object_storage_proxy-0.3.1}/img/request_stages.svg +0 -0
- {object_storage_proxy-0.2.16 → object_storage_proxy-0.3.1}/pyproject.toml +0 -0
- {object_storage_proxy-0.2.16 → object_storage_proxy-0.3.1}/requirements.txt +0 -0
- {object_storage_proxy-0.2.16 → object_storage_proxy-0.3.1}/src/credentials/hmac_keystore.rs +0 -0
- {object_storage_proxy-0.2.16 → object_storage_proxy-0.3.1}/src/credentials/mod.rs +0 -0
- {object_storage_proxy-0.2.16 → object_storage_proxy-0.3.1}/src/credentials/models.rs +0 -0
- {object_storage_proxy-0.2.16 → object_storage_proxy-0.3.1}/src/credentials/secrets_proxy.rs +0 -0
- {object_storage_proxy-0.2.16 → object_storage_proxy-0.3.1}/src/object_storage_proxy.pyi +0 -0
- {object_storage_proxy-0.2.16 → object_storage_proxy-0.3.1}/src/parsers/cos_map.rs +0 -0
- {object_storage_proxy-0.2.16 → object_storage_proxy-0.3.1}/src/parsers/mod.rs +0 -0
- {object_storage_proxy-0.2.16 → object_storage_proxy-0.3.1}/src/parsers/path.rs +0 -0
- {object_storage_proxy-0.2.16 → object_storage_proxy-0.3.1}/src/utils/mod.rs +0 -0
- {object_storage_proxy-0.2.16 → object_storage_proxy-0.3.1}/src/utils/validator.rs +0 -0
- {object_storage_proxy-0.2.16 → object_storage_proxy-0.3.1}/test_server.py +0 -0
- {object_storage_proxy-0.2.16 → object_storage_proxy-0.3.1}/uv.lock +0 -0
|
@@ -1701,7 +1701,7 @@ dependencies = [
|
|
|
1701
1701
|
|
|
1702
1702
|
[[package]]
|
|
1703
1703
|
name = "object-storage-proxy"
|
|
1704
|
-
version = "0.
|
|
1704
|
+
version = "0.3.1"
|
|
1705
1705
|
dependencies = [
|
|
1706
1706
|
"async-trait",
|
|
1707
1707
|
"chrono",
|
|
@@ -1713,6 +1713,7 @@ dependencies = [
|
|
|
1713
1713
|
"http",
|
|
1714
1714
|
"log",
|
|
1715
1715
|
"nom 8.0.0",
|
|
1716
|
+
"percent-encoding",
|
|
1716
1717
|
"pingora",
|
|
1717
1718
|
"prometheus 0.14.0",
|
|
1718
1719
|
"pyo3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "object-storage-proxy"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.1"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
|
|
6
6
|
[dependencies]
|
|
@@ -33,6 +33,7 @@ sha256 = "1.6.0"
|
|
|
33
33
|
ring = "0.17.14"
|
|
34
34
|
hex = "0.4.3"
|
|
35
35
|
regex = "1.11.1"
|
|
36
|
+
percent-encoding = "2.3.1"
|
|
36
37
|
|
|
37
38
|
# [build-dependencies]
|
|
38
39
|
# openssl-sys = { version = "0.9", features = ["vendored"] }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: object-storage-proxy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
6
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
use chrono::{DateTime, NaiveDateTime, Utc};
|
|
2
2
|
use http::header::HeaderMap;
|
|
3
3
|
use pingora::{http::RequestHeader, proxy::Session};
|
|
4
|
-
use rustls::crypto::hash::Hash;
|
|
5
4
|
use sha256::digest;
|
|
6
|
-
use
|
|
7
|
-
use
|
|
8
|
-
use std::{collections::{HashMap, HashSet}, fmt, sync::Arc};
|
|
5
|
+
use tracing::{debug, error};
|
|
6
|
+
use std::{collections::HashMap, fmt};
|
|
9
7
|
use url::Url;
|
|
10
8
|
|
|
11
9
|
use crate::parsers::{cos_map::CosMapItem, credentials::{parse_credential_scope, parse_token_from_header}};
|
|
@@ -62,6 +60,25 @@ where
|
|
|
62
60
|
body: &'a [u8],
|
|
63
61
|
}
|
|
64
62
|
|
|
63
|
+
/// Create a new AwsSign instance
|
|
64
|
+
///
|
|
65
|
+
/// # Arguments
|
|
66
|
+
///
|
|
67
|
+
/// * `method` - HTTP method (GET, POST, etc.)
|
|
68
|
+
/// * `url` - URL to sign
|
|
69
|
+
/// * `datetime` - Date and time of the request
|
|
70
|
+
/// * `headers` - HTTP headers
|
|
71
|
+
/// * `region` - AWS region
|
|
72
|
+
/// * `access_key` - AWS access key
|
|
73
|
+
/// * `secret_key` - AWS secret key
|
|
74
|
+
/// * `service` - AWS service code
|
|
75
|
+
/// * `body` - Request body
|
|
76
|
+
/// * `signed_headers` - Optional list of signed headers, used to check inbound request signature
|
|
77
|
+
///
|
|
78
|
+
/// # Returns
|
|
79
|
+
///
|
|
80
|
+
/// A new instance of `AwsSign`
|
|
81
|
+
///
|
|
65
82
|
impl<'a> AwsSign<'a, HashMap<String, String>> {
|
|
66
83
|
pub fn new<B: AsRef<[u8]> + ?Sized>(
|
|
67
84
|
method: &'a str,
|
|
@@ -75,6 +92,8 @@ impl<'a> AwsSign<'a, HashMap<String, String>> {
|
|
|
75
92
|
body: &'a B,
|
|
76
93
|
signed_headers: Option<&'a Vec<String>>,
|
|
77
94
|
) -> Self {
|
|
95
|
+
|
|
96
|
+
|
|
78
97
|
let allowed: Vec<&str> = if let Some(sh) = signed_headers {
|
|
79
98
|
sh.iter().map(String::as_str).collect()
|
|
80
99
|
} else {
|
|
@@ -86,15 +105,8 @@ impl<'a> AwsSign<'a, HashMap<String, String>> {
|
|
|
86
105
|
"x-amz-security-token",
|
|
87
106
|
]
|
|
88
107
|
};
|
|
89
|
-
// let allowed = [
|
|
90
|
-
// "host",
|
|
91
|
-
// "x-amz-date",
|
|
92
|
-
// "range",
|
|
93
|
-
// "x-amz-content-sha256",
|
|
94
|
-
// "x-amz-security-token",
|
|
95
|
-
// ];
|
|
96
108
|
|
|
97
|
-
|
|
109
|
+
debug!("{:#?}", &url);
|
|
98
110
|
let url: Url = url.parse().unwrap();
|
|
99
111
|
let headers: HashMap<String, String> = headers
|
|
100
112
|
.iter()
|
|
@@ -124,6 +136,7 @@ impl<'a> AwsSign<'a, HashMap<String, String>> {
|
|
|
124
136
|
}
|
|
125
137
|
}
|
|
126
138
|
|
|
139
|
+
|
|
127
140
|
/// custom debug implementation to redact secret_key
|
|
128
141
|
impl<'a, T> fmt::Debug for AwsSign<'a, T>
|
|
129
142
|
where
|
|
@@ -140,6 +153,7 @@ where
|
|
|
140
153
|
.field("service", &self.service)
|
|
141
154
|
.field("body", &self.body)
|
|
142
155
|
.field("headers", &self.headers)
|
|
156
|
+
.field("payload_override", &self.payload_override)
|
|
143
157
|
.finish()
|
|
144
158
|
}
|
|
145
159
|
}
|
|
@@ -148,6 +162,10 @@ impl<'a, T> AwsSign<'a, T>
|
|
|
148
162
|
where
|
|
149
163
|
&'a T: std::iter::IntoIterator<Item = (&'a String, &'a String)>, T: std::fmt::Debug
|
|
150
164
|
{
|
|
165
|
+
/// for streaming uploads, we need to override the payload hash
|
|
166
|
+
/// with the actual payload hash
|
|
167
|
+
/// this is used for the `UNSIGNED-PAYLOAD` case
|
|
168
|
+
/// and for the `payload_override` case
|
|
151
169
|
pub fn set_payload_override(&mut self, h: String) {
|
|
152
170
|
self.payload_override = Some(h);
|
|
153
171
|
}
|
|
@@ -201,14 +219,16 @@ where
|
|
|
201
219
|
let signature = hex::encode(tag.as_ref());
|
|
202
220
|
let signed_headers = self.signed_header_string();
|
|
203
221
|
|
|
204
|
-
format!(
|
|
222
|
+
let sign_string = format!(
|
|
205
223
|
"AWS4-HMAC-SHA256 Credential={access_key}/{scope},\
|
|
206
224
|
SignedHeaders={signed_headers},Signature={signature}",
|
|
207
225
|
access_key = self.access_key,
|
|
208
226
|
scope = scope_string(self.datetime, self.region, self.service),
|
|
209
227
|
signed_headers = signed_headers,
|
|
210
228
|
signature = signature
|
|
211
|
-
)
|
|
229
|
+
);
|
|
230
|
+
debug!("sign_string: {}", sign_string);
|
|
231
|
+
sign_string
|
|
212
232
|
}
|
|
213
233
|
}
|
|
214
234
|
|
|
@@ -290,6 +310,14 @@ pub fn signing_key(
|
|
|
290
310
|
Ok(signing_tag.as_ref().to_vec())
|
|
291
311
|
}
|
|
292
312
|
|
|
313
|
+
|
|
314
|
+
/// Sign the request with the AWS V4 signature
|
|
315
|
+
/// # Arguments
|
|
316
|
+
/// * `request` - The request to sign
|
|
317
|
+
/// * `cos_map` - The COS map item containing the credentials
|
|
318
|
+
/// # Returns
|
|
319
|
+
/// * `Ok(())` if the request was signed successfully
|
|
320
|
+
/// * `Err` if there was an error signing the request
|
|
293
321
|
pub(crate) async fn sign_request(
|
|
294
322
|
request: &mut RequestHeader,
|
|
295
323
|
cos_map: &CosMapItem,
|
|
@@ -360,22 +388,53 @@ pub(crate) async fn sign_request(
|
|
|
360
388
|
}
|
|
361
389
|
|
|
362
390
|
|
|
363
|
-
|
|
391
|
+
/// Core signature validation: compares provided vs computed
|
|
392
|
+
async fn signature_is_valid_core(
|
|
393
|
+
method: &str,
|
|
394
|
+
provided_signature: &str,
|
|
395
|
+
region: &str,
|
|
396
|
+
service: &str,
|
|
397
|
+
datetime: DateTime<Utc>,
|
|
398
|
+
full_url: &str,
|
|
399
|
+
headers: &HeaderMap,
|
|
400
|
+
payload_override: Option<String>,
|
|
401
|
+
access_key: &str,
|
|
402
|
+
secret_key: &str,
|
|
403
|
+
signed_headers: &Vec<String>,
|
|
404
|
+
body_bytes: &[u8],
|
|
405
|
+
) -> Result<bool, Box<dyn std::error::Error>> {
|
|
406
|
+
// Build AwsSign for authorization header style
|
|
407
|
+
debug!("{:#?}", &headers);
|
|
408
|
+
let mut signer = AwsSign::new(
|
|
409
|
+
method,
|
|
410
|
+
full_url,
|
|
411
|
+
&datetime,
|
|
412
|
+
headers,
|
|
413
|
+
region,
|
|
414
|
+
access_key,
|
|
415
|
+
secret_key,
|
|
416
|
+
service,
|
|
417
|
+
body_bytes,
|
|
418
|
+
Some(&signed_headers),
|
|
419
|
+
);
|
|
420
|
+
if let Some(ov) = payload_override {
|
|
421
|
+
signer.set_payload_override(ov);
|
|
422
|
+
}
|
|
423
|
+
let signature = signer.sign();
|
|
424
|
+
let computed = signature.split("Signature=").nth(1).unwrap_or_default();
|
|
425
|
+
debug!("Provided signature: {}", provided_signature);
|
|
426
|
+
debug!("Computed signature: {}", computed);
|
|
427
|
+
Ok(computed == provided_signature)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/// Validate standard S3 Authorization header
|
|
431
|
+
pub async fn signature_is_valid_for_request(
|
|
364
432
|
auth_header: &str,
|
|
365
433
|
session: &Session,
|
|
366
434
|
secret_key: &str,
|
|
367
435
|
) -> Result<bool, Box<dyn std::error::Error>> {
|
|
368
436
|
|
|
369
|
-
|
|
370
|
-
.split("SignedHeaders=")
|
|
371
|
-
.nth(1)
|
|
372
|
-
.and_then(|s| s.split(',').next())
|
|
373
|
-
.unwrap_or("");
|
|
374
|
-
let signed_headers: Vec<String> =
|
|
375
|
-
signed_headers_str.split(';').map(|s| s.to_lowercase()).collect();
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
// extract access key from Authorization header
|
|
437
|
+
|
|
379
438
|
let (_, local_access_key) = parse_token_from_header(auth_header)
|
|
380
439
|
.map_err(|_| pingora::Error::new_str("Failed to parse token"))?;
|
|
381
440
|
let local_access_key = local_access_key.to_string();
|
|
@@ -383,49 +442,26 @@ pub async fn signature_is_valid(
|
|
|
383
442
|
error!("Missing access key");
|
|
384
443
|
return Ok(false);
|
|
385
444
|
}
|
|
386
|
-
|
|
387
|
-
// extract provided signature
|
|
388
445
|
let provided_signature = auth_header
|
|
389
446
|
.split("Signature=")
|
|
390
447
|
.nth(1)
|
|
391
|
-
.
|
|
392
|
-
.to_string();
|
|
393
|
-
|
|
394
|
-
// parse x-amz-date header
|
|
395
|
-
let dt_header = session
|
|
396
|
-
.req_header()
|
|
397
|
-
.headers
|
|
398
|
-
.get("x-amz-date")
|
|
399
|
-
.ok_or_else(|| pingora::Error::new_str("Missing x-amz-date header"))?
|
|
400
|
-
.to_str()?;
|
|
401
|
-
|
|
402
|
-
// use NaiveDateTime then assign Utc timezone (format: %Y%m%dT%H%M%SZ)
|
|
403
|
-
let naive = NaiveDateTime::parse_from_str(dt_header, LONG_DATETIME)
|
|
404
|
-
.map_err(|_| {
|
|
405
|
-
pingora::Error::new_str("invalid date")
|
|
406
|
-
})?;
|
|
407
|
-
let datetime = naive.and_utc();
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
info!("parsing the region and service");
|
|
448
|
+
.ok_or("Missing Signature")?;
|
|
411
449
|
|
|
412
450
|
let (_, (region, service)) = parse_credential_scope(auth_header)
|
|
413
451
|
.map_err(|_| pingora::Error::new_str("Invalid Credential scope"))?;
|
|
414
452
|
|
|
415
|
-
|
|
416
|
-
|
|
453
|
+
let method = session.req_header().method.to_string();
|
|
454
|
+
// Parse date header
|
|
455
|
+
let dt_header = session.req_header().headers.get("x-amz-date").unwrap().to_str()?;
|
|
456
|
+
let datetime = NaiveDateTime::parse_from_str(dt_header, LONG_DATETIME)?.and_utc();
|
|
457
|
+
|
|
417
458
|
|
|
418
|
-
// determine payload hash header
|
|
419
459
|
let content_sha256 = session
|
|
420
460
|
.req_header()
|
|
421
461
|
.headers
|
|
422
462
|
.get("x-amz-content-sha256")
|
|
423
463
|
.and_then(|h| h.to_str().ok())
|
|
424
464
|
.ok_or_else(|| pingora::Error::new_str("Missing x-amz-content-sha256 header"))?;
|
|
425
|
-
// let body_bytes: &[u8] = match content_sha256 {
|
|
426
|
-
// "UNSIGNED-PAYLOAD" => b"UNSIGNED-PAYLOAD",
|
|
427
|
-
// _ => &[],
|
|
428
|
-
// };
|
|
429
465
|
|
|
430
466
|
let (body_bytes, payload_override) = if content_sha256 == "UNSIGNED-PAYLOAD" {
|
|
431
467
|
(b"UNSIGNED-PAYLOAD" as &[u8], None)
|
|
@@ -435,6 +471,7 @@ pub async fn signature_is_valid(
|
|
|
435
471
|
(&[] as &[u8], Some(content_sha256.to_owned()))
|
|
436
472
|
};
|
|
437
473
|
|
|
474
|
+
// Build full URL
|
|
438
475
|
let original_uri = session.req_header().uri.to_string();
|
|
439
476
|
let full_url = if original_uri.starts_with('/') {
|
|
440
477
|
let host = session
|
|
@@ -448,36 +485,159 @@ pub async fn signature_is_valid(
|
|
|
448
485
|
original_uri
|
|
449
486
|
};
|
|
450
487
|
|
|
451
|
-
//
|
|
452
|
-
let
|
|
453
|
-
|
|
488
|
+
// Signed headers list
|
|
489
|
+
let signed_headers_str = auth_header
|
|
490
|
+
.split("SignedHeaders=")
|
|
491
|
+
.nth(1)
|
|
492
|
+
.unwrap()
|
|
493
|
+
.split(',')
|
|
494
|
+
.next()
|
|
495
|
+
.unwrap();
|
|
496
|
+
|
|
497
|
+
let signed_headers: Vec<String> = signed_headers_str.split(';').map(str::to_string).collect();
|
|
498
|
+
|
|
499
|
+
signature_is_valid_core(
|
|
454
500
|
&method,
|
|
501
|
+
provided_signature,
|
|
502
|
+
region,
|
|
503
|
+
service,
|
|
504
|
+
datetime,
|
|
455
505
|
&full_url,
|
|
456
|
-
&datetime,
|
|
457
506
|
&session.req_header().headers,
|
|
458
|
-
|
|
507
|
+
payload_override,
|
|
459
508
|
&local_access_key,
|
|
460
|
-
|
|
461
|
-
|
|
509
|
+
secret_key,
|
|
510
|
+
&signed_headers,
|
|
462
511
|
body_bytes,
|
|
463
|
-
|
|
464
|
-
|
|
512
|
+
)
|
|
513
|
+
.await
|
|
514
|
+
}
|
|
465
515
|
|
|
466
|
-
|
|
467
|
-
|
|
516
|
+
|
|
517
|
+
/// Validate presigned URL signature
|
|
518
|
+
pub async fn signature_is_valid_for_presigned(
|
|
519
|
+
session: &Session,
|
|
520
|
+
secret_key: &str,
|
|
521
|
+
) -> Result<bool, Box<dyn std::error::Error>> {
|
|
522
|
+
// Extract query params from the URL
|
|
523
|
+
|
|
524
|
+
let uri = session.req_header().uri.to_string();
|
|
525
|
+
let full_uri = if uri.starts_with('/') {
|
|
526
|
+
// build an absolute URL: scheme://host + path?query
|
|
527
|
+
let host = session
|
|
528
|
+
.req_header()
|
|
529
|
+
.headers
|
|
530
|
+
.get("host")
|
|
531
|
+
.ok_or("Missing host header")?
|
|
532
|
+
.to_str()?;
|
|
533
|
+
format!("https://{}{}", host, uri)
|
|
534
|
+
} else {
|
|
535
|
+
uri
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
let mut url = Url::parse(&full_uri)?;
|
|
540
|
+
debug!("full_url: {}", url);
|
|
541
|
+
let mut provided_signature = None;
|
|
542
|
+
let mut qp: Vec<(String,String)> = vec![];
|
|
543
|
+
for (k, v) in url.query_pairs() {
|
|
544
|
+
if k == "X-Amz-Signature" {
|
|
545
|
+
provided_signature = Some(v.into_owned());
|
|
546
|
+
} else {
|
|
547
|
+
qp.push((k.into_owned(), v.into_owned()));
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
let provided_signature = provided_signature.ok_or("Missing X-Amz-Signature")?;
|
|
551
|
+
|
|
552
|
+
// rebuild query string without the signature
|
|
553
|
+
qp.sort();
|
|
554
|
+
let new_query = qp.iter()
|
|
555
|
+
.map(|(k,v)| format!("{k}={v}"))
|
|
556
|
+
.collect::<Vec<_>>()
|
|
557
|
+
.join("&");
|
|
558
|
+
url.set_query(Some(&new_query));
|
|
559
|
+
|
|
560
|
+
// params map (also without the signature)
|
|
561
|
+
let params: HashMap<_, _> = qp.into_iter().collect();
|
|
562
|
+
debug!("params: {:?}", params);
|
|
563
|
+
debug!("url: {:?}", url);
|
|
564
|
+
|
|
565
|
+
debug!("provided signature: {}", provided_signature);
|
|
566
|
+
let credential = params
|
|
567
|
+
.get("X-Amz-Credential")
|
|
568
|
+
.ok_or("Missing X-Amz-Credential")?;
|
|
569
|
+
|
|
570
|
+
debug!("credential: {}", credential);
|
|
571
|
+
|
|
572
|
+
// Parse credential: <access_key>/<date>/<region>/<service>/aws4_request
|
|
573
|
+
let mut parts = credential.split('/');
|
|
574
|
+
let access_key = parts.next().ok_or("Malformed Credential")?;
|
|
575
|
+
let _credential_date = parts.next().ok_or("Malformed Credential")?;
|
|
576
|
+
let region = parts.next().ok_or("Malformed Credential")?;
|
|
577
|
+
let service = parts.next().ok_or("Malformed Credential")?;
|
|
578
|
+
|
|
579
|
+
debug!("access_key: {}", access_key);
|
|
580
|
+
debug!("region: {}", region);
|
|
581
|
+
debug!("service: {}", service);
|
|
582
|
+
|
|
583
|
+
// Parse date from query
|
|
584
|
+
let date_str = params
|
|
585
|
+
.get("X-Amz-Date")
|
|
586
|
+
.ok_or("Missing X-Amz-Date")?;
|
|
587
|
+
let datetime = NaiveDateTime::parse_from_str(date_str, LONG_DATETIME)?.and_utc();
|
|
588
|
+
|
|
589
|
+
debug!("datetime: {}", datetime);
|
|
590
|
+
|
|
591
|
+
let body_bytes: &[u8] = b"UNSIGNED-PAYLOAD";
|
|
592
|
+
let payload_override = None;
|
|
593
|
+
|
|
594
|
+
debug!("body_bytes: {:?}", body_bytes);
|
|
595
|
+
|
|
596
|
+
// Collect signed headers list
|
|
597
|
+
let signed_headers = params
|
|
598
|
+
.get("X-Amz-SignedHeaders")
|
|
599
|
+
.unwrap()
|
|
600
|
+
.split(';')
|
|
601
|
+
.map(str::to_string)
|
|
602
|
+
.collect::<Vec<_>>();
|
|
603
|
+
|
|
604
|
+
let mut signed_hdrs = HeaderMap::new();
|
|
605
|
+
|
|
606
|
+
let host_header = match url.port_or_known_default() {
|
|
607
|
+
Some(443) | Some(80) | None => url.host_str().unwrap().to_owned(),
|
|
608
|
+
Some(p) => format!("{}:{}", url.host_str().unwrap(), p),
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
signed_hdrs.insert("host", host_header.parse()?);
|
|
612
|
+
|
|
613
|
+
// copy any additional headers that appear in X-Amz-SignedHeaders (rare)
|
|
614
|
+
for h in &["x-amz-date", "x-amz-content-sha256", "range", "x-amz-security-token"] {
|
|
615
|
+
if signed_headers.contains(&h.to_string()) {
|
|
616
|
+
if let Some(v) = session.req_header().headers.get(*h) {
|
|
617
|
+
signed_hdrs.insert(*h, v.clone());
|
|
618
|
+
}
|
|
619
|
+
}
|
|
468
620
|
}
|
|
469
621
|
|
|
470
|
-
let signature = signer.sign();
|
|
471
|
-
let computed_signature = signature
|
|
472
|
-
.split("Signature=")
|
|
473
|
-
.nth(1)
|
|
474
|
-
.unwrap_or_default();
|
|
475
622
|
|
|
476
|
-
info!("Provided signature: {}", provided_signature);
|
|
477
|
-
info!("Computed signature: {}", computed_signature);
|
|
478
|
-
Ok(computed_signature == provided_signature)
|
|
479
|
-
}
|
|
480
623
|
|
|
624
|
+
debug!("signed_headers: {:?}", signed_headers);
|
|
625
|
+
// Delegate to core validator
|
|
626
|
+
signature_is_valid_core(
|
|
627
|
+
session.req_header().method.as_str(),
|
|
628
|
+
&provided_signature,
|
|
629
|
+
region,
|
|
630
|
+
service,
|
|
631
|
+
datetime,
|
|
632
|
+
url.as_str(),
|
|
633
|
+
&signed_hdrs,
|
|
634
|
+
payload_override,
|
|
635
|
+
access_key,
|
|
636
|
+
secret_key,
|
|
637
|
+
&signed_headers,
|
|
638
|
+
body_bytes,
|
|
639
|
+
).await
|
|
640
|
+
}
|
|
481
641
|
|
|
482
642
|
#[cfg(test)]
|
|
483
643
|
mod tests {
|
|
@@ -671,4 +831,5 @@ mod tests {
|
|
|
671
831
|
let qs = canonical_query_string(&parsed);
|
|
672
832
|
assert_eq!(qs, "a=1%20space&b=2");
|
|
673
833
|
}
|
|
834
|
+
|
|
674
835
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#![warn(clippy::all)]
|
|
2
2
|
use async_trait::async_trait;
|
|
3
|
-
use credentials::signer::
|
|
3
|
+
use credentials::signer::{signature_is_valid_for_presigned, signature_is_valid_for_request};
|
|
4
4
|
use dotenv::dotenv;
|
|
5
5
|
use http::Uri;
|
|
6
6
|
use http::uri::Authority;
|
|
7
7
|
use parsers::cos_map::{CosMapItem, parse_cos_map};
|
|
8
8
|
use parsers::keystore::parse_hmac_list;
|
|
9
|
+
use pingora::http::ResponseHeader;
|
|
9
10
|
use pingora::Result;
|
|
10
11
|
use pingora::proxy::{ProxyHttp, Session};
|
|
11
12
|
use pingora::server::Server;
|
|
@@ -28,7 +29,7 @@ use tracing_subscriber::EnvFilter;
|
|
|
28
29
|
use tracing_subscriber::fmt::time::ChronoLocal;
|
|
29
30
|
|
|
30
31
|
pub mod parsers;
|
|
31
|
-
use parsers::credentials::parse_token_from_header;
|
|
32
|
+
use parsers::credentials::{parse_presigned_params, parse_token_from_header};
|
|
32
33
|
use parsers::path::parse_path;
|
|
33
34
|
|
|
34
35
|
pub mod credentials;
|
|
@@ -197,6 +198,7 @@ pub struct MyCtx {
|
|
|
197
198
|
validator: Option<PyObject>,
|
|
198
199
|
bucket_creds_fetcher: Option<PyObject>,
|
|
199
200
|
hmac_fetcher: Option<PyObject>,
|
|
201
|
+
is_presigned: Option<bool>,
|
|
200
202
|
|
|
201
203
|
}
|
|
202
204
|
|
|
@@ -221,6 +223,7 @@ impl ProxyHttp for MyProxy {
|
|
|
221
223
|
.hmac_fetcher
|
|
222
224
|
.as_ref()
|
|
223
225
|
.map(|v| Python::with_gil(|py| v.clone_ref(py))),
|
|
226
|
+
is_presigned: None,
|
|
224
227
|
}
|
|
225
228
|
}
|
|
226
229
|
|
|
@@ -311,19 +314,30 @@ impl ProxyHttp for MyProxy {
|
|
|
311
314
|
let map = ctx.cos_mapping.read().await;
|
|
312
315
|
map.get(bucket).and_then(|c| c.ttl).unwrap_or(0)
|
|
313
316
|
};
|
|
317
|
+
let mut access_key: String = String::new();
|
|
318
|
+
|
|
319
|
+
if auth_header.is_empty() {
|
|
320
|
+
if let Some(q) = session.req_header().uri.query() {
|
|
321
|
+
if q.contains("X-Amz-Credential") {
|
|
322
|
+
let (_, p) = parse_presigned_params(&format!("?{q}"))
|
|
323
|
+
.map_err(|_| pingora::Error::new_str("Failed to parse presigned params"))?;
|
|
324
|
+
access_key = p.access_key.clone();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
} else {
|
|
328
|
+
access_key = parse_token_from_header(&auth_header)
|
|
329
|
+
.map_err(|_| pingora::Error::new_str("Failed to parse access_key"))?
|
|
330
|
+
.1
|
|
331
|
+
.to_string();
|
|
332
|
+
}
|
|
314
333
|
|
|
315
334
|
let is_authorized = if let Some(py_cb) = &ctx.validator {
|
|
316
|
-
let access_key = parse_token_from_header(&auth_header)
|
|
317
|
-
.map_err(|_| pingora::Error::new_str("Failed to parse access_key"))?
|
|
318
|
-
.1
|
|
319
|
-
.to_string();
|
|
320
|
-
|
|
321
|
-
let is_multipart = session
|
|
322
|
-
.req_header()
|
|
323
|
-
.uri
|
|
324
|
-
.query()
|
|
325
|
-
.map_or(false, |q| q.contains("uploadId="));
|
|
326
335
|
|
|
336
|
+
let is_multipart = session
|
|
337
|
+
.req_header()
|
|
338
|
+
.uri
|
|
339
|
+
.query()
|
|
340
|
+
.map_or(false, |q| q.contains("uploadId="));
|
|
327
341
|
|
|
328
342
|
|
|
329
343
|
info!("CHECKING SIGNATURE");
|
|
@@ -333,6 +347,62 @@ impl ProxyHttp for MyProxy {
|
|
|
333
347
|
// continue
|
|
334
348
|
|
|
335
349
|
} else {
|
|
350
|
+
// presigned
|
|
351
|
+
info!("Checking presigned signature");
|
|
352
|
+
let uri_q = session.req_header().uri.query().unwrap_or("");
|
|
353
|
+
|
|
354
|
+
if auth_header.is_empty() && uri_q.contains("X-Amz-Signature") {
|
|
355
|
+
ctx.is_presigned = Some(true);
|
|
356
|
+
|
|
357
|
+
// ensure we have the secret_key in the keystore
|
|
358
|
+
if !ctx.hmac_keystore.read().await.contains_key(&access_key) {
|
|
359
|
+
debug!("No key in keystore, trying to fetch via hmac_fetcher for ->{}<-", access_key);
|
|
360
|
+
// fetch via hmac_fetcher exactly as you do below…
|
|
361
|
+
if let Some(py_fetcher) = &ctx.hmac_fetcher {
|
|
362
|
+
// call Python callback
|
|
363
|
+
let cb = py_fetcher;
|
|
364
|
+
let secret: PyResult<String> = Python::with_gil(|py| {
|
|
365
|
+
cb.call1(py, (&access_key,))
|
|
366
|
+
.and_then(|r| r.extract(py))
|
|
367
|
+
});
|
|
368
|
+
debug!("Got secret: {:#?}", secret);
|
|
369
|
+
match secret {
|
|
370
|
+
Ok(secret_key) => {
|
|
371
|
+
debug!("got key and inserting into keystore");
|
|
372
|
+
ctx.hmac_keystore.write().await.insert(access_key.clone().to_string(), secret_key);
|
|
373
|
+
}
|
|
374
|
+
Err(_) => {
|
|
375
|
+
// no key → unauthorized
|
|
376
|
+
session.respond_error(401).await?;
|
|
377
|
+
return Ok(true);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
session.respond_error(401).await?;
|
|
382
|
+
return Ok(true);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
}
|
|
386
|
+
debug!("now checking if the signature is valid for presigned...");
|
|
387
|
+
let sk = ctx.hmac_keystore.read().await.get(&access_key).unwrap().clone();
|
|
388
|
+
debug!("got secret {} from keystore", sk);
|
|
389
|
+
debug!("RAW_PATH = {}", &session.req_header().uri);
|
|
390
|
+
debug!("RAW_HOST_HDR = {:?}", &session.req_header().headers.get("host"));
|
|
391
|
+
let ok = match signature_is_valid_for_presigned(&session, &sk).await {
|
|
392
|
+
Ok(b) => b,
|
|
393
|
+
Err(e) => {
|
|
394
|
+
error!("presigned-URL validation error: {e}"); // <-- keep the info
|
|
395
|
+
return Err(pingora::Error::new_str("Failed to check signature"));
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
info!("is signature valid?: {}", ok);
|
|
399
|
+
if !ok {
|
|
400
|
+
session.respond_error(401).await?;
|
|
401
|
+
return Ok(true);
|
|
402
|
+
}
|
|
403
|
+
} else {
|
|
404
|
+
info!("processing a regular request");
|
|
405
|
+
|
|
336
406
|
let has_key = {
|
|
337
407
|
let map = ctx.hmac_keystore.read().await;
|
|
338
408
|
map.contains_key(&access_key)
|
|
@@ -361,12 +431,12 @@ impl ProxyHttp for MyProxy {
|
|
|
361
431
|
}
|
|
362
432
|
}
|
|
363
433
|
let secret_key = {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
434
|
+
let map = ctx.hmac_keystore.read().await;
|
|
435
|
+
map.get(&access_key).cloned()
|
|
436
|
+
};
|
|
367
437
|
|
|
368
438
|
info!("Checking signature");
|
|
369
|
-
let sig_ok = match
|
|
439
|
+
let sig_ok = match signature_is_valid_for_request(
|
|
370
440
|
&auth_header,
|
|
371
441
|
&session,
|
|
372
442
|
&secret_key.unwrap(),
|
|
@@ -389,17 +459,19 @@ impl ProxyHttp for MyProxy {
|
|
|
389
459
|
session.respond_error(401).await?;
|
|
390
460
|
return Ok(true);
|
|
391
461
|
}
|
|
462
|
+
}
|
|
392
463
|
}
|
|
393
464
|
}
|
|
394
|
-
|
|
395
|
-
let cache_key = format!("{}:{}", access_key, bucket);
|
|
465
|
+
info!("Signature check passed, continuing now onto the bespoke validation");
|
|
466
|
+
let cache_key = format!("{}:{}", &access_key, bucket);
|
|
467
|
+
debug!("Cache key: {}", cache_key);
|
|
396
468
|
|
|
397
469
|
let bucket_clone = bucket.to_string();
|
|
398
470
|
let callback_clone: PyObject = Python::with_gil(|py| py_cb.clone_ref(py));
|
|
399
|
-
|
|
471
|
+
let move_access_key = access_key.clone();
|
|
400
472
|
ctx.auth_cache
|
|
401
473
|
.get_or_validate(&cache_key, Duration::from_secs(ttl), move || {
|
|
402
|
-
let tk =
|
|
474
|
+
let tk = move_access_key.clone();
|
|
403
475
|
let bu = bucket_clone.clone();
|
|
404
476
|
let cb = Python::with_gil(|py| callback_clone.clone_ref(py));
|
|
405
477
|
async move {
|
|
@@ -423,10 +495,8 @@ impl ProxyHttp for MyProxy {
|
|
|
423
495
|
let map = ctx.cos_mapping.read().await;
|
|
424
496
|
map.get(&hdr_bucket).cloned()
|
|
425
497
|
};
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
.1
|
|
429
|
-
.to_string();
|
|
498
|
+
|
|
499
|
+
debug!("Access key: {}", &access_key);
|
|
430
500
|
|
|
431
501
|
// we have to check for some available credentials here to be able to return unauthorized already if not
|
|
432
502
|
match bucket_config.clone() {
|
|
@@ -473,6 +543,31 @@ impl ProxyHttp for MyProxy {
|
|
|
473
543
|
ctx: &mut Self::CTX,
|
|
474
544
|
) -> Result<()> {
|
|
475
545
|
|
|
546
|
+
if let Some(presigned) = ctx.is_presigned {
|
|
547
|
+
if presigned {
|
|
548
|
+
debug!("upstream_request_filter::presigned");
|
|
549
|
+
let cleaned_q = upstream_request
|
|
550
|
+
.uri
|
|
551
|
+
.query()
|
|
552
|
+
.unwrap_or("")
|
|
553
|
+
.split('&')
|
|
554
|
+
.filter(|kv| !kv.starts_with("X-Amz-"))
|
|
555
|
+
.collect::<Vec<_>>()
|
|
556
|
+
.join("&");
|
|
557
|
+
|
|
558
|
+
let _ = upstream_request.remove_header("authorization");
|
|
559
|
+
|
|
560
|
+
let new_path_and_query = if cleaned_q.is_empty() {
|
|
561
|
+
upstream_request.uri.path().to_owned()
|
|
562
|
+
} else {
|
|
563
|
+
format!("{}?{}", upstream_request.uri.path(), cleaned_q)
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
upstream_request.set_uri(new_path_and_query.try_into().unwrap());
|
|
567
|
+
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
|
|
476
571
|
let _ = upstream_request.remove_header("accept-encoding");
|
|
477
572
|
|
|
478
573
|
debug!("upstream_request_filter::start");
|
|
@@ -587,6 +682,19 @@ impl ProxyHttp for MyProxy {
|
|
|
587
682
|
Ok(())
|
|
588
683
|
}
|
|
589
684
|
|
|
685
|
+
async fn response_filter(
|
|
686
|
+
&self,
|
|
687
|
+
_session: &mut Session,
|
|
688
|
+
resp: &mut ResponseHeader,
|
|
689
|
+
_ctx: &mut Self::CTX,
|
|
690
|
+
) -> Result<()> {
|
|
691
|
+
let _ = resp.remove_header("server");
|
|
692
|
+
|
|
693
|
+
let _ = resp.insert_header("Server", "Object-Storage-Proxy");
|
|
694
|
+
|
|
695
|
+
Ok(())
|
|
696
|
+
}
|
|
697
|
+
|
|
590
698
|
}
|
|
591
699
|
|
|
592
700
|
pub fn init_tracing() {
|
|
@@ -618,7 +726,7 @@ pub fn run_server(py: Python, run_args: &ProxyServerConfig) {
|
|
|
618
726
|
parse_hmac_list(py, &run_args.hmac_keystore).unwrap_or(HashMap::new())
|
|
619
727
|
};
|
|
620
728
|
|
|
621
|
-
|
|
729
|
+
debug!("HMAC keys: {:#?}", &local_hmac_map);
|
|
622
730
|
|
|
623
731
|
let cosmap = Arc::new(RwLock::new(parse_cos_map(py, &run_args.cos_map).unwrap()));
|
|
624
732
|
let hmac_keystore = Arc::new(RwLock::new(local_hmac_map));
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
use std::collections::HashMap;
|
|
2
|
+
|
|
3
|
+
use nom::{
|
|
4
|
+
bytes::complete::{tag, take_until, take_while1}, multi::separated_list1, sequence::{preceded, separated_pair}, IResult, Parser
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
use nom::character::complete::char as nomchar;
|
|
8
|
+
use percent_encoding::percent_decode_str;
|
|
9
|
+
use nom::error::{Error, ErrorKind, make_error};
|
|
10
|
+
|
|
11
|
+
fn miss<'a>(i: &'a str) -> nom::Err<Error<&'a str>> {
|
|
12
|
+
nom::Err::Error(make_error(i, ErrorKind::Tag))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
pub fn parse_token_from_header(header: &str) -> IResult<&str, &str> {
|
|
16
|
+
let (_, token) =
|
|
17
|
+
(preceded(tag("AWS4-HMAC-SHA256 Credential="), take_until("/"))).parse(header)?;
|
|
18
|
+
|
|
19
|
+
Ok(("", token))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
pub fn parse_credential_scope(input: &str) -> IResult<&str, (&str, &str)> {
|
|
23
|
+
let (input, _) = take_until("Credential=")(input)?;
|
|
24
|
+
let (remaining, (_, _, _, _, _, region, _, service, _)) = (
|
|
25
|
+
tag("Credential="), // prefix
|
|
26
|
+
take_until("/"), // access key
|
|
27
|
+
tag("/"),
|
|
28
|
+
take_until("/"), // date
|
|
29
|
+
tag("/"),
|
|
30
|
+
take_until("/"), // region
|
|
31
|
+
tag("/"),
|
|
32
|
+
take_until("/aws4_request"),// service
|
|
33
|
+
tag("/aws4_request"), // trailing
|
|
34
|
+
).parse(input)?;
|
|
35
|
+
Ok((remaining, (region, service)))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#[derive(Debug, PartialEq)]
|
|
39
|
+
pub struct PresignedParams {
|
|
40
|
+
pub algorithm: String,
|
|
41
|
+
pub access_key: String,
|
|
42
|
+
pub credential_date: String,
|
|
43
|
+
pub region: String,
|
|
44
|
+
pub service: String,
|
|
45
|
+
pub amz_date: String,
|
|
46
|
+
pub expires: String,
|
|
47
|
+
pub signed_headers: String,
|
|
48
|
+
pub signature: String,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// key chars: letters, digits, dash, dot
|
|
52
|
+
fn is_key_char(c: char) -> bool {
|
|
53
|
+
c.is_alphanumeric() || c == '-' || c == '.'
|
|
54
|
+
}
|
|
55
|
+
/// val chars: anything except `&`
|
|
56
|
+
fn is_val_char(c: char) -> bool {
|
|
57
|
+
c != '&'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Parse the `?` and then a list of `key=val` pairs separated by `&`
|
|
61
|
+
fn query_pairs(input: &str) -> IResult<&str, Vec<(&str, &str)>> {
|
|
62
|
+
// skip everything up to the '?'
|
|
63
|
+
let (input, _) = if let Some(i) = input.find('?') {
|
|
64
|
+
// consume everything up to – and including – the first '?'
|
|
65
|
+
nom::bytes::complete::take::<_, _, nom::error::Error<_>>(i + 1usize)(input)?
|
|
66
|
+
} else {
|
|
67
|
+
// the input *is* the query string, start parsing immediately
|
|
68
|
+
("", input)
|
|
69
|
+
}; // then parse key=val (& key=val)* until the end
|
|
70
|
+
separated_list1(
|
|
71
|
+
nomchar('&'),
|
|
72
|
+
separated_pair(
|
|
73
|
+
take_while1(is_key_char),
|
|
74
|
+
nomchar('='),
|
|
75
|
+
take_while1(is_val_char),
|
|
76
|
+
),
|
|
77
|
+
).parse(input)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Top-level parser
|
|
81
|
+
pub fn parse_presigned_params(input: &str) -> IResult<&str, PresignedParams> {
|
|
82
|
+
let (rest, pairs) = query_pairs(input)?;
|
|
83
|
+
// build a little map
|
|
84
|
+
let mut m = HashMap::new();
|
|
85
|
+
for (k, v) in pairs {
|
|
86
|
+
// percent-decode the value
|
|
87
|
+
let val = percent_decode_str(v).decode_utf8_lossy().into_owned();
|
|
88
|
+
m.insert(k, val);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// pull out each required field (error if missing)
|
|
92
|
+
let algorithm = m.remove("X-Amz-Algorithm" ).ok_or_else(|| miss(rest))?;
|
|
93
|
+
let credential_raw = m.remove("X-Amz-Credential" ).ok_or_else(|| miss(rest))?;
|
|
94
|
+
let amz_date = m.remove("X-Amz-Date" ).ok_or_else(|| miss(rest))?;
|
|
95
|
+
let expires = m.remove("X-Amz-Expires" ).ok_or_else(|| miss(rest))?;
|
|
96
|
+
let signed_headers = m.remove("X-Amz-SignedHeaders" ).ok_or_else(|| miss(rest))?;
|
|
97
|
+
let signature = m.remove("X-Amz-Signature" ).ok_or_else(|| miss(rest))?;
|
|
98
|
+
|
|
99
|
+
// split the credential into its components
|
|
100
|
+
// format is: ACCESSKEY/DATE/REGION/SERVICE/aws4_request
|
|
101
|
+
let mut parts = credential_raw.split('/');
|
|
102
|
+
let access_key = parts.next().unwrap_or("").to_string();
|
|
103
|
+
let credential_date= parts.next().unwrap_or("").to_string();
|
|
104
|
+
let region = parts.next().unwrap_or("").to_string();
|
|
105
|
+
let service = parts.next().unwrap_or("").to_string();
|
|
106
|
+
// ignore the final “aws4_request”
|
|
107
|
+
|
|
108
|
+
Ok((rest, PresignedParams {
|
|
109
|
+
algorithm,
|
|
110
|
+
access_key,
|
|
111
|
+
credential_date,
|
|
112
|
+
region,
|
|
113
|
+
service,
|
|
114
|
+
amz_date,
|
|
115
|
+
expires,
|
|
116
|
+
signed_headers,
|
|
117
|
+
signature,
|
|
118
|
+
}))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#[cfg(test)]
|
|
122
|
+
mod tests {
|
|
123
|
+
use super::*;
|
|
124
|
+
use nom::Err;
|
|
125
|
+
|
|
126
|
+
#[test]
|
|
127
|
+
fn test_parse_token_from_header() {
|
|
128
|
+
let input = "AWS4-HMAC-SHA256 Credential=MYLOCAL123/20250417/eu-west-3/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=ec323a7db4d0b8bd27eced3b2bb0d59f9b9dd";
|
|
129
|
+
let result = parse_token_from_header(input);
|
|
130
|
+
assert_eq!(result, Ok(("", ("MYLOCAL123"))));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#[test]
|
|
134
|
+
fn parse_token_from_header_success_and_error() {
|
|
135
|
+
let input = "AWS4-HMAC-SHA256 Credential=TOKEN123/20250417/eu-west-1/s3/aws4_request, SignedHeaders=host,Signature=abc";
|
|
136
|
+
let result = parse_token_from_header(input);
|
|
137
|
+
assert!(result.is_ok());
|
|
138
|
+
let (remaining, token) = result.unwrap();
|
|
139
|
+
assert_eq!(token, "TOKEN123");
|
|
140
|
+
assert_eq!(remaining, "");
|
|
141
|
+
|
|
142
|
+
let bad = "NoCredentialHere";
|
|
143
|
+
assert!(parse_token_from_header(bad).is_err());
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
#[test]
|
|
148
|
+
fn test_parse_valid_scope() {
|
|
149
|
+
let header = "Credential=AKIAEXAMPLE/20250425/us-west-2/s3/aws4_request, SignedHeaders=host;x-amz-date";
|
|
150
|
+
let (rem, (region, service)) = parse_credential_scope(header).expect("parse failed");
|
|
151
|
+
assert_eq!(region, "us-west-2");
|
|
152
|
+
assert_eq!(service, "s3");
|
|
153
|
+
assert!(rem.starts_with(", SignedHeaders"));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#[test]
|
|
157
|
+
fn test_parse_invalid_scope() {
|
|
158
|
+
let header = "Credential=AKIAEXAMPLE/20250425/us-west-2/s3/some_request";
|
|
159
|
+
assert!(matches!(parse_credential_scope(header), Err(Err::Error(_))));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#[test]
|
|
163
|
+
fn test_parse_with_prefix() {
|
|
164
|
+
let header = "Authorization: AWS4-HMAC-SHA256 Credential=XYZ/20250425/eu-central-1/dynamodb/aws4_request/extra";
|
|
165
|
+
let idx = header.find("Credential=").unwrap();
|
|
166
|
+
let substr = &header[idx..];
|
|
167
|
+
let (rem, (region, service)) = parse_credential_scope(substr).expect("parse failed");
|
|
168
|
+
assert_eq!(region, "eu-central-1");
|
|
169
|
+
assert_eq!(service, "dynamodb");
|
|
170
|
+
assert!(rem.starts_with("/extra"));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#[test]
|
|
174
|
+
fn parses_all_fields() {
|
|
175
|
+
let url = "http://localhost:6190/proxy-aws-bucket01/mandelbrot/?\
|
|
176
|
+
X-Amz-Algorithm=AWS4-HMAC-SHA256&\
|
|
177
|
+
X-Amz-Credential=MYLOCAL123%2F20250426%2Feu-west-3%2Fs3%2Faws4_request&\
|
|
178
|
+
X-Amz-Date=20250426T143249Z&\
|
|
179
|
+
X-Amz-Expires=3600&\
|
|
180
|
+
X-Amz-SignedHeaders=host&\
|
|
181
|
+
X-Amz-Signature=53cb3d8a12c8c1078fba3fcd55ced9c93fcdc8e2f98184e9ffea50245f4ebea5";
|
|
182
|
+
|
|
183
|
+
let (_, p) = parse_presigned_params(url).unwrap();
|
|
184
|
+
assert_eq!(p.algorithm, "AWS4-HMAC-SHA256");
|
|
185
|
+
assert_eq!(p.access_key, "MYLOCAL123");
|
|
186
|
+
assert_eq!(p.credential_date, "20250426");
|
|
187
|
+
assert_eq!(p.region, "eu-west-3");
|
|
188
|
+
assert_eq!(p.service, "s3");
|
|
189
|
+
assert_eq!(p.amz_date, "20250426T143249Z");
|
|
190
|
+
assert_eq!(p.expires, "3600");
|
|
191
|
+
assert_eq!(p.signed_headers, "host");
|
|
192
|
+
assert_eq!(p.signature, "53cb3d8a12c8c1078fba3fcd55ced9c93fcdc8e2f98184e9ffea50245f4ebea5");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
#[test]
|
|
196
|
+
fn fails_if_missing_signature() {
|
|
197
|
+
let url = "https://example.com/?X-Amz-Credential=AK/20250426/us-west-2/s3/aws4_request";
|
|
198
|
+
assert!(parse_presigned_params(url).is_err());
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#[test]
|
|
202
|
+
fn parses_presigned_params_from_raw_query() {
|
|
203
|
+
// the very same query string that shows up in the log
|
|
204
|
+
let q = "X-Amz-Algorithm=AWS4-HMAC-SHA256&\
|
|
205
|
+
X-Amz-Credential=MYLOCAL123%2F20250426%2Feu-west-3%2Fs3%2Faws4_request&\
|
|
206
|
+
X-Amz-Date=20250426T143249Z&\
|
|
207
|
+
X-Amz-Expires=3600&\
|
|
208
|
+
X-Amz-SignedHeaders=host&\
|
|
209
|
+
X-Amz-Signature=53cb3d8a12c8c1078fba3fcd55ced9c93fcdc8e2f98184e9ffea50245f4ebea5";
|
|
210
|
+
|
|
211
|
+
// ❌ This is what the proxy does today — and it should FAIL until we fix the parser.
|
|
212
|
+
assert!(
|
|
213
|
+
parse_presigned_params(q).is_err(),
|
|
214
|
+
"the parser should reject a query that has no leading '?'"
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// ✅ This is what the proxy *should* do (or what the parser should accept):
|
|
218
|
+
let wrapped = format!("?{q}");
|
|
219
|
+
let (_, p) = parse_presigned_params(&wrapped)
|
|
220
|
+
.expect("parser must succeed when a leading '?' is present");
|
|
221
|
+
|
|
222
|
+
assert_eq!(p.algorithm, "AWS4-HMAC-SHA256");
|
|
223
|
+
assert_eq!(p.access_key, "MYLOCAL123");
|
|
224
|
+
assert_eq!(p.credential_date, "20250426");
|
|
225
|
+
assert_eq!(p.region, "eu-west-3");
|
|
226
|
+
assert_eq!(p.service, "s3");
|
|
227
|
+
assert_eq!(p.amz_date, "20250426T143249Z");
|
|
228
|
+
assert_eq!(p.expires, "3600");
|
|
229
|
+
assert_eq!(p.signed_headers, "host");
|
|
230
|
+
assert_eq!(p.signature, "53cb3d8a12c8c1078fba3fcd55ced9c93fcdc8e2f98184e9ffea50245f4ebea5");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
cd /Users/jeroen/projects/mandelbrot
|
|
4
|
+
|
|
5
|
+
echo "generate testfile"
|
|
6
|
+
dd if=/dev/random of=/Users/jeroen/projects/mandelbrot/testfile_10 bs=1M count=10
|
|
7
|
+
|
|
8
|
+
echo "uploading 10MB file."
|
|
9
|
+
aws s3 cp testfile_10 s3://proxy-aws-bucket01/mandelbrot/testfile_10bis --profile osp
|
|
10
|
+
if [ $? -eq 0 ]; then
|
|
11
|
+
echo -e "\033[1;32mOK\033[0m"
|
|
12
|
+
else
|
|
13
|
+
echo -e "\033[1;31mERROR\033[0m"
|
|
14
|
+
fi
|
|
15
|
+
echo "listing .."
|
|
16
|
+
aws s3 ls s3://proxy-aws-bucket01/mandelbrot/testfile_10bis --profile osp
|
|
17
|
+
if [ $? -eq 0 ]; then
|
|
18
|
+
echo -e "\033[1;32mOK\033[0m"
|
|
19
|
+
else
|
|
20
|
+
echo -e "\033[1;31mERROR\033[0m"
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
echo "download a 10MB file"
|
|
24
|
+
aws s3 cp s3://proxy-aws-bucket01/mandelbrot/testfile_10bis testfile_10bis --profile osp
|
|
25
|
+
if [ $? -eq 0 ]; then
|
|
26
|
+
echo -e "\033[1;32mOK\033[0m"
|
|
27
|
+
else
|
|
28
|
+
echo -e "\033[1;31mERROR\033[0m"
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
echo "listing .."
|
|
33
|
+
ls -latrh testfile_10bis
|
|
34
|
+
|
|
35
|
+
if [ $? -eq 0 ]; then
|
|
36
|
+
echo -e "\033[1;32mOK\033[0m"
|
|
37
|
+
else
|
|
38
|
+
echo -e "\033[1;31mERROR\033[0m"
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
echo "deleting .."
|
|
43
|
+
aws s3 rm s3://proxy-aws-bucket01/mandelbrot/testfile_10bis --profile osp
|
|
44
|
+
if [ $? -eq 0 ]; then
|
|
45
|
+
echo -e "\033[1;32mOK\033[0m"
|
|
46
|
+
else
|
|
47
|
+
echo -e "\033[1;31mERROR\033[0m"
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
echo "listing .."
|
|
52
|
+
aws s3 ls s3://proxy-aws-bucket01/mandelbrot/testfile_10bis --profile osp
|
|
53
|
+
if [ $? -eq 1 ]; then
|
|
54
|
+
echo -e "\033[1;32mOK\033[0m"
|
|
55
|
+
else
|
|
56
|
+
echo -e "\033[1;31mERROR\033[0m"
|
|
57
|
+
fi
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
use nom::{
|
|
2
|
-
bytes::complete::{tag, take_until}, sequence::{preceded, tuple}, IResult, Parser
|
|
3
|
-
};
|
|
4
|
-
|
|
5
|
-
pub fn parse_token_from_header(header: &str) -> IResult<&str, &str> {
|
|
6
|
-
let (_, token) =
|
|
7
|
-
(preceded(tag("AWS4-HMAC-SHA256 Credential="), take_until("/"))).parse(header)?;
|
|
8
|
-
|
|
9
|
-
Ok(("", token))
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
pub fn parse_credential_scope(input: &str) -> IResult<&str, (&str, &str)> {
|
|
13
|
-
let (input, _) = take_until("Credential=")(input)?;
|
|
14
|
-
let (remaining, (_, _, _, _, _, region, _, service, _)) = (
|
|
15
|
-
tag("Credential="), // prefix
|
|
16
|
-
take_until("/"), // access key
|
|
17
|
-
tag("/"),
|
|
18
|
-
take_until("/"), // date
|
|
19
|
-
tag("/"),
|
|
20
|
-
take_until("/"), // region
|
|
21
|
-
tag("/"),
|
|
22
|
-
take_until("/aws4_request"),// service
|
|
23
|
-
tag("/aws4_request"), // trailing
|
|
24
|
-
).parse(input)?;
|
|
25
|
-
Ok((remaining, (region, service)))
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
#[cfg(test)]
|
|
30
|
-
mod tests {
|
|
31
|
-
use super::*;
|
|
32
|
-
use nom::Err;
|
|
33
|
-
|
|
34
|
-
#[test]
|
|
35
|
-
fn test_parse_token_from_header() {
|
|
36
|
-
let input = "AWS4-HMAC-SHA256 Credential=MYLOCAL123/20250417/eu-west-3/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=ec323a7db4d0b8bd27eced3b2bb0d59f9b9dd";
|
|
37
|
-
let result = parse_token_from_header(input);
|
|
38
|
-
assert_eq!(result, Ok(("", ("MYLOCAL123"))));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
#[test]
|
|
42
|
-
fn parse_token_from_header_success_and_error() {
|
|
43
|
-
let input = "AWS4-HMAC-SHA256 Credential=TOKEN123/20250417/eu-west-1/s3/aws4_request, SignedHeaders=host,Signature=abc";
|
|
44
|
-
let result = parse_token_from_header(input);
|
|
45
|
-
assert!(result.is_ok());
|
|
46
|
-
let (remaining, token) = result.unwrap();
|
|
47
|
-
assert_eq!(token, "TOKEN123");
|
|
48
|
-
assert_eq!(remaining, "");
|
|
49
|
-
|
|
50
|
-
let bad = "NoCredentialHere";
|
|
51
|
-
assert!(parse_token_from_header(bad).is_err());
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
#[test]
|
|
56
|
-
fn test_parse_valid_scope() {
|
|
57
|
-
let header = "Credential=AKIAEXAMPLE/20250425/us-west-2/s3/aws4_request, SignedHeaders=host;x-amz-date";
|
|
58
|
-
let (rem, (region, service)) = parse_credential_scope(header).expect("parse failed");
|
|
59
|
-
assert_eq!(region, "us-west-2");
|
|
60
|
-
assert_eq!(service, "s3");
|
|
61
|
-
assert!(rem.starts_with(", SignedHeaders"));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
#[test]
|
|
65
|
-
fn test_parse_invalid_scope() {
|
|
66
|
-
let header = "Credential=AKIAEXAMPLE/20250425/us-west-2/s3/some_request";
|
|
67
|
-
assert!(matches!(parse_credential_scope(header), Err(Err::Error(_))));
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
#[test]
|
|
71
|
-
fn test_parse_with_prefix() {
|
|
72
|
-
let header = "Authorization: AWS4-HMAC-SHA256 Credential=XYZ/20250425/eu-central-1/dynamodb/aws4_request/extra";
|
|
73
|
-
let idx = header.find("Credential=").unwrap();
|
|
74
|
-
let substr = &header[idx..];
|
|
75
|
-
let (rem, (region, service)) = parse_credential_scope(substr).expect("parse failed");
|
|
76
|
-
assert_eq!(region, "eu-central-1");
|
|
77
|
-
assert_eq!(service, "dynamodb");
|
|
78
|
-
assert!(rem.starts_with("/extra"));
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|