object-storage-proxy 0.3.8__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.
- {object_storage_proxy-0.3.8 → object_storage_proxy-0.3.9}/Cargo.lock +10 -5
- {object_storage_proxy-0.3.8 → object_storage_proxy-0.3.9}/Cargo.toml +6 -1
- {object_storage_proxy-0.3.8 → object_storage_proxy-0.3.9}/PKG-INFO +1 -1
- {object_storage_proxy-0.3.8 → object_storage_proxy-0.3.9}/src/credentials/signer.rs +68 -0
- {object_storage_proxy-0.3.8 → object_storage_proxy-0.3.9}/src/lib.rs +59 -8
- {object_storage_proxy-0.3.8 → object_storage_proxy-0.3.9}/.cargo/config.toml +0 -0
- {object_storage_proxy-0.3.8 → object_storage_proxy-0.3.9}/.github/workflows/ci.yml +0 -0
- {object_storage_proxy-0.3.8 → object_storage_proxy-0.3.9}/.gitignore +0 -0
- {object_storage_proxy-0.3.8 → object_storage_proxy-0.3.9}/LICENSE +0 -0
- {object_storage_proxy-0.3.8 → object_storage_proxy-0.3.9}/README.md +0 -0
- {object_storage_proxy-0.3.8 → object_storage_proxy-0.3.9}/img/logo.svg +0 -0
- {object_storage_proxy-0.3.8 → object_storage_proxy-0.3.9}/img/request_lifecycle.svg +0 -0
- {object_storage_proxy-0.3.8 → object_storage_proxy-0.3.9}/img/request_stages.svg +0 -0
- {object_storage_proxy-0.3.8 → object_storage_proxy-0.3.9}/pyproject.toml +0 -0
- {object_storage_proxy-0.3.8 → object_storage_proxy-0.3.9}/requirements.txt +0 -0
- {object_storage_proxy-0.3.8 → object_storage_proxy-0.3.9}/src/credentials/hmac_keystore.rs +0 -0
- {object_storage_proxy-0.3.8 → object_storage_proxy-0.3.9}/src/credentials/mod.rs +0 -0
- {object_storage_proxy-0.3.8 → object_storage_proxy-0.3.9}/src/credentials/models.rs +0 -0
- {object_storage_proxy-0.3.8 → object_storage_proxy-0.3.9}/src/credentials/secrets_proxy.rs +0 -0
- {object_storage_proxy-0.3.8 → object_storage_proxy-0.3.9}/src/object_storage_proxy.pyi +0 -0
- {object_storage_proxy-0.3.8 → object_storage_proxy-0.3.9}/src/parsers/cos_map.rs +0 -0
- {object_storage_proxy-0.3.8 → object_storage_proxy-0.3.9}/src/parsers/credentials.rs +0 -0
- {object_storage_proxy-0.3.8 → object_storage_proxy-0.3.9}/src/parsers/keystore.rs +0 -0
- {object_storage_proxy-0.3.8 → object_storage_proxy-0.3.9}/src/parsers/mod.rs +0 -0
- {object_storage_proxy-0.3.8 → object_storage_proxy-0.3.9}/src/parsers/path.rs +0 -0
- {object_storage_proxy-0.3.8 → object_storage_proxy-0.3.9}/src/utils/mod.rs +0 -0
- {object_storage_proxy-0.3.8 → object_storage_proxy-0.3.9}/src/utils/validator.rs +0 -0
- {object_storage_proxy-0.3.8 → object_storage_proxy-0.3.9}/test_integration.sh +0 -0
- {object_storage_proxy-0.3.8 → object_storage_proxy-0.3.9}/test_server.py +0 -0
- {object_storage_proxy-0.3.8 → 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.
|
|
385
|
+
version = "1.10.1"
|
|
386
386
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
387
|
-
checksum = "
|
|
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.
|
|
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.
|
|
3264
|
+
version = "0.7.15"
|
|
3260
3265
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
3261
|
-
checksum = "
|
|
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.
|
|
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"] }
|
|
@@ -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
|
-
|
|
555
|
+
session: &mut Session,
|
|
554
556
|
upstream_request: &mut pingora::http::RequestHeader,
|
|
555
557
|
ctx: &mut Self::CTX,
|
|
556
558
|
) -> Result<()> {
|
|
@@ -670,12 +672,61 @@ impl ProxyHttp for MyProxy {
|
|
|
670
672
|
|
|
671
673
|
if maybe_hmac {
|
|
672
674
|
debug!("HMAC: Signing request for bucket: {}", hdr_bucket);
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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")
|
|
678
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
|
+
|
|
679
730
|
debug!("Request signed for bucket: {}", hdr_bucket);
|
|
680
731
|
debug!("{:#?}", &upstream_request.headers);
|
|
681
732
|
} else {
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|