object-storage-proxy 0.3.7__tar.gz → 0.3.9__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 (30) hide show
  1. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/Cargo.lock +10 -5
  2. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/Cargo.toml +6 -1
  3. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/PKG-INFO +1 -1
  4. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/src/credentials/signer.rs +68 -0
  5. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/src/lib.rs +69 -9
  6. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/.cargo/config.toml +0 -0
  7. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/.github/workflows/ci.yml +0 -0
  8. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/.gitignore +0 -0
  9. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/LICENSE +0 -0
  10. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/README.md +0 -0
  11. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/img/logo.svg +0 -0
  12. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/img/request_lifecycle.svg +0 -0
  13. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/img/request_stages.svg +0 -0
  14. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/pyproject.toml +0 -0
  15. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/requirements.txt +0 -0
  16. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/src/credentials/hmac_keystore.rs +0 -0
  17. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/src/credentials/mod.rs +0 -0
  18. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/src/credentials/models.rs +0 -0
  19. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/src/credentials/secrets_proxy.rs +0 -0
  20. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/src/object_storage_proxy.pyi +0 -0
  21. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/src/parsers/cos_map.rs +0 -0
  22. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/src/parsers/credentials.rs +0 -0
  23. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/src/parsers/keystore.rs +0 -0
  24. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/src/parsers/mod.rs +0 -0
  25. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/src/parsers/path.rs +0 -0
  26. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/src/utils/mod.rs +0 -0
  27. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/src/utils/validator.rs +0 -0
  28. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/test_integration.sh +0 -0
  29. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/test_server.py +0 -0
  30. {object_storage_proxy-0.3.7 → object_storage_proxy-0.3.9}/uv.lock +0 -0
@@ -382,9 +382,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
382
382
 
383
383
  [[package]]
384
384
  name = "bytes"
385
- version = "1.10.0"
385
+ version = "1.10.1"
386
386
  source = "registry+https://github.com/rust-lang/crates.io-index"
387
- checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9"
387
+ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
388
388
 
389
389
  [[package]]
390
390
  name = "cc"
