initial commit
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.vs
|
||||||
|
certmgr/obj/*
|
||||||
|
certmgr/Example.cs
|
||||||
|
certmgr/Obsolete/*
|
||||||
23
BuildOutput/bin/Debug/net9.0/certmgr.deps.json
Normal file
23
BuildOutput/bin/Debug/net9.0/certmgr.deps.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"runtimeTarget": {
|
||||||
|
"name": ".NETCoreApp,Version=v9.0",
|
||||||
|
"signature": ""
|
||||||
|
},
|
||||||
|
"compilationOptions": {},
|
||||||
|
"targets": {
|
||||||
|
".NETCoreApp,Version=v9.0": {
|
||||||
|
"certmgr/1.0.0": {
|
||||||
|
"runtime": {
|
||||||
|
"certmgr.dll": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"libraries": {
|
||||||
|
"certmgr/1.0.0": {
|
||||||
|
"type": "project",
|
||||||
|
"serviceable": false,
|
||||||
|
"sha512": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
BuildOutput/bin/Debug/net9.0/certmgr.dll
Normal file
BIN
BuildOutput/bin/Debug/net9.0/certmgr.dll
Normal file
Binary file not shown.
BIN
BuildOutput/bin/Debug/net9.0/certmgr.exe
Normal file
BIN
BuildOutput/bin/Debug/net9.0/certmgr.exe
Normal file
Binary file not shown.
BIN
BuildOutput/bin/Debug/net9.0/certmgr.pdb
Normal file
BIN
BuildOutput/bin/Debug/net9.0/certmgr.pdb
Normal file
Binary file not shown.
12
BuildOutput/bin/Debug/net9.0/certmgr.runtimeconfig.json
Normal file
12
BuildOutput/bin/Debug/net9.0/certmgr.runtimeconfig.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"runtimeOptions": {
|
||||||
|
"tfm": "net9.0",
|
||||||
|
"framework": {
|
||||||
|
"name": "Microsoft.NETCore.App",
|
||||||
|
"version": "9.0.0"
|
||||||
|
},
|
||||||
|
"configProperties": {
|
||||||
|
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
certmgr.sln
Normal file
25
certmgr.sln
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.14.36511.14 d17.14
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "certmgr", "certmgr\certmgr.csproj", "{EB5C73A6-8EBF-4C9C-845F-4828C4985B64}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{EB5C73A6-8EBF-4C9C-845F-4828C4985B64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{EB5C73A6-8EBF-4C9C-845F-4828C4985B64}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{EB5C73A6-8EBF-4C9C-845F-4828C4985B64}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{EB5C73A6-8EBF-4C9C-845F-4828C4985B64}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {AFA87038-4B40-4962-A801-DA2E96351533}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
17
certmgr/CertGen/CertGenException.cs
Normal file
17
certmgr/CertGen/CertGenException.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
namespace CertMgr.CertGen;
|
||||||
|
|
||||||
|
public class CertGenException : Exception
|
||||||
|
{
|
||||||
|
public CertGenException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
public CertGenException(string message, params object?[] args)
|
||||||
|
: base(string.Format(message, args))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
public CertGenException(Exception innerException, string message)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
7
certmgr/CertGen/CertificateAlgorithm.cs
Normal file
7
certmgr/CertGen/CertificateAlgorithm.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace CertMgr.CertGen;
|
||||||
|
|
||||||
|
public enum CertificateAlgorithm
|
||||||
|
{
|
||||||
|
RSA = 1,
|
||||||
|
ECDsa
|
||||||
|
}
|
||||||
238
certmgr/CertGen/CertificateGeneratorBase.cs
Normal file
238
certmgr/CertGen/CertificateGeneratorBase.cs
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
|
||||||
|
using CertMgr.Core.Exceptions;
|
||||||
|
|
||||||
|
namespace CertMgr.CertGen;
|
||||||
|
|
||||||
|
internal abstract class CertificateGeneratorBase<TAlgorithm, TSettings> : ICertificateGenerator
|
||||||
|
where TAlgorithm : AsymmetricAlgorithm
|
||||||
|
where TSettings : GeneratorSettings
|
||||||
|
{
|
||||||
|
internal CertificateGeneratorBase(TSettings settings)
|
||||||
|
{
|
||||||
|
Settings = settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected TSettings Settings { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public async Task<X509Certificate2> CreateAsync(CertificateSettings settings, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ValidateSettings(settings);
|
||||||
|
|
||||||
|
X509Certificate2 cert = await CreateInternalAsync(settings, cancellationToken);
|
||||||
|
return cert;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract bool IsEphemeral(TAlgorithm key);
|
||||||
|
|
||||||
|
protected virtual void ValidateSettings(CertificateSettings settings)
|
||||||
|
{
|
||||||
|
if (settings.Issuer != null)
|
||||||
|
{
|
||||||
|
if (!settings.Issuer.HasPrivateKey)
|
||||||
|
{
|
||||||
|
throw new CertGenException("Certificate without private key cannot be used for signing");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected HashAlgorithmName GetHashAlgorithm()
|
||||||
|
{
|
||||||
|
HashAlgorithmName han;
|
||||||
|
|
||||||
|
switch (Settings.HashAlgorithm)
|
||||||
|
{
|
||||||
|
case HashAlgorithm.Sha256:
|
||||||
|
han = HashAlgorithmName.SHA256;
|
||||||
|
break;
|
||||||
|
case HashAlgorithm.Sha384:
|
||||||
|
han = HashAlgorithmName.SHA384;
|
||||||
|
break;
|
||||||
|
case HashAlgorithm.Sha512:
|
||||||
|
han = HashAlgorithmName.SHA512;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new UnsupportedValueException(Settings.HashAlgorithm);
|
||||||
|
}
|
||||||
|
|
||||||
|
return han;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract TAlgorithm CreatePrivateKey();
|
||||||
|
|
||||||
|
protected abstract CertificateRequest DoCreateRequest(string subjectName, TAlgorithm privateKey);
|
||||||
|
|
||||||
|
protected abstract X509Certificate2 JoinPrivateKey(X509Certificate2 publicOnlyCert, TAlgorithm privateKey);
|
||||||
|
|
||||||
|
protected abstract string? GetContainerUniqueName(TAlgorithm privateKey);
|
||||||
|
|
||||||
|
protected abstract TAlgorithm? GetPrivateKey(X509Certificate2 cert);
|
||||||
|
|
||||||
|
private CertificateRequest CreateRequest(CertificateSettings settings, TAlgorithm privateKey)
|
||||||
|
{
|
||||||
|
string commonName = CreateCommonName(settings.SubjectName);
|
||||||
|
CertificateRequest request = DoCreateRequest(commonName, privateKey);
|
||||||
|
|
||||||
|
SubjectAlternativeNameBuilder altNames = new SubjectAlternativeNameBuilder();
|
||||||
|
foreach (string subjName in settings.SubjectAlternateNames)
|
||||||
|
{
|
||||||
|
altNames.AddDnsName(subjName);
|
||||||
|
}
|
||||||
|
request.CertificateExtensions.Add(altNames.Build());
|
||||||
|
|
||||||
|
if (settings.IsCertificateAuthority)
|
||||||
|
{
|
||||||
|
request.CertificateExtensions.Add(new X509BasicConstraintsExtension(settings.IsCertificateAuthority, false, 0, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.KeyUsage != X509KeyUsageFlags.None)
|
||||||
|
{
|
||||||
|
request.CertificateExtensions.Add(new X509KeyUsageExtension(settings.KeyUsage, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<X509Certificate2> CreateInternalAsync(CertificateSettings settings, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
X509Certificate2 cert;
|
||||||
|
|
||||||
|
using (TAlgorithm privateKey = CreatePrivateKey())
|
||||||
|
{
|
||||||
|
CertificateRequest request = CreateRequest(settings, privateKey);
|
||||||
|
|
||||||
|
DateTimeOffset notBefore = DateTimeOffset.UtcNow.AddSeconds(-1);
|
||||||
|
DateTimeOffset notAfter = DateTimeOffset.UtcNow.Add(settings.ValidityPeriod);
|
||||||
|
if (settings.Issuer != null)
|
||||||
|
{
|
||||||
|
byte[] serial = GenerateSerial();
|
||||||
|
X509SignatureGenerator sgen = GetSignatureGenerator(settings.Issuer);
|
||||||
|
using (X509Certificate2 publicOnlyCert = request.Create(settings.Issuer.SubjectName, sgen, notBefore, notAfter, serial))
|
||||||
|
{
|
||||||
|
cert = JoinPrivateKey(publicOnlyCert, privateKey);
|
||||||
|
// using (X509Certificate2 temp = JoinPrivateKey(publicOnlyCert, privateKey))
|
||||||
|
// {
|
||||||
|
// // Generated instance of the cert can't be added to cert-store (private key is missing) if requested by the caller.
|
||||||
|
// // To avoid this recreate the cert
|
||||||
|
// cert = Recreate(temp, settings);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cert = request.CreateSelfSigned(notBefore, notAfter);
|
||||||
|
// using (X509Certificate2 temp = request.CreateSelfSigned(notBefore, notAfter))
|
||||||
|
// {
|
||||||
|
// // Generated instance of the cert can't be added to cert-store (private key is missing) if requested by the caller.
|
||||||
|
// // To avoid this recreate the cert
|
||||||
|
// cert = Recreate(temp, settings);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(cert);
|
||||||
|
}
|
||||||
|
|
||||||
|
private X509SignatureGenerator GetSignatureGenerator(X509Certificate2 issuerCertificate)
|
||||||
|
{
|
||||||
|
X509SignatureGenerator? sgen = null;
|
||||||
|
|
||||||
|
ECDsa? ecdsa = issuerCertificate.GetECDsaPrivateKey();
|
||||||
|
if (ecdsa != null)
|
||||||
|
{
|
||||||
|
sgen = X509SignatureGenerator.CreateForECDsa(ecdsa);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sgen == null)
|
||||||
|
{
|
||||||
|
RSA? rsa = issuerCertificate.GetRSAPrivateKey();
|
||||||
|
if (rsa != null)
|
||||||
|
{
|
||||||
|
sgen = X509SignatureGenerator.CreateForRSA(rsa, RSASignaturePadding.Pkcs1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sgen == null)
|
||||||
|
{
|
||||||
|
DSA? dsa = issuerCertificate.GetDSAPrivateKey();
|
||||||
|
if (dsa != null)
|
||||||
|
{
|
||||||
|
throw new CertGenException("Unsupported type of DSA private-key: '{0}'", dsa.GetType().FullName);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AsymmetricAlgorithm pk = issuerCertificate.PrivateKey;
|
||||||
|
throw new CertGenException("Unsupported type of private-key: '{0}'", pk?.GetType().FullName ?? "<null>");
|
||||||
|
}
|
||||||
|
catch (CertGenException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
throw new CertGenException("Unsupported type of private-key");
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new CertGenException("Unsupported type of private-key (no DSA or CAPI)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sgen;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] GenerateSerial()
|
||||||
|
{
|
||||||
|
byte[] serial = new byte[12];
|
||||||
|
|
||||||
|
using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
|
||||||
|
{
|
||||||
|
rng.GetBytes(serial);
|
||||||
|
}
|
||||||
|
|
||||||
|
return serial;
|
||||||
|
}
|
||||||
|
|
||||||
|
private X509Certificate2 Recreate(X509Certificate2 source, CertificateSettings settings)
|
||||||
|
{
|
||||||
|
byte[] data = source.Export(X509ContentType.Pfx, string.Empty);
|
||||||
|
X509KeyStorageFlags flags = X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet;
|
||||||
|
if (!settings.ExportableKeys)
|
||||||
|
{
|
||||||
|
flags &= ~X509KeyStorageFlags.Exportable;
|
||||||
|
}
|
||||||
|
|
||||||
|
X509Certificate2 target = X509CertificateLoader.LoadPkcs12(data, string.Empty);
|
||||||
|
target.FriendlyName = settings.FriendlyName;
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string CreateCommonName(string? subjectName)
|
||||||
|
{
|
||||||
|
string cn;
|
||||||
|
|
||||||
|
if (subjectName.StartsWith("CN=", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
cn = subjectName;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cn = string.Format("CN={0}", subjectName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return string.Format("Type = {0}", this is RsaCertificateGenerator ? "RSA" : this is EcdsaCertificateGenerator ? "ECDSA" : "<unknown>");
|
||||||
|
}
|
||||||
|
}
|
||||||
29
certmgr/CertGen/CertificateManager.cs
Normal file
29
certmgr/CertGen/CertificateManager.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
|
||||||
|
using CertMgr.Core.Exceptions;
|
||||||
|
|
||||||
|
namespace CertMgr.CertGen;
|
||||||
|
|
||||||
|
public sealed class CertificateManager
|
||||||
|
{
|
||||||
|
public async Task<X509Certificate2> CreateAsync(CertificateSettings cs, GeneratorSettings gs, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ICertificateGenerator generator;
|
||||||
|
|
||||||
|
if (gs is EcdsaGeneratorSettings egs)
|
||||||
|
{
|
||||||
|
generator = new EcdsaCertificateGenerator(egs);
|
||||||
|
}
|
||||||
|
else if (gs is RsaGeneratorSettings rgs)
|
||||||
|
{
|
||||||
|
generator = new RsaCertificateGenerator(rgs);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new UnsupportedValueException(gs.Type);
|
||||||
|
}
|
||||||
|
|
||||||
|
X509Certificate2 cert = await generator.CreateAsync(cs, cancellationToken);
|
||||||
|
return cert;
|
||||||
|
}
|
||||||
|
}
|
||||||
61
certmgr/CertGen/CertificateSettings.cs
Normal file
61
certmgr/CertGen/CertificateSettings.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
|
||||||
|
using CertMgr.Core.Utils;
|
||||||
|
using CertMgr.Core.Validation;
|
||||||
|
|
||||||
|
namespace CertMgr.CertGen;
|
||||||
|
|
||||||
|
public sealed class CertificateSettings
|
||||||
|
{
|
||||||
|
internal CertificateSettings()
|
||||||
|
{
|
||||||
|
SubjectAlternateNames = new SubjectAlternateNames([NetUtils.MachineName]);
|
||||||
|
|
||||||
|
SubjectName = NetUtils.MachineName;
|
||||||
|
Issuer = null;
|
||||||
|
FriendlyName = NetUtils.MachineName;
|
||||||
|
ValidityPeriod = TimeSpan.FromDays(3650 + 2);
|
||||||
|
ExportableKeys = true;
|
||||||
|
IsCertificateAuthority = false;
|
||||||
|
KeyUsage = X509KeyUsageFlags.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SubjectAlternateNames SubjectAlternateNames { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public string? SubjectName { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||||
|
|
||||||
|
public X509Certificate2? Issuer { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||||
|
|
||||||
|
public string? FriendlyName { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||||
|
|
||||||
|
public TimeSpan ValidityPeriod { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||||
|
|
||||||
|
public bool ExportableKeys { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||||
|
|
||||||
|
public bool IsCertificateAuthority { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||||
|
|
||||||
|
public X509KeyUsageFlags KeyUsage { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||||
|
|
||||||
|
public Task<ValidationResults> ValidateAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ValidationResults results = new ValidationResults();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(SubjectName))
|
||||||
|
{
|
||||||
|
results.AddInvalid(nameof(SubjectName), "must not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ValidityPeriod < TimeSpan.FromSeconds(1))
|
||||||
|
{
|
||||||
|
results.AddInvalid(nameof(ValidityPeriod), "must be greater than 1sec");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return string.Format("issuer = {0}, subject-name = '{1}', validity-period = {2} days, exportable = {3}, is-ca = {4}", Issuer?.Subject ?? "<not-set>", SubjectAlternateNames.First(), ValidityPeriod.TotalDays, ExportableKeys ? "yes" : "no", IsCertificateAuthority ? "yes" : "no");
|
||||||
|
}
|
||||||
|
}
|
||||||
67
certmgr/CertGen/EcdsaCertificateGenerator.cs
Normal file
67
certmgr/CertGen/EcdsaCertificateGenerator.cs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
|
||||||
|
using CertMgr.Core.Exceptions;
|
||||||
|
|
||||||
|
namespace CertMgr.CertGen;
|
||||||
|
|
||||||
|
internal sealed class EcdsaCertificateGenerator : CertificateGeneratorBase<ECDsa, EcdsaGeneratorSettings>
|
||||||
|
{
|
||||||
|
internal EcdsaCertificateGenerator(EcdsaGeneratorSettings settings)
|
||||||
|
: base(settings)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override ECDsa CreatePrivateKey()
|
||||||
|
{
|
||||||
|
return ECDsa.Create(GetCurve());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override CertificateRequest DoCreateRequest(string subjectName, ECDsa privateKey)
|
||||||
|
{
|
||||||
|
return new CertificateRequest(subjectName, privateKey, GetHashAlgorithm());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override X509Certificate2 JoinPrivateKey(X509Certificate2 publicOnlyCert, ECDsa privateKey)
|
||||||
|
{
|
||||||
|
return publicOnlyCert.CopyWithPrivateKey(privateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override ECDsa? GetPrivateKey(X509Certificate2 cert)
|
||||||
|
{
|
||||||
|
ECDsa? privateKey = cert.GetECDsaPrivateKey();
|
||||||
|
return privateKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string? GetContainerUniqueName(ECDsa privateKey)
|
||||||
|
{
|
||||||
|
return ((ECDsaCng)privateKey).Key.UniqueName;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IsEphemeral(ECDsa key)
|
||||||
|
{
|
||||||
|
return ((ECDsaCng)key).Key.IsEphemeral;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ECCurve GetCurve()
|
||||||
|
{
|
||||||
|
ECCurve curve;
|
||||||
|
|
||||||
|
switch (Settings.Curve)
|
||||||
|
{
|
||||||
|
case EcdsaCurve.P256:
|
||||||
|
curve = ECCurve.NamedCurves.nistP256;
|
||||||
|
break;
|
||||||
|
case EcdsaCurve.P384:
|
||||||
|
curve = ECCurve.NamedCurves.nistP384;
|
||||||
|
break;
|
||||||
|
case EcdsaCurve.P521:
|
||||||
|
curve = ECCurve.NamedCurves.nistP521;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new UnsupportedValueException(Settings.Curve);
|
||||||
|
}
|
||||||
|
|
||||||
|
return curve;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
certmgr/CertGen/EcdsaCurve.cs
Normal file
8
certmgr/CertGen/EcdsaCurve.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace CertMgr.CertGen;
|
||||||
|
|
||||||
|
public enum EcdsaCurve
|
||||||
|
{
|
||||||
|
P256 = 1,
|
||||||
|
P384,
|
||||||
|
P521
|
||||||
|
}
|
||||||
44
certmgr/CertGen/EcdsaGeneratorSettings.cs
Normal file
44
certmgr/CertGen/EcdsaGeneratorSettings.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
using CertMgr.Core.Exceptions;
|
||||||
|
|
||||||
|
namespace CertMgr.CertGen;
|
||||||
|
|
||||||
|
public sealed class EcdsaGeneratorSettings : GeneratorSettings
|
||||||
|
{
|
||||||
|
public EcdsaGeneratorSettings(EcdsaCurve curve)
|
||||||
|
: base(GeneratorType.Ecdsa)
|
||||||
|
{
|
||||||
|
Curve = curve;
|
||||||
|
HashAlgorithm = GetHashAlgorithm(curve);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EcdsaCurve Curve { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
private HashAlgorithm GetHashAlgorithm(EcdsaCurve curve)
|
||||||
|
{
|
||||||
|
HashAlgorithm ha;
|
||||||
|
|
||||||
|
switch (Curve)
|
||||||
|
{
|
||||||
|
case EcdsaCurve.P256:
|
||||||
|
ha = HashAlgorithm.Sha256;
|
||||||
|
break;
|
||||||
|
case EcdsaCurve.P384:
|
||||||
|
ha = HashAlgorithm.Sha384;
|
||||||
|
break;
|
||||||
|
case EcdsaCurve.P521:
|
||||||
|
ha = HashAlgorithm.Sha512;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new UnsupportedValueException(Curve);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ha;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return string.Format("type = '{0}', curve = '{1}', hash-algorithm = '{2}'", Type, Curve, HashAlgorithm);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
certmgr/CertGen/GeneratorSettings.cs
Normal file
20
certmgr/CertGen/GeneratorSettings.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace CertMgr.CertGen;
|
||||||
|
|
||||||
|
public abstract class GeneratorSettings
|
||||||
|
{
|
||||||
|
protected GeneratorSettings(GeneratorType type)
|
||||||
|
{
|
||||||
|
Type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GeneratorType Type { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public HashAlgorithm HashAlgorithm { [DebuggerStepThrough] get; [DebuggerStepThrough] protected set; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return string.Format("type = '{0}', hash-algorithm = '{1}'", Type, HashAlgorithm);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
certmgr/CertGen/GeneratorType.cs
Normal file
7
certmgr/CertGen/GeneratorType.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace CertMgr.CertGen;
|
||||||
|
|
||||||
|
public enum GeneratorType
|
||||||
|
{
|
||||||
|
Ecdsa = 1,
|
||||||
|
Rsa
|
||||||
|
}
|
||||||
8
certmgr/CertGen/HashAlgorithm.cs
Normal file
8
certmgr/CertGen/HashAlgorithm.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace CertMgr.CertGen;
|
||||||
|
|
||||||
|
public enum HashAlgorithm
|
||||||
|
{
|
||||||
|
Sha256 = 1,
|
||||||
|
Sha384,
|
||||||
|
Sha512
|
||||||
|
}
|
||||||
8
certmgr/CertGen/ICertificateGenerator.cs
Normal file
8
certmgr/CertGen/ICertificateGenerator.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
|
||||||
|
namespace CertMgr.CertGen;
|
||||||
|
|
||||||
|
public interface ICertificateGenerator : IAsyncDisposable
|
||||||
|
{
|
||||||
|
Task<X509Certificate2> CreateAsync(CertificateSettings settings, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
67
certmgr/CertGen/RsaCertificateGenerator.cs
Normal file
67
certmgr/CertGen/RsaCertificateGenerator.cs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
|
||||||
|
using CertMgr.Core.Exceptions;
|
||||||
|
|
||||||
|
namespace CertMgr.CertGen;
|
||||||
|
|
||||||
|
internal sealed class RsaCertificateGenerator : CertificateGeneratorBase<RSA, RsaGeneratorSettings>
|
||||||
|
{
|
||||||
|
internal RsaCertificateGenerator(RsaGeneratorSettings settings)
|
||||||
|
: base(settings)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override RSA CreatePrivateKey()
|
||||||
|
{
|
||||||
|
return RSA.Create(GetKeySize());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override CertificateRequest DoCreateRequest(string subjectName, RSA privateKey)
|
||||||
|
{
|
||||||
|
return new CertificateRequest(subjectName, privateKey, GetHashAlgorithm(), RSASignaturePadding.Pkcs1);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override X509Certificate2 JoinPrivateKey(X509Certificate2 publicOnlyCert, RSA privateKey)
|
||||||
|
{
|
||||||
|
return publicOnlyCert.CopyWithPrivateKey(privateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override RSA? GetPrivateKey(X509Certificate2 cert)
|
||||||
|
{
|
||||||
|
RSA? privateKey = cert.GetRSAPrivateKey();
|
||||||
|
return privateKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string? GetContainerUniqueName(RSA privateKey)
|
||||||
|
{
|
||||||
|
return ((RSACng)privateKey).Key.UniqueName;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IsEphemeral(RSA key)
|
||||||
|
{
|
||||||
|
return ((RSACng)key).Key.IsEphemeral;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetKeySize()
|
||||||
|
{
|
||||||
|
int keySize;
|
||||||
|
|
||||||
|
switch (Settings.KeySize)
|
||||||
|
{
|
||||||
|
case RsaKeySize.KeySize2048:
|
||||||
|
keySize = 2048;
|
||||||
|
break;
|
||||||
|
case RsaKeySize.KeySize4096:
|
||||||
|
keySize = 4096;
|
||||||
|
break;
|
||||||
|
case RsaKeySize.KeySize8192:
|
||||||
|
keySize = 8192;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new UnsupportedValueException(Settings.KeySize);
|
||||||
|
}
|
||||||
|
|
||||||
|
return keySize;
|
||||||
|
}
|
||||||
|
}
|
||||||
45
certmgr/CertGen/RsaGeneratorSettings.cs
Normal file
45
certmgr/CertGen/RsaGeneratorSettings.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
using CertMgr.Core.Exceptions;
|
||||||
|
|
||||||
|
namespace CertMgr.CertGen;
|
||||||
|
|
||||||
|
public sealed class RsaGeneratorSettings : GeneratorSettings
|
||||||
|
{
|
||||||
|
public RsaGeneratorSettings(RsaKeySize keySize)
|
||||||
|
: base(GeneratorType.Rsa)
|
||||||
|
{
|
||||||
|
KeySize = keySize;
|
||||||
|
|
||||||
|
HashAlgorithm = GetHashAlgorithm(keySize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RsaKeySize KeySize { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
private HashAlgorithm GetHashAlgorithm(RsaKeySize keySize)
|
||||||
|
{
|
||||||
|
HashAlgorithm ha;
|
||||||
|
|
||||||
|
switch (keySize)
|
||||||
|
{
|
||||||
|
case RsaKeySize.KeySize2048:
|
||||||
|
ha = HashAlgorithm.Sha256;
|
||||||
|
break;
|
||||||
|
case RsaKeySize.KeySize4096:
|
||||||
|
ha = HashAlgorithm.Sha384;
|
||||||
|
break;
|
||||||
|
case RsaKeySize.KeySize8192:
|
||||||
|
ha = HashAlgorithm.Sha512;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new UnsupportedValueException(keySize);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ha;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return string.Format("type = '{0}', key-size = '{1}', hash-algorithm = '{2}'", Type, KeySize, HashAlgorithm);
|
||||||
|
}
|
||||||
|
}
|
||||||
8
certmgr/CertGen/RsaKeySize.cs
Normal file
8
certmgr/CertGen/RsaKeySize.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace CertMgr.CertGen;
|
||||||
|
|
||||||
|
public enum RsaKeySize
|
||||||
|
{
|
||||||
|
KeySize2048 = 1,
|
||||||
|
KeySize4096,
|
||||||
|
KeySize8192
|
||||||
|
}
|
||||||
41
certmgr/CertGen/SubjectAlternateNames.cs
Normal file
41
certmgr/CertGen/SubjectAlternateNames.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using System.Collections;
|
||||||
|
|
||||||
|
namespace CertMgr.CertGen;
|
||||||
|
|
||||||
|
public sealed class SubjectAlternateNames : IReadOnlyCollection<string>
|
||||||
|
{
|
||||||
|
private readonly HashSet<string> _items;
|
||||||
|
|
||||||
|
public SubjectAlternateNames()
|
||||||
|
{
|
||||||
|
_items = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SubjectAlternateNames(IReadOnlyCollection<string> items)
|
||||||
|
{
|
||||||
|
_items = new HashSet<string>(items, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Count => _items.Count;
|
||||||
|
|
||||||
|
public bool Add(string name)
|
||||||
|
{
|
||||||
|
bool added = _items.Add(name);
|
||||||
|
return added;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator<string> GetEnumerator()
|
||||||
|
{
|
||||||
|
return _items.GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
{
|
||||||
|
return GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return string.Format("count = {0}", _items.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
certmgr/CertGen/Utils/CollectionEquivalencyComparer.cs
Normal file
26
certmgr/CertGen/Utils/CollectionEquivalencyComparer.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using CertMgr.Core.Utils;
|
||||||
|
|
||||||
|
namespace CertMgr.CertGen.Utils;
|
||||||
|
|
||||||
|
public sealed class CollectionEquivalencyComparer<T> : IEqualityComparer<IEnumerable<T>>
|
||||||
|
{
|
||||||
|
public bool Equals(IEnumerable<T>? x, IEnumerable<T>? y)
|
||||||
|
{
|
||||||
|
if (x == y)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (x == null || y == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool equivalent = x.Equivalent(y);
|
||||||
|
return equivalent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetHashCode(IEnumerable<T> obj)
|
||||||
|
{
|
||||||
|
return obj?.Sum(it => it?.GetHashCode() ?? 0) ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
49
certmgr/CertGen/Utils/StorageToX509CertificateAdapter.cs
Normal file
49
certmgr/CertGen/Utils/StorageToX509CertificateAdapter.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/*using System.Security.Cryptography.X509Certificates;
|
||||||
|
|
||||||
|
using CertMgr.Core.Storage;
|
||||||
|
|
||||||
|
namespace CertMgr.CertGen.Utils;
|
||||||
|
|
||||||
|
public sealed class StorageToX509CertificateAdapter : StorageAdapter<X509Certificate2>
|
||||||
|
{
|
||||||
|
private readonly X509Certificate2? _cert;
|
||||||
|
private readonly string _password;
|
||||||
|
private readonly X509KeyStorageFlags? _flags;
|
||||||
|
|
||||||
|
public StorageToX509CertificateAdapter(X509Certificate2 cert, string? password)
|
||||||
|
{
|
||||||
|
_cert = cert;
|
||||||
|
_password = password ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StorageToX509CertificateAdapter(string? password, X509KeyStorageFlags flags)
|
||||||
|
{
|
||||||
|
_password = password ?? string.Empty;
|
||||||
|
_flags = flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<X509Certificate2> DoReadAsync(IStorage source, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
X509Certificate2 cert;
|
||||||
|
|
||||||
|
using (MemoryStream ms = new MemoryStream())
|
||||||
|
{
|
||||||
|
await source.ReadAsync(ms, cancellationToken);
|
||||||
|
cert = X509CertificateLoader.LoadPkcs12(ms.GetBuffer(), _password, _flags.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cert;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<StoreResult> DoWriteAsync(IStorage target, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
byte[] data = _cert.Export(X509ContentType.Pfx, _password);
|
||||||
|
using (MemoryStream ms = new MemoryStream())
|
||||||
|
{
|
||||||
|
await ms.WriteAsync(data, cancellationToken);
|
||||||
|
ms.Position = 0;
|
||||||
|
return await target.WriteAsync(ms, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
29
certmgr/Core/Attributes/SettingAttribute.cs
Normal file
29
certmgr/Core/Attributes/SettingAttribute.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Attributes;
|
||||||
|
|
||||||
|
public sealed class SettingAttribute : Attribute
|
||||||
|
{
|
||||||
|
public SettingAttribute(string name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
IsMandatory = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public string[] AlternateNames { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||||
|
|
||||||
|
public bool IsMandatory { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||||
|
|
||||||
|
public Type? Validator { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||||
|
|
||||||
|
public Type? Converter { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||||
|
|
||||||
|
public object? Default { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return string.Format("name = '{0}', is-mandatory = {1}, validator = {2}, converter = {3}", Name, IsMandatory ? "yes" : "no", Validator?.GetType().Name ?? "<not-set>", Converter?.GetType().Name ?? "<not-set>");
|
||||||
|
}
|
||||||
|
}
|
||||||
15
certmgr/Core/CertMgrException.cs
Normal file
15
certmgr/Core/CertMgrException.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using CertMgr.Core.Utils;
|
||||||
|
|
||||||
|
namespace CertMgr.Core;
|
||||||
|
|
||||||
|
public class CertMgrException : Exception
|
||||||
|
{
|
||||||
|
internal CertMgrException(string messageFormat, params object[] messageArgs)
|
||||||
|
: base(StringFormatter.Format(messageFormat, messageArgs))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
internal CertMgrException(Exception innerException, string messageFormat, params object[] messageArgs)
|
||||||
|
: base(StringFormatter.Format(messageFormat, messageArgs), innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
11
certmgr/Core/Cli/CliException.cs
Normal file
11
certmgr/Core/Cli/CliException.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using CertMgr.Core.Utils;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Cli;
|
||||||
|
|
||||||
|
public class CliException : Exception
|
||||||
|
{
|
||||||
|
public CliException(string messageFormat, params object[] messageArgs)
|
||||||
|
: base(StringFormatter.Format(messageFormat, messageArgs))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
61
certmgr/Core/Cli/CliParser.cs
Normal file
61
certmgr/Core/Cli/CliParser.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
using CertMgr.Core.Log;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Cli;
|
||||||
|
|
||||||
|
public sealed class CliParser
|
||||||
|
{
|
||||||
|
public Task<RawArguments> ParseAsync(string[] args, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
CLog.Info("parsing arguments...");
|
||||||
|
|
||||||
|
RawArguments rawArgs = new RawArguments(args.Length);
|
||||||
|
|
||||||
|
foreach (string arg in args)
|
||||||
|
{
|
||||||
|
int nameStartIndex = -1;
|
||||||
|
if (arg.StartsWith("--"))
|
||||||
|
{
|
||||||
|
nameStartIndex = 2;
|
||||||
|
}
|
||||||
|
else if (arg.StartsWith("-"))
|
||||||
|
{
|
||||||
|
nameStartIndex = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nameStartIndex < 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int separatorIndex = arg.IndexOf('=', nameStartIndex);
|
||||||
|
if (separatorIndex < 0)
|
||||||
|
{
|
||||||
|
// --my-value
|
||||||
|
string name = arg.Substring(nameStartIndex);
|
||||||
|
rawArgs.Add(new RawArgument(name));
|
||||||
|
}
|
||||||
|
else if (separatorIndex == nameStartIndex + 1)
|
||||||
|
{
|
||||||
|
// --= or --=xxx => name missing
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// --myvalue= or --my-value=xxx
|
||||||
|
string name = arg.Substring(nameStartIndex, separatorIndex - nameStartIndex);
|
||||||
|
if (separatorIndex < arg.Length)
|
||||||
|
{
|
||||||
|
string value = arg.Substring(separatorIndex + 1);
|
||||||
|
rawArgs.Add(new RawArgument(name, value));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rawArgs.Add(new RawArgument(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CLog.Info("parsing arguments... done (found {0} arguments)", rawArgs.Count);
|
||||||
|
|
||||||
|
return Task.FromResult(rawArgs);
|
||||||
|
}
|
||||||
|
}
|
||||||
58
certmgr/Core/Cli/RawArgument.cs
Normal file
58
certmgr/Core/Cli/RawArgument.cs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Cli;
|
||||||
|
|
||||||
|
public sealed class RawArgument
|
||||||
|
{
|
||||||
|
internal RawArgument(string name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Values = new List<string>(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal RawArgument(string name, string value)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Values = new List<string> { value };
|
||||||
|
}
|
||||||
|
|
||||||
|
internal RawArgument(string name, IReadOnlyList<string> values, string value)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
List<string> tmp = new List<string>(values.Count + 1);
|
||||||
|
Values = tmp;
|
||||||
|
tmp.AddRange(values);
|
||||||
|
tmp.Add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal RawArgument(string name, IReadOnlyList<string> values1, IReadOnlyList<string> values2)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
List<string> tmp = new List<string>(values1.Count + values2.Count);
|
||||||
|
Values = tmp;
|
||||||
|
tmp.AddRange(values1);
|
||||||
|
tmp.AddRange(values2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public IReadOnlyList<string> Values { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
string result;
|
||||||
|
switch (Values.Count)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
result = string.Format("name = '{0}', no-values", Name);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
result = string.Format("name = '{0}', value = '{1}'", Name, Values.First() ?? "<null>");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
result = string.Format("name = '{0}', first-value = '{1}', values-count = '{2}'", Name, Values[0] ?? "<null>", Values.Count);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
61
certmgr/Core/Cli/RawArguments.cs
Normal file
61
certmgr/Core/Cli/RawArguments.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Cli;
|
||||||
|
|
||||||
|
public sealed class RawArguments : IReadOnlyCollection<RawArgument>
|
||||||
|
{
|
||||||
|
private Dictionary<string, RawArgument> _items;
|
||||||
|
|
||||||
|
internal RawArguments()
|
||||||
|
{
|
||||||
|
_items = new Dictionary<string, RawArgument>();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal RawArguments(int initialCapacity)
|
||||||
|
{
|
||||||
|
_items = new Dictionary<string, RawArgument>(initialCapacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Count => _items.Count;
|
||||||
|
|
||||||
|
public IReadOnlyList<string> Names => _items.Keys.ToList();
|
||||||
|
|
||||||
|
public RawArgument this[string name]
|
||||||
|
{
|
||||||
|
get => _items[name];
|
||||||
|
set => _items[name] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void Add(RawArgument rawArg)
|
||||||
|
{
|
||||||
|
if (_items.Remove(rawArg.Name, out RawArgument? existing))
|
||||||
|
{
|
||||||
|
_items.Add(rawArg.Name, new RawArgument(rawArg.Name, existing.Values, rawArg.Values));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_items.Add(rawArg.Name, rawArg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGet(string name, [NotNullWhen(true)] out RawArgument? rawArg)
|
||||||
|
{
|
||||||
|
return _items.TryGetValue(name, out rawArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator<RawArgument> GetEnumerator()
|
||||||
|
{
|
||||||
|
return _items.Values.GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
{
|
||||||
|
return GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return string.Format("count = {0}", _items.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
6
certmgr/Core/Converters/ConverterContext.cs
Normal file
6
certmgr/Core/Converters/ConverterContext.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace CertMgr.Core.Converters;
|
||||||
|
|
||||||
|
public class ConverterContext
|
||||||
|
{
|
||||||
|
public static readonly ConverterContext Empty = new ConverterContext();
|
||||||
|
}
|
||||||
15
certmgr/Core/Converters/ConverterException.cs
Normal file
15
certmgr/Core/Converters/ConverterException.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using CertMgr.Core.Utils;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Converters;
|
||||||
|
|
||||||
|
public sealed class ConverterException : Exception
|
||||||
|
{
|
||||||
|
internal ConverterException(string messageFormat, params object?[] messageArgs)
|
||||||
|
: base(StringFormatter.Format(messageFormat, messageArgs))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
internal ConverterException(Exception? innerException, string messageFormat, params object?[] messageArgs)
|
||||||
|
: base(StringFormatter.Format(messageFormat, messageArgs), innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
19
certmgr/Core/Converters/ConverterFactory.cs
Normal file
19
certmgr/Core/Converters/ConverterFactory.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using CertMgr.Core.Converters.Impl;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Converters;
|
||||||
|
|
||||||
|
public sealed class ConverterFactory
|
||||||
|
{
|
||||||
|
internal ConverterFactory()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConverterStash CreateDefault()
|
||||||
|
{
|
||||||
|
ConverterStash stash = new ConverterStash();
|
||||||
|
stash.Set(typeof(int), typeof(IntConverter));
|
||||||
|
stash.Set(typeof(string), typeof(StringConverter));
|
||||||
|
stash.Set(typeof(Enum), typeof(EnumConverter));
|
||||||
|
return stash;
|
||||||
|
}
|
||||||
|
}
|
||||||
102
certmgr/Core/Converters/ConverterStash.cs
Normal file
102
certmgr/Core/Converters/ConverterStash.cs
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Converters;
|
||||||
|
|
||||||
|
public sealed class ConverterStash
|
||||||
|
{
|
||||||
|
private readonly Dictionary<Type, ConverterDescriptor> _items;
|
||||||
|
|
||||||
|
internal ConverterStash()
|
||||||
|
{
|
||||||
|
_items = new Dictionary<Type, ConverterDescriptor>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Set(Type conversionType, Type converterType, bool replace = false)
|
||||||
|
{
|
||||||
|
bool added = false;
|
||||||
|
|
||||||
|
if (_items.ContainsKey(conversionType))
|
||||||
|
{
|
||||||
|
if (replace)
|
||||||
|
{
|
||||||
|
_items[conversionType] = new ConverterDescriptor(converterType);
|
||||||
|
added = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_items.Add(conversionType, new ConverterDescriptor(converterType));
|
||||||
|
added = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return added;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGet(Type type, [NotNullWhen(true)] out IValueConverter? converter)
|
||||||
|
{
|
||||||
|
converter = null;
|
||||||
|
|
||||||
|
if (_items.TryGetValue(type, out ConverterDescriptor? descriptor))
|
||||||
|
{
|
||||||
|
converter = descriptor.Instance;
|
||||||
|
}
|
||||||
|
else if (type.IsEnum)
|
||||||
|
{
|
||||||
|
if (_items.TryGetValue(typeof(Enum), out descriptor))
|
||||||
|
{
|
||||||
|
converter = descriptor.Instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return converter != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return string.Format("count = {0}", _items.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ConverterDescriptor
|
||||||
|
{
|
||||||
|
private IValueConverter? _converter;
|
||||||
|
|
||||||
|
public ConverterDescriptor(Type converterType)
|
||||||
|
{
|
||||||
|
ConverterType = converterType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type ConverterType { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public IValueConverter Instance
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_converter == null)
|
||||||
|
{
|
||||||
|
lock (this)
|
||||||
|
{
|
||||||
|
if (_converter == null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_converter = (IValueConverter?)Activator.CreateInstance(ConverterType);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new ConverterException(e, "Failed to create instance of converter of type '{0}'", ConverterType.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_converter == null)
|
||||||
|
{
|
||||||
|
throw new ConverterException("Failed to create instance of converter of type '{0}'", ConverterType.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _converter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
certmgr/Core/Converters/EnumConverterContext.cs
Normal file
13
certmgr/Core/Converters/EnumConverterContext.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Converters;
|
||||||
|
|
||||||
|
public class EnumConverterContext : ConverterContext
|
||||||
|
{
|
||||||
|
internal EnumConverterContext(Type targetType)
|
||||||
|
{
|
||||||
|
TargetType = targetType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type TargetType { [DebuggerStepThrough] get; }
|
||||||
|
}
|
||||||
6
certmgr/Core/Converters/IValueConverter.cs
Normal file
6
certmgr/Core/Converters/IValueConverter.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace CertMgr.Core.Converters;
|
||||||
|
|
||||||
|
public interface IValueConverter
|
||||||
|
{
|
||||||
|
Task<object?> ConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
6
certmgr/Core/Converters/IValueConverterT.cs
Normal file
6
certmgr/Core/Converters/IValueConverterT.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace CertMgr.Core.Converters;
|
||||||
|
|
||||||
|
public interface IValueConverter<T> : IValueConverter
|
||||||
|
{
|
||||||
|
new Task<T?> ConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
43
certmgr/Core/Converters/Impl/EnumConverter.cs
Normal file
43
certmgr/Core/Converters/Impl/EnumConverter.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using CertMgr.Core.Utils;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Converters.Impl;
|
||||||
|
|
||||||
|
public sealed class EnumConverter : ValueConverter<Enum>
|
||||||
|
{
|
||||||
|
protected override Task<Enum?> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Type resultType;
|
||||||
|
|
||||||
|
bool isNullable = targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Nullable<>);
|
||||||
|
if (isNullable)
|
||||||
|
{
|
||||||
|
Type[] args = targetType.GetGenericArguments();
|
||||||
|
if (args.Length != 1)
|
||||||
|
{
|
||||||
|
throw new ConverterException("Cannot convert nullable type '{0}' to enum", targetType.ToString(false));
|
||||||
|
}
|
||||||
|
if (args[0].IsEnum)
|
||||||
|
{
|
||||||
|
resultType = args[0];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ConverterException("Cannot convert nullable type '{0}' as enum as it is not enum", targetType.ToString(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!targetType.IsEnum)
|
||||||
|
{
|
||||||
|
resultType = targetType;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ConverterException("Cannot convert type '{0}' as enum as it is not enum", targetType.ToString(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object? result = Enum.Parse(resultType, rawValue, true);
|
||||||
|
return Task.FromResult((Enum?)result);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
certmgr/Core/Converters/Impl/EnumNullableConverter.cs
Normal file
15
certmgr/Core/Converters/Impl/EnumNullableConverter.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace CertMgr.Core.Converters.Impl;
|
||||||
|
|
||||||
|
public sealed class EnumNullableConverter : ValueConverter<Enum?>
|
||||||
|
{
|
||||||
|
protected override Task<Enum?> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!targetType.IsEnum)
|
||||||
|
{
|
||||||
|
throw new ConverterException("Cannot convert type '{0}' as enum as it is not enum", targetType.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
object? result = Enum.Parse(targetType, rawValue, true);
|
||||||
|
return Task.FromResult((Enum?)result);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
certmgr/Core/Converters/Impl/IntConverter.cs
Normal file
10
certmgr/Core/Converters/Impl/IntConverter.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace CertMgr.Core.Converters.Impl;
|
||||||
|
|
||||||
|
public sealed class IntConverter : ValueConverter<int>
|
||||||
|
{
|
||||||
|
protected override Task<int> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
int result = int.Parse(rawValue);
|
||||||
|
return Task.FromResult(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
121
certmgr/Core/Converters/Impl/StorageConverter.cs
Normal file
121
certmgr/Core/Converters/Impl/StorageConverter.cs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
using CertMgr.Core.Log;
|
||||||
|
using CertMgr.Core.Storage;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Converters.Impl;
|
||||||
|
|
||||||
|
internal sealed class StorageConverter : ValueConverter<IStorage>
|
||||||
|
{
|
||||||
|
private const char Separator = '|';
|
||||||
|
|
||||||
|
protected override Task<IStorage?> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ReadOnlySpan<char> rawSpan = rawValue.AsSpan();
|
||||||
|
|
||||||
|
int storageTypeSplitIndex = rawSpan.IndexOf(Separator);
|
||||||
|
if (storageTypeSplitIndex == -1)
|
||||||
|
{
|
||||||
|
return Task.FromResult((IStorage?)EmptyStorage.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
IStorage? storage;
|
||||||
|
|
||||||
|
ReadOnlySpan<char> storageTypeSpan = rawSpan.Slice(0, storageTypeSplitIndex);
|
||||||
|
ReadOnlySpan<char> storageDefinition = rawSpan.Slice(storageTypeSplitIndex + 1);
|
||||||
|
switch (storageTypeSpan)
|
||||||
|
{
|
||||||
|
case "file":
|
||||||
|
if (!TryGetFileStore(storageDefinition, out storage))
|
||||||
|
{
|
||||||
|
storage = EmptyStorage.Empty;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
storage = EmptyStorage.Empty;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult((IStorage?)storage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetFileStore(ReadOnlySpan<char> storageDefinition, [NotNullWhen(true)] out IStorage? storage)
|
||||||
|
{
|
||||||
|
// expecting that 'storageDefinition' is something like:
|
||||||
|
// <store-mode>|<file-path>
|
||||||
|
// or
|
||||||
|
// |<file-path>
|
||||||
|
// or
|
||||||
|
// <file-path>
|
||||||
|
//
|
||||||
|
// where <store-mode> can be:
|
||||||
|
// 'o' or 'overwrite' or 'overwriteornew'
|
||||||
|
// or
|
||||||
|
// 'a' or 'append' or 'appendornew'
|
||||||
|
// or
|
||||||
|
// 'c' or 'create' or 'createnew'
|
||||||
|
//
|
||||||
|
// where <file-path> can be:
|
||||||
|
// 'c:\path\myfile.txt'
|
||||||
|
// or
|
||||||
|
// '/path/myfile.txt'
|
||||||
|
// or
|
||||||
|
// './path/myfile.txt'
|
||||||
|
// in addition - <file-path> can be either quoted or double-quoted
|
||||||
|
|
||||||
|
storage = null;
|
||||||
|
|
||||||
|
FileStoreMode defaultStoreMode = FileStoreMode.OverwriteOrNew;
|
||||||
|
FileStoreMode storeMode;
|
||||||
|
ReadOnlySpan<char> filename;
|
||||||
|
|
||||||
|
int firstSplitIndex = storageDefinition.IndexOf(Separator);
|
||||||
|
if (firstSplitIndex != -1)
|
||||||
|
{
|
||||||
|
// there is a splitter. On the left side of it there must be <store-mode>. On the right there is a <file-path>
|
||||||
|
|
||||||
|
ReadOnlySpan<char> firstPart = storageDefinition.Slice(0, firstSplitIndex);
|
||||||
|
filename = storageDefinition.Slice(firstSplitIndex + 1);
|
||||||
|
|
||||||
|
if (firstPart.Length == 0)
|
||||||
|
{
|
||||||
|
storeMode = defaultStoreMode;
|
||||||
|
}
|
||||||
|
else if (Enum.TryParse(firstPart, true, out FileStoreMode tmpMode))
|
||||||
|
{
|
||||||
|
storeMode = tmpMode;
|
||||||
|
}
|
||||||
|
else if (firstPart.Equals("w", StringComparison.OrdinalIgnoreCase) || firstPart.Equals("overwrite", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
storeMode = FileStoreMode.OverwriteOrNew;
|
||||||
|
}
|
||||||
|
else if (firstPart.Equals("c", StringComparison.OrdinalIgnoreCase) || firstPart.Equals("createnew", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
storeMode = FileStoreMode.Create;
|
||||||
|
}
|
||||||
|
else if (firstPart.Equals("a", StringComparison.OrdinalIgnoreCase) || firstPart.Equals("append", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
storeMode = FileStoreMode.AppendOrNew;
|
||||||
|
}
|
||||||
|
else if (firstPart.Equals("o", StringComparison.OrdinalIgnoreCase) || firstPart.Equals("open", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
storeMode = FileStoreMode.Open;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// it is not store-mode or there is a typo or unsupported value
|
||||||
|
CLog.Error(string.Concat("Unsupported store-mode '", firstPart, "'"));
|
||||||
|
storeMode = defaultStoreMode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// no split-char => just filename
|
||||||
|
storeMode = defaultStoreMode;
|
||||||
|
filename = storageDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
storage = new FileStorage(filename.ToString(), storeMode);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
certmgr/Core/Converters/Impl/StringConverter.cs
Normal file
10
certmgr/Core/Converters/Impl/StringConverter.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace CertMgr.Core.Converters.Impl;
|
||||||
|
|
||||||
|
public sealed class StringConverter : ValueConverter<string>
|
||||||
|
{
|
||||||
|
protected override Task<string?> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.FromResult(rawValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
49
certmgr/Core/Converters/Impl/TimeSpanConverter.cs
Normal file
49
certmgr/Core/Converters/Impl/TimeSpanConverter.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
namespace CertMgr.Core.Converters.Impl;
|
||||||
|
|
||||||
|
public sealed class TimeSpanConverter : ValueConverter<TimeSpan?>
|
||||||
|
{
|
||||||
|
protected override Task<TimeSpan?> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(rawValue) || rawValue.Length == 1)
|
||||||
|
{
|
||||||
|
return Task.FromResult((TimeSpan?)null);
|
||||||
|
}
|
||||||
|
|
||||||
|
ReadOnlySpan<char> rawSpan = rawValue.AsSpan();
|
||||||
|
ReadOnlySpan<char> valueSpan = rawSpan.Slice(0, rawSpan.Length - 1);
|
||||||
|
char unit = char.ToLower(rawValue[rawValue.Length - 1]);
|
||||||
|
|
||||||
|
TimeSpan? result;
|
||||||
|
|
||||||
|
if (int.TryParse(valueSpan, out int value))
|
||||||
|
{
|
||||||
|
switch (unit)
|
||||||
|
{
|
||||||
|
case 's':
|
||||||
|
result = TimeSpan.FromSeconds(value);
|
||||||
|
break;
|
||||||
|
case 'm':
|
||||||
|
result = TimeSpan.FromMinutes(value);
|
||||||
|
break;
|
||||||
|
case 'h':
|
||||||
|
result = TimeSpan.FromHours(value);
|
||||||
|
break;
|
||||||
|
case 'd':
|
||||||
|
result = TimeSpan.FromDays(value);
|
||||||
|
break;
|
||||||
|
case 'y':
|
||||||
|
result = TimeSpan.FromDays(value * 365);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
result = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
certmgr/Core/Converters/ValueConverter.cs
Normal file
11
certmgr/Core/Converters/ValueConverter.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace CertMgr.Core.Converters;
|
||||||
|
|
||||||
|
public abstract class ValueConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public Task<object?> ConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return DoConvertAsync(rawValue, targetType, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract Task<object?> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
30
certmgr/Core/Converters/ValueConverterT.cs
Normal file
30
certmgr/Core/Converters/ValueConverterT.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
namespace CertMgr.Core.Converters;
|
||||||
|
|
||||||
|
public abstract class ValueConverter<T> : IValueConverter<T>
|
||||||
|
{
|
||||||
|
public Task<T?> ConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (typeof(T) != targetType)
|
||||||
|
{
|
||||||
|
throw new ConverterException("This converter ('{0}') converts string value to type '{1}' but type '{2}' is expected this time", GetType().Name, typeof(T), targetType.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return DoConvertAsync(rawValue, targetType, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new ConverterException(e, "Failed to convert value '{0}' to type '{1}'", rawValue ?? "<null>", typeof(T).Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task<object?> IValueConverter.ConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
T? typedValue = await DoConvertAsync(rawValue, targetType, cancellationToken);
|
||||||
|
return typedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract Task<T?> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
9
certmgr/Core/Exceptions/UnsupportedValueException.cs
Normal file
9
certmgr/Core/Exceptions/UnsupportedValueException.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace CertMgr.Core.Exceptions;
|
||||||
|
|
||||||
|
public class UnsupportedValueException : Exception
|
||||||
|
{
|
||||||
|
public UnsupportedValueException(object unsupportedValue)
|
||||||
|
: base(string.Format("Unsupported value '{0}' of type '{1}'", unsupportedValue != null ? unsupportedValue.ToString() : "<null>", unsupportedValue != null ? unsupportedValue.GetType().FullName : "<null>"))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
24
certmgr/Core/JobDescriptor.cs
Normal file
24
certmgr/Core/JobDescriptor.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace CertMgr.Core;
|
||||||
|
|
||||||
|
internal sealed class JobDescriptor
|
||||||
|
{
|
||||||
|
internal JobDescriptor(Type jobType, Type settingsType, Type? resultType)
|
||||||
|
{
|
||||||
|
JobType = jobType;
|
||||||
|
SettingsType = settingsType;
|
||||||
|
ResultType = resultType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type JobType { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public Type SettingsType { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public Type? ResultType { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return string.Format("job-type = '{0}', settings-type = '{1}', result-type = '{2}'", JobType.Name, SettingsType.Name, ResultType?.Name ?? "<null>");
|
||||||
|
}
|
||||||
|
}
|
||||||
125
certmgr/Core/JobExecutor.cs
Normal file
125
certmgr/Core/JobExecutor.cs
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
using CertMgr.Core.Cli;
|
||||||
|
using CertMgr.Core.Jobs;
|
||||||
|
using CertMgr.Core.Log;
|
||||||
|
using CertMgr.Core.Utils;
|
||||||
|
using CertMgr.Core.Validation;
|
||||||
|
|
||||||
|
namespace CertMgr.Core;
|
||||||
|
|
||||||
|
internal sealed class JobExecutor
|
||||||
|
{
|
||||||
|
public async Task<int> ExecuteAsync(string[] args, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
int errorLevel = JobResult.Failure;
|
||||||
|
|
||||||
|
CliParser parser = new CliParser();
|
||||||
|
RawArguments rawArgs = await parser.ParseAsync(args, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (rawArgs.TryGet("job", out RawArgument? rawArg))
|
||||||
|
{
|
||||||
|
JobRegistry jobs = new JobRegistry();
|
||||||
|
await jobs.LoadAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (jobs.TryGet(rawArg.Values.First(), out JobDescriptor? descriptor))
|
||||||
|
{
|
||||||
|
IJob job = await CreateJobAsync(descriptor, rawArgs, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
JobResult result = await job.ExecuteAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
errorLevel = result.ErrorLevel;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CLog.Error("Unknown job '{0}'", rawArg.Values.First());
|
||||||
|
errorLevel = JobResult.Failure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CLog.Error("Argument '{0}' must be specified", "job");
|
||||||
|
errorLevel = JobResult.Failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IJob> CreateJobAsync(JobDescriptor descriptor, RawArguments rawArgs, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
IJob? job;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
job = (IJob?)Activator.CreateInstance(descriptor.JobType);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new CertMgrException(e, "Failed to instantiate job '{0}'", descriptor.JobType.Name);
|
||||||
|
}
|
||||||
|
if (job == null)
|
||||||
|
{
|
||||||
|
throw new CertMgrException("Failed to instantiate job '{0}' (Possibly missing parameterless ctor?)", descriptor.JobType.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsBuilder settingsBuilder = new SettingsBuilder(rawArgs, descriptor.SettingsType);
|
||||||
|
JobSettings settings = await settingsBuilder.LoadAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!settings.ValidationResults.IsValid)
|
||||||
|
{
|
||||||
|
StringBuilderCache.ScopedBuilder lease = StringBuilderCache.AcquireScoped();
|
||||||
|
StringBuilder sb = lease.Builder;
|
||||||
|
|
||||||
|
sb.AppendFormat("Validation of settings for job '{0}' found {1} error(s):", job.Name, settings.ValidationResults.Count(vr => !vr.IsValid));
|
||||||
|
sb.AppendLine();
|
||||||
|
foreach (ValidationResult vr in settings.ValidationResults)
|
||||||
|
{
|
||||||
|
if (vr.IsValid)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sb.AppendFormat("\t- {0}: {1}", vr.PropertyName, vr.Justification);
|
||||||
|
sb.AppendLine();
|
||||||
|
}
|
||||||
|
throw new CertMgrException(sb.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
MethodInfo? settingsSetter = GetPropertyWithPrivateSetter(descriptor.JobType, "Settings");
|
||||||
|
if (settingsSetter != null)
|
||||||
|
{
|
||||||
|
settingsSetter.Invoke(job, [settings]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new CertMgrException("Failed to initialize job '{0}'. Missing property 'Settings' (Possibly the job doesn't inherit from '{1}'?)", descriptor.JobType.Name, typeof(Job<>).Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return job;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MethodInfo? GetPropertyWithPrivateSetter(Type type, string name)
|
||||||
|
{
|
||||||
|
MethodInfo? setter = null;
|
||||||
|
|
||||||
|
for (Type currentType = type; currentType != null; currentType = currentType.BaseType)
|
||||||
|
{
|
||||||
|
if (currentType.ContainsGenericParameters)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
PropertyInfo? property = currentType.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
|
||||||
|
if (property == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
setter = property.GetSetMethod(true);
|
||||||
|
if (setter != null)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return setter;
|
||||||
|
}
|
||||||
|
}
|
||||||
153
certmgr/Core/JobRegistry.cs
Normal file
153
certmgr/Core/JobRegistry.cs
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
using CertMgr.Core.Jobs;
|
||||||
|
using CertMgr.Core.Log;
|
||||||
|
using CertMgr.Core.Utils;
|
||||||
|
|
||||||
|
namespace CertMgr.Core;
|
||||||
|
|
||||||
|
internal sealed class JobRegistry
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, JobDescriptor> _items;
|
||||||
|
|
||||||
|
public JobRegistry()
|
||||||
|
{
|
||||||
|
_items = new Dictionary<string, JobDescriptor>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<string> Names => _items.Keys.ToList();
|
||||||
|
|
||||||
|
public Task LoadAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
CLog.Info("Loading job registry...");
|
||||||
|
|
||||||
|
if (TryGetAssemblyTypes(Assembly.GetExecutingAssembly(), out Type[] types))
|
||||||
|
{
|
||||||
|
foreach (Type type in types)
|
||||||
|
{
|
||||||
|
if (type.IsAbstract)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
JobUtils.TryGetJobName(type, out string? jobName, out string? errorMessage);
|
||||||
|
TryGetGenericTypes(type, out Type? settingsType, out Type? resultType);
|
||||||
|
if (jobName != null && settingsType != null)
|
||||||
|
{
|
||||||
|
if (_items.TryGetValue(jobName, out JobDescriptor? alreadyRegisteredType))
|
||||||
|
{
|
||||||
|
CLog.Error("Job '{0}' has multiple implementations. Types '{0}' and '{1}'. First win.", jobName, alreadyRegisteredType.JobType.ToString(false), type.ToString(false));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_items.Add(jobName, new JobDescriptor(type, settingsType, resultType));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (jobName == null && settingsType != null)
|
||||||
|
{
|
||||||
|
CLog.Error(errorMessage);
|
||||||
|
}
|
||||||
|
if (jobName != null && settingsType == null)
|
||||||
|
{
|
||||||
|
CLog.Error("Job '{0}' has job-name '{1}' but the class doesn't inherit from '{2}'", type.ToString(false), jobName, typeof(Job<>).ToString(false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CLog.Info("Loading job registry... done (registered {0} jobs)", _items.Count);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGet(string name, [NotNullWhen(true)] out JobDescriptor? jobDescriptor)
|
||||||
|
{
|
||||||
|
jobDescriptor = null;
|
||||||
|
|
||||||
|
if (_items.TryGetValue(name, out JobDescriptor? descriptor))
|
||||||
|
{
|
||||||
|
jobDescriptor = descriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobDescriptor != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetGenericTypes(Type type, [NotNullWhen(true)] out Type? settingType, out Type? resultType)
|
||||||
|
{
|
||||||
|
settingType = null;
|
||||||
|
resultType = null;
|
||||||
|
|
||||||
|
for (Type t = type; t != null && type != typeof(object); t = t.BaseType!)
|
||||||
|
{
|
||||||
|
if (t.IsGenericType)
|
||||||
|
{
|
||||||
|
Type[] genericTypes = t.GetGenericArguments();
|
||||||
|
Type currentGenericType = t.GetGenericTypeDefinition();
|
||||||
|
if (currentGenericType == typeof(Job<,>))
|
||||||
|
{
|
||||||
|
if (typeof(JobSettings).IsAssignableFrom(genericTypes[0]))
|
||||||
|
{
|
||||||
|
settingType = genericTypes[0];
|
||||||
|
resultType = genericTypes[1];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
settingType = genericTypes[1];
|
||||||
|
resultType = genericTypes[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (currentGenericType == typeof(JobBase<>))
|
||||||
|
{
|
||||||
|
if (typeof(JobSettings).IsAssignableFrom(genericTypes[0]))
|
||||||
|
{
|
||||||
|
settingType = genericTypes[0];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return settingType != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetAssemblyTypes(Assembly assembly, out Type[] types)
|
||||||
|
{
|
||||||
|
bool succeeded;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
types = assembly.GetTypes();
|
||||||
|
succeeded = true;
|
||||||
|
}
|
||||||
|
catch (ReflectionTypeLoadException e)
|
||||||
|
{
|
||||||
|
using StringBuilderCache.ScopedBuilder lease = StringBuilderCache.AcquireScoped(256);
|
||||||
|
StringBuilder sb = lease.Builder;
|
||||||
|
sb.AppendFormat("Failed to load assembly types. {0}: {1} (exceptions-count = {2}).{3}", e.GetType().ToString(false), e.Message, e.LoaderExceptions?.Length ?? -1, Environment.NewLine);
|
||||||
|
foreach (Exception? le in e.LoaderExceptions ?? Array.Empty<Exception>())
|
||||||
|
{
|
||||||
|
if (le == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sb.AppendFormat("\t{0}: {1}{2}", le.GetType().ToString(false), le.Message, Environment.NewLine);
|
||||||
|
}
|
||||||
|
CLog.Error(StringBuilderCache.GetStringAndRelease(sb));
|
||||||
|
|
||||||
|
types = Array.Empty<Type>();
|
||||||
|
succeeded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return succeeded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return string.Format("count = {0}", _items.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
10
certmgr/Core/Jobs/IJob.cs
Normal file
10
certmgr/Core/Jobs/IJob.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Jobs;
|
||||||
|
|
||||||
|
public interface IJob
|
||||||
|
{
|
||||||
|
string Name { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
Task<JobResult> ExecuteAsync(CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
6
certmgr/Core/Jobs/IJobT.cs
Normal file
6
certmgr/Core/Jobs/IJobT.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace CertMgr.Core.Jobs;
|
||||||
|
|
||||||
|
public interface IJob<TResult> : IJob
|
||||||
|
{
|
||||||
|
new Task<JobResult<TResult>> ExecuteAsync(CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
20
certmgr/Core/Jobs/JobBase.cs
Normal file
20
certmgr/Core/Jobs/JobBase.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Jobs;
|
||||||
|
|
||||||
|
public abstract class JobBase<TSettings> where TSettings : JobSettings, new()
|
||||||
|
{
|
||||||
|
protected JobBase()
|
||||||
|
{
|
||||||
|
Name = JobUtils.GetJobName(GetType());
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public TSettings Settings { [DebuggerStepThrough] get; [DebuggerStepThrough] private set; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return string.Format("name = '{0}', settings-type = '{1}'", Name, Settings?.GetType().Name ?? "<null>");
|
||||||
|
}
|
||||||
|
}
|
||||||
15
certmgr/Core/Jobs/JobException.cs
Normal file
15
certmgr/Core/Jobs/JobException.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using CertMgr.Core.Utils;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Jobs;
|
||||||
|
|
||||||
|
public class JobException : Exception
|
||||||
|
{
|
||||||
|
internal JobException(string messageFormat, params object?[] messageArgs)
|
||||||
|
: base(StringFormatter.Format(messageFormat, messageArgs))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
internal JobException(Exception? innerException, string messageFormat, params object?[] messageArgs)
|
||||||
|
: base(StringFormatter.Format(messageFormat, messageArgs), innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
41
certmgr/Core/Jobs/JobRS.cs
Normal file
41
certmgr/Core/Jobs/JobRS.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
|
||||||
|
namespace CertMgr.Core.Jobs;
|
||||||
|
|
||||||
|
public abstract class Job<TResult, TSettings> : JobBase<TSettings>, IJob<TResult> where TSettings : JobSettings, new()
|
||||||
|
{
|
||||||
|
public async Task<JobResult<TResult>> ExecuteAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
JobResult<TResult> result = await DoExecuteAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return CreateFailure(default, e, "Failed to execute job '{0}'", Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract Task<JobResult<TResult>> DoExecuteAsync(CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
async Task<JobResult> IJob.ExecuteAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
JobResult<TResult> result = await ExecuteAsync(cancellationToken);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected JobResult<TResult> CreateSuccess(TResult? result, string messageFormat, params object[] messageArgs)
|
||||||
|
{
|
||||||
|
return new JobResult<TResult>(this, result, JobResult.Success, messageFormat, messageArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected JobResult<TResult> CreateFailure(TResult? result, string messageFormat, params object[] messageArgs)
|
||||||
|
{
|
||||||
|
return new JobResult<TResult>(this, result, JobResult.Success, messageFormat, messageArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected JobResult<TResult> CreateFailure(TResult? result, Exception exception, string messageFormat, params object[] messageArgs)
|
||||||
|
{
|
||||||
|
return new JobResult<TResult>(this, result, JobResult.Success, messageFormat, messageArgs);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
certmgr/Core/Jobs/JobResult.cs
Normal file
41
certmgr/Core/Jobs/JobResult.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
using CertMgr.Core.Utils;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Jobs;
|
||||||
|
|
||||||
|
public class JobResult
|
||||||
|
{
|
||||||
|
public const int Success = 0;
|
||||||
|
public const int Failure = 1;
|
||||||
|
|
||||||
|
internal JobResult(IJob job, int errorLevel, string messageFormat, params object[] messageArgs)
|
||||||
|
{
|
||||||
|
Job = job;
|
||||||
|
ErrorLevel = errorLevel;
|
||||||
|
Message = StringFormatter.Format(messageFormat, messageArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal JobResult(IJob job, int errorLevel, Exception? exception, string messageFormat, params object[] messageArgs)
|
||||||
|
{
|
||||||
|
Job = job;
|
||||||
|
ErrorLevel = errorLevel;
|
||||||
|
Message = StringFormatter.Format(messageFormat, messageArgs);
|
||||||
|
Exception = exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IJob Job { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public int ErrorLevel { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public string Message { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public Exception? Exception { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public bool IsSuccess => ErrorLevel == 0;
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return string.Format("result = {0}, message = '{1}'", IsSuccess ? "success" : "failure", Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
certmgr/Core/Jobs/JobResultT.cs
Normal file
20
certmgr/Core/Jobs/JobResultT.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Jobs;
|
||||||
|
|
||||||
|
public class JobResult<TResult> : JobResult
|
||||||
|
{
|
||||||
|
internal JobResult(IJob<TResult> job, TResult? result, int errorLevel, string messageFormat, params object[] messageArgs)
|
||||||
|
: base(job, errorLevel, messageFormat, messageArgs)
|
||||||
|
{
|
||||||
|
Result = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal JobResult(IJob<TResult> job, TResult? result, int errorLevel, Exception? exception, string messageFormat, params object[] messageArgs)
|
||||||
|
: base(job, errorLevel, exception, messageFormat, messageArgs)
|
||||||
|
{
|
||||||
|
Result = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TResult? Result { [DebuggerStepThrough] get; }
|
||||||
|
}
|
||||||
36
certmgr/Core/Jobs/JobS.cs
Normal file
36
certmgr/Core/Jobs/JobS.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
namespace CertMgr.Core.Jobs;
|
||||||
|
|
||||||
|
public abstract class Job<TSettings> : JobBase<TSettings>, IJob where TSettings : JobSettings, new()
|
||||||
|
{
|
||||||
|
public async Task<JobResult> ExecuteAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
JobResult result = await DoExecuteAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return CreateFailure(e, "Failed to execute job '{0}'", Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract Task<JobResult> DoExecuteAsync(CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
protected JobResult CreateSuccess(string messageFormat, params object[] messageArgs)
|
||||||
|
{
|
||||||
|
return new JobResult(this, JobResult.Success, messageFormat, messageArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected JobResult CreateFailure(string messageFormat, params object[] messageArgs)
|
||||||
|
{
|
||||||
|
return new JobResult(this, JobResult.Failure, messageFormat, messageArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected JobResult CreateFailure(Exception exception, string messageFormat, params object[] messageArgs)
|
||||||
|
{
|
||||||
|
return new JobResult(this, JobResult.Failure, exception, messageFormat, messageArgs);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
certmgr/Core/Jobs/JobSettings.cs
Normal file
25
certmgr/Core/Jobs/JobSettings.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
using CertMgr.Core.Validation;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Jobs;
|
||||||
|
|
||||||
|
public abstract class JobSettings
|
||||||
|
{
|
||||||
|
protected JobSettings()
|
||||||
|
{
|
||||||
|
ValidationResults = new ValidationResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValidationResults ValidationResults { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public async Task ValidateAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await DoValidateAsync(ValidationResults, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual Task DoValidateAsync(ValidationResults results, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
54
certmgr/Core/Jobs/JobUtils.cs
Normal file
54
certmgr/Core/Jobs/JobUtils.cs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
using CertMgr.Core.Cli;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Jobs;
|
||||||
|
|
||||||
|
internal static class JobUtils
|
||||||
|
{
|
||||||
|
private const string IdFieldName = "ID";
|
||||||
|
|
||||||
|
internal static string GetJobName(Type jobType)
|
||||||
|
{
|
||||||
|
if (!TryGetJobName(jobType, out string? name, out string? errorMessage))
|
||||||
|
{
|
||||||
|
throw new CliException(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool TryGetJobName(Type jobType, [NotNullWhen(true)] out string? name, [NotNullWhen(false)] out string? errorMessage)
|
||||||
|
{
|
||||||
|
name = null;
|
||||||
|
errorMessage = null;
|
||||||
|
|
||||||
|
FieldInfo? finfo = jobType.GetField(IdFieldName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly);
|
||||||
|
if (finfo != null)
|
||||||
|
{
|
||||||
|
if (finfo.FieldType == typeof(string))
|
||||||
|
{
|
||||||
|
string? value = finfo.GetValue(null) as string;
|
||||||
|
if (!string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
name = value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
errorMessage = string.Format("Value of field '{0}' in class '{1}' must not be null or empty (actual = '{2}')", IdFieldName, jobType.Name, value == null ? "<null>" : "<empty>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
errorMessage = string.Format("Type of field '{0}' in class '{1}' must be '{2}' (actual = '{3})", IdFieldName, jobType.Name, typeof(string), finfo.FieldType.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
errorMessage = string.Format("Class '{0}' doesn't have field '{1}' of type '{2}'", jobType.Name, IdFieldName, typeof(string));
|
||||||
|
}
|
||||||
|
|
||||||
|
return name != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
117
certmgr/Core/Log/CLog.cs
Normal file
117
certmgr/Core/Log/CLog.cs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
using CertMgr.Core.Utils;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Log;
|
||||||
|
|
||||||
|
public static class CLog
|
||||||
|
{
|
||||||
|
public const string TimestampFormat = "HH:mm:ss,fff";
|
||||||
|
private const string ConsoleHeaderFormat = "({0})({1})(@{2})(#{3}) ";
|
||||||
|
|
||||||
|
private static readonly object _syncObj = new object();
|
||||||
|
|
||||||
|
public static void Debug(string messageFormat, params object?[] messageArgs)
|
||||||
|
{
|
||||||
|
Write(LogLevel.Debug, null, messageFormat, messageArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Info(string messageFormat, params object?[] messageArgs)
|
||||||
|
{
|
||||||
|
Write(LogLevel.Info, null, messageFormat, messageArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Warning(string messageFormat, params object?[] messageArgs)
|
||||||
|
{
|
||||||
|
Write(LogLevel.Warning, null, messageFormat, messageArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Error(string messageFormat, params object?[] messageArgs)
|
||||||
|
{
|
||||||
|
Write(LogLevel.Error, null, messageFormat, messageArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Error(Exception? exception, string messageFormat, params object?[] messageArgs)
|
||||||
|
{
|
||||||
|
Write(LogLevel.Error, exception, messageFormat, messageArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Write(LogLevel level, string messageFormat, params object?[] messageArgs)
|
||||||
|
{
|
||||||
|
Write(level, null, messageFormat, messageArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Write(LogLevel level, Exception? exception, string messageFormat, params object?[] messageArgs)
|
||||||
|
{
|
||||||
|
using StringBuilderCache.ScopedBuilder lease = StringBuilderCache.AcquireScoped(exception != null ? 256 : 128);
|
||||||
|
StringBuilder sb = lease.Builder;
|
||||||
|
|
||||||
|
AppendHeader(sb, ConsoleHeaderFormat, level, DateTime.Now, Process.GetCurrentProcess().Id, Thread.CurrentThread.ManagedThreadId);
|
||||||
|
AppendMessage(sb, exception, messageFormat, messageArgs);
|
||||||
|
|
||||||
|
WriteColorized(level, sb.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void WriteNoHeader(LogLevel level, string message)
|
||||||
|
{
|
||||||
|
WriteColorized(level, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendHeader(StringBuilder sb, string headerFormat, LogLevel level, DateTime timestamp, int processId, int threadId)
|
||||||
|
{
|
||||||
|
char levelChar = level.ToString()[0];
|
||||||
|
sb.AppendFormat(headerFormat, levelChar, timestamp.ToString(TimestampFormat), processId.ToString().PadLeft(5), threadId.ToString().PadLeft(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendMessage(StringBuilder sb, Exception? exception, string messageFormat, params object?[] messageArgs)
|
||||||
|
{
|
||||||
|
sb.AppendFormat(messageFormat, messageArgs);
|
||||||
|
|
||||||
|
if (exception != null)
|
||||||
|
{
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine(exception.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void WriteColorized(LogLevel level, string message)
|
||||||
|
{
|
||||||
|
ConsoleColor foreColor = GetForeColor(level);
|
||||||
|
lock (_syncObj)
|
||||||
|
{
|
||||||
|
Console.ForegroundColor = foreColor;
|
||||||
|
Console.WriteLine(message);
|
||||||
|
Console.ResetColor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ConsoleColor GetForeColor(LogLevel level)
|
||||||
|
{
|
||||||
|
ConsoleColor color;
|
||||||
|
|
||||||
|
switch (level)
|
||||||
|
{
|
||||||
|
case LogLevel.Debug:
|
||||||
|
color = ConsoleColor.Gray;
|
||||||
|
break;
|
||||||
|
case LogLevel.Info:
|
||||||
|
color = ConsoleColor.White;
|
||||||
|
break;
|
||||||
|
case LogLevel.Warning:
|
||||||
|
color = ConsoleColor.DarkYellow;
|
||||||
|
break;
|
||||||
|
case LogLevel.Error:
|
||||||
|
color = ConsoleColor.DarkRed;
|
||||||
|
break;
|
||||||
|
case LogLevel.Critical:
|
||||||
|
color = ConsoleColor.DarkMagenta;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
color = ConsoleColor.DarkCyan;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
certmgr/Core/Log/LogLevel.cs
Normal file
10
certmgr/Core/Log/LogLevel.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace CertMgr.Core.Log;
|
||||||
|
|
||||||
|
public enum LogLevel
|
||||||
|
{
|
||||||
|
Debug = 1,
|
||||||
|
Info,
|
||||||
|
Warning,
|
||||||
|
Error,
|
||||||
|
Critical
|
||||||
|
}
|
||||||
332
certmgr/Core/SettingsBuilder.cs
Normal file
332
certmgr/Core/SettingsBuilder.cs
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
using CertMgr.Core.Attributes;
|
||||||
|
using CertMgr.Core.Cli;
|
||||||
|
using CertMgr.Core.Converters;
|
||||||
|
using CertMgr.Core.Jobs;
|
||||||
|
using CertMgr.Core.Log;
|
||||||
|
using CertMgr.Core.Validation;
|
||||||
|
|
||||||
|
namespace CertMgr.Core;
|
||||||
|
|
||||||
|
internal sealed class SettingsBuilder
|
||||||
|
{
|
||||||
|
private readonly RawArguments _rawArgs;
|
||||||
|
private readonly Type _settingsType;
|
||||||
|
|
||||||
|
internal SettingsBuilder(RawArguments rawArgs, Type settingsType)
|
||||||
|
{
|
||||||
|
_rawArgs = rawArgs;
|
||||||
|
_settingsType = settingsType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<JobSettings> LoadAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
JobSettings settings = CreateSettingsInstance();
|
||||||
|
|
||||||
|
foreach ((PropertyInfo propertyInfo, SettingAttribute settingAttribute) in GetPropertiesWithSettingAttribute())
|
||||||
|
{
|
||||||
|
if (TryGetRawArgument(settingAttribute, out RawArgument? rawArg))
|
||||||
|
{
|
||||||
|
(bool isCollection, Type elementType) = GetValueType(propertyInfo);
|
||||||
|
|
||||||
|
(bool converted, object? convertedValue) = await ConvertRawValueAsync(settingAttribute, rawArg, isCollection, elementType, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (converted)
|
||||||
|
{
|
||||||
|
propertyInfo.SetValue(settings, convertedValue);
|
||||||
|
|
||||||
|
if (settingAttribute.Validator != null)
|
||||||
|
{
|
||||||
|
ISettingValidator? validator = (ISettingValidator?)Activator.CreateInstance(settingAttribute.Validator, [settingAttribute.Name]);
|
||||||
|
if (validator != null)
|
||||||
|
{
|
||||||
|
ValidationResult valres = await validator.ValidateAsync(convertedValue, cancellationToken).ConfigureAwait(false);
|
||||||
|
settings.ValidationResults.Add(valres);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (settingAttribute.IsMandatory)
|
||||||
|
{
|
||||||
|
ValidationResult valres = new ValidationResult(settingAttribute.Name, false, "Mandatory argument is missing");
|
||||||
|
settings.ValidationResults.Add(valres);
|
||||||
|
CLog.Error("mandatory argument '{0}' is missing", settingAttribute.Name);
|
||||||
|
}
|
||||||
|
else if (settingAttribute.Default != null)
|
||||||
|
{
|
||||||
|
(bool isCollection, Type elementType) = GetValueType(propertyInfo);
|
||||||
|
if (settingAttribute.Default.GetType() == elementType)
|
||||||
|
{
|
||||||
|
propertyInfo.SetValue(settings, settingAttribute.Default);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CLog.Error("Default value for argument '{0}' is specified, but its type is '{1}' instead of expected '{2}'", settingAttribute.Name, settingAttribute.Default?.GetType().Name ?? "<null>", elementType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await settings.ValidateAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetRawArgument(SettingAttribute settingAttribute, [NotNullWhen(true)] out RawArgument? rawArg)
|
||||||
|
{
|
||||||
|
rawArg = null;
|
||||||
|
|
||||||
|
if (!_rawArgs.TryGet(settingAttribute.Name, out rawArg))
|
||||||
|
{
|
||||||
|
if (settingAttribute.AlternateNames != null)
|
||||||
|
{
|
||||||
|
foreach (string altName in settingAttribute.AlternateNames)
|
||||||
|
{
|
||||||
|
if (_rawArgs.TryGet(altName, out rawArg))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawArg != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private (bool isCollection, Type elementType) GetValueType(PropertyInfo propertyInfo)
|
||||||
|
{
|
||||||
|
(bool isCollection, Type? elemType) = UnwrapCollection(propertyInfo.PropertyType);
|
||||||
|
Type targetType = isCollection ? elemType! : propertyInfo.PropertyType;
|
||||||
|
return (isCollection, targetType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<(PropertyInfo, SettingAttribute)> GetPropertiesWithSettingAttribute()
|
||||||
|
{
|
||||||
|
foreach (PropertyInfo propertyInfo in _settingsType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||||
|
{
|
||||||
|
SettingAttribute? settingAttribute = propertyInfo.GetCustomAttribute<SettingAttribute>();
|
||||||
|
if (settingAttribute is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return (propertyInfo, settingAttribute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private JobSettings CreateSettingsInstance()
|
||||||
|
{
|
||||||
|
object? instance;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
instance = Activator.CreateInstance(_settingsType);
|
||||||
|
if (instance == null)
|
||||||
|
{
|
||||||
|
throw new CertMgrException("Failed to create instance for settings of type '{0}'", _settingsType.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new CertMgrException(e, "Failed to create instance for settings of type '{0}'", _settingsType.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instance is not JobSettings settings)
|
||||||
|
{
|
||||||
|
throw new CertMgrException("Failed to create instance for settings of type '{0}'. The type is not of type '{1}'", _settingsType.Name, typeof(JobSettings).Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(bool success, object? convertedValue)> ConvertRawValueAsync(SettingAttribute settingAttribute, RawArgument rawArg, bool isCollection, Type targetType, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
bool success = false;
|
||||||
|
object? convertedValue = null;
|
||||||
|
|
||||||
|
if (isCollection)
|
||||||
|
{
|
||||||
|
if (TryGetCustomConverter(settingAttribute, out IValueConverter? customConverter))
|
||||||
|
{
|
||||||
|
List<object?> values = new List<object?>();
|
||||||
|
foreach (string rawValue in rawArg.Values)
|
||||||
|
{
|
||||||
|
convertedValue = await customConverter.ConvertAsync(rawValue, targetType, cancellationToken).ConfigureAwait(false);
|
||||||
|
values.Add(convertedValue);
|
||||||
|
}
|
||||||
|
convertedValue = values;
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
List<object?> values = new List<object?>();
|
||||||
|
foreach (string rawValue in rawArg.Values)
|
||||||
|
{
|
||||||
|
if (TryConvertValue(rawValue, targetType, out convertedValue))
|
||||||
|
{
|
||||||
|
values.Add(convertedValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
convertedValue = values;
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (TryGetCustomConverter(settingAttribute, out IValueConverter? customConverter))
|
||||||
|
{
|
||||||
|
convertedValue = await customConverter.ConvertAsync(rawArg.Values.First(), targetType, cancellationToken).ConfigureAwait(false);
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
else if (TryConvertValue(rawArg.Values.First(), targetType, out convertedValue))
|
||||||
|
{
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (success, convertedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetCustomConverter(SettingAttribute settingAttribute, [NotNullWhen(true)] out IValueConverter? customConverter)
|
||||||
|
{
|
||||||
|
customConverter = null;
|
||||||
|
|
||||||
|
Type? valueConverter = settingAttribute.Converter;
|
||||||
|
if (valueConverter != null)
|
||||||
|
{
|
||||||
|
if (typeof(IValueConverter).IsAssignableFrom(valueConverter))
|
||||||
|
{
|
||||||
|
customConverter = (IValueConverter?)Activator.CreateInstance(valueConverter);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CLog.Error("Argument '{0}' has converter specified but its type doesn't implement '{1}' and cannot be used", settingAttribute.Name, typeof(IValueConverter));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return customConverter != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryConvertValue(string rawValue, Type targetType, out object? convertedValue)
|
||||||
|
{
|
||||||
|
convertedValue = null;
|
||||||
|
|
||||||
|
if (targetType.IsEnum)
|
||||||
|
{
|
||||||
|
if (Enum.TryParse(targetType, rawValue, true, out object? result))
|
||||||
|
{
|
||||||
|
convertedValue = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TypeConverter converter = TypeDescriptor.GetConverter(targetType);
|
||||||
|
if (converter.CanConvertFrom(typeof(string)))
|
||||||
|
{
|
||||||
|
convertedValue = converter.ConvertFrom(null, CultureInfo.InvariantCulture, rawValue)!;
|
||||||
|
}
|
||||||
|
else if (targetType == typeof(string))
|
||||||
|
{
|
||||||
|
convertedValue = rawValue;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
convertedValue = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return convertedValue != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (bool isCollection, Type? elementType) UnwrapCollection(Type type)
|
||||||
|
{
|
||||||
|
if (type == typeof(string))
|
||||||
|
{
|
||||||
|
return (false, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Type? underlying = Nullable.GetUnderlyingType(type);
|
||||||
|
if (underlying != null)
|
||||||
|
{
|
||||||
|
type = underlying;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type.IsArray)
|
||||||
|
{
|
||||||
|
return (true, type.GetElementType()!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
|
||||||
|
{
|
||||||
|
return (true, type.GetGenericArguments()[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type.GetInterfaces().Any(i =>
|
||||||
|
i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
|
||||||
|
{
|
||||||
|
return (true, type.GetGenericArguments()[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (type.IsGenericType && typeof(IEnumerable<>).IsAssignableFrom(type.GetGenericTypeDefinition()))
|
||||||
|
// {
|
||||||
|
// return (true, type.GetGenericArguments()[0]);
|
||||||
|
// }
|
||||||
|
// if (t.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICollection<>)))
|
||||||
|
// {
|
||||||
|
// return (true, t.GetInterfaces().First(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICollection<>)).GetGenericArguments()[0]);
|
||||||
|
// }
|
||||||
|
// if (t.IsGenericType && (t.GetGenericTypeDefinition() == typeof(List<>)))
|
||||||
|
// {
|
||||||
|
// return (true, t.GetGenericArguments()[0]);
|
||||||
|
// }
|
||||||
|
|
||||||
|
return (false, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsEnumerableType(Type type)
|
||||||
|
{
|
||||||
|
if (type == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == typeof(string))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type.IsArray)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof(IEnumerable).IsAssignableFrom(type))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type.GetInterfaces().Any(i =>
|
||||||
|
i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Type UnwrapNullable(Type type)
|
||||||
|
{
|
||||||
|
Type? underlying = Nullable.GetUnderlyingType(type);
|
||||||
|
if (underlying != null)
|
||||||
|
{
|
||||||
|
return underlying;
|
||||||
|
}
|
||||||
|
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
certmgr/Core/Storage/EmptyStorage.cs
Normal file
24
certmgr/Core/Storage/EmptyStorage.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
namespace CertMgr.Core.Storage;
|
||||||
|
|
||||||
|
public sealed class EmptyStorage : Storage
|
||||||
|
{
|
||||||
|
public static readonly IStorage Empty = new EmptyStorage();
|
||||||
|
|
||||||
|
private EmptyStorage()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanWrite => true;
|
||||||
|
|
||||||
|
public override bool CanRead => true;
|
||||||
|
|
||||||
|
protected override Task<StoreResult> DoWriteAsync(Stream source, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.FromResult(StoreResult.CreateSuccess());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task<StoreResult> DoReadAsync(Stream target, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.FromResult(StoreResult.CreateSuccess());
|
||||||
|
}
|
||||||
|
}
|
||||||
44
certmgr/Core/Storage/FileStorage.cs
Normal file
44
certmgr/Core/Storage/FileStorage.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using CertMgr.Core.Utils;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Storage;
|
||||||
|
|
||||||
|
public sealed class FileStorage : Storage
|
||||||
|
{
|
||||||
|
private readonly string _fileFullPath;
|
||||||
|
private readonly FileStoreMode _mode;
|
||||||
|
|
||||||
|
public FileStorage(string fileFullPath, FileStoreMode mode)
|
||||||
|
{
|
||||||
|
_fileFullPath = fileFullPath;
|
||||||
|
_mode = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanWrite => _mode.IsAnyOf(FileStoreMode.AppendOrNew, FileStoreMode.Create, FileStoreMode.OverwriteOrNew);
|
||||||
|
|
||||||
|
public override bool CanRead => _mode == FileStoreMode.Open;
|
||||||
|
|
||||||
|
protected override async Task<StoreResult> DoWriteAsync(Stream source, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using (FileStream fs = new FileStream(_fileFullPath, (FileMode)_mode, FileAccess.Write, FileShare.None))
|
||||||
|
{
|
||||||
|
await source.CopyToAsync(fs, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return StoreResult.CreateSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<StoreResult> DoReadAsync(Stream target, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using (FileStream fs = new FileStream(_fileFullPath, (FileMode)_mode, FileAccess.Read, FileShare.Read))
|
||||||
|
{
|
||||||
|
await fs.CopyToAsync(target, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return StoreResult.CreateSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return string.Format("{0}: mode = '{1}', path = '{2}'", GetType().Name, _mode, _fileFullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
certmgr/Core/Storage/FileStoreMode.cs
Normal file
16
certmgr/Core/Storage/FileStoreMode.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
namespace CertMgr.Core.Storage;
|
||||||
|
|
||||||
|
public enum FileStoreMode
|
||||||
|
{
|
||||||
|
/// <summary>Create new file or append to existing.</summary>
|
||||||
|
AppendOrNew = FileMode.Append,
|
||||||
|
|
||||||
|
/// <summary>Create new file or overwrite existing.</summary>
|
||||||
|
OverwriteOrNew = FileMode.Create,
|
||||||
|
|
||||||
|
/// <summary>Create new file. Throw if already exists.</summary>
|
||||||
|
Create = FileMode.CreateNew,
|
||||||
|
|
||||||
|
/// <summary>Open existing file. Throw if doesn't exist.</summary>
|
||||||
|
Open = FileMode.Open,
|
||||||
|
}
|
||||||
12
certmgr/Core/Storage/IStorage.cs
Normal file
12
certmgr/Core/Storage/IStorage.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace CertMgr.Core.Storage;
|
||||||
|
|
||||||
|
public interface IStorage
|
||||||
|
{
|
||||||
|
Task<StoreResult> WriteAsync(Stream source, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
// Task<StoreResult> WriteFromAsync<TResult>(StorageAdapter<TResult> adapter, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
Task<StoreResult> ReadAsync(Stream target, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
// Task<StoreResult<T>> ReadToAsync<T>(StorageAdapter<T> adapter, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
83
certmgr/Core/Storage/Storage.cs
Normal file
83
certmgr/Core/Storage/Storage.cs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
using CertMgr.Core.Utils;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Storage;
|
||||||
|
|
||||||
|
public abstract class Storage : IStorage
|
||||||
|
{
|
||||||
|
|
||||||
|
public virtual bool CanRead => false;
|
||||||
|
|
||||||
|
public virtual bool CanWrite => false;
|
||||||
|
|
||||||
|
public async Task<StoreResult> WriteAsync(Stream source, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!CanWrite)
|
||||||
|
{
|
||||||
|
throw new StorageException("Cannot write. Storage not writable");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await DoWriteAsync(source, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
StorageException ex = new StorageException(e, "Failed to write to storage of type '{0}' from stream of type '{1}'", GetType().ToString(false), source.GetType().ToString(false));
|
||||||
|
return StoreResult.CreateFailure(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract Task<StoreResult> DoWriteAsync(Stream source, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
public async Task<StoreResult> ReadAsync(Stream target, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!CanRead)
|
||||||
|
{
|
||||||
|
throw new StorageException("Cannot read. Storage not readable");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await DoReadAsync(target, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
StorageException ex = new StorageException(e, "Failed to read from storage of type '{0}' to stream of type '{1}'", GetType().ToString(false), target.GetType().ToString(false));
|
||||||
|
return StoreResult.CreateFailure(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract Task<StoreResult> DoReadAsync(Stream target, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/*public async Task<StoreResult> WriteFromAsync<TResult>(StorageAdapter<TResult> adapter, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await adapter.WriteAsync(this, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
StorageException ex = new StorageException(e, "Failed to write to storage of type '{0}' adapter of type '{1}'", GetType().ToString(false), adapter.GetType().ToString(false));
|
||||||
|
return StoreResult.CreateFailure(ex);
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
/*public async Task<StoreResult<TResult>> ReadToAsync<TResult>(StorageAdapter<TResult> adapter, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
TResult result = await adapter.ReadAsync(this, cancellationToken).ConfigureAwait(false);
|
||||||
|
return new StoreResult<TResult>(result, true);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
StorageException ex = new StorageException(e, "Failed to read as type '{0}' from storage of type '{1}' using adapter of type '{1}'", typeof(TResult).ToString(false), GetType().ToString(false), adapter.GetType().ToString(false));
|
||||||
|
return new StoreResult<TResult>(default, false, ex);
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return string.Format("{0}: can-read = {1}, can-write = {2}", GetType().Name, CanRead ? "yes" : "no", CanWrite ? "yes" : "no");
|
||||||
|
}
|
||||||
|
}
|
||||||
15
certmgr/Core/Storage/StorageException.cs
Normal file
15
certmgr/Core/Storage/StorageException.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using CertMgr.Core.Utils;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Storage;
|
||||||
|
|
||||||
|
public class StorageException : Exception
|
||||||
|
{
|
||||||
|
internal StorageException(string messageFormat, params object?[] messageArgs)
|
||||||
|
: base(StringFormatter.Format(messageFormat, messageArgs))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
internal StorageException(Exception? innerException, string messageFormat, params object?[] messageArgs)
|
||||||
|
: base(StringFormatter.Format(messageFormat, messageArgs), innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
6
certmgr/Core/Storage/StorageType.cs
Normal file
6
certmgr/Core/Storage/StorageType.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace CertMgr.Core.Storage;
|
||||||
|
|
||||||
|
public enum StorageType
|
||||||
|
{
|
||||||
|
File = 1
|
||||||
|
}
|
||||||
39
certmgr/Core/Storage/StoreResult.cs
Normal file
39
certmgr/Core/Storage/StoreResult.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Storage;
|
||||||
|
|
||||||
|
public class StoreResult
|
||||||
|
{
|
||||||
|
private static readonly StoreResult Success = new StoreResult(true);
|
||||||
|
private static readonly StoreResult Failure = new StoreResult(false);
|
||||||
|
|
||||||
|
public StoreResult(bool isSuccess)
|
||||||
|
{
|
||||||
|
IsSuccess = isSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StoreResult(bool isSuccess, Exception? exception)
|
||||||
|
{
|
||||||
|
IsSuccess = isSuccess;
|
||||||
|
Exception = exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsSuccess { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public Exception? Exception { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public static StoreResult CreateSuccess()
|
||||||
|
{
|
||||||
|
return Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StoreResult CreateFailure()
|
||||||
|
{
|
||||||
|
return Failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StoreResult CreateFailure(Exception? exception)
|
||||||
|
{
|
||||||
|
return exception != null ? new StoreResult(false, exception) : Failure;
|
||||||
|
}
|
||||||
|
}
|
||||||
30
certmgr/Core/Storage/StoreResultT.cs
Normal file
30
certmgr/Core/Storage/StoreResultT.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Storage;
|
||||||
|
|
||||||
|
public sealed class StoreResult<TResult> : StoreResult
|
||||||
|
{
|
||||||
|
public StoreResult(TResult? result, bool isSuccess)
|
||||||
|
: base(isSuccess)
|
||||||
|
{
|
||||||
|
Result = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StoreResult(TResult? result, bool isSuccess, Exception? exception)
|
||||||
|
: base(isSuccess, exception)
|
||||||
|
{
|
||||||
|
Result = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TResult? Result { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public static StoreResult<TResult> CreateFailure(TResult? result, Exception? exception)
|
||||||
|
{
|
||||||
|
return exception != null ? new StoreResult<TResult>(result, false, exception) : new StoreResult<TResult>(result, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StoreResult<TResult> CreateSuccess(TResult? result)
|
||||||
|
{
|
||||||
|
return new StoreResult<TResult>(result, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
45
certmgr/Core/Utils/AsyncLock.cs
Normal file
45
certmgr/Core/Utils/AsyncLock.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
namespace CertMgr.Core.Utils;
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
public sealed class AsyncLock
|
||||||
|
{
|
||||||
|
private readonly SemaphoreSlim _semaphore;
|
||||||
|
private readonly Task<IDisposable> _disposer;
|
||||||
|
|
||||||
|
public AsyncLock()
|
||||||
|
{
|
||||||
|
_semaphore = new SemaphoreSlim(1, 1);
|
||||||
|
_disposer = Task.FromResult((IDisposable)new AsyncLockDisposer(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IDisposable> LockAsync(CancellationToken cancellation = default)
|
||||||
|
{
|
||||||
|
Task wait = _semaphore.WaitAsync();
|
||||||
|
if (wait.IsCompleted)
|
||||||
|
{
|
||||||
|
return _disposer;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return wait.ContinueWith((_, state) => (IDisposable)state!, _disposer.Result, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class AsyncLockDisposer : IDisposable
|
||||||
|
{
|
||||||
|
private readonly AsyncLock _owner;
|
||||||
|
|
||||||
|
internal AsyncLockDisposer(AsyncLock owner)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_owner._semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
248
certmgr/Core/Utils/Extenders.cs
Normal file
248
certmgr/Core/Utils/Extenders.cs
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Utils;
|
||||||
|
|
||||||
|
internal static class Extenders
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<Type, string> _typeAliases = new Dictionary<Type, string>
|
||||||
|
{
|
||||||
|
{ typeof(long), "long" },
|
||||||
|
{ typeof(ulong), "ulong" },
|
||||||
|
{ typeof(int), "int" },
|
||||||
|
{ typeof(uint), "uint" },
|
||||||
|
{ typeof(short), "short" },
|
||||||
|
{ typeof(ushort), "ushort" },
|
||||||
|
{ typeof(byte), "byte" },
|
||||||
|
{ typeof(sbyte), "sbyte" },
|
||||||
|
{ typeof(double), "double" },
|
||||||
|
{ typeof(float), "float" },
|
||||||
|
{ typeof(decimal), "decimal" },
|
||||||
|
{ typeof(char), "char" },
|
||||||
|
{ typeof(string), "string" },
|
||||||
|
{ typeof(void), "void" },
|
||||||
|
{ typeof(bool), "bool" },
|
||||||
|
{ typeof(object), "object" },
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string ToSeparatedList<T>(this IEnumerable<T> items, Func<T, string> formatter, string itemSeparator, string? lastItemSeparator = null)
|
||||||
|
{
|
||||||
|
using StringBuilderCache.ScopedBuilder lease = StringBuilderCache.AcquireScoped();
|
||||||
|
StringBuilder sb = lease.Builder;
|
||||||
|
|
||||||
|
ToSeparatedList(items, sb, formatter, itemSeparator, lastItemSeparator);
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ToSeparatedList<T>(this IEnumerable<T> items, StringBuilder sb, Func<T, string> formatter, string itemSeparator, string? lastItemSeparator = null)
|
||||||
|
{
|
||||||
|
if (!items.Any())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formatter == null)
|
||||||
|
{
|
||||||
|
formatter = item => item?.ToString() ?? "<null>";
|
||||||
|
}
|
||||||
|
|
||||||
|
lastItemSeparator = lastItemSeparator ?? itemSeparator;
|
||||||
|
|
||||||
|
using (IEnumerator<T> enu = items.GetEnumerator())
|
||||||
|
{
|
||||||
|
if (enu.MoveNext())
|
||||||
|
{
|
||||||
|
sb.Append(formatter(enu.Current));
|
||||||
|
bool hasNext = enu.MoveNext();
|
||||||
|
while (hasNext)
|
||||||
|
{
|
||||||
|
T current = enu.Current;
|
||||||
|
hasNext = enu.MoveNext();
|
||||||
|
|
||||||
|
sb.Append(hasNext ? itemSeparator : lastItemSeparator);
|
||||||
|
sb.Append(formatter(current));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToString(this Type type, bool includeNamespace)
|
||||||
|
{
|
||||||
|
if (type == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
using StringBuilderCache.ScopedBuilder lease = StringBuilderCache.AcquireScoped();
|
||||||
|
StringBuilder sb = lease.Builder;
|
||||||
|
type.AppendStringType(sb, includeNamespace);
|
||||||
|
string typeString = sb.ToString();
|
||||||
|
return typeString;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendStringType(this Type type, StringBuilder sb, bool includeNamespace)
|
||||||
|
{
|
||||||
|
if (type == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type.IsGenericType)
|
||||||
|
{
|
||||||
|
if (includeNamespace)
|
||||||
|
{
|
||||||
|
sb.Append(type.Namespace);
|
||||||
|
sb.Append('.');
|
||||||
|
}
|
||||||
|
sb.Append(type.Name.Substring(0, type.Name.IndexOf('`')));
|
||||||
|
sb.Append('<');
|
||||||
|
Type[] genericArguments = type.GenericTypeArguments;
|
||||||
|
for (int i = 0; i < genericArguments.Length; i++)
|
||||||
|
{
|
||||||
|
if (i > 0)
|
||||||
|
{
|
||||||
|
sb.Append(',');
|
||||||
|
}
|
||||||
|
genericArguments[i].AppendStringType(sb, includeNamespace);
|
||||||
|
}
|
||||||
|
sb.Append('>');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (_typeAliases.TryGetValue(type, out string? alias))
|
||||||
|
{
|
||||||
|
sb.Append(alias);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (includeNamespace)
|
||||||
|
{
|
||||||
|
sb.Append(type.Namespace);
|
||||||
|
sb.Append('.');
|
||||||
|
}
|
||||||
|
sb.Append(type.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Equivalent<T>(this IEnumerable<T?> left, IEnumerable<T?> right, IEqualityComparer<T?>? comparer = null)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(left, right))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (left == null || right == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (left.TryGetCount(out int lcount) && right.TryGetCount(out int rcount))
|
||||||
|
{
|
||||||
|
if (lcount != rcount)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int nullCount = 0;
|
||||||
|
Dictionary<T, int> cnt = new Dictionary<T, int>(comparer ?? EqualityComparer<T?>.Default);
|
||||||
|
foreach (T? item in left)
|
||||||
|
{
|
||||||
|
if (item == null)
|
||||||
|
{
|
||||||
|
nullCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (cnt.TryGetValue(item, out int value))
|
||||||
|
{
|
||||||
|
cnt[item] = ++value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cnt.Add(item, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (T? item in right)
|
||||||
|
{
|
||||||
|
if (item == null)
|
||||||
|
{
|
||||||
|
nullCount--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (cnt.ContainsKey(item))
|
||||||
|
{
|
||||||
|
cnt[item]--;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool equals = nullCount == 0 && cnt.Values.All(c => c == 0);
|
||||||
|
return equals;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetCount<T>(this IEnumerable<T> items, out int count)
|
||||||
|
{
|
||||||
|
count = -1;
|
||||||
|
|
||||||
|
bool success = false;
|
||||||
|
|
||||||
|
if (items is ICollection<T> colt)
|
||||||
|
{
|
||||||
|
count = colt.Count;
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
else if (items is System.Collections.ICollection col)
|
||||||
|
{
|
||||||
|
count = col.Count;
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
else if (items is IReadOnlyCollection<T> rocol)
|
||||||
|
{
|
||||||
|
count = rocol.Count;
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsAnyOf<T>(this T value, params T[] availableValues) where T : struct
|
||||||
|
{
|
||||||
|
if (!typeof(T).IsEnum)
|
||||||
|
{
|
||||||
|
throw new Exception(string.Format("Expected that the type is enum, got '{0}'", typeof(T).ToString(false)));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isOneOf = false;
|
||||||
|
|
||||||
|
Attribute? flags = typeof(T).GetCustomAttribute(typeof(FlagsAttribute));
|
||||||
|
if (flags == null)
|
||||||
|
{
|
||||||
|
if (availableValues != null && availableValues.Length > 0)
|
||||||
|
{
|
||||||
|
isOneOf = availableValues.Any(av => value.Equals(av));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ulong v = Convert.ToUInt64(value);
|
||||||
|
foreach (T availableValue in availableValues)
|
||||||
|
{
|
||||||
|
ulong av = Convert.ToUInt64(availableValue);
|
||||||
|
if ((v & av) == v)
|
||||||
|
{
|
||||||
|
isOneOf = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isOneOf;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
17
certmgr/Core/Utils/MachineNameFormat.cs
Normal file
17
certmgr/Core/Utils/MachineNameFormat.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
namespace CertMgr.Core.Utils;
|
||||||
|
|
||||||
|
public enum MachineNameFormat
|
||||||
|
{
|
||||||
|
/// <summary>NetBIOS name of the local computer. Limited to 15 characters and may be a truncated version of DNS host name.</summary>
|
||||||
|
NetBios = 1,
|
||||||
|
|
||||||
|
/// <summary>DNS name of the local computer.</summary>
|
||||||
|
Hostname = 2,
|
||||||
|
|
||||||
|
/// <summary>Fully qualified DNS name of the local computer. Combination of DNS hostname and DNS domain using form <HostName>.<DomainName></summary>
|
||||||
|
FullyQualified = 3,
|
||||||
|
|
||||||
|
/// <summary>Name of the DNS domain assigned to the local computer.</summary>
|
||||||
|
Domain = 4
|
||||||
|
}
|
||||||
|
|
||||||
37
certmgr/Core/Utils/NetUtils.cs
Normal file
37
certmgr/Core/Utils/NetUtils.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.NetworkInformation;
|
||||||
|
|
||||||
|
using CertMgr.Core.Exceptions;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Utils;
|
||||||
|
|
||||||
|
public static class NetUtils
|
||||||
|
{
|
||||||
|
/// <summary>Returns DNS name of the local computer.</summary>
|
||||||
|
public static string MachineName => GetMachineName(MachineNameFormat.Hostname);
|
||||||
|
|
||||||
|
public static string GetMachineName(MachineNameFormat format)
|
||||||
|
{
|
||||||
|
string name;
|
||||||
|
|
||||||
|
switch (format)
|
||||||
|
{
|
||||||
|
case MachineNameFormat.NetBios:
|
||||||
|
name = Environment.MachineName;
|
||||||
|
break;
|
||||||
|
case MachineNameFormat.Hostname:
|
||||||
|
name = Dns.GetHostName();
|
||||||
|
break;
|
||||||
|
case MachineNameFormat.FullyQualified:
|
||||||
|
name = Dns.GetHostEntry("localhost").HostName;
|
||||||
|
break;
|
||||||
|
case MachineNameFormat.Domain:
|
||||||
|
name = IPGlobalProperties.GetIPGlobalProperties().DomainName;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new UnsupportedValueException(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
172
certmgr/Core/Utils/StringBuilderCache.cs
Normal file
172
certmgr/Core/Utils/StringBuilderCache.cs
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Utils;
|
||||||
|
|
||||||
|
internal static class StringBuilderCache
|
||||||
|
{
|
||||||
|
private static readonly ConcurrentStack<StringBuilder> _cache = new ConcurrentStack<StringBuilder>();
|
||||||
|
private static readonly int _maxCapacityOfCachedItem = 8;
|
||||||
|
private static readonly int _maxCountOfItemsInCache = 4 * 1024;
|
||||||
|
|
||||||
|
private static int _pooledCount;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static StringBuilder? _hotSlot;
|
||||||
|
|
||||||
|
public static StringBuilder Acquire(int initialCapacity = 16)
|
||||||
|
{
|
||||||
|
StringBuilder? sb = Interlocked.Exchange(ref _hotSlot, null);
|
||||||
|
if (sb != null)
|
||||||
|
{
|
||||||
|
Prepare(sb, initialCapacity);
|
||||||
|
return sb;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_cache.TryPop(out sb))
|
||||||
|
{
|
||||||
|
Interlocked.Decrement(ref _pooledCount);
|
||||||
|
Prepare(sb, initialCapacity);
|
||||||
|
return sb;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StringBuilder(initialCapacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ScopedBuilder AcquireScoped(int initialCapacity = 16)
|
||||||
|
{
|
||||||
|
return new ScopedBuilder(Acquire(initialCapacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Release(StringBuilder sb)
|
||||||
|
{
|
||||||
|
if (sb == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sb.Capacity > _maxCountOfItemsInCache)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Clear();
|
||||||
|
|
||||||
|
if (Interlocked.CompareExchange(ref _hotSlot, sb, null) == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int newCount = Interlocked.Increment(ref _pooledCount);
|
||||||
|
if (newCount <= _maxCapacityOfCachedItem)
|
||||||
|
{
|
||||||
|
_cache.Push(sb);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Interlocked.Decrement(ref _pooledCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetStringAndRelease(StringBuilder sb)
|
||||||
|
{
|
||||||
|
string s = sb.ToString();
|
||||||
|
Release(sb);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Prepare(StringBuilder sb, int minCapacity)
|
||||||
|
{
|
||||||
|
sb.Clear();
|
||||||
|
if (sb.Capacity < minCapacity)
|
||||||
|
{
|
||||||
|
sb.EnsureCapacity(minCapacity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ScopedBuilder : IDisposable, IAsyncDisposable
|
||||||
|
{
|
||||||
|
private StringBuilder _sb;
|
||||||
|
|
||||||
|
private int _disposed;
|
||||||
|
|
||||||
|
internal ScopedBuilder(StringBuilder sb)
|
||||||
|
{
|
||||||
|
_sb = sb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (Interlocked.Exchange(ref _disposed, 1) == 0)
|
||||||
|
{
|
||||||
|
StringBuilder? sb = _sb;
|
||||||
|
_sb = null;
|
||||||
|
Release(sb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
Dispose();
|
||||||
|
return new ValueTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetStringAndRelease()
|
||||||
|
{
|
||||||
|
if (Interlocked.CompareExchange(ref _disposed, 1, 1) == 1)
|
||||||
|
{
|
||||||
|
throw new ObjectDisposedException(string.Format("Cannot get string from this instance of StringBuilderCache as it has already been disposed"));
|
||||||
|
}
|
||||||
|
string s = _sb.ToString();
|
||||||
|
Dispose();
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringBuilder Builder => _sb;
|
||||||
|
|
||||||
|
public static implicit operator StringBuilder(ScopedBuilder b)
|
||||||
|
{
|
||||||
|
return b.Builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*internal static class StringBuilderCache
|
||||||
|
{
|
||||||
|
internal const int MaxBuilderSize = 360;
|
||||||
|
internal const int DefaultCapacity = 16;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static StringBuilder? CachedInstance;
|
||||||
|
|
||||||
|
public static StringBuilder Acquire(int capacity = DefaultCapacity)
|
||||||
|
{
|
||||||
|
if (capacity <= 360)
|
||||||
|
{
|
||||||
|
StringBuilder? cachedInstance = CachedInstance;
|
||||||
|
if (cachedInstance != null && capacity <= cachedInstance.Capacity)
|
||||||
|
{
|
||||||
|
CachedInstance = null;
|
||||||
|
cachedInstance.Clear();
|
||||||
|
return cachedInstance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StringBuilder(capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Release(StringBuilder sb)
|
||||||
|
{
|
||||||
|
if (sb.Capacity <= 360)
|
||||||
|
{
|
||||||
|
CachedInstance = sb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetStringAndRelease(StringBuilder sb)
|
||||||
|
{
|
||||||
|
string result = sb.ToString();
|
||||||
|
Release(sb);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}*/
|
||||||
30
certmgr/Core/Utils/StringFormatter.cs
Normal file
30
certmgr/Core/Utils/StringFormatter.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Utils;
|
||||||
|
|
||||||
|
internal static class StringFormatter
|
||||||
|
{
|
||||||
|
public static string Format(string messageFormat, params object?[] messageArgs)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return string.Format(messageFormat, messageArgs);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
using StringBuilderCache.ScopedBuilder lease = StringBuilderCache.AcquireScoped();
|
||||||
|
StringBuilder sb = lease.Builder;
|
||||||
|
|
||||||
|
sb.AppendFormat("Failed to format message: '{0}'. Arguments (count = {1}) = ", messageFormat, messageArgs?.Length ?? -1);
|
||||||
|
if (messageArgs == null)
|
||||||
|
{
|
||||||
|
sb.Append("<null>");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
messageArgs.ToSeparatedList(sb, obj => obj?.ToString() ?? "<null>", ",");
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
certmgr/Core/Validation/ISettingValidator.cs
Normal file
10
certmgr/Core/Validation/ISettingValidator.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Validation;
|
||||||
|
|
||||||
|
public interface ISettingValidator
|
||||||
|
{
|
||||||
|
string SettingName { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
Task<ValidationResult> ValidateAsync(object? value, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
6
certmgr/Core/Validation/ISettingValidatorT.cs
Normal file
6
certmgr/Core/Validation/ISettingValidatorT.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace CertMgr.Core.Validation;
|
||||||
|
|
||||||
|
public interface ISettingValidator<T> : ISettingValidator
|
||||||
|
{
|
||||||
|
Task<ValidationResult> ValidateAsync(T? settingValue, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
27
certmgr/Core/Validation/StringValidator.cs
Normal file
27
certmgr/Core/Validation/StringValidator.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Validation;
|
||||||
|
|
||||||
|
public sealed class StringValidator
|
||||||
|
{
|
||||||
|
public sealed class IsNotNull : ISettingValidator<string>
|
||||||
|
{
|
||||||
|
public IsNotNull(string settingName)
|
||||||
|
{
|
||||||
|
SettingName = settingName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string SettingName { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public Task<ValidationResult> ValidateAsync(string? value, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ValidationResult result = new ValidationResult(SettingName, value != null, "value is null");
|
||||||
|
return Task.FromResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<ValidationResult> ValidateAsync(object? value, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return ValidateAsync(value as string, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
certmgr/Core/Validation/ValidationResult.cs
Normal file
26
certmgr/Core/Validation/ValidationResult.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
using CertMgr.Core.Utils;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Validation;
|
||||||
|
|
||||||
|
public sealed class ValidationResult
|
||||||
|
{
|
||||||
|
public ValidationResult(string propertyName, bool isValid, string justificationFormat, params object?[] justificationArgs)
|
||||||
|
{
|
||||||
|
PropertyName = propertyName;
|
||||||
|
Justification = StringFormatter.Format(justificationFormat, justificationArgs);
|
||||||
|
IsValid = isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string PropertyName { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public string Justification { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public bool IsValid { [DebuggerStepThrough] get; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return string.Format("{0}: {1} => {2}", PropertyName, IsValid ? "valid" : "not valid", Justification);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
certmgr/Core/Validation/ValidationResults.cs
Normal file
52
certmgr/Core/Validation/ValidationResults.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
using System.Collections;
|
||||||
|
|
||||||
|
namespace CertMgr.Core.Validation;
|
||||||
|
|
||||||
|
public sealed class ValidationResults : IReadOnlyCollection<ValidationResult>
|
||||||
|
{
|
||||||
|
private readonly List<ValidationResult> _results;
|
||||||
|
|
||||||
|
internal ValidationResults()
|
||||||
|
{
|
||||||
|
_results = new List<ValidationResult>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Count => _results.Count;
|
||||||
|
|
||||||
|
public bool IsValid => _results.All(res => res.IsValid);
|
||||||
|
|
||||||
|
internal void Add(ValidationResult result)
|
||||||
|
{
|
||||||
|
_results.Add(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void Add(IReadOnlyCollection<ValidationResult> results)
|
||||||
|
{
|
||||||
|
_results.AddRange(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void AddValid(string propertyName, string justificationFormat, params object?[] justificationArgs)
|
||||||
|
{
|
||||||
|
Add(new ValidationResult(propertyName, true, justificationFormat, justificationArgs));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void AddInvalid(string propertyName, string justificationFormat, params object?[] justificationArgs)
|
||||||
|
{
|
||||||
|
Add(new ValidationResult(propertyName, false, justificationFormat, justificationArgs));
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator<ValidationResult> GetEnumerator()
|
||||||
|
{
|
||||||
|
return _results.GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
{
|
||||||
|
return GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return string.Format("is-valid = {0}, total-count = {1}, invalid-count = {2}", IsValid ? "yes" : "no", _results.Count, _results.Where(r => !r.IsValid).Count());
|
||||||
|
}
|
||||||
|
}
|
||||||
90
certmgr/Jobs/CertificateSettings.cs
Normal file
90
certmgr/Jobs/CertificateSettings.cs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
using CertMgr.CertGen;
|
||||||
|
using CertMgr.Core.Attributes;
|
||||||
|
using CertMgr.Core.Converters.Impl;
|
||||||
|
using CertMgr.Core.Jobs;
|
||||||
|
using CertMgr.Core.Storage;
|
||||||
|
using CertMgr.Core.Validation;
|
||||||
|
|
||||||
|
namespace CertMgr.Jobs;
|
||||||
|
|
||||||
|
public sealed class CertificateSettings : JobSettings
|
||||||
|
{
|
||||||
|
public CertificateSettings()
|
||||||
|
{
|
||||||
|
Algorithm = CertificateAlgorithm.ECDsa;
|
||||||
|
Curve = EcdsaCurve.P384;
|
||||||
|
HashAlgorithm = CertGen.HashAlgorithm.Sha384;
|
||||||
|
ValidityPeriod = TimeSpan.FromDays(365);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Setting("subject", IsMandatory = true, Validator = typeof(StringValidator.IsNotNull))]
|
||||||
|
public string? Subject { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||||
|
|
||||||
|
[Setting("subject-alternate-name", AlternateNames = ["san"])]
|
||||||
|
public IReadOnlyCollection<string>? SubjectAlternateNames { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||||
|
|
||||||
|
[Setting("algorithm", Default = CertificateAlgorithm.ECDsa, Converter = typeof(EnumConverter))]
|
||||||
|
public CertificateAlgorithm? Algorithm { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||||
|
|
||||||
|
[Setting("ecdsa-curve")]
|
||||||
|
public EcdsaCurve? Curve { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||||
|
|
||||||
|
[Setting("rsa-key-size")]
|
||||||
|
public RsaKeySize? RsaKeySize { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||||
|
|
||||||
|
[Setting("hash-algorithm", AlternateNames = ["ha"])]
|
||||||
|
public HashAlgorithm? HashAlgorithm { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||||
|
|
||||||
|
[Setting("is-certificate-authority", Default = false, AlternateNames = ["isca"])]
|
||||||
|
public bool IsCertificateAuthority { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||||
|
|
||||||
|
[Setting("issuer", Converter = typeof(StorageConverter))]
|
||||||
|
public IStorage? Issuer { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||||
|
|
||||||
|
[Setting("issuer-password")]
|
||||||
|
public string? IssuerPassword { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||||
|
|
||||||
|
[Setting("storage", IsMandatory = true, Converter = typeof(StorageConverter))]
|
||||||
|
public IStorage? Storage { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||||
|
|
||||||
|
[Setting("password")]
|
||||||
|
public string? Password { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||||
|
|
||||||
|
[Setting("validity-period", Default = "365d", Converter = typeof(TimeSpanConverter))]
|
||||||
|
public TimeSpan? ValidityPeriod { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||||
|
|
||||||
|
protected override Task DoValidateAsync(ValidationResults results, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(Subject))
|
||||||
|
{
|
||||||
|
results.AddInvalid(nameof(Subject), "must not be empty");
|
||||||
|
}
|
||||||
|
if (Algorithm == CertificateAlgorithm.ECDsa)
|
||||||
|
{
|
||||||
|
if (!Curve.HasValue || !Enum.IsDefined(Curve.Value))
|
||||||
|
{
|
||||||
|
results.AddInvalid(nameof(Curve), "valid value must be specified: '{0}'", Curve?.ToString() ?? "<null>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Algorithm == CertificateAlgorithm.RSA)
|
||||||
|
{
|
||||||
|
if (!RsaKeySize.HasValue || !Enum.IsDefined(RsaKeySize.Value))
|
||||||
|
{
|
||||||
|
results.AddInvalid(nameof(RsaKeySize), "value value must be specified: '{0}'", RsaKeySize?.ToString() ?? "<null>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!HashAlgorithm.HasValue || !Enum.IsDefined(HashAlgorithm.Value))
|
||||||
|
{
|
||||||
|
results.AddInvalid(nameof(HashAlgorithm), "value value must be specified: '{0}'", HashAlgorithm?.ToString() ?? "<null>");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Storage == null)
|
||||||
|
{
|
||||||
|
results.AddInvalid(nameof(Storage), "must be specified");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
95
certmgr/Jobs/CreateCertificateJob.cs
Normal file
95
certmgr/Jobs/CreateCertificateJob.cs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
|
||||||
|
using CertMgr.CertGen;
|
||||||
|
using CertMgr.Core.Exceptions;
|
||||||
|
using CertMgr.Core.Jobs;
|
||||||
|
using CertMgr.Core.Log;
|
||||||
|
using CertMgr.Core.Storage;
|
||||||
|
using CertMgr.Core.Utils;
|
||||||
|
|
||||||
|
namespace CertMgr.Jobs;
|
||||||
|
|
||||||
|
public sealed class CreateCertificateJob : Job<CertificateSettings>
|
||||||
|
{
|
||||||
|
public const string ID = "create-certificate";
|
||||||
|
|
||||||
|
protected override async Task<JobResult> DoExecuteAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
CertificateSettings cs = Settings;
|
||||||
|
CLog.Info("creating certificate using settings: subject = '{0}', algorithm = '{1}', curve = '{2}'", cs.Subject, cs.Algorithm?.ToString() ?? "<null>", cs.Curve);
|
||||||
|
|
||||||
|
GeneratorSettings gs = CreateGeneratorSettings();
|
||||||
|
CertGen.CertificateSettings cgcs = await CreateCertificateSettingsAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
CertificateManager cm = new CertificateManager();
|
||||||
|
using (X509Certificate2 cert = await cm.CreateAsync(cgcs, gs, cancellationToken).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
if (Settings.Storage != null)
|
||||||
|
{
|
||||||
|
byte[] data = cert.Export(X509ContentType.Pfx, Settings.Password);
|
||||||
|
using (MemoryStream ms = new MemoryStream())
|
||||||
|
{
|
||||||
|
await ms.WriteAsync(data, cancellationToken);
|
||||||
|
ms.Position = 0;
|
||||||
|
StoreResult writeResult = await Settings.Storage.WriteAsync(ms, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!writeResult.IsSuccess)
|
||||||
|
{
|
||||||
|
throw new JobException(writeResult.Exception, "Failed to write create certificate to target storage (type = '{0}', storage = '{1}')", Settings.Storage.GetType().ToString(false), Settings.Storage.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CreateSuccess("Certificate was successfully created");
|
||||||
|
}
|
||||||
|
|
||||||
|
private GeneratorSettings CreateGeneratorSettings()
|
||||||
|
{
|
||||||
|
GeneratorSettings gs;
|
||||||
|
|
||||||
|
switch (Settings.Algorithm)
|
||||||
|
{
|
||||||
|
case CertificateAlgorithm.ECDsa:
|
||||||
|
gs = new EcdsaGeneratorSettings(Settings.Curve.Value);
|
||||||
|
break;
|
||||||
|
case CertificateAlgorithm.RSA:
|
||||||
|
gs = new RsaGeneratorSettings(Settings.RsaKeySize.Value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new UnsupportedValueException(Settings.Algorithm);
|
||||||
|
}
|
||||||
|
|
||||||
|
return gs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<CertGen.CertificateSettings> CreateCertificateSettingsAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
CertGen.CertificateSettings cgcs = new CertGen.CertificateSettings();
|
||||||
|
|
||||||
|
X509KeyStorageFlags flags = X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet;
|
||||||
|
cgcs.ValidityPeriod = Settings.ValidityPeriod.HasValue ? Settings.ValidityPeriod.Value : TimeSpan.FromDays(365);
|
||||||
|
cgcs.ExportableKeys = true;
|
||||||
|
cgcs.FriendlyName = "I'm your friend";
|
||||||
|
if (Settings.Issuer != null)
|
||||||
|
{
|
||||||
|
using (MemoryStream ms = new MemoryStream())
|
||||||
|
{
|
||||||
|
await Settings.Issuer.ReadAsync(ms, cancellationToken).ConfigureAwait(false);
|
||||||
|
cgcs.Issuer = X509CertificateLoader.LoadPkcs12(ms.GetBuffer(), Settings.IssuerPassword, flags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cgcs.SubjectName = Settings.Subject;
|
||||||
|
if (Settings.SubjectAlternateNames != null)
|
||||||
|
{
|
||||||
|
foreach (string altName in Settings.SubjectAlternateNames)
|
||||||
|
{
|
||||||
|
cgcs.SubjectAlternateNames.Add(altName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cgcs;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
certmgr/Program.cs
Normal file
17
certmgr/Program.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using CertMgr.Core;
|
||||||
|
|
||||||
|
namespace CertMgr;
|
||||||
|
|
||||||
|
internal static class Program
|
||||||
|
{
|
||||||
|
private static async Task<int> Main(string[] args)
|
||||||
|
{
|
||||||
|
args = ["--job=create-certificate", "--issuer=file|o|c:\\friend2.pfx", "--issuer-password=aaa", "--subject=hello", "--san=world", "--algorithm=ecdsa", "--ecdsa-curve=p384", "--storage=file|w|c:\\mycert.pfx", "--validity-period=2d"];
|
||||||
|
// args = ["--job=create-certificate", "--subject=hello", "--algorithm=ecdsa", "--curve=p384"];
|
||||||
|
using CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));
|
||||||
|
|
||||||
|
JobExecutor executor = new JobExecutor();
|
||||||
|
int errorLevel = await executor.ExecuteAsync(args, cts.Token).ConfigureAwait(false);
|
||||||
|
return errorLevel;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
certmgr/certmgr.csproj
Normal file
12
certmgr/certmgr.csproj
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<BaseOutputPath>..\BuildOutput\bin</BaseOutputPath>
|
||||||
|
<RootNamespace>CertMgr</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Reference in New Issue
Block a user