@@ -14,9 +14,18 @@ use tracing::{info, warn};
1414
1515/// Header name for pinning service endpoint
1616pub const HEADER_PINNING_SERVICE : & str = "x-pinning-service" ;
17+ /// S3-compatible metadata header for pinning service endpoint
18+ pub const HEADER_AMZ_PINNING_SERVICE : & str = "x-amz-meta-x-pinning-service" ;
1719
1820/// Header name for pinning service token
1921pub const HEADER_PINNING_TOKEN : & str = "x-pinning-token" ;
22+ /// S3-compatible metadata header for pinning service token
23+ pub const HEADER_AMZ_PINNING_TOKEN : & str = "x-amz-meta-x-pinning-token" ;
24+
25+ /// Header name for pinning name
26+ pub const HEADER_PINNING_NAME : & str = "x-pinning-name" ;
27+ /// S3-compatible metadata header for pinning name
28+ pub const HEADER_AMZ_PINNING_NAME : & str = "x-amz-meta-x-pinning-name" ;
2029
2130/// Extracted pinning credentials from request headers
2231#[ derive( Debug , Clone ) ]
@@ -32,13 +41,14 @@ pub struct PinningCredentials {
3241impl PinningCredentials {
3342 /// Extract pinning credentials from request headers
3443 ///
44+ /// Checks both direct headers (x-pinning-*) and S3-compatible metadata headers
45+ /// (x-amz-meta-x-pinning-*) for compatibility with S3 client libraries like MinIO.
46+ ///
3547 /// Returns None if headers are not present (pinning not requested)
3648 /// Security audit fix #3: Validates endpoint to prevent SSRF
3749 pub fn from_headers ( headers : & HeaderMap ) -> Option < Self > {
38- let endpoint = headers
39- . get ( HEADER_PINNING_SERVICE )
40- . and_then ( |v| v. to_str ( ) . ok ( ) )
41- . map ( |s| s. to_string ( ) ) ?;
50+ // Check direct header first, then fall back to x-amz-meta-* prefixed version
51+ let endpoint = Self :: get_header_value ( headers, HEADER_PINNING_SERVICE , HEADER_AMZ_PINNING_SERVICE ) ?;
4252
4353 // Security audit fix #3: Validate endpoint
4454 if !Self :: is_valid_pinning_endpoint ( & endpoint) {
@@ -48,16 +58,10 @@ impl PinningCredentials {
4858 return None ;
4959 }
5060
51- let token = headers
52- . get ( HEADER_PINNING_TOKEN )
53- . and_then ( |v| v. to_str ( ) . ok ( ) )
54- . map ( |s| s. to_string ( ) ) ?;
61+ let token = Self :: get_header_value ( headers, HEADER_PINNING_TOKEN , HEADER_AMZ_PINNING_TOKEN ) ?;
5562
56- // Optional: pin name from x-pinning-name header
57- let name = headers
58- . get ( "x-pinning-name" )
59- . and_then ( |v| v. to_str ( ) . ok ( ) )
60- . map ( |s| s. to_string ( ) ) ;
63+ // Optional: pin name from x-pinning-name header (or x-amz-meta-x-pinning-name)
64+ let name = Self :: get_header_value ( headers, HEADER_PINNING_NAME , HEADER_AMZ_PINNING_NAME ) ;
6165
6266 Some ( Self {
6367 endpoint,
@@ -66,6 +70,15 @@ impl PinningCredentials {
6670 } )
6771 }
6872
73+ /// Get a header value, checking the direct header first, then the x-amz-meta-* prefixed version
74+ fn get_header_value ( headers : & HeaderMap , direct : & str , amz_meta : & str ) -> Option < String > {
75+ headers
76+ . get ( direct)
77+ . or_else ( || headers. get ( amz_meta) )
78+ . and_then ( |v| v. to_str ( ) . ok ( ) )
79+ . map ( |s| s. to_string ( ) )
80+ }
81+
6982 /// Security audit fix #3: Validate pinning endpoint to prevent SSRF
7083 /// - Must use https scheme
7184 /// - Must not be a private/localhost address
@@ -124,12 +137,15 @@ impl PinningCredentials {
124137pub async fn pin_for_user ( headers : & HeaderMap , cid : & Cid , object_key : Option < & str > ) {
125138 // Security audit fix #2: Don't log header VALUES (contains tokens)
126139 // Only log presence of headers, never their values
127- let has_service = headers. get ( HEADER_PINNING_SERVICE ) . is_some ( ) ;
128- let has_token = headers. get ( HEADER_PINNING_TOKEN ) . is_some ( ) ;
140+ // Check both direct and x-amz-meta-* prefixed headers
141+ let has_service = headers. get ( HEADER_PINNING_SERVICE ) . is_some ( )
142+ || headers. get ( HEADER_AMZ_PINNING_SERVICE ) . is_some ( ) ;
143+ let has_token = headers. get ( HEADER_PINNING_TOKEN ) . is_some ( )
144+ || headers. get ( HEADER_AMZ_PINNING_TOKEN ) . is_some ( ) ;
129145 tracing:: debug!(
130146 has_pinning_service = has_service,
131147 has_pinning_token = has_token,
132- "Checking for pinning credentials"
148+ "Checking for pinning credentials (direct or x-amz-meta-* headers) "
133149 ) ;
134150
135151 if let Some ( creds) = PinningCredentials :: from_headers ( headers) {
@@ -263,4 +279,55 @@ mod tests {
263279 // Missing token
264280 assert ! ( PinningCredentials :: from_headers( & headers) . is_none( ) ) ;
265281 }
282+
283+ #[ test]
284+ fn test_s3_compatible_amz_meta_headers ( ) {
285+ // Test that x-amz-meta-x-pinning-* headers work (S3 client compatibility)
286+ let mut headers = HeaderMap :: new ( ) ;
287+ headers. insert (
288+ HEADER_AMZ_PINNING_SERVICE ,
289+ HeaderValue :: from_static ( "https://api.pinata.cloud/psa" ) ,
290+ ) ;
291+ headers. insert (
292+ HEADER_AMZ_PINNING_TOKEN ,
293+ HeaderValue :: from_static ( "test-token-456" ) ,
294+ ) ;
295+ headers. insert (
296+ HEADER_AMZ_PINNING_NAME ,
297+ HeaderValue :: from_static ( "my-pin-name" ) ,
298+ ) ;
299+
300+ let creds = PinningCredentials :: from_headers ( & headers) . unwrap ( ) ;
301+ assert_eq ! ( creds. endpoint, "https://api.pinata.cloud/psa" ) ;
302+ assert_eq ! ( creds. token, "test-token-456" ) ;
303+ assert_eq ! ( creds. name, Some ( "my-pin-name" . to_string( ) ) ) ;
304+ }
305+
306+ #[ test]
307+ fn test_direct_headers_take_precedence ( ) {
308+ // Direct headers should take precedence over x-amz-meta-* headers
309+ let mut headers = HeaderMap :: new ( ) ;
310+ // Direct headers
311+ headers. insert (
312+ HEADER_PINNING_SERVICE ,
313+ HeaderValue :: from_static ( "https://direct.example.com/psa" ) ,
314+ ) ;
315+ headers. insert (
316+ HEADER_PINNING_TOKEN ,
317+ HeaderValue :: from_static ( "direct-token" ) ,
318+ ) ;
319+ // Also add x-amz-meta-* headers (should be ignored)
320+ headers. insert (
321+ HEADER_AMZ_PINNING_SERVICE ,
322+ HeaderValue :: from_static ( "https://amz.example.com/psa" ) ,
323+ ) ;
324+ headers. insert (
325+ HEADER_AMZ_PINNING_TOKEN ,
326+ HeaderValue :: from_static ( "amz-token" ) ,
327+ ) ;
328+
329+ let creds = PinningCredentials :: from_headers ( & headers) . unwrap ( ) ;
330+ assert_eq ! ( creds. endpoint, "https://direct.example.com/psa" ) ;
331+ assert_eq ! ( creds. token, "direct-token" ) ;
332+ }
266333}
0 commit comments