Rename Concelier Source modules to Connector
This commit is contained in:
		@@ -0,0 +1,360 @@
 | 
			
		||||
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))
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    AddTrustedCertificate(options, certificate);
 | 
			
		||||
                    logger?.LogInformation(
 | 
			
		||||
                        "Source HTTP client '{ClientName}' loaded trusted root certificate '{Thumbprint}' from '{Path}'.",
 | 
			
		||||
                        clientName,
 | 
			
		||||
                        certificate.Thumbprint,
 | 
			
		||||
                        resolvedPath);
 | 
			
		||||
                }
 | 
			
		||||
                finally
 | 
			
		||||
                {
 | 
			
		||||
                    certificate.Dispose();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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 void AddTrustedCertificate(SourceHttpClientOptions options, X509Certificate2 certificate)
 | 
			
		||||
    {
 | 
			
		||||
        if (certificate is null)
 | 
			
		||||
        {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (options.TrustedRootCertificates.Any(existing =>
 | 
			
		||||
                string.Equals(existing.Thumbprint, certificate.Thumbprint, StringComparison.OrdinalIgnoreCase)))
 | 
			
		||||
        {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        options.TrustedRootCertificates.Add(certificate);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user