@@ -1701,19 +1701,23 @@ dependencies = [
1701
1701
 
1702
1702
  [[package]]
1703
1703
  name = "object-storage-proxy"
1704
- version = "0.3.7"
1704
+ version = "0.3.9"
1705
1705
  dependencies = [
1706
+ "async-stream",
1706
1707
  "async-trait",
1708
+ "bytes",
1707
1709
  "chrono",
1708
1710
  "clap 4.5.37",
1709
1711
  "dotenv",
1710
1712
  "env_logger",
1711
1713
  "futures",
1714
+ "futures-core",
1712
1715
  "hex",
1713
1716
  "http",
1714
1717
  "log",
1715
1718
  "nom 8.0.0",
1716
1719
  "percent-encoding",
1720
+ "pin-project-lite",
1717
1721
  "pingora",
1718
1722
  "prometheus 0.14.0",
1719
1723
  "pyo3",
@@ -1725,6 +1729,7 @@ dependencies = [
1725
1729
  "serde_json",
1726
1730
  "sha256",
1727
1731
  "tokio",
1732
+ "tokio-util",
1728
1733
  "tracing",
1729
1734
  "tracing-subscriber",
1730
1735
  "url",
@@ -3256,9 +3261,9 @@ dependencies = [
3256
3261
 
3257
3262
  [[package]]
3258
3263
  name = "tokio-util"
3259
- version = "0.7.13"
3264
+ version = "0.7.15"
3260
3265
  source = "registry+https://github.com/rust-lang/crates.io-index"
3261
- checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078"
3266
+ checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
3262
3267
  dependencies = [
3263
3268
  "bytes",
3264
3269
  "futures-core",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "object-storage-proxy"
3
- version = "0.3.7"
3
+ version = "0.3.9"
4
4
  edition = "2024"
5
5
 
6
6
  [dependencies]
@@ -34,6 +34,11 @@ ring = "0.17.14"
34
34
  hex = "0.4.3"
35
35
  regex = "1.11.1"
36
36
  percent-encoding = "2.3.1"
37
+ bytes = "1.10.1"
38
+ async-stream = "0.3.6"
39
+ tokio-util = "0.7.15"
40
+ futures-core = "0.3.31"
41
+ pin-project-lite = "0.2.16"
37
42
 
38
43
  # [build-dependencies]
39
44
  # 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.7
3
+ Version: 0.3.9
4
4
  Classifier: License :: Other/Proprietary License
5
5
  Classifier: Programming Language :: Rust
6
6
  Classifier: Programming Language :: Python :: Implementation :: CPython
@@ -6,6 +6,10 @@ use tracing::{debug, error};
6
6
  use std::{collections::{HashMap, HashSet}, fmt};
7
7
  use url::Url;
8
8
 
9
+ use ring::hmac;
10
+
11
+ use bytes::{Bytes, BytesMut};
12
+
9
13
  use crate::parsers::{cos_map::CosMapItem, credentials::{parse_credential_scope, parse_token_from_header}};
10
14
 
11
15
  const SHORT_DATE: &str = "%Y%m%d";
@@ -728,6 +732,70 @@ pub async fn signature_is_valid_for_presigned(
728
732
  ).await
729
733
  }
730
734
 
735
+
736
+
737
+ /// Build a stream whose items are *already* wrapped in
738
+ /// “AWS-chunk-signed” envelopes.
739
+ ///
740
+ /// * `body` – raw payload implementing `AsyncRead`
741
+ /// * `signing_key` – result of the usual `signing_key()` step
742
+ /// * `scope` – e.g. `"20250501/eu-west-3/s3/aws4_request"`
743
+ /// * `ts` – the `X-Amz-Date` you put in the header (`YYYYMMDDThhmmssZ`)
744
+ /// * `seed_sig` – the `Signature=` value you computed for the
745
+ /// *headers* (the one that goes into `Authorization:`)
746
+ ///
747
+ /// ```text
748
+ /// ┌──── header chunk ────┐┌── data ─┐┌─ CRLF ─┐
749
+ /// <hex-len>;chunk-signature=<sig>\r\n<bytes>\r\n
750
+ /// ```
751
+ ///
752
+ /// The very last frame is
753
+ /// ```text
754
+ /// 0;chunk-signature=<final-sig>\r\n\r\n
755
+ /// ```
756
+ pub async fn wrap_streaming_body(
757
+ session: &mut Session,
758
+ upstream_request: &mut RequestHeader,
759
+ region: &str,
760
+ access_key: &str,
761
+ secret_key: &str,
762
+ ) -> Result<(), Box<dyn std::error::Error>> {
763
+ // 1. pull the COMPLETE body from the client
764
+ let body: Bytes = session.read_request_body().await.expect("Failed to read request body").unwrap();
765
+
766
+ // 2. overwrite the x-amz-* headers so that we can sign UNSIGNED-PAYLOAD
767
+ upstream_request.insert_header("x-amz-content-sha256", "UNSIGNED-PAYLOAD")?;
768
+ upstream_request.remove_header("x-amz-decoded-content-length");
769
+ upstream_request.insert_header("content-length", body.len().to_string())?;
770
+
771
+ // 3. resign
772
+ let ts = chrono::Utc::now();
773
+ let url = upstream_request.uri.to_string();
774
+ upstream_request.insert_header("x-amz-date", ts.format("%Y%m%dT%H%M%SZ").to_string())?;
775
+ let signer = AwsSign::new(
776
+ upstream_request.method.as_str(),
777
+ &url,
778
+ &ts,
779
+ &upstream_request.headers,
780
+ region,
781
+ access_key,
782
+ secret_key,
783
+ "s3",
784
+ b"UNSIGNED-PAYLOAD",
785
+ None,
786
+ );
787
+ let auth = signer.sign();
788
+ upstream_request.insert_header("authorization", auth)?;
789
+
790
+ let end_of_stream: bool = session.is_body_done();
791
+
792
+ // 4. finally attach the body
793
+ session.write_response_body(Some(body), end_of_stream).await?;
794
+
795
+ Ok(())
796
+ }
797
+
798
+
731
799
  #[cfg(test)]
732
800
  mod tests {
733
801
  use super::*;
@@ -1,13 +1,12 @@
1
1
  #![warn(clippy::all)]
2
2
  use async_trait::async_trait;
3
- use credentials::signer::{signature_is_valid_for_presigned, signature_is_valid_for_request};
3
+ use credentials::signer::{signature_is_valid_for_presigned, signature_is_valid_for_request, wrap_streaming_body};
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
9
  use pingora::http::ResponseHeader;
10
- use pingora::protocols::ALPN;
11
10
  use pingora::Result;
12
11
  use pingora::proxy::{ProxyHttp, Session};
13
12
  use pingora::server::Server;
@@ -29,6 +28,9 @@ use tracing::{debug, error, info};
29
28
  use tracing_subscriber::EnvFilter;
30
29
  use tracing_subscriber::fmt::time::ChronoLocal;
31
30
 
31
+ use tokio_util::io::StreamReader;
32
+ use futures::TryStreamExt;
33
+
32
34
  pub mod parsers;
33
35
  use parsers::credentials::{parse_presigned_params, parse_token_from_header};
34
36
  use parsers::path::parse_path;
@@ -550,7 +552,7 @@ impl ProxyHttp for MyProxy {
550
552
 
551
553
  async fn upstream_request_filter(
552
554
  &self,
553
- _session: &mut Session,
555
+ session: &mut Session,
554
556
  upstream_request: &mut pingora::http::RequestHeader,
555
557
  ctx: &mut Self::CTX,
556
558
  ) -> Result<()> {
@@ -647,26 +649,84 @@ impl ProxyHttp for MyProxy {
647
649
  // "x-amz-trailer",
648
650
  ];
649
651
 
652
+
650
653
  let to_check: Vec<String> = upstream_request
651
654
  .headers
652
655
  .iter()
653
656
  .map(|(name, _)| name.as_str().to_owned())
654
657
  .collect();
655
658
 
659
+ // for name in to_check {
660
+ // if !allowed.contains(&name.as_str()) {
661
+ // let _ = upstream_request.remove_header(&name);
662
+ // }
663
+ // }
664
+
656
665
  for name in to_check {
657
- if !allowed.contains(&name.as_str()) {
666
+ let keep = allowed.contains(&name.as_str())
667
+ || name.starts_with("x-amz-checksum-");
668
+ if !keep {
658
669
  let _ = upstream_request.remove_header(&name);
659
670
  }
660
671
  }
661
672
 
662
673
  if maybe_hmac {
663
674
  debug!("HMAC: Signing request for bucket: {}", hdr_bucket);
664
- sign_request(upstream_request, bucket_config.as_ref().unwrap())
665
- .await
666
- .map_err(|e| {
667
- error!("Failed to sign request for {}: {e}", hdr_bucket);
668
- pingora::Error::new_str("Failed to sign request")
675
+
676
+ let streaming = {
677
+ upstream_request
678
+ .headers
679
+ .get("x-amz-content-sha256")
680
+ .map(|v| v.as_bytes().starts_with(b"STREAMING-AWS4"))
681
+ .unwrap_or(false)
682
+ };
683
+
684
+ if streaming {
685
+ let auth_header = session
686
+ .req_header()
687
+ .headers
688
+ .get("authorization")
689
+ .and_then(|h| h.to_str().ok())
690
+ .map(ToString::to_string)
691
+ .unwrap_or_default();
692
+
693
+ let access_key = parse_token_from_header(&auth_header)
694
+ .map_err(|_| pingora::Error::new_str("Failed to parse access_key"))?
695
+ .1
696
+ .to_string();
697
+
698
+ let secret_key = {
699
+ let map = ctx.hmac_keystore.read().await;
700
+ map.get(&access_key).cloned()
701
+ };
702
+
703
+ if secret_key.is_none() {
704
+ error!("No secret key found for access key: {}", access_key);
705
+ return Err(pingora::Error::new_str("No secret key found"));
706
+ }
707
+ let secret_key = secret_key.unwrap();
708
+
709
+ wrap_streaming_body(
710
+ session,
711
+ upstream_request,
712
+ "eu-west-3", // <- region for the bucket
713
+ &access_key, // <- retrieved from your CosMapItem
714
+ &secret_key,
715
+ )
716
+ .await.map_err(|e| {
717
+ error!("Failed to wrap streaming body: {e}");
718
+ pingora::Error::new_str("Failed to wrap streaming body")
669
719
  })?;
720
+ dbg!("streaming signature!!");
721
+ } else {
722
+ sign_request(upstream_request, bucket_config.as_ref().unwrap())
723
+ .await
724
+ .map_err(|e| {
725
+ error!("Failed to sign request for {}: {e}", hdr_bucket);
726
+ pingora::Error::new_str("Failed to sign request")
727
+ })?;
728
+ }
729
+
670
730
  debug!("Request signed for bucket: {}", hdr_bucket);
671
731
  debug!("{:#?}", &upstream_request.headers);
672
732
  } else {