Files
git.stella-ops.org/src/StellaOps.Concelier.Connector.Common/Http/SourceHttpClientConfigurationBinder.cs
master 95daa159c4 feat: Implement console session management with tenant and profile handling
- Add ConsoleSessionStore for managing console session state including tenants, profile, and token information.
- Create OperatorContextService to manage operator context for orchestrator actions.
- Implement OperatorMetadataInterceptor to enrich HTTP requests with operator context metadata.
- Develop ConsoleProfileComponent to display user profile and session details, including tenant information and access tokens.
- Add corresponding HTML and SCSS for ConsoleProfileComponent to enhance UI presentation.
- Write unit tests for ConsoleProfileComponent to ensure correct rendering and functionality.
2025-10-28 09:59:09 +02:00

367 lines
13 KiB
C#

using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.IO;
using System.Net.Security;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace StellaOps.Concelier.Connector.Common.Http;
internal static class SourceHttpClientConfigurationBinder
{
private const string ConcelierSection = "concelier";
private const string HttpClientsSection = "httpClients";
private const string SourcesSection = "sources";
private const string HttpSection = "http";
private const string AllowInvalidKey = "allowInvalidCertificates";
private const string TrustedRootPathsKey = "trustedRootPaths";
private const string ProxySection = "proxy";
private const string ProxyAddressKey = "address";
private const string ProxyBypassOnLocalKey = "bypassOnLocal";
private const string ProxyBypassListKey = "bypassList";
private const string ProxyUseDefaultCredentialsKey = "useDefaultCredentials";
private const string ProxyUsernameKey = "username";
private const string ProxyPasswordKey = "password";
private const string OfflineRootKey = "offlineRoot";
private const string OfflineRootEnvironmentVariable = "CONCELIER_OFFLINE_ROOT";
public static void Apply(IServiceProvider services, string clientName, SourceHttpClientOptions options)
{
var configuration = services.GetService(typeof(IConfiguration)) as IConfiguration;
if (configuration is null)
{
return;
}
var loggerFactory = services.GetService(typeof(ILoggerFactory)) as ILoggerFactory;
var logger = loggerFactory?.CreateLogger("SourceHttpClientConfiguration");
var hostEnvironment = services.GetService(typeof(IHostEnvironment)) as IHostEnvironment;
var processed = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var section in EnumerateCandidateSections(configuration, clientName))
{
if (section is null || !section.Exists() || !processed.Add(section.Path))
{
continue;
}
ApplySection(section, configuration, hostEnvironment, clientName, options, logger);
}
}
private static IEnumerable<IConfigurationSection> EnumerateCandidateSections(IConfiguration configuration, string clientName)
{
var names = BuildCandidateNames(clientName);
foreach (var name in names)
{
var httpClientSection = GetSection(configuration, ConcelierSection, HttpClientsSection, name);
if (httpClientSection is not null && httpClientSection.Exists())
{
yield return httpClientSection;
}
var sourceHttpSection = GetSection(configuration, ConcelierSection, SourcesSection, name, HttpSection);
if (sourceHttpSection is not null && sourceHttpSection.Exists())
{
yield return sourceHttpSection;
}
}
}
private static IEnumerable<string> BuildCandidateNames(string clientName)
{
yield return clientName;
if (clientName.StartsWith("source.", StringComparison.OrdinalIgnoreCase) && clientName.Length > "source.".Length)
{
yield return clientName["source.".Length..];
}
var noDots = clientName.Replace('.', '_');
if (!string.Equals(noDots, clientName, StringComparison.OrdinalIgnoreCase))
{
yield return noDots;
}
}
private static IConfigurationSection? GetSection(IConfiguration configuration, params string[] pathSegments)
{
IConfiguration? current = configuration;
foreach (var segment in pathSegments)
{
if (current is null)
{
return null;
}
current = current.GetSection(segment);
}
return current as IConfigurationSection;
}
private static void ApplySection(
IConfigurationSection section,
IConfiguration rootConfiguration,
IHostEnvironment? hostEnvironment,
string clientName,
SourceHttpClientOptions options,
ILogger? logger)
{
var allowInvalid = section.GetValue<bool?>(AllowInvalidKey);
if (allowInvalid == true)
{
options.AllowInvalidServerCertificates = true;
var previous = options.ServerCertificateCustomValidation;
options.ServerCertificateCustomValidation = (certificate, chain, errors) =>
{
if (allowInvalid == true)
{
return true;
}
return previous?.Invoke(certificate, chain, errors) ?? errors == SslPolicyErrors.None;
};
logger?.LogWarning(
"Source HTTP client '{ClientName}' is configured to bypass TLS certificate validation.",
clientName);
}
var offlineRoot = section.GetValue<string?>(OfflineRootKey)
?? rootConfiguration.GetSection(ConcelierSection).GetValue<string?>(OfflineRootKey)
?? Environment.GetEnvironmentVariable(OfflineRootEnvironmentVariable);
ApplyTrustedRoots(section, offlineRoot, hostEnvironment, clientName, options, logger);
ApplyProxyConfiguration(section, clientName, options, logger);
}
private static void ApplyTrustedRoots(
IConfigurationSection section,
string? offlineRoot,
IHostEnvironment? hostEnvironment,
string clientName,
SourceHttpClientOptions options,
ILogger? logger)
{
var trustedRootSection = section.GetSection(TrustedRootPathsKey);
if (!trustedRootSection.Exists())
{
return;
}
var paths = trustedRootSection.Get<string[]?>();
if (paths is null || paths.Length == 0)
{
return;
}
foreach (var rawPath in paths)
{
if (string.IsNullOrWhiteSpace(rawPath))
{
continue;
}
var resolvedPath = ResolvePath(rawPath, offlineRoot, hostEnvironment);
if (!File.Exists(resolvedPath))
{
var message = string.Format(
CultureInfo.InvariantCulture,
"Trusted root certificate '{0}' resolved to '{1}' but was not found.",
rawPath,
resolvedPath);
throw new FileNotFoundException(message, resolvedPath);
}
foreach (var certificate in LoadCertificates(resolvedPath))
{
var thumbprint = certificate.Thumbprint;
var added = AddTrustedCertificate(options, certificate);
if (added)
{
logger?.LogInformation(
"Source HTTP client '{ClientName}' loaded trusted root certificate '{Thumbprint}' from '{Path}'.",
clientName,
thumbprint,
resolvedPath);
}
}
}
}
private static void ApplyProxyConfiguration(
IConfigurationSection section,
string clientName,
SourceHttpClientOptions options,
ILogger? logger)
{
var proxySection = section.GetSection(ProxySection);
if (!proxySection.Exists())
{
return;
}
var address = proxySection.GetValue<string?>(ProxyAddressKey);
if (!string.IsNullOrWhiteSpace(address))
{
if (Uri.TryCreate(address, UriKind.Absolute, out var uri))
{
options.ProxyAddress = uri;
}
else
{
logger?.LogWarning(
"Source HTTP client '{ClientName}' has invalid proxy address '{ProxyAddress}'.",
clientName,
address);
}
}
var bypassOnLocal = proxySection.GetValue<bool?>(ProxyBypassOnLocalKey);
if (bypassOnLocal.HasValue)
{
options.ProxyBypassOnLocal = bypassOnLocal.Value;
}
var bypassListSection = proxySection.GetSection(ProxyBypassListKey);
if (bypassListSection.Exists())
{
var entries = bypassListSection.Get<string[]?>();
options.ProxyBypassList.Clear();
if (entries is not null)
{
foreach (var entry in entries)
{
if (!string.IsNullOrWhiteSpace(entry))
{
options.ProxyBypassList.Add(entry.Trim());
}
}
}
}
var useDefaultCredentials = proxySection.GetValue<bool?>(ProxyUseDefaultCredentialsKey);
if (useDefaultCredentials.HasValue)
{
options.ProxyUseDefaultCredentials = useDefaultCredentials.Value;
}
var username = proxySection.GetValue<string?>(ProxyUsernameKey);
if (!string.IsNullOrWhiteSpace(username))
{
options.ProxyUsername = username.Trim();
}
var password = proxySection.GetValue<string?>(ProxyPasswordKey);
if (!string.IsNullOrWhiteSpace(password))
{
options.ProxyPassword = password;
}
}
private static string ResolvePath(string path, string? offlineRoot, IHostEnvironment? hostEnvironment)
{
if (Path.IsPathRooted(path))
{
return path;
}
if (!string.IsNullOrWhiteSpace(offlineRoot))
{
return Path.GetFullPath(Path.Combine(offlineRoot!, path));
}
var baseDirectory = hostEnvironment?.ContentRootPath ?? AppContext.BaseDirectory;
return Path.GetFullPath(Path.Combine(baseDirectory, path));
}
private static IEnumerable<X509Certificate2> LoadCertificates(string path)
{
var certificates = new List<X509Certificate2>();
var extension = Path.GetExtension(path);
if (extension.Equals(".pem", StringComparison.OrdinalIgnoreCase) || extension.Equals(".crt", StringComparison.OrdinalIgnoreCase))
{
var collection = new X509Certificate2Collection();
try
{
collection.ImportFromPemFile(path);
}
catch (CryptographicException)
{
collection.Clear();
}
if (collection.Count > 0)
{
foreach (var certificate in collection)
{
certificates.Add(certificate.CopyWithPrivateKeyIfAvailable());
}
}
else
{
certificates.Add(X509Certificate2.CreateFromPemFile(path));
}
}
else
{
// Use X509CertificateLoader to load certificates from PKCS#12 files (.pfx, .p12, etc.)
var certificate = System.Security.Cryptography.X509Certificates.X509CertificateLoader.LoadPkcs12(
File.ReadAllBytes(path),
password: null);
certificates.Add(certificate);
}
return certificates;
}
private static bool AddTrustedCertificate(SourceHttpClientOptions options, X509Certificate2 certificate)
{
if (certificate is null)
{
return false;
}
var thumbprint = certificate.Thumbprint;
if (string.IsNullOrWhiteSpace(thumbprint))
{
certificate.Dispose();
return false;
}
if (options.TrustedRootCertificates.Any(existing =>
string.Equals(existing.Thumbprint, thumbprint, StringComparison.OrdinalIgnoreCase)))
{
certificate.Dispose();
return false;
}
options.TrustedRootCertificates.Add(certificate);
return true;
}
// Helper extension method to copy certificate (preserves private key if present)
private static X509Certificate2 CopyWithPrivateKeyIfAvailable(this X509Certificate2 certificate)
{
// In .NET 9+, use X509CertificateLoader instead of obsolete constructors
if (certificate.HasPrivateKey)
{
// Export with private key and re-import using X509CertificateLoader
var exported = certificate.Export(X509ContentType.Pkcs12);
return X509CertificateLoader.LoadPkcs12(exported, password: null);
}
else
{
// For certificates without private keys, load from raw data
return X509CertificateLoader.LoadCertificate(certificate.RawData);
}
}
}