@@ -9,21 +9,26 @@ use aws_sigv4::http_request::{
99 SignableBody , SignableRequest , SignatureLocation , SigningSettings , sign,
1010} ;
1111use aws_sigv4:: sign:: v4;
12+ use aws_smithy_runtime_api:: box_error:: BoxError ;
1213use aws_smithy_runtime_api:: client:: http:: {
1314 HttpClient , HttpConnector , HttpConnectorFuture , HttpConnectorSettings , SharedHttpConnector ,
1415} ;
16+ use aws_smithy_runtime_api:: client:: interceptors:: Intercept ;
17+ use aws_smithy_runtime_api:: client:: interceptors:: context:: BeforeTransmitInterceptorContextMut ;
1518use aws_smithy_runtime_api:: client:: orchestrator:: HttpRequest ;
1619use aws_smithy_runtime_api:: client:: result:: ConnectorError ;
1720use aws_smithy_runtime_api:: client:: runtime_components:: RuntimeComponents ;
1821use aws_smithy_runtime_api:: http:: { Response , StatusCode } ;
1922use aws_smithy_types:: body:: SdkBody ;
23+ use aws_smithy_types:: config_bag:: ConfigBag ;
2024use bytes:: Bytes ;
2125use jiff:: Timestamp ;
2226use quick_xml:: de:: from_str as from_xml_str;
2327use rc_core:: {
2428 Alias , BucketNotification , Capabilities , CorsRule , Error , LifecycleRule , ListOptions ,
2529 ListResult , NotificationTarget , ObjectInfo , ObjectStore , ObjectVersion ,
26- ObjectVersionListResult , RemotePath , ReplicationConfiguration , Result , SelectOptions ,
30+ ObjectVersionListResult , RemotePath , ReplicationConfiguration , RequestHeader , Result ,
31+ SelectOptions , global_request_headers,
2732} ;
2833use reqwest:: Method ;
2934use reqwest:: header:: { CONTENT_TYPE , HeaderMap , HeaderName , HeaderValue } ;
@@ -673,6 +678,7 @@ pub struct S3Client {
673678 inner : aws_sdk_s3:: Client ,
674679 xml_http_client : reqwest:: Client ,
675680 alias : Alias ,
681+ request_headers : Vec < RequestHeader > ,
676682}
677683
678684/// Request-level options for delete operations.
@@ -682,6 +688,33 @@ pub struct DeleteRequestOptions {
682688 pub force_delete : bool ,
683689}
684690
691+ #[ derive( Debug , Clone ) ]
692+ struct CustomHeaderInterceptor {
693+ headers : Vec < RequestHeader > ,
694+ }
695+
696+ impl Intercept for CustomHeaderInterceptor {
697+ fn name ( & self ) -> & ' static str {
698+ "CustomHeaderInterceptor"
699+ }
700+
701+ fn modify_before_signing (
702+ & self ,
703+ context : & mut BeforeTransmitInterceptorContextMut < ' _ > ,
704+ _runtime_components : & RuntimeComponents ,
705+ _cfg : & mut ConfigBag ,
706+ ) -> std:: result:: Result < ( ) , BoxError > {
707+ let request = context. request_mut ( ) ;
708+ for header in & self . headers {
709+ request
710+ . headers_mut ( )
711+ . try_insert ( header. name . clone ( ) , header. value . clone ( ) )
712+ . map_err ( |error| Box :: new ( error) as BoxError ) ?;
713+ }
714+ Ok ( ( ) )
715+ }
716+ }
717+
685718impl S3Client {
686719 /// Create a new S3 client from an alias configuration
687720 pub async fn new ( alias : Alias ) -> Result < Self > {
@@ -718,7 +751,8 @@ impl S3Client {
718751 let config = config_loader. load ( ) . await ;
719752
720753 // Build S3 client with path-style addressing for compatibility
721- let s3_config = aws_sdk_s3:: config:: Builder :: from ( & config)
754+ let request_headers = global_request_headers ( ) ;
755+ let mut s3_config_builder = aws_sdk_s3:: config:: Builder :: from ( & config)
722756 . force_path_style ( force_path_style_for_alias ( & alias) )
723757 // Improve compatibility with S3-compatible backends by only sending request
724758 // checksums when the operation explicitly requires them.
@@ -727,15 +761,23 @@ impl S3Client {
727761 )
728762 . response_checksum_validation (
729763 aws_sdk_s3:: config:: ResponseChecksumValidation :: WhenRequired ,
730- )
731- . build ( ) ;
764+ ) ;
765+
766+ if !request_headers. is_empty ( ) {
767+ s3_config_builder = s3_config_builder. interceptor ( CustomHeaderInterceptor {
768+ headers : request_headers. clone ( ) ,
769+ } ) ;
770+ }
771+
772+ let s3_config = s3_config_builder. build ( ) ;
732773
733774 let client = aws_sdk_s3:: Client :: from_conf ( s3_config) ;
734775
735776 Ok ( Self {
736777 inner : client,
737778 xml_http_client,
738779 alias,
780+ request_headers,
739781 } )
740782 }
741783
@@ -1148,6 +1190,14 @@ impl S3Client {
11481190 ) ;
11491191 }
11501192
1193+ for header in & self . request_headers {
1194+ let name = HeaderName :: from_bytes ( header. name . as_bytes ( ) )
1195+ . map_err ( |e| Error :: Auth ( format ! ( "Invalid custom header name: {e}" ) ) ) ?;
1196+ let value = HeaderValue :: from_str ( & header. value )
1197+ . map_err ( |e| Error :: Auth ( format ! ( "Invalid custom header value: {e}" ) ) ) ?;
1198+ headers. insert ( name, value) ;
1199+ }
1200+
11511201 let signed_headers = self
11521202 . sign_xml_request ( & method, url. as_str ( ) , & headers, & body)
11531203 . await ?;
@@ -2767,6 +2817,14 @@ mod tests {
27672817 fn test_s3_client_with_endpoint (
27682818 endpoint : & str ,
27692819 response : Option < http:: Response < SdkBody > > ,
2820+ ) -> ( S3Client , CaptureRequestReceiver ) {
2821+ test_s3_client_with_endpoint_and_headers ( endpoint, response, Vec :: new ( ) )
2822+ }
2823+
2824+ fn test_s3_client_with_endpoint_and_headers (
2825+ endpoint : & str ,
2826+ response : Option < http:: Response < SdkBody > > ,
2827+ request_headers : Vec < RequestHeader > ,
27702828 ) -> ( S3Client , CaptureRequestReceiver ) {
27712829 let ( http_client, request_receiver) = capture_request ( response) ;
27722830 let credentials = Credentials :: new (
@@ -2776,20 +2834,28 @@ mod tests {
27762834 None ,
27772835 "rc-test-credentials" ,
27782836 ) ;
2779- let config = aws_sdk_s3:: config:: Builder :: new ( )
2837+ let mut config_builder = aws_sdk_s3:: config:: Builder :: new ( )
27802838 . credentials_provider ( credentials)
27812839 . endpoint_url ( endpoint)
27822840 . region ( aws_sdk_s3:: config:: Region :: new ( "us-east-1" ) )
27832841 . force_path_style ( true )
27842842 . behavior_version_latest ( )
2785- . http_client ( http_client)
2786- . build ( ) ;
2843+ . http_client ( http_client) ;
2844+
2845+ if !request_headers. is_empty ( ) {
2846+ config_builder = config_builder. interceptor ( CustomHeaderInterceptor {
2847+ headers : request_headers. clone ( ) ,
2848+ } ) ;
2849+ }
2850+
2851+ let config = config_builder. build ( ) ;
27872852
27882853 let alias = Alias :: new ( "test" , endpoint, "access-key" , "secret-key" ) ;
27892854 let client = S3Client {
27902855 inner : aws_sdk_s3:: Client :: from_conf ( config) ,
27912856 xml_http_client : reqwest:: Client :: new ( ) ,
27922857 alias,
2858+ request_headers,
27932859 } ;
27942860
27952861 ( client, request_receiver)
@@ -3469,6 +3535,34 @@ mod tests {
34693535 assert_eq ! ( request. headers( ) . get( "x-rustfs-force-delete" ) , Some ( "true" ) ) ;
34703536 }
34713537
3538+ #[ tokio:: test]
3539+ async fn custom_headers_are_added_before_sending_sdk_requests ( ) {
3540+ let ( client, request_receiver) = test_s3_client_with_endpoint_and_headers (
3541+ "https://example.com" ,
3542+ None ,
3543+ vec ! [ RequestHeader {
3544+ name: "x-amz-bucket-encrypt-enabled" . to_string( ) ,
3545+ value: "1" . to_string( ) ,
3546+ } ] ,
3547+ ) ;
3548+ let path = RemotePath :: new ( "test" , "bucket" , "key.txt" ) ;
3549+
3550+ let _ = client. delete_object ( & path) . await ;
3551+
3552+ let request = request_receiver. expect_request ( ) ;
3553+ assert_eq ! (
3554+ request. headers( ) . get( "x-amz-bucket-encrypt-enabled" ) ,
3555+ Some ( "1" )
3556+ ) ;
3557+ assert ! (
3558+ request
3559+ . headers( )
3560+ . get( "authorization" )
3561+ . expect( "authorization header" )
3562+ . contains( "x-amz-bucket-encrypt-enabled" )
3563+ ) ;
3564+ }
3565+
34723566 #[ tokio:: test]
34733567 async fn delete_object_without_force_delete_omits_rustfs_header ( ) {
34743568 let ( client, request_receiver) = test_s3_client ( None ) ;
0 commit comments