object-storage-proxy 0.2.15__tar.gz → 0.2.16__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.
Files changed (31) hide show
  1. {object_storage_proxy-0.2.15 → object_storage_proxy-0.2.16}/Cargo.lock +1 -1
  2. {object_storage_proxy-0.2.15 → object_storage_proxy-0.2.16}/Cargo.toml +1 -1
  3. {object_storage_proxy-0.2.15 → object_storage_proxy-0.2.16}/PKG-INFO +69 -12
  4. {object_storage_proxy-0.2.15 → object_storage_proxy-0.2.16}/README.md +68 -11
  5. object_storage_proxy-0.2.16/src/credentials/hmac_keystore.rs +34 -0
  6. {object_storage_proxy-0.2.15 → object_storage_proxy-0.2.16}/src/credentials/mod.rs +1 -0
  7. {object_storage_proxy-0.2.15 → object_storage_proxy-0.2.16}/src/credentials/signer.rs +162 -16
  8. {object_storage_proxy-0.2.15 → object_storage_proxy-0.2.16}/src/lib.rs +134 -9
  9. object_storage_proxy-0.2.16/src/parsers/credentials.rs +82 -0
  10. object_storage_proxy-0.2.16/src/parsers/keystore.rs +59 -0
  11. {object_storage_proxy-0.2.15 → object_storage_proxy-0.2.16}/src/parsers/mod.rs +1 -0
  12. {object_storage_proxy-0.2.15 → object_storage_proxy-0.2.16}/src/utils/validator.rs +1 -0
  13. {object_storage_proxy-0.2.15 → object_storage_proxy-0.2.16}/test_server.py +28 -1
  14. object_storage_proxy-0.2.16/uv.lock +43 -0
  15. object_storage_proxy-0.2.15/src/parsers/credentials.rs +0 -37
  16. object_storage_proxy-0.2.15/uv.lock +0 -1398
  17. {object_storage_proxy-0.2.15 → object_storage_proxy-0.2.16}/.cargo/config.toml +0 -0
  18. {object_storage_proxy-0.2.15 → object_storage_proxy-0.2.16}/.github/workflows/ci.yml +0 -0
  19. {object_storage_proxy-0.2.15 → object_storage_proxy-0.2.16}/.gitignore +0 -0
  20. {object_storage_proxy-0.2.15 → object_storage_proxy-0.2.16}/LICENSE +0 -0
  21. {object_storage_proxy-0.2.15 → object_storage_proxy-0.2.16}/img/logo.svg +0 -0
  22. {object_storage_proxy-0.2.15 → object_storage_proxy-0.2.16}/img/request_lifecycle.svg +0 -0
  23. {object_storage_proxy-0.2.15 → object_storage_proxy-0.2.16}/img/request_stages.svg +0 -0
  24. {object_storage_proxy-0.2.15 → object_storage_proxy-0.2.16}/pyproject.toml +0 -0
  25. {object_storage_proxy-0.2.15 → object_storage_proxy-0.2.16}/requirements.txt +0 -0
  26. {object_storage_proxy-0.2.15 → object_storage_proxy-0.2.16}/src/credentials/models.rs +0 -0
  27. {object_storage_proxy-0.2.15 → object_storage_proxy-0.2.16}/src/credentials/secrets_proxy.rs +0 -0
  28. {object_storage_proxy-0.2.15 → object_storage_proxy-0.2.16}/src/object_storage_proxy.pyi +0 -0
  29. {object_storage_proxy-0.2.15 → object_storage_proxy-0.2.16}/src/parsers/cos_map.rs +0 -0
  30. {object_storage_proxy-0.2.15 → object_storage_proxy-0.2.16}/src/parsers/path.rs +0 -0
  31. {object_storage_proxy-0.2.15 → object_storage_proxy-0.2.16}/src/utils/mod.rs +0 -0
