-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCouchbaseHttpClientFactory.cs
More file actions
146 lines (130 loc) · 6.52 KB
/
CouchbaseHttpClientFactory.cs
File metadata and controls
146 lines (130 loc) · 6.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
#region License
/* ************************************************************
*
* @author Couchbase <info@couchbase.com>
* @copyright 2025 Couchbase, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* ************************************************************/
#endregion
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using Couchbase.AnalyticsClient.Certificates;
using Couchbase.AnalyticsClient.HTTP;
using Couchbase.AnalyticsClient.Internal.DnsUtil;
using Couchbase.AnalyticsClient.Internal.DnsUtil.Strategies;
using Couchbase.AnalyticsClient.Internal.Utils;
using Couchbase.AnalyticsClient.Options;
using Microsoft.Extensions.Logging;
namespace Couchbase.AnalyticsClient.Internal.HTTP;
internal class CouchbaseHttpClientFactory : ICouchbaseHttpClientFactory
{
private readonly ICredential _credential;
private readonly SecurityOptions _securityOptions;
private readonly TimeoutOptions _timeoutOptions;
private readonly ILogger<CouchbaseHttpClientFactory> _logger;
private readonly AuthenticationHandler _sharedHandler;
public CouchbaseHttpClientFactory(ICredential credential, ClusterOptions options,
ILogger<CouchbaseHttpClientFactory> logger)
{
ArgumentNullException.ThrowIfNull(options);
_credential = credential;
_securityOptions = options.SecurityOptions ?? throw new ArgumentNullException(nameof(options));
_timeoutOptions = options.TimeoutOptions ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_sharedHandler = CreateClientHandler();
}
/// <summary>
/// Creates and configures an HTTP handler with bidirectional certificate authentication.
/// The handler sets up client certificates so the server can authenticate the client,
/// and configures a RemoteCertificateValidationCallback so the client can authenticate the server.
/// </summary>
/// <returns>
/// An <see cref="AuthenticationHandler"/> configured with:
/// - Client certificates for mutual TLS authentication (when required by the server)
/// - Server certificate validation callback based on the configured trust settings
/// </returns>
/// <remarks>
/// The handler is not configured for Certificate Based Authentication. A username/password credential is still required.
/// </remarks>
private AuthenticationHandler CreateClientHandler()
{
var handler = new SocketsHttpHandler();
ConfigureDnsResolverCallback(handler);
ConfigureClientCertificates(handler);
handler.SslOptions.RemoteCertificateValidationCallback =
CertificateValidation.CreateRemoteCertificateValidationCallback(_securityOptions, _logger);
return new AuthenticationHandler(handler, _credential);
}
private void ConfigureClientCertificates(SocketsHttpHandler handler)
{
handler.SslOptions.EnabledSslProtocols = _securityOptions.SslProtocols;
X509Certificate2Collection certCollection = new X509Certificate2Collection();
switch (_securityOptions.TrustMode)
{
case CertificateTrustMode.CapellaOnly:
certCollection.Add(CertificateValidation.CapellaCaCert);
break;
case CertificateTrustMode.CertificatesOnly:
certCollection.AddRange(_securityOptions.CertificatesValue);
break;
case CertificateTrustMode.PemFilePath:
certCollection.Add(new X509Certificate2(_securityOptions.PathToPemFileValue));
break;
case CertificateTrustMode.PemString:
certCollection.Add(new X509Certificate2(
rawData: System.Text.Encoding.ASCII.GetBytes(_securityOptions.CertificateValue)));
break;
case CertificateTrustMode.Default:
break;
default:
throw new ArgumentOutOfRangeException(nameof(_securityOptions.TrustMode));
}
handler.SslOptions.ClientCertificates = certCollection;
// This emulates the behavior of HttpClientHandler in Manual mode, which selects the first certificate
// from the list which is eligible for use as a client certificate based on having a private key and
// the correct key usage flags.
handler.SslOptions.LocalCertificateSelectionCallback =
(_, _, _, _, _) => CertificateValidation.GetClientCertificate(certCollection)!;
}
/// <summary>
/// Registers a ConnectCallback to the handler to configure the behaviour of each connection attempt.
/// The current behaviour is:
/// - Refresh the DNS record on every request
/// - Connect to a random endpoint from the resolved DNS record
/// - Use the <see cref="TimeoutOptions.ConnectTimeout"/> for each endpoint connection attempt.
/// This means that if ConnectTimeout is set to 10 seconds and there are 3 endpoints, the total time to connect could be up to 30 seconds if all endpoints are slow to respond or unavailable.
/// </summary>
/// <param name="handler">The Http Handler</param>
private void ConfigureDnsResolverCallback(SocketsHttpHandler handler)
{
var connector = new DnsEndpointConnector(
new CountBasedDnsRefreshStrategy(1), // Refresh DNS entries on every request
_timeoutOptions.ConnectTimeout,
EndpointSelectionMode.RandomFromUnusedEndpoints
);
handler.ConnectCallback = async (context, cancellation) =>
{
var socket = await connector.ConnectAsync(context.DnsEndPoint, cancellation).ConfigureAwait(false);
return new NetworkStream(socket, ownsSocket: true);
};
}
public HttpClient Create()
{
var httpClient = new HttpClient(_sharedHandler, false);
ClientIdentifier.SetUserAgent(httpClient.DefaultRequestHeaders);
return httpClient;
}
public HttpCompletionOption DefaultCompletionOption { get; set; } = HttpCompletionOption.ResponseHeadersRead;
}