feat: harden control-plane deployment
Build web service image / build (push) Successful in 48s

This commit is contained in:
2026-09-12 11:09:42 +08:00
parent 13c31fc902
commit 0d29b828bb
22 changed files with 1213 additions and 71 deletions
+47 -3
View File
@@ -1,4 +1,6 @@
using System.Net.Http.Headers;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
@@ -18,13 +20,24 @@ public sealed class RemoteControlClient : IDisposable
private readonly bool _ownsHttp;
private readonly RemoteAgentOptions _options;
private readonly Uri? _baseAddress;
private readonly X509Certificate2? _clientCertificate;
private readonly X509Certificate2? _serverCaCertificate;
private bool _authenticated;
public RemoteControlClient(RemoteAgentOptions options, HttpClient? httpClient = null)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_ownsHttp = httpClient is null;
_http = httpClient ?? new HttpClient();
if (httpClient is null)
{
_clientCertificate = LoadClientCertificate(options);
_serverCaCertificate = LoadServerCaCertificate(options);
_http = CreateHttpClient(_clientCertificate, _serverCaCertificate);
}
else
{
_http = httpClient;
}
if (Uri.TryCreate(options.AuthAddress, UriKind.Absolute, out var address))
_baseAddress = new Uri($"{address.Scheme}://{address.Authority}/", UriKind.Absolute);
AuthState = options.IsConfigured ? RemoteAuthState.NotConfigured : RemoteAuthState.NotConfigured;
@@ -174,7 +187,7 @@ public sealed class RemoteControlClient : IDisposable
using var request = new HttpRequestMessage(method, new Uri(_baseAddress, path.TrimStart('/')));
request.Headers.TryAddWithoutValidation("X-Correlation-Id", NewCorrelationId());
if (authenticated || method == HttpMethod.Post && path == "/v1/nodes/register")
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _options.Token);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _options.GetToken());
if (body is not null)
{
var bytes = JsonSerializer.SerializeToUtf8Bytes(body, RemoteJson.Options);
@@ -220,7 +233,7 @@ public sealed class RemoteControlClient : IDisposable
if (!_options.IsConfigured)
{
AuthState = RemoteAuthState.NotConfigured;
throw new WxAgentException(WxAgentErrorCode.InvalidArgument, "Remote authAddress, token and nodeId must all be configured.");
throw new WxAgentException(WxAgentErrorCode.InvalidArgument, "Remote authAddress, token or tokenFile and nodeId must all be configured.");
}
_options.Validate();
}
@@ -249,11 +262,42 @@ public sealed class RemoteControlClient : IDisposable
return new RemoteApiError("RemoteRequestFailed", "Remote request failed.", correlationId);
}
private static X509Certificate2? LoadClientCertificate(RemoteAgentOptions options)
{
if (string.IsNullOrWhiteSpace(options.ClientCertificateFile)) return null;
return X509Certificate2.CreateFromPemFile(options.ClientCertificateFile, options.ClientCertificateKeyFile!);
}
private static X509Certificate2? LoadServerCaCertificate(RemoteAgentOptions options) =>
string.IsNullOrWhiteSpace(options.ServerCaFile) ? null : new X509Certificate2(options.ServerCaFile);
private static HttpClient CreateHttpClient(X509Certificate2? clientCertificate, X509Certificate2? serverCaCertificate)
{
var handler = new HttpClientHandler();
if (clientCertificate is not null) handler.ClientCertificates.Add(clientCertificate);
if (serverCaCertificate is not null)
{
handler.ServerCertificateCustomValidationCallback = (_, certificate, _, errors) =>
{
if (certificate is null || (errors & (SslPolicyErrors.RemoteCertificateNameMismatch | SslPolicyErrors.RemoteCertificateNotAvailable)) != 0)
return false;
using var chain = new X509Chain();
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
chain.ChainPolicy.CustomTrustStore.Add(serverCaCertificate);
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
return chain.Build(new X509Certificate2(certificate));
};
}
return new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(15) };
}
private static string Escape(string value) => Uri.EscapeDataString(value);
private static string NewCorrelationId() => Guid.NewGuid().ToString("N");
public void Dispose()
{
if (_ownsHttp) _http.Dispose();
_clientCertificate?.Dispose();
_serverCaCertificate?.Dispose();
}
}