@@ -1701,7 +1701,7 @@ dependencies = [
1701
1701
 
1702
1702
  [[package]]
1703
1703
  name = "object-storage-proxy"
1704
- version = "0.2.15"
1704
+ version = "0.2.16"
1705
1705
  dependencies = [
1706
1706
  "async-trait",
1707
1707
  "chrono",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "object-storage-proxy"
3
- version = "0.2.15"
3
+ version = "0.2.16"
4
4
  edition = "2024"
5
5
 
6
6
  [dependencies]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: object-storage-proxy
3
- Version: 0.2.15
3
+ Version: 0.2.16
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: Implementation :: PyPy
@@ -30,11 +30,16 @@ Project-URL: BugTracker, https://github.com/opensourceworks-org/object-storage-p
30
30
 
31
31
  A fast and safe in-process reverse proxy server, based on Cloudflare's [pingora](https://github.com/cloudflare/pingora?tab=readme-ov-file), to reverse proxy AWS and IBM Cloud Object Storage buckets and integrate your Authentication and Authorization services.
32
32
 
33
+ Decouples frontend from backend authentication and authorization.
34
+
35
+
33
36
  - [x] Takes a Python authorization callable (allows you to plug in your own authorization services) and api_key fetch callback function and cos bucket dictionary.
34
37
  - [x] The validation is cached with optional ttl (default 5min, keep it short).
35
38
  - [x] The apikey is used to authenticate against IBM's IAM endpoint and is cached and renewed on expiration. (IBM only)
36
- - [x] If no apikey is provided, a Python function can be passed in to fetch the apikey or hmac keys for any given bucket (run once).
37
- - [x] HMAC support: passing in access and secret id keys (or as json string from python credentials callable), will be used to sign the request (AWS/IBM/..)
39
+ - [x] If no apikey or hmac keypair is provided, a Python function can be passed in to fetch the apikey or hmac keys for any given bucket (run once).
40
+ - [x] HMAC support: passing in access and secret id keys (or as json string from python credentials callable), will be used to sign the upstream request (AWS/IBM/..)
41
+ - [x] frontend request is signed with your own custom keypair
42
+ - [x] supports running in cloud environments / proxy headers for request signature validation
38
43
 
39
44
  The bucket dict contains for each bucket:
40
45
 
@@ -85,6 +90,8 @@ The Python callables take two arguments:
85
90
  def your_request_authorizer(token: str, bucket: str) -> bool
86
91
  ```
87
92
 
93
+ Proxy Configuration
94
+
88
95
 
89
96
 
90
97
 
@@ -120,7 +127,7 @@ Pass in a function which maps bucket to instance (credentials), and a function t
120
127
  The advantage is we can plug in a python authentication function and another function for authorization, allowing for fine-grained control.
121
128
 
122
129
  ## authentication
123
- We use the standard aws hmac header.
130
+ We use the standard aws hmac header and aws v4 request signing algorithm.
124
131
 
125
132
  ## authorization
126
133
  Pass in a callable from python which will be called from rust. This will be cached (ttl) for consecutive requests.
@@ -177,7 +184,8 @@ def strtobool(val: str) -> bool:
177
184
  raise ValueError(f"invalid truth value {val!r}")
178
185
 
179
186
 
180
- def do_api_creds(bucket) -> str:
187
+ def do_api_creds(token: str, bucket: str) -> str:
188
+ """Fetch credentials (ro, rw, access_denied) for the given bucket, depending on the token. """
181
189
  apikey = os.getenv("COS_API_KEY")
182
190
  if not apikey:
183
191
  raise ValueError("COS_API_KEY environment variable not set")
@@ -186,11 +194,13 @@ def do_api_creds(bucket) -> str:
186
194
  return apikey
187
195
 
188
196
 
189
- def do_hmac_creds(bucket) -> str:
197
+ def do_hmac_creds(token: str, bucket: str) -> str:
198
+ """ Fetch HMAC credentials (ro, rw, access_denied) for the given bucket, depending on the token """
190
199
  access_key = os.getenv("ACCESS_KEY")
191
200
  secret_key = os.getenv("SECRET_KEY")
192
201
  if not access_key or not secret_key:
193
202
  raise ValueError("ACCESS_KEY or SECRET_KEY environment variable not set")
203
+
194
204
  print(f"Fetching HMAC credentials for {bucket}...")
195
205
 
196
206
  return json.dumps({
@@ -198,8 +208,27 @@ def do_hmac_creds(bucket) -> str:
198
208
  "secret_key": secret_key
199
209
  })
200
210
 
211
+ def lookup_secret_key(access_key: str) -> str | None:
212
+ # get all environment variables ending in ACCESS_KEY
213
+ access_keys = [{key:value} for key, value in os.environ.items() if key.endswith("ACCESS_KEY") and value==access_key ]
214
+
215
+ if len(access_keys) > 0:
216
+ access_key_var = next((k for k, v in access_keys[0].items() if v == access_key), None)
217
+
218
+ secret_key_var = access_key_var.replace("ACCESS_KEY", "SECRET_KEY")
219
+ return os.getenv(secret_key_var, None)
220
+ else:
221
+ print(f"no access keys found for : {access_key}")
222
+
201
223
 
202
224
  def do_validation(token: str, bucket: str) -> bool:
225
+ """ Authorize the request based on token for the given bucket.
226
+ You can plug in your own authorization service here.
227
+ The token is the authorization token passed in the request.
228
+ The bucket is the bucket name.
229
+ The function should return True if the request is authorized, False otherwise.
230
+ """
231
+
203
232
  print(f"PYTHON: Validating headers: {token} for {bucket}...")
204
233
  # return random.choice([True, False])
205
234
  return True
@@ -214,7 +243,6 @@ def main() -> None:
214
243
  osp.enable_request_counting()
215
244
  print("Request counting enabled")
216
245
 
217
-
218
246
  apikey = os.getenv("COS_API_KEY")
219
247
  if not apikey:
220
248
  raise ValueError("COS_API_KEY environment variable not set")
@@ -240,16 +268,48 @@ def main() -> None:
240
268
  # "secret_key": os.getenv("SECRET_KEY"),
241
269
  "port": 443,
242
270
  "ttl": 300
271
+ },
272
+ "proxy-bucket05": {
273
+ "host": "s3.eu-de.cloud-object-storage.appdomain.cloud",
274
+ "region": "eu-de",
275
+ "access_key": os.getenv("PROXY_BUCKET05_ACCESS_KEY"),
276
+ "secret_key": os.getenv("PROXY_BUCKET05_SECRET_KEY"),
277
+ "port": 443,
278
+ "ttl": 300
279
+ },
280
+ "proxy-aws-bucket01": {
281
+ "host": "s3.eu-west-3.amazonaws.com",
282
+ "region": "eu-west-3",
283
+ "access_key": os.getenv("AWS_ACCESS_KEY"),
284
+ "secret_key": os.getenv("AWS_SECRET_KEY"),
285
+ "port": 443,
286
+ "ttl": 300
243
287
  }
244
288
  }
245
289
 
290
+ hmac_keys= [
291
+ # {
292
+ # "access_key": os.getenv("LOCAL_ACCESS_KEY"),
293
+ # "secret_key": os.getenv("LOCAL_SECRET_KEY")
294
+ # },
295
+ {
296
+ "access_key": os.getenv("LOCAL2_ACCESS_KEY"),
297
+ "secret_key": os.getenv("LOCAL2_SECRET_KEY")
298
+ },
299
+
300
+ ]
301
+
246
302
  ra = ProxyServerConfig(
247
303
  cos_map=cos_map,
248
- bucket_creds_fetcher=do_hmac_creds, # or: do_api_creds
304
+ bucket_creds_fetcher=do_hmac_creds,
249
305
  validator=do_validation,
250
306
  http_port=6190,
251
- https_port=8443,
307
+ # https_port=8443,
252
308
  threads=1,
309
+ # verify=False,
310
+ hmac_keystore=hmac_keys,
311
+ skip_signature_validation=False,
312
+ hmac_fetcher=lookup_secret_key
253
313
  )
254
314
 
255
315
  start_server(ra)
@@ -258,9 +318,6 @@ def main() -> None:
258
318
  if __name__ == "__main__":
259
319
  main()
260
320
 
261
-
262
-
263
-
264
321
  ```
265
322
 
266
323
  Run with [aws-cli](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) (but could be anything compatible with the aws s3 api like polars, spark, presto, ...):
@@ -11,11 +11,16 @@
11
11
 
12
12
  A fast and safe in-process reverse proxy server, based on Cloudflare's [pingora](https://github.com/cloudflare/pingora?tab=readme-ov-file), to reverse proxy AWS and IBM Cloud Object Storage buckets and integrate your Authentication and Authorization services.
13
13
 
14
+ Decouples frontend from backend authentication and authorization.
15
+
16
+
14
17
  - [x] Takes a Python authorization callable (allows you to plug in your own authorization services) and api_key fetch callback function and cos bucket dictionary.
15
18
  - [x] The validation is cached with optional ttl (default 5min, keep it short).
16
19
  - [x] The apikey is used to authenticate against IBM's IAM endpoint and is cached and renewed on expiration. (IBM only)
17
- - [x] If no apikey is provided, a Python function can be passed in to fetch the apikey or hmac keys for any given bucket (run once).
18
- - [x] HMAC support: passing in access and secret id keys (or as json string from python credentials callable), will be used to sign the request (AWS/IBM/..)
20
+ - [x] If no apikey or hmac keypair is provided, a Python function can be passed in to fetch the apikey or hmac keys for any given bucket (run once).
21
+ - [x] HMAC support: passing in access and secret id keys (or as json string from python credentials callable), will be used to sign the upstream request (AWS/IBM/..)
22
+ - [x] frontend request is signed with your own custom keypair
23
+ - [x] supports running in cloud environments / proxy headers for request signature validation
19
24
 
20
25
  The bucket dict contains for each bucket:
21
26
 
@@ -66,6 +71,8 @@ The Python callables take two arguments:
66
71
  def your_request_authorizer(token: str, bucket: str) -> bool
67
72
  ```
68
73
 
74
+ Proxy Configuration
75
+
69
76
 
70
77
 
71
78
 
@@ -101,7 +108,7 @@ Pass in a function which maps bucket to instance (credentials), and a function t
101
108
  The advantage is we can plug in a python authentication function and another function for authorization, allowing for fine-grained control.
102
109
 
103
110
  ## authentication
104
- We use the standard aws hmac header.
111
+ We use the standard aws hmac header and aws v4 request signing algorithm.
105
112
 
106
113
  ## authorization
107
114
  Pass in a callable from python which will be called from rust. This will be cached (ttl) for consecutive requests.
@@ -158,7 +165,8 @@ def strtobool(val: str) -> bool:
158
165
  raise ValueError(f"invalid truth value {val!r}")
159
166
 
160
167
 
161
- def do_api_creds(bucket) -> str:
168
+ def do_api_creds(token: str, bucket: str) -> str:
169
+ """Fetch credentials (ro, rw, access_denied) for the given bucket, depending on the token. """
162
170
  apikey = os.getenv("COS_API_KEY")
163
171
  if not apikey:
164
172
  raise ValueError("COS_API_KEY environment variable not set")
@@ -167,11 +175,13 @@ def do_api_creds(bucket) -> str:
167
175
  return apikey
168
176
 
169
177
 
170
- def do_hmac_creds(bucket) -> str:
178
+ def do_hmac_creds(token: str, bucket: str) -> str:
179
+ """ Fetch HMAC credentials (ro, rw, access_denied) for the given bucket, depending on the token """
171
180
  access_key = os.getenv("ACCESS_KEY")
172
181
  secret_key = os.getenv("SECRET_KEY")
173
182
  if not access_key or not secret_key:
174
183
  raise ValueError("ACCESS_KEY or SECRET_KEY environment variable not set")
184
+
175
185
  print(f"Fetching HMAC credentials for {bucket}...")
176
186
 
177
187
  return json.dumps({
@@ -179,8 +189,27 @@ def do_hmac_creds(bucket) -> str:
179
189
  "secret_key": secret_key
180
190
  })
181
191
 
192
+ def lookup_secret_key(access_key: str) -> str | None:
193
+ # get all environment variables ending in ACCESS_KEY
194
+ access_keys = [{key:value} for key, value in os.environ.items() if key.endswith("ACCESS_KEY") and value==access_key ]
195
+
196
+ if len(access_keys) > 0:
197
+ access_key_var = next((k for k, v in access_keys[0].items() if v == access_key), None)
198
+
199
+ secret_key_var = access_key_var.replace("ACCESS_KEY", "SECRET_KEY")
200
+ return os.getenv(secret_key_var, None)
201
+ else:
202
+ print(f"no access keys found for : {access_key}")
203
+
182
204
 
183
205
  def do_validation(token: str, bucket: str) -> bool:
206
+ """ Authorize the request based on token for the given bucket.
207
+ You can plug in your own authorization service here.
208
+ The token is the authorization token passed in the request.
209
+ The bucket is the bucket name.
210
+ The function should return True if the request is authorized, False otherwise.
211
+ """
212
+
184
213
  print(f"PYTHON: Validating headers: {token} for {bucket}...")
185
214
  # return random.choice([True, False])
186
215
  return True
@@ -195,7 +224,6 @@ def main() -> None:
195
224
  osp.enable_request_counting()
196
225
  print("Request counting enabled")
197
226
 
198
-
199
227
  apikey = os.getenv("COS_API_KEY")
200
228
  if not apikey:
201
229
  raise ValueError("COS_API_KEY environment variable not set")
@@ -221,16 +249,48 @@ def main() -> None:
221
249
  # "secret_key": os.getenv("SECRET_KEY"),
222
250
  "port": 443,
223
251
  "ttl": 300
252
+ },
253
+ "proxy-bucket05": {
254
+ "host": "s3.eu-de.cloud-object-storage.appdomain.cloud",
255
+ "region": "eu-de",
256
+ "access_key": os.getenv("PROXY_BUCKET05_ACCESS_KEY"),
257
+ "secret_key": os.getenv("PROXY_BUCKET05_SECRET_KEY"),
258
+ "port": 443,
259
+ "ttl": 300
260
+ },
261
+ "proxy-aws-bucket01": {
262
+ "host": "s3.eu-west-3.amazonaws.com",
263
+ "region": "eu-west-3",
264
+ "access_key": os.getenv("AWS_ACCESS_KEY"),
265
+ "secret_key": os.getenv("AWS_SECRET_KEY"),
266
+ "port": 443,
267
+ "ttl": 300
224
268
  }
225
269
  }
226
270
 
271
+ hmac_keys= [
272
+ # {
273
+ # "access_key": os.getenv("LOCAL_ACCESS_KEY"),
274
+ # "secret_key": os.getenv("LOCAL_SECRET_KEY")
275
+ # },
276
+ {
277
+ "access_key": os.getenv("LOCAL2_ACCESS_KEY"),
278
+ "secret_key": os.getenv("LOCAL2_SECRET_KEY")
279
+ },
280
+
281
+ ]
282
+
227
283
  ra = ProxyServerConfig(
228
284
  cos_map=cos_map,
229
- bucket_creds_fetcher=do_hmac_creds, # or: do_api_creds
285
+ bucket_creds_fetcher=do_hmac_creds,
230
286
  validator=do_validation,
231
287
  http_port=6190,
232
- https_port=8443,
288
+ # https_port=8443,
233
289
  threads=1,
290
+ # verify=False,
291
+ hmac_keystore=hmac_keys,
292
+ skip_signature_validation=False,
293
+ hmac_fetcher=lookup_secret_key
234
294
  )
235
295
 
236
296
  start_server(ra)
@@ -239,9 +299,6 @@ def main() -> None:
239
299
  if __name__ == "__main__":
240
300
  main()
241
301
 
242
-
243
-
244
-
245
302
  ```
246
303
 
247
304
  Run with [aws-cli](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) (but could be anything compatible with the aws s3 api like polars, spark, presto, ...):
@@ -0,0 +1,34 @@
1
+ use pyo3::pyclass;
2
+
3
+ #[pyclass]
4
+ #[derive(Debug, Clone)]
5
+ pub struct HmacKeyStore {
6
+ access_key: String,
7
+ secret_key: String,
8
+
9
+ }
10
+
11
+ impl HmacKeyStore {
12
+ pub fn new(access_key: String, secret_key: String) -> Self {
13
+ HmacKeyStore {
14
+ access_key,
15
+ secret_key,
16
+ }
17
+ }
18
+
19
+ pub fn get_access_key(&self) -> &str {
20
+ &self.access_key
21
+ }
22
+
23
+ pub fn get_secret_key(&self) -> &str {
24
+ &self.secret_key
25
+ }
26
+ }
27
+ impl Default for HmacKeyStore {
28
+ fn default() -> Self {
29
+ HmacKeyStore {
30
+ access_key: String::new(),
31
+ secret_key: String::new(),
32
+ }
33
+ }
34
+ }
@@ -1,3 +1,4 @@
1
1
  pub mod models;
2
2
  pub mod secrets_proxy;
3
3
  pub mod signer;
4
+ pub mod hmac_keystore;
@@ -1,17 +1,19 @@
1
- use chrono::{DateTime, Utc};
1
+ use chrono::{DateTime, NaiveDateTime, Utc};
2
2
  use http::header::HeaderMap;
3
- use pingora::http::RequestHeader;
3
+ use pingora::{http::RequestHeader, proxy::Session};
4
+ use rustls::crypto::hash::Hash;
4
5
  use sha256::digest;
5
- use tracing::debug;
6
- use std::{collections::HashMap, fmt};
6
+ use tokio::sync::RwLock;
7
+ use tracing::{debug, error, info};
8
+ use std::{collections::{HashMap, HashSet}, fmt, sync::Arc};
7
9
  use url::Url;
8
10
 
9
- use crate::parsers::cos_map::CosMapItem;
11
+ use crate::parsers::{cos_map::CosMapItem, credentials::{parse_credential_scope, parse_token_from_header}};
10
12
 
11
13
  const SHORT_DATE: &str = "%Y%m%d";
12
14
  const LONG_DATETIME: &str = "%Y%m%dT%H%M%SZ";
13
15
 
14
- // AwsSign copied and slightly modified from https://github.com/psnszsn/aws-sign-v4
16
+ // AwsSign copied and modified from https://github.com/psnszsn/aws-sign-v4
15
17
 
16
18
  pub struct AwsSign<'a, T: 'a>
17
19
  where
@@ -24,6 +26,7 @@ where
24
26
  access_key: &'a str,
25
27
  secret_key: &'a str,
26
28
  headers: T,
29
+ payload_override: Option<String>,
27
30
 
28
31
  /*
29
32
  service is the <aws-service-code> that can be found in the service-quotas api.
@@ -70,15 +73,28 @@ impl<'a> AwsSign<'a, HashMap<String, String>> {
70
73
  secret_key: &'a str,
71
74
  service: &'a str,
72
75
  body: &'a B,
76
+ signed_headers: Option<&'a Vec<String>>,
73
77
  ) -> Self {
74
- let allowed = [
75
- "host",
76
- "content-length",
77
- "x-amz-date",
78
- "x-amz-content-sha256",
79
- "x-amz-security-token",
80
- ];
81
-
78
+ let allowed: Vec<&str> = if let Some(sh) = signed_headers {
79
+ sh.iter().map(String::as_str).collect()
80
+ } else {
81
+ vec![
82
+ "host",
83
+ "x-amz-date",
84
+ "range",
85
+ "x-amz-content-sha256",
86
+ "x-amz-security-token",
87
+ ]
88
+ };
89
+ // let allowed = [
90
+ // "host",
91
+ // "x-amz-date",
92
+ // "range",
93
+ // "x-amz-content-sha256",
94
+ // "x-amz-security-token",
95
+ // ];
96
+
97
+ dbg!(&url);
82
98
  let url: Url = url.parse().unwrap();
83
99
  let headers: HashMap<String, String> = headers
84
100
  .iter()
@@ -103,6 +119,7 @@ impl<'a> AwsSign<'a, HashMap<String, String>> {
103
119
  headers,
104
120
  service,
105
121
  body: body.as_ref(),
122
+ payload_override: None,
106
123
  }
107
124
  }
108
125
  }
@@ -131,6 +148,10 @@ impl<'a, T> AwsSign<'a, T>
131
148
  where
132
149
  &'a T: std::iter::IntoIterator<Item = (&'a String, &'a String)>, T: std::fmt::Debug
133
150
  {
151
+ pub fn set_payload_override(&mut self, h: String) {
152
+ self.payload_override = Some(h);
153
+ }
154
+
134
155
  pub fn canonical_header_string(&'a self) -> String {
135
156
  let mut keyvalues = self
136
157
  .headers
@@ -153,7 +174,9 @@ where
153
174
 
154
175
  pub fn canonical_request(&'a self) -> String {
155
176
  let url: &str = self.url.path().into();
156
- let payload_line = if self.body == b"UNSIGNED-PAYLOAD" {
177
+ let payload_line = if let Some(ov) = &self.payload_override {
178
+ ov.clone()
179
+ } else if self.body == b"UNSIGNED-PAYLOAD" {
157
180
  "UNSIGNED-PAYLOAD".into()
158
181
  } else {
159
182
  digest(self.body)
@@ -321,6 +344,7 @@ pub(crate) async fn sign_request(
321
344
  secret_key,
322
345
  "s3",
323
346
  body_bytes,
347
+ None,
324
348
  );
325
349
  debug!("{:#?}", &auth_header);
326
350
 
@@ -335,6 +359,126 @@ pub(crate) async fn sign_request(
335
359
  Ok(())
336
360
  }
337
361
 
362
+
363
+ pub async fn signature_is_valid(
364
+ auth_header: &str,
365
+ session: &Session,
366
+ secret_key: &str,
367
+ ) -> Result<bool, Box<dyn std::error::Error>> {
368
+
369
+ let signed_headers_str = auth_header
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
379
+ let (_, local_access_key) = parse_token_from_header(auth_header)
380
+ .map_err(|_| pingora::Error::new_str("Failed to parse token"))?;
381
+ let local_access_key = local_access_key.to_string();
382
+ if local_access_key.is_empty() {
383
+ error!("Missing access key");
384
+ return Ok(false);
385
+ }
386
+
387
+ // extract provided signature
388
+ let provided_signature = auth_header
389
+ .split("Signature=")
390
+ .nth(1)
391
+ .ok_or_else(|| pingora::Error::new_str("Invalid Authorization header: no Signature"))?
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");
411
+
412
+ let (_, (region, service)) = parse_credential_scope(auth_header)
413
+ .map_err(|_| pingora::Error::new_str("Invalid Credential scope"))?;
414
+
415
+ dbg!(&region);
416
+ dbg!(&service);
417
+
418
+ // determine payload hash header
419
+ let content_sha256 = session
420
+ .req_header()
421
+ .headers
422
+ .get("x-amz-content-sha256")
423
+ .and_then(|h| h.to_str().ok())
424
+ .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
+
430
+ let (body_bytes, payload_override) = if content_sha256 == "UNSIGNED-PAYLOAD" {
431
+ (b"UNSIGNED-PAYLOAD" as &[u8], None)
432
+ } else {
433
+ // we don't have the raw body here, but we do have its hash:
434
+ // tell AwsSign to use this string directly
435
+ (&[] as &[u8], Some(content_sha256.to_owned()))
436
+ };
437
+
438
+ let original_uri = session.req_header().uri.to_string();
439
+ let full_url = if original_uri.starts_with('/') {
440
+ let host = session
441
+ .req_header()
442
+ .headers
443
+ .get("host")
444
+ .ok_or_else(|| pingora::Error::new_str("Missing host header"))?
445
+ .to_str()?;
446
+ format!("https://{}{}", host, original_uri)
447
+ } else {
448
+ original_uri
449
+ };
450
+
451
+ // construct AwsSign and compute signature
452
+ let method = session.req_header().method.to_string();
453
+ let mut signer = AwsSign::new(
454
+ &method,
455
+ &full_url,
456
+ &datetime,
457
+ &session.req_header().headers,
458
+ region,
459
+ &local_access_key,
460
+ &secret_key,
461
+ service,
462
+ body_bytes,
463
+ Some(&signed_headers),
464
+ );
465
+
466
+ if let Some(ov) = payload_override {
467
+ signer.set_payload_override(ov);
468
+ }
469
+
470
+ let signature = signer.sign();
471
+ let computed_signature = signature
472
+ .split("Signature=")
473
+ .nth(1)
474
+ .unwrap_or_default();
475
+
476
+ info!("Provided signature: {}", provided_signature);
477
+ info!("Computed signature: {}", computed_signature);
478
+ Ok(computed_signature == provided_signature)
479
+ }
480
+
481
+
338
482
  #[cfg(test)]
339
483
  mod tests {
340
484
  use super::*;
@@ -349,7 +493,7 @@ mod tests {
349
493
  let datetime = chrono::Utc::now();
350
494
  let url: &str = "https://hi.s3.us-east-1.amazonaws.com/Prod/graphql";
351
495
  let map: HeaderMap = HeaderMap::new();
352
- let aws_sign = AwsSign::new("GET", url, &datetime, &map, "us-east-1", "a", "b", "s3", "");
496
+ let aws_sign = AwsSign::new("GET", url, &datetime, &map, "us-east-1", "a", "b", "s3", "", None);
353
497
  let s = aws_sign.canonical_request();
354
498
  assert_eq!(
355
499
  s,
@@ -372,6 +516,7 @@ mod tests {
372
516
  "b",
373
517
  "s3",
374
518
  "".as_bytes(),
519
+ None,
375
520
  );
376
521
  let s = aws_sign.canonical_request();
377
522
  assert_eq!(
@@ -396,6 +541,7 @@ mod tests {
396
541
  "b",
397
542
  "s3",
398
543
  &body,
544
+ None,
399
545
  );
400
546
  let s = aws_sign.canonical_request();
401
547
  assert_eq!(