@@ -21,6 +21,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121*/
2222
2323using System . Net . Http . Headers ;
24+ using System . Security . Cryptography . X509Certificates ;
2425using Apache . Arrow . Flight ;
2526using Apache . Arrow . Flight . Client ;
2627using Grpc . Core ;
@@ -39,7 +40,7 @@ internal class SpiceFlightClient : IDisposable
3940 private readonly HttpClient ? _httpClient ;
4041 private readonly AsyncRetryPolicy _retryPolicy ;
4142
42- private static GrpcChannelOptions GetGrpcChannelOptions ( string ? appId , string ? apiKey , string ? userAgent , bool useTls )
43+ private static GrpcChannelOptions GetGrpcChannelOptions ( string ? appId , string ? apiKey , string ? userAgent , bool useTls , string ? tlsClientCertFile = null , string ? tlsClientKeyFile = null , string ? tlsRootCertFile = null )
4344 {
4445 var options = new GrpcChannelOptions ( ) ;
4546
@@ -57,12 +58,27 @@ private static GrpcChannelOptions GetGrpcChannelOptions(string? appId, string? a
5758 var handler = new SocketsHttpHandler
5859 {
5960 EnableMultipleHttp2Connections = true ,
60- // Force periodic connection recycling to trigger DNS re-resolution.
61- // Without this, HTTP/2 connections are kept alive indefinitely and
62- // the client can get stuck on stale IPs when backend targets change
63- // (e.g. AWS ALB target rotation).
6461 PooledConnectionLifetime = TimeSpan . FromMinutes ( 5 ) ,
6562 } ;
63+ if ( tlsClientCertFile != null && tlsClientKeyFile != null )
64+ {
65+ var clientCert = X509Certificate2 . CreateFromPemFile ( tlsClientCertFile , tlsClientKeyFile ) ;
66+ handler . SslOptions . ClientCertificates = new X509Certificate2Collection { clientCert } ;
67+ }
68+ if ( tlsRootCertFile != null )
69+ {
70+ #pragma warning disable SYSLIB0057
71+ var caCert = new X509Certificate2 ( tlsRootCertFile ) ;
72+ #pragma warning restore SYSLIB0057
73+ handler . SslOptions . RemoteCertificateValidationCallback = ( sender , cert , chain , errors ) =>
74+ {
75+ if ( errors == System . Net . Security . SslPolicyErrors . None ) return true ;
76+ if ( cert == null || chain == null ) return false ;
77+ chain . ChainPolicy . TrustMode = X509ChainTrustMode . CustomRootTrust ;
78+ chain . ChainPolicy . CustomTrustStore . Add ( caCert ) ;
79+ return chain . Build ( new X509Certificate2 ( cert ) ) ;
80+ } ;
81+ }
6682 options . HttpHandler = handler ;
6783 }
6884#endif
@@ -77,15 +93,31 @@ private static GrpcChannelOptions GetGrpcChannelOptions(string? appId, string? a
7793#if NET8_0_OR_GREATER
7894 if ( useTls )
7995 {
80- messageHandler = new SocketsHttpHandler
96+ var handler = new SocketsHttpHandler
8197 {
8298 EnableMultipleHttp2Connections = true ,
83- // Force periodic connection recycling to trigger DNS re-resolution.
84- // Without this, HTTP/2 connections are kept alive indefinitely and
85- // the client can get stuck on stale IPs when backend targets change
86- // (e.g. AWS ALB target rotation).
8799 PooledConnectionLifetime = TimeSpan . FromMinutes ( 5 ) ,
88100 } ;
101+ if ( tlsClientCertFile != null && tlsClientKeyFile != null )
102+ {
103+ var clientCert = X509Certificate2 . CreateFromPemFile ( tlsClientCertFile , tlsClientKeyFile ) ;
104+ handler . SslOptions . ClientCertificates = new X509Certificate2Collection { clientCert } ;
105+ }
106+ if ( tlsRootCertFile != null )
107+ {
108+ #pragma warning disable SYSLIB0057
109+ var caCert = new X509Certificate2 ( tlsRootCertFile ) ;
110+ #pragma warning restore SYSLIB0057
111+ handler . SslOptions . RemoteCertificateValidationCallback = ( sender , cert , chain , errors ) =>
112+ {
113+ if ( errors == System . Net . Security . SslPolicyErrors . None ) return true ;
114+ if ( cert == null || chain == null ) return false ;
115+ chain . ChainPolicy . TrustMode = X509ChainTrustMode . CustomRootTrust ;
116+ chain . ChainPolicy . CustomTrustStore . Add ( caCert ) ;
117+ return chain . Build ( new X509Certificate2 ( cert ) ) ;
118+ } ;
119+ }
120+ messageHandler = handler ;
89121 }
90122 else
91123#endif
@@ -112,13 +144,13 @@ private static GrpcChannelOptions GetGrpcChannelOptions(string? appId, string? a
112144 return responseHeaders . Get ( "authorization" ) ?? trailers . Get ( "authorization" ) ;
113145 }
114146
115- internal SpiceFlightClient ( string address , int maxRetries , string ? appId , string ? apiKey , string ? userAgent , bool useTls )
147+ internal SpiceFlightClient ( string address , int maxRetries , string ? appId , string ? apiKey , string ? userAgent , bool useTls , string ? tlsClientCertFile = null , string ? tlsClientKeyFile = null , string ? tlsRootCertFile = null )
116148 {
117149 _retryPolicy = RetryPolicyFactory . CreateRpcRetryPolicy (
118150 maxRetries ,
119151 ( ex , ts , attempt ) => RetryPolicyFactory . LogRetry ( "Flight" , ex , ts , attempt ) ) ;
120152
121- var options = GetGrpcChannelOptions ( appId , apiKey , userAgent , useTls ) ;
153+ var options = GetGrpcChannelOptions ( appId , apiKey , userAgent , useTls , tlsClientCertFile , tlsClientKeyFile , tlsRootCertFile ) ;
122154 _httpClient = options . HttpClient ;
123155
124156 _channel = GrpcChannel . ForAddress ( address , options ) ;
0 commit comments