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