some fixes
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
.vs
|
||||
certmgr/obj/*
|
||||
certmgr/Obsolete/*
|
||||
certmgr/Obsolete/*
|
||||
certmgrTest/obj/*
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,10 +1,12 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.14.36511.14 d17.14
|
||||
VisualStudioVersion = 17.14.36511.14
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "certmgr", "certmgr\certmgr.csproj", "{EB5C73A6-8EBF-4C9C-845F-4828C4985B64}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "certmgrTest", "certmgrTest\certmgrTest.csproj", "{D6D02CCB-DA1F-4575-9422-FCD632B782B3}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -15,6 +17,10 @@ Global
|
||||
{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
|
||||
{D6D02CCB-DA1F-4575-9422-FCD632B782B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D6D02CCB-DA1F-4575-9422-FCD632B782B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D6D02CCB-DA1F-4575-9422-FCD632B782B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D6D02CCB-DA1F-4575-9422-FCD632B782B3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
@@ -10,8 +11,9 @@ internal abstract class CertificateGeneratorBase<TAlgorithm, TSettings> : ICerti
|
||||
where TAlgorithm : AsymmetricAlgorithm
|
||||
where TSettings : GeneratorSettings
|
||||
{
|
||||
internal CertificateGeneratorBase(TSettings settings)
|
||||
internal CertificateGeneratorBase(GeneratorType type, TSettings settings)
|
||||
{
|
||||
Type = type;
|
||||
Settings = settings;
|
||||
}
|
||||
|
||||
@@ -20,6 +22,8 @@ internal abstract class CertificateGeneratorBase<TAlgorithm, TSettings> : ICerti
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public GeneratorType Type { [DebuggerStepThrough] get; }
|
||||
|
||||
protected TSettings Settings { [DebuggerStepThrough] get; }
|
||||
|
||||
public async Task<X509Certificate2> CreateAsync(CertificateSettings settings, CancellationToken cancellationToken)
|
||||
@@ -30,8 +34,6 @@ internal abstract class CertificateGeneratorBase<TAlgorithm, TSettings> : ICerti
|
||||
return cert;
|
||||
}
|
||||
|
||||
protected abstract bool IsEphemeral(TAlgorithm key);
|
||||
|
||||
protected virtual void ValidateSettings(CertificateSettings settings)
|
||||
{
|
||||
if (settings.Issuer != null)
|
||||
@@ -71,37 +73,12 @@ internal abstract class CertificateGeneratorBase<TAlgorithm, TSettings> : ICerti
|
||||
|
||||
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)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
X509Certificate2 cert;
|
||||
|
||||
using (TAlgorithm privateKey = CreatePrivateKey())
|
||||
@@ -128,6 +105,50 @@ internal abstract class CertificateGeneratorBase<TAlgorithm, TSettings> : ICerti
|
||||
return Task.FromResult(cert);
|
||||
}
|
||||
|
||||
private CertificateRequest CreateRequest(CertificateSettings settings, TAlgorithm privateKey)
|
||||
{
|
||||
string commonName = CreateCommonName(settings.SubjectName);
|
||||
CertificateRequest request = DoCreateRequest(commonName, privateKey);
|
||||
|
||||
SubjectAlternativeNameBuilder altNames = new SubjectAlternativeNameBuilder();
|
||||
string subj = commonName.Substring("CN=".Length);
|
||||
if (!settings.SubjectAlternateNames.Contains(new SubjectAlternateName(SANKind.DNS, subj)))
|
||||
{
|
||||
altNames.AddDnsName(subj);
|
||||
}
|
||||
|
||||
foreach (SubjectAlternateName altName in settings.SubjectAlternateNames)
|
||||
{
|
||||
switch (altName.Kind)
|
||||
{
|
||||
case SANKind.DNS:
|
||||
altNames.AddDnsName(altName.Value);
|
||||
break;
|
||||
case SANKind.IP:
|
||||
if (IPAddress.TryParse(altName.Value, out IPAddress? ipAddress))
|
||||
{
|
||||
altNames.AddIpAddress(ipAddress);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedValueException(altName.Kind);
|
||||
}
|
||||
}
|
||||
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 X509SignatureGenerator GetSignatureGenerator(X509Certificate2 issuerCertificate)
|
||||
{
|
||||
X509SignatureGenerator? sgen = null;
|
||||
@@ -183,7 +204,7 @@ internal abstract class CertificateGeneratorBase<TAlgorithm, TSettings> : ICerti
|
||||
|
||||
using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
|
||||
{
|
||||
rng.GetBytes(serial);
|
||||
rng.GetNonZeroBytes(serial);
|
||||
}
|
||||
|
||||
return serial;
|
||||
@@ -212,6 +233,6 @@ internal abstract class CertificateGeneratorBase<TAlgorithm, TSettings> : ICerti
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("Type = {0}", this is RsaCertificateGenerator ? "RSA" : this is EcdsaCertificateGenerator ? "ECDSA" : "<unknown>");
|
||||
return string.Format("Type = {0}", Type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ public sealed class CertificateSettings
|
||||
{
|
||||
internal CertificateSettings()
|
||||
{
|
||||
SubjectAlternateNames = new SubjectAlternateNames([NetUtils.MachineName]);
|
||||
SubjectAlternateNames = new SubjectAlternateNames();
|
||||
|
||||
SubjectName = NetUtils.MachineName;
|
||||
Issuer = null;
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace CertMgr.CertGen;
|
||||
internal sealed class EcdsaCertificateGenerator : CertificateGeneratorBase<ECDsa, EcdsaGeneratorSettings>
|
||||
{
|
||||
internal EcdsaCertificateGenerator(EcdsaGeneratorSettings settings)
|
||||
: base(settings)
|
||||
: base(GeneratorType.Ecdsa, settings)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -33,16 +33,6 @@ internal sealed class EcdsaCertificateGenerator : CertificateGeneratorBase<ECDsa
|
||||
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;
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
public enum GeneratorType
|
||||
{
|
||||
Ecdsa = 1,
|
||||
Rsa
|
||||
RSA
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace CertMgr.CertGen;
|
||||
internal sealed class RsaCertificateGenerator : CertificateGeneratorBase<RSA, RsaGeneratorSettings>
|
||||
{
|
||||
internal RsaCertificateGenerator(RsaGeneratorSettings settings)
|
||||
: base(settings)
|
||||
: base(GeneratorType.RSA, settings)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -33,16 +33,6 @@ internal sealed class RsaCertificateGenerator : CertificateGeneratorBase<RSA, Rs
|
||||
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;
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace CertMgr.CertGen;
|
||||
public sealed class RsaGeneratorSettings : GeneratorSettings
|
||||
{
|
||||
public RsaGeneratorSettings(RsaKeySize keySize)
|
||||
: base(GeneratorType.Rsa)
|
||||
: base(GeneratorType.RSA)
|
||||
{
|
||||
KeySize = keySize;
|
||||
|
||||
|
||||
7
certmgr/CertGen/SANKind.cs
Normal file
7
certmgr/CertGen/SANKind.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace CertMgr.CertGen;
|
||||
|
||||
public enum SANKind
|
||||
{
|
||||
DNS = 1,
|
||||
IP
|
||||
}
|
||||
57
certmgr/CertGen/SubjectAlternateName.cs
Normal file
57
certmgr/CertGen/SubjectAlternateName.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CertMgr.CertGen;
|
||||
|
||||
public sealed class SubjectAlternateName : IEquatable<SubjectAlternateName>
|
||||
{
|
||||
public SubjectAlternateName(SANKind kind, string value)
|
||||
{
|
||||
Kind = kind;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public SubjectAlternateName(SANKind kind, ReadOnlySpan<char> value)
|
||||
{
|
||||
Kind = kind;
|
||||
Value = value.ToString();
|
||||
}
|
||||
|
||||
public SANKind Kind { [DebuggerStepThrough] get; }
|
||||
|
||||
public string Value { [DebuggerStepThrough] get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("{0}: '{1}'", Kind, Value);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return (Kind, Value).GetHashCode();
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return Equals(obj as SubjectAlternateName);
|
||||
}
|
||||
|
||||
public bool Equals(SubjectAlternateName? other)
|
||||
{
|
||||
if (other is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Kind != other.Kind)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,119 @@
|
||||
using System.Collections;
|
||||
|
||||
using CertMgr.Core.Log;
|
||||
using CertMgr.Core.Utils;
|
||||
|
||||
namespace CertMgr.CertGen;
|
||||
|
||||
public sealed class SubjectAlternateNames : IReadOnlyCollection<string>
|
||||
public sealed class SubjectAlternateNames : IReadOnlyCollection<SubjectAlternateName>
|
||||
{
|
||||
private readonly HashSet<string> _items;
|
||||
private readonly HashSet<SubjectAlternateName> _items;
|
||||
|
||||
public SubjectAlternateNames()
|
||||
{
|
||||
_items = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
_items = new HashSet<SubjectAlternateName>();
|
||||
}
|
||||
|
||||
public SubjectAlternateNames(IReadOnlyCollection<string> items)
|
||||
{
|
||||
_items = new HashSet<string>(items, StringComparer.OrdinalIgnoreCase);
|
||||
_items = new HashSet<SubjectAlternateName>();
|
||||
foreach (string item in items)
|
||||
{
|
||||
Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
public int Count => _items.Count;
|
||||
|
||||
public bool Add(string name)
|
||||
{
|
||||
bool added = _items.Add(name);
|
||||
bool added = false;
|
||||
|
||||
// any kind of value except for DNS must be prefixed with its kind,
|
||||
// e.g. "IP:w.x.y.z" or "IP:2001:db8::1" or "DNS:example.com"
|
||||
// if not prefixed then it must be DNS
|
||||
|
||||
ReadOnlySpan<char> span = name.AsSpan();
|
||||
if (span.StartsWith("DNS:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
added = AddDnsName(span);
|
||||
}
|
||||
else if (span.StartsWith("IP:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
added = AddIPAddress(span);
|
||||
}
|
||||
else
|
||||
{
|
||||
// fallback to dns name as other alt-names (UPN, URI, email..) are not supported
|
||||
added = AddDnsName(span);
|
||||
}
|
||||
|
||||
return added;
|
||||
}
|
||||
|
||||
public IEnumerator<string> GetEnumerator()
|
||||
public bool AddDnsName(string name)
|
||||
{
|
||||
bool added = AddDnsName(name.AsSpan());
|
||||
return added;
|
||||
}
|
||||
|
||||
public bool AddDnsName(ReadOnlySpan<char> name)
|
||||
{
|
||||
bool added = false;
|
||||
|
||||
ReadOnlySpan<char> span = name;
|
||||
if (span.StartsWith("DNS:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
span = span.Slice(4);
|
||||
}
|
||||
|
||||
if (NetUtils.IsValidDns(span))
|
||||
{
|
||||
added = _items.Add(new SubjectAlternateName(SANKind.DNS, span));
|
||||
}
|
||||
else
|
||||
{
|
||||
CLog.Error("{0}: Value '{1}' is not valid DNS name", nameof(SubjectAlternateNames), name.ToString());
|
||||
}
|
||||
|
||||
return added;
|
||||
}
|
||||
|
||||
public bool AddIPAddress(string ip)
|
||||
{
|
||||
bool added = AddIPAddress(ip.AsSpan());
|
||||
return added;
|
||||
}
|
||||
|
||||
public bool AddIPAddress(ReadOnlySpan<char> ip)
|
||||
{
|
||||
bool added = false;
|
||||
|
||||
ReadOnlySpan<char> span = ip;
|
||||
if (span.StartsWith("IP:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
span = span.Slice(3);
|
||||
}
|
||||
|
||||
if (NetUtils.IsValidIPAny(span))
|
||||
{
|
||||
added = _items.Add(new SubjectAlternateName(SANKind.IP, span));
|
||||
}
|
||||
else
|
||||
{
|
||||
CLog.Error("{0}: Value '{1}' is not valid IP address", nameof(SubjectAlternateNames), ip.ToString());
|
||||
}
|
||||
|
||||
return added;
|
||||
}
|
||||
|
||||
public bool Contains(SubjectAlternateName name)
|
||||
{
|
||||
bool contains = _items.Contains(name);
|
||||
return contains;
|
||||
}
|
||||
|
||||
public IEnumerator<SubjectAlternateName> GetEnumerator()
|
||||
{
|
||||
return _items.GetEnumerator();
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
/*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);
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
40
certmgr/CertGen/Utils/SubjectValidator.cs
Normal file
40
certmgr/CertGen/Utils/SubjectValidator.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System.Diagnostics;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
using CertMgr.Core.Validation;
|
||||
|
||||
namespace CertMgr.CertGen.Utils;
|
||||
|
||||
public sealed class SubjectValidator : ISettingValidator<string>
|
||||
{
|
||||
public SubjectValidator(string settingName)
|
||||
{
|
||||
SettingName = settingName;
|
||||
}
|
||||
|
||||
public string SettingName { [DebuggerStepThrough] get; }
|
||||
|
||||
public Task<ValidationResult> ValidateAsync(string? settingValue, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(settingValue))
|
||||
{
|
||||
return Task.FromResult(new ValidationResult(SettingName, false, "must not be null"));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
X500DistinguishedName dn = new X500DistinguishedName(settingValue);
|
||||
|
||||
return Task.FromResult(new ValidationResult(SettingName, true, "success"));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Task.FromResult(new ValidationResult(SettingName, false, "invalid value: '{0}'. Exception = {1}: {2}", settingValue ?? "<null>", e.GetType().Name, e.Message));
|
||||
}
|
||||
}
|
||||
|
||||
public Task<ValidationResult> ValidateAsync(object? value, CancellationToken cancellationToken)
|
||||
{
|
||||
return ValidateAsync(value as string, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -8,36 +8,26 @@ public sealed class EnumConverter : ValueConverter<Enum>
|
||||
{
|
||||
Type resultType;
|
||||
|
||||
bool isNullable = targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Nullable<>);
|
||||
if (isNullable)
|
||||
Type? underlying = Nullable.GetUnderlyingType(targetType);
|
||||
if (underlying != null)
|
||||
{
|
||||
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));
|
||||
}
|
||||
resultType = underlying;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!targetType.IsEnum)
|
||||
{
|
||||
resultType = targetType;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ConverterException("Cannot convert type '{0}' as enum as it is not enum", targetType.ToString(false));
|
||||
}
|
||||
resultType = targetType;
|
||||
}
|
||||
|
||||
object? result = Enum.Parse(resultType, rawValue, true);
|
||||
return Task.FromResult((Enum?)result);
|
||||
if (!resultType.IsEnum)
|
||||
{
|
||||
throw new ConverterException("Cannot convert type '{0}' as enum as it is not enum or nullable", targetType.ToString(false));
|
||||
}
|
||||
|
||||
if (Enum.TryParse(resultType, rawValue, true, out object? result) && Enum.IsDefined(resultType, result))
|
||||
{
|
||||
return Task.FromResult((Enum?)result);
|
||||
}
|
||||
|
||||
return Task.FromResult((Enum?)null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
@@ -25,62 +26,138 @@ internal sealed class SettingsBuilder
|
||||
_settingsType = settingsType;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<Type> GetValidatedTypes(Type validatorType)
|
||||
{
|
||||
if (validatorType == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(validatorType));
|
||||
}
|
||||
|
||||
List<Type> hits = new List<Type>();
|
||||
|
||||
if (validatorType.IsInterface && validatorType.IsGenericType && validatorType.GetGenericTypeDefinition() == typeof(ISettingValidator<>))
|
||||
{
|
||||
Type candidate = validatorType.GetGenericArguments()[0];
|
||||
hits.Add(candidate);
|
||||
}
|
||||
|
||||
foreach (Type iface in validatorType.GetInterfaces())
|
||||
{
|
||||
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(ISettingValidator<>))
|
||||
{
|
||||
Type candidate = iface.GetGenericArguments()[0];
|
||||
if (!hits.Contains(candidate))
|
||||
{
|
||||
hits.Add(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hits;
|
||||
}
|
||||
|
||||
private async Task<AsyncResult<object?>> SetPropertyValueAsync(JobSettings settings, PropertyInfo propertyInfo, TypeInfo propertyType, SettingAttribute settingAttribute, CancellationToken cancellationToken)
|
||||
{
|
||||
object? convertedValue = null;
|
||||
bool valueSet = false;
|
||||
|
||||
if (TryGetRawArgument(settingAttribute, out RawArgument? rawArg))
|
||||
{
|
||||
try
|
||||
{
|
||||
AsyncResult<object?> conversionResult = await ConvertRawValueAsync(settingAttribute, rawArg, propertyType.IsCollection, propertyInfo.PropertyType, propertyType.ElementType, cancellationToken).ConfigureAwait(false);
|
||||
if (conversionResult.IsSuccess)
|
||||
{
|
||||
propertyInfo.SetValue(settings, conversionResult.Value);
|
||||
convertedValue = conversionResult.Value;
|
||||
valueSet = true;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
CLog.Error(e, "Failed to process property '{0}' (of type '{1}') in settings of type '{2}'", propertyInfo.Name, propertyInfo.PropertyType.ToString(false), _settingsType.ToString(false));
|
||||
throw new CertMgrException(e, "Failed to process property '{0}' (of type '{1}') in settings of type '{2}'", propertyInfo.Name, propertyInfo.PropertyType.ToString(false), _settingsType.ToString(false));
|
||||
}
|
||||
}
|
||||
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)
|
||||
{
|
||||
TypeInfo typeInfo = UnwrapCollection(propertyInfo.PropertyType);
|
||||
if (settingAttribute.Default.GetType() == typeInfo.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().ToString(false) ?? "<null>", typeInfo.ElementType.ToString(false));
|
||||
}
|
||||
|
||||
}
|
||||
return new AsyncResult<object?>(valueSet, convertedValue);
|
||||
}
|
||||
|
||||
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);
|
||||
TypeInfo propertyType = UnwrapCollection(propertyInfo.PropertyType);
|
||||
|
||||
AsyncResult<object?> setPropertyResult = await SetPropertyValueAsync(settings, propertyInfo, propertyType, settingAttribute, cancellationToken).ConfigureAwait(false);
|
||||
if (setPropertyResult.IsSuccess)
|
||||
{
|
||||
try
|
||||
{
|
||||
(bool converted, object? convertedValue) = await ConvertRawValueAsync(settingAttribute, rawArg, isCollection, propertyInfo.PropertyType, elementType, cancellationToken).ConfigureAwait(false);
|
||||
if (converted)
|
||||
if (settingAttribute.Validator != null)
|
||||
{
|
||||
propertyInfo.SetValue(settings, convertedValue);
|
||||
IReadOnlyList<Type> validatedTypes = GetValidatedTypes(settingAttribute.Validator);
|
||||
|
||||
if (settingAttribute.Validator != null)
|
||||
ISettingValidator? validatorInst = (ISettingValidator?)Activator.CreateInstance(settingAttribute.Validator, [settingAttribute.Name]);
|
||||
|
||||
if (propertyType.IsCollection)
|
||||
{
|
||||
ISettingValidator? validator = (ISettingValidator?)Activator.CreateInstance(settingAttribute.Validator, [settingAttribute.Name]);
|
||||
if (validator != null)
|
||||
TypeInfo validatedTypeInfo = UnwrapCollection(validatedTypes[0]);
|
||||
if (validatedTypeInfo.IsCollection)
|
||||
{
|
||||
ValidationResult valres = await validator.ValidateAsync(convertedValue, cancellationToken).ConfigureAwait(false);
|
||||
// validator validates collection => send whole collection as argument to ValidateAsync(..)
|
||||
ValidationResult valres = await validatorInst.ValidateAsync(setPropertyResult.Value, cancellationToken).ConfigureAwait(false);
|
||||
settings.ValidationResults.Add(valres);
|
||||
}
|
||||
else
|
||||
{
|
||||
// validator validates elements => send items one by one to ValidateAsync(..)
|
||||
if (setPropertyResult.Value is IList list)
|
||||
{
|
||||
foreach (object value in list)
|
||||
{
|
||||
ValidationResult valres = await validatorInst.ValidateAsync(value, cancellationToken).ConfigureAwait(false);
|
||||
settings.ValidationResults.Add(valres);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// setting is not a collection. lets assume the validator doesn't validate collection as well
|
||||
ValidationResult valres = await validatorInst.ValidateAsync(setPropertyResult.Value, cancellationToken).ConfigureAwait(false);
|
||||
settings.ValidationResults.Add(valres);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
CLog.Error(e, "Failed to process property '{0}' (of type '{1}') in settings of type '{2}'", propertyInfo.Name, propertyInfo.PropertyType.ToString(false), _settingsType.ToString(false));
|
||||
CLog.Error(e, "Failed to validate property '{0}' (of type '{1}') in settings of type '{2}'", propertyInfo.Name, propertyInfo.PropertyType.ToString(false), _settingsType.ToString(false));
|
||||
throw new CertMgrException(e, "Failed to process property '{0}' (of type '{1}') in settings of type '{2}'", propertyInfo.Name, propertyInfo.PropertyType.ToString(false), _settingsType.ToString(false));
|
||||
}
|
||||
}
|
||||
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().ToString(false) ?? "<null>", elementType.ToString(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await settings.ValidateAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
@@ -105,13 +182,6 @@ internal sealed class SettingsBuilder
|
||||
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))
|
||||
@@ -150,62 +220,65 @@ internal sealed class SettingsBuilder
|
||||
return settings;
|
||||
}
|
||||
|
||||
private async Task<(bool success, object? convertedValue)> ConvertRawValueAsync(SettingAttribute settingAttribute, RawArgument rawArg, bool isCollection, Type collectionType, Type elementType, CancellationToken cancellationToken)
|
||||
private async Task<AsyncResult<object?>> ConvertRawValueAsync(SettingAttribute settingAttribute, RawArgument rawArg, bool isCollection, Type collectionType, Type elementType, CancellationToken cancellationToken)
|
||||
{
|
||||
bool success = false;
|
||||
object? convertedValue = null;
|
||||
|
||||
IValueConverter? customConverter = GetCustomConverter(settingAttribute);
|
||||
|
||||
if (isCollection)
|
||||
{
|
||||
if (TryGetCustomConverter(settingAttribute, out IValueConverter? customConverter))
|
||||
{
|
||||
Type listType = typeof(List<>).MakeGenericType(elementType);
|
||||
IList values = (IList)Activator.CreateInstance(listType)!;
|
||||
Type listType = typeof(List<>).MakeGenericType(elementType);
|
||||
IList values = (IList)Activator.CreateInstance(listType)!;
|
||||
|
||||
foreach (string rawValue in rawArg.Values)
|
||||
{
|
||||
convertedValue = await customConverter.ConvertAsync(rawValue, elementType, cancellationToken).ConfigureAwait(false);
|
||||
values.Add(convertedValue);
|
||||
}
|
||||
convertedValue = values;
|
||||
success = true;
|
||||
}
|
||||
else
|
||||
foreach (string rawValue in rawArg.Values)
|
||||
{
|
||||
Type listType = typeof(List<>).MakeGenericType(elementType);
|
||||
IList values = (IList)Activator.CreateInstance(listType)!;
|
||||
|
||||
foreach (string rawValue in rawArg.Values)
|
||||
AsyncResult<object?> converted = await ConvertValueAsync(rawValue, customConverter, elementType, cancellationToken).ConfigureAwait(false);
|
||||
if (converted.IsSuccess)
|
||||
{
|
||||
if (TryConvertValue(rawValue, elementType, out convertedValue))
|
||||
{
|
||||
values.Add(convertedValue);
|
||||
}
|
||||
values.Add(converted.Value);
|
||||
}
|
||||
convertedValue = values; // BuildCollectionValue(collectionType, elementType, values);
|
||||
success = true;
|
||||
}
|
||||
convertedValue = values;
|
||||
success = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (TryGetCustomConverter(settingAttribute, out IValueConverter? customConverter))
|
||||
{
|
||||
convertedValue = await customConverter.ConvertAsync(rawArg.Values.First(), elementType, cancellationToken).ConfigureAwait(false);
|
||||
success = true;
|
||||
}
|
||||
else if (TryConvertValue(rawArg.Values.First(), elementType, out convertedValue))
|
||||
AsyncResult<object?> converted = await ConvertValueAsync(rawArg.Values.First(), customConverter, elementType, cancellationToken).ConfigureAwait(false);
|
||||
if (converted.IsSuccess)
|
||||
{
|
||||
convertedValue = converted.Value;
|
||||
success = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
CLog.Error("Cannot convert value '{0}' of argument '{1}' to type '{2}'", rawArg.Values.First(), settingAttribute.Name, elementType.ToString(false));
|
||||
}
|
||||
}
|
||||
|
||||
return (success, convertedValue);
|
||||
return new AsyncResult<object?>(success, convertedValue);
|
||||
}
|
||||
|
||||
private static object BuildCollectionValue(Type collectionType, Type elementType, IReadOnlyList<object?> items)
|
||||
private async Task<AsyncResult<object?>> ConvertValueAsync(string rawValue, IValueConverter? customConverter, Type elementType, CancellationToken cancellationToken)
|
||||
{
|
||||
bool success;
|
||||
object? convertedValue;
|
||||
if (customConverter != null)
|
||||
{
|
||||
convertedValue = await customConverter.ConvertAsync(rawValue, elementType, cancellationToken).ConfigureAwait(false);
|
||||
success = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
success = TryConvertValue(rawValue, elementType, out convertedValue);
|
||||
}
|
||||
|
||||
return new AsyncResult<object?>(success, convertedValue);
|
||||
}
|
||||
|
||||
|
||||
/*private static object BuildCollectionValue(Type collectionType, Type elementType, IReadOnlyList<object?> items)
|
||||
{
|
||||
// convert source collection with 'items' of type 'object?' to collection with items of requested type:
|
||||
Type listType = typeof(List<>).MakeGenericType(elementType);
|
||||
@@ -255,11 +328,11 @@ internal sealed class SettingsBuilder
|
||||
Array fallback = Array.CreateInstance(elementType, typedList.Count);
|
||||
typedList.CopyTo(fallback, 0);
|
||||
return fallback;
|
||||
}
|
||||
}*/
|
||||
|
||||
private bool TryGetCustomConverter(SettingAttribute settingAttribute, [NotNullWhen(true)] out IValueConverter? customConverter)
|
||||
private IValueConverter? GetCustomConverter(SettingAttribute settingAttribute)
|
||||
{
|
||||
customConverter = null;
|
||||
IValueConverter? customConverter = null;
|
||||
|
||||
Type? valueConverter = settingAttribute.Converter;
|
||||
if (valueConverter != null)
|
||||
@@ -274,16 +347,22 @@ internal sealed class SettingsBuilder
|
||||
}
|
||||
}
|
||||
|
||||
return customConverter != null;
|
||||
return customConverter;
|
||||
}
|
||||
|
||||
private bool TryConvertValue(string rawValue, Type targetType, out object? convertedValue)
|
||||
{
|
||||
convertedValue = null;
|
||||
|
||||
Type? underlying = Nullable.GetUnderlyingType(targetType);
|
||||
if (underlying != null)
|
||||
{
|
||||
targetType = underlying;
|
||||
}
|
||||
|
||||
if (targetType.IsEnum)
|
||||
{
|
||||
if (Enum.TryParse(targetType, rawValue, true, out object? result))
|
||||
if (Enum.TryParse(targetType, rawValue, true, out object? result) && Enum.IsDefined(targetType, result))
|
||||
{
|
||||
convertedValue = result;
|
||||
}
|
||||
@@ -308,37 +387,51 @@ internal sealed class SettingsBuilder
|
||||
return convertedValue != null;
|
||||
}
|
||||
|
||||
private static (bool isCollection, Type? elementType) UnwrapCollection(Type type)
|
||||
private static TypeInfo UnwrapCollection(Type type)
|
||||
{
|
||||
if (type == typeof(string))
|
||||
{
|
||||
return (false, null);
|
||||
return new TypeInfo(false, type);
|
||||
}
|
||||
|
||||
Type testedType = type;
|
||||
Type? underlying = Nullable.GetUnderlyingType(type);
|
||||
if (underlying != null)
|
||||
{
|
||||
type = underlying;
|
||||
testedType = underlying;
|
||||
}
|
||||
|
||||
if (type.IsArray)
|
||||
if (testedType.IsArray)
|
||||
{
|
||||
return (true, type.GetElementType()!);
|
||||
return new TypeInfo(true, testedType.GetElementType()!);
|
||||
}
|
||||
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
|
||||
if (testedType.IsGenericType && testedType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
|
||||
{
|
||||
return (true, type.GetGenericArguments()[0]);
|
||||
return new TypeInfo(true, testedType.GetGenericArguments()[0]);
|
||||
}
|
||||
|
||||
foreach (Type i in type.GetInterfaces())
|
||||
foreach (Type i in testedType.GetInterfaces())
|
||||
{
|
||||
if (i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>))
|
||||
{
|
||||
return (true, i.GetGenericArguments()[0]);
|
||||
return new TypeInfo(true, i.GetGenericArguments()[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return (false, null);
|
||||
return new TypeInfo(false, type);
|
||||
}
|
||||
|
||||
private sealed class TypeInfo
|
||||
{
|
||||
public TypeInfo(bool isCollection, Type elementType)
|
||||
{
|
||||
IsCollection = isCollection;
|
||||
ElementType = elementType;
|
||||
}
|
||||
|
||||
public bool IsCollection { [DebuggerStepThrough] get; }
|
||||
|
||||
public Type ElementType { [DebuggerStepThrough] get; }
|
||||
}
|
||||
}
|
||||
|
||||
21
certmgr/Core/Utils/AsyncResult.cs
Normal file
21
certmgr/Core/Utils/AsyncResult.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CertMgr.Core.Utils;
|
||||
|
||||
public struct AsyncResult<T>
|
||||
{
|
||||
public AsyncResult(bool success, T value)
|
||||
{
|
||||
IsSuccess = success;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public bool IsSuccess { [DebuggerStepThrough] get; }
|
||||
|
||||
public T Value { [DebuggerStepThrough] get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("{0}: {1}", IsSuccess ? "Succeeded" : "Failed", Value?.ToString() ?? "<null>");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using CertMgr.Core.Exceptions;
|
||||
|
||||
@@ -7,6 +9,87 @@ namespace CertMgr.Core.Utils;
|
||||
|
||||
public static class NetUtils
|
||||
{
|
||||
// following regex validates string as DNS names:
|
||||
// - every label has up to 63 chars
|
||||
// - label contains only [a-z][A-Z][0-9]-
|
||||
// - label cannot start and/or end with dash char ('-')
|
||||
// - full name has up to 253 chars
|
||||
// - case insensitive
|
||||
// private static readonly Regex DNSRegex = new Regex(@"^(?=.{1,253}$)(?:[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?)(?:\.[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?)*$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
|
||||
|
||||
// following regex accepts (in addition to previous one):
|
||||
// - wildcard certificates (e.g. *.example.com) and
|
||||
// - 'root label' (terminating dot) (e.g. example.com.)
|
||||
private static readonly Regex DNSRegex = new Regex(@"^(?=.{1,254}$)(?:\*\.)?(?:[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?)(?:\.[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?)*\.?$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
|
||||
|
||||
/*
|
||||
* Following code is not used as IPAddress.TryParse(...) seems to be simpler choice to verify input string, but keeping both ipv4 and ipv6 regex just in case
|
||||
|
||||
private const string IPv4octet = "(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])";
|
||||
private const string IPv6hextet = "[0-9A-F]"; // should be [0-9A-Fa-f] but since Regex is created with RegexOptions.IgnoreCase it is not necessary
|
||||
private const string IPv4 = "(?:" + IPv4octet + @"\.){3}" + IPv4octet;
|
||||
|
||||
private static readonly Regex IPv4Regex = new Regex("^" + IPv4 + "$", RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
||||
|
||||
private static readonly Regex IPv6Regex = new Regex(
|
||||
@"^(?:(?:" + IPv6hextet + @"{1,4}:){7}" + IPv6hextet + @"{1,4}|(?:" + IPv6hextet + @"{1,4}:){1,7}:" +
|
||||
@"|" +
|
||||
@"(?:" + IPv6hextet + @"{1,4}:){1,6}:" + IPv6hextet + @"{1,4}" +
|
||||
@"|" +
|
||||
@"(?:" + IPv6hextet + @"{1,4}:){1,5}(?::" + IPv6hextet + @"{1,4}){1,2}" +
|
||||
@"|" +
|
||||
@"(?:" + IPv6hextet + @"{1,4}:){1,4}(?::" + IPv6hextet + @"{1,4}){1,3}" +
|
||||
@"|" +
|
||||
@"(?:" + IPv6hextet + @"{1,4}:){1,3}(?::" + IPv6hextet + @"{1,4}){1,4}" +
|
||||
@"|" +
|
||||
@"(?:" + IPv6hextet + @"{1,4}:){1,2}(?::" + IPv6hextet + @"{1,4}){1,5}" +
|
||||
@"|" +
|
||||
@"" + IPv6hextet + @"{1,4}:(?:(?::" + IPv6hextet + @"{1,4}){1,6})" +
|
||||
@"|" +
|
||||
@":(?:(?::" + IPv6hextet + @"{1,4}){1,7}|:)" +
|
||||
@"|" +
|
||||
@"(?:" + IPv6hextet + @"{1,4}:){6}" + IPv4 +
|
||||
@"|" +
|
||||
@"(?:" + IPv6hextet + @"{1,4}:){1,5}:" + IPv4 +
|
||||
@"|" +
|
||||
@"(?:" + IPv6hextet + @"{1,4}:){1,4}:(?::" + IPv6hextet + @"{1,4})?" +
|
||||
@"(?::" + IPv6hextet + @"{1,4})?" + @":" + IPv4 +
|
||||
@"|" +
|
||||
@"(?:" + IPv6hextet + @"{1,4}:){1,3}(?::" + IPv6hextet + @"{1,4}){1,2}:" + IPv4 +
|
||||
@"|" +
|
||||
@"(?:" + IPv6hextet + @"{1,4}:){1,2}(?::" + IPv6hextet + @"{1,4}){1,3}:" + IPv4 +
|
||||
@"|" +
|
||||
@"" + IPv6hextet + @"{1,4}(?::" + IPv6hextet + @"{1,4}){1,4}:" + IPv4 +
|
||||
@"|" +
|
||||
@"::(?:[0-9A-Fa-f]{1,4}:){1,5}" + IPv4 +
|
||||
@"|" +
|
||||
@"::" + IPv4 +
|
||||
@")$",
|
||||
RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
|
||||
|
||||
public static bool IsValidIPv4(ReadOnlySpan<char> value)
|
||||
{
|
||||
if (value.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool match = IPv4Regex.IsMatch(value);
|
||||
return match;
|
||||
}
|
||||
|
||||
public static bool IsValidIPv6(ReadOnlySpan<char> value)
|
||||
{
|
||||
if (value.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool match = IPv6Regex.IsMatch(value);
|
||||
return match;
|
||||
}
|
||||
*/
|
||||
|
||||
/// <summary>Returns DNS name of the local computer.</summary>
|
||||
public static string MachineName => GetMachineName(MachineNameFormat.Hostname);
|
||||
|
||||
@@ -34,4 +117,260 @@ public static class NetUtils
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
public static bool IsValidDns(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return IsValidDns(value.AsSpan());
|
||||
}
|
||||
|
||||
public static bool IsValidDns(ReadOnlySpan<char> value)
|
||||
{
|
||||
if (value.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool match = DNSRegex.IsMatch(value);
|
||||
return match;
|
||||
}
|
||||
|
||||
public static bool IsValidIPv4(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return IsValidIPv4(value.AsSpan());
|
||||
}
|
||||
|
||||
public static bool IsValidIPv4(ReadOnlySpan<char> value)
|
||||
{
|
||||
bool valid = false;
|
||||
|
||||
if (IPAddress.TryParse(value, out IPAddress? ip))
|
||||
{
|
||||
valid = ip.AddressFamily == AddressFamily.InterNetwork;
|
||||
}
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
public static bool IsValidIPv6(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return IsValidIPv6(value.AsSpan());
|
||||
}
|
||||
|
||||
public static bool IsValidIPv6(ReadOnlySpan<char> value)
|
||||
{
|
||||
bool valid = false;
|
||||
|
||||
// it seems that IPAddress.TryParse doesn't handle zone/scope-id correctly (at least on windows)
|
||||
// %eth0 or %eth1 and basically all non-numeric strings are "translated" to scope-id = 0
|
||||
// (possibly because such a zones/scope-ids doesn't exist on my machine?)
|
||||
// on the other hand - when ip ends with %5 then the scope-id is kept (even if such a zone/scope-id doesn't exist on my machine
|
||||
if (IPAddress.TryParse(value, out IPAddress? ip))
|
||||
{
|
||||
valid = ip.AddressFamily == AddressFamily.InterNetworkV6;
|
||||
}
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
public static bool IsValidIPAny(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return IsValidIPAny(value.AsSpan());
|
||||
}
|
||||
|
||||
public static bool IsValidIPAny(ReadOnlySpan<char> value)
|
||||
{
|
||||
if (value.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool match = IsValidIPv4(value) || IsValidIPv6(value);
|
||||
return match;
|
||||
}
|
||||
|
||||
public static bool IsSameMachine(string machineA, string machineB)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(machineA) || string.IsNullOrWhiteSpace(machineB))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
machineA = machineA.Trim();
|
||||
machineB = machineB.Trim();
|
||||
|
||||
string textualA = machineA.TrimEnd('.').ToLowerInvariant();
|
||||
string textualB = machineB.TrimEnd('.').ToLowerInvariant();
|
||||
if (textualA == textualB)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
IPAddress[] addrSetA = Dns.GetHostAddresses(machineA);
|
||||
IPAddress[] addrSetB = Dns.GetHostAddresses(machineB);
|
||||
|
||||
if (addrSetA.Length == 0 || addrSetB.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
HashSet<IPAddress> normalizedAddrSetA = new HashSet<IPAddress>(addrSetA.Select(NormalizeIp));
|
||||
foreach (IPAddress addrB in addrSetB)
|
||||
{
|
||||
IPAddress normalizedAddrB = NormalizeIp(addrB);
|
||||
if (normalizedAddrSetA.Contains(normalizedAddrB))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
e.GetType();
|
||||
return false;
|
||||
}
|
||||
catch (ArgumentException e)
|
||||
{
|
||||
e.GetType();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsLocalMachine(string machine)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(machine))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string normalizedMachine = machine.Trim().TrimEnd('.').ToLowerInvariant();
|
||||
|
||||
if (normalizedMachine == "localhost" || normalizedMachine == "127.0.0.1" || normalizedMachine == "::1" || normalizedMachine == MachineName.ToLowerInvariant())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
HashSet<IPAddress> localAddrSet = GetNormalizedLocalIpSet(true);
|
||||
|
||||
// possibly it is IP - this can be solved without involving the DNS:
|
||||
if (IPAddress.TryParse(normalizedMachine, out IPAddress? machineIp))
|
||||
{
|
||||
if (IPAddress.IsLoopback(machineIp))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
IPAddress normalizedMachineIp = NormalizeIp(machineIp);
|
||||
return localAddrSet.Contains(normalizedMachineIp);
|
||||
}
|
||||
|
||||
// not IP, DNS needed:
|
||||
try
|
||||
{
|
||||
IPAddress[] resolved = Dns.GetHostAddresses(normalizedMachine);
|
||||
if (resolved.Length > 0)
|
||||
{
|
||||
for (int i = 0; i < resolved.Length; i++)
|
||||
{
|
||||
IPAddress normalized = NormalizeIp(resolved[i]);
|
||||
if (localAddrSet.Contains(normalized))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
e.GetType();
|
||||
// DNS failed
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static HashSet<IPAddress> GetNormalizedLocalIpSet(bool includeLoopbacks)
|
||||
{
|
||||
HashSet<IPAddress> result = new HashSet<IPAddress>();
|
||||
|
||||
foreach (NetworkInterface ni in NetworkInterface.GetAllNetworkInterfaces())
|
||||
{
|
||||
if (ni.OperationalStatus != OperationalStatus.Up)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
IPInterfaceProperties? props;
|
||||
try
|
||||
{
|
||||
props = ni.GetIPProperties();
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (UnicastIPAddressInformation ua in props.UnicastAddresses)
|
||||
{
|
||||
IPAddress address = ua.Address;
|
||||
|
||||
if (!includeLoopbacks && IPAddress.IsLoopback(address))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
result.Add(NormalizeIp(address));
|
||||
}
|
||||
}
|
||||
|
||||
if (includeLoopbacks)
|
||||
{
|
||||
result.Add(NormalizeIp(IPAddress.Loopback));
|
||||
result.Add(NormalizeIp(IPAddress.IPv6Loopback));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static IPAddress NormalizeIp(IPAddress ip)
|
||||
{
|
||||
// convert 'IPv6-mapped IPv4' to IPv4 (e.g. (::ffff:a.b.c.d => a.b.c.d):
|
||||
if (ip.AddressFamily == AddressFamily.InterNetworkV6 && ip.IsIPv4MappedToIPv6)
|
||||
{
|
||||
return ip.MapToIPv4();
|
||||
}
|
||||
|
||||
// remove scope-id (e.g. fe80::1%eth0 → fe80::1):
|
||||
if (ip.AddressFamily == AddressFamily.InterNetworkV6)
|
||||
{
|
||||
byte[] bytes = ip.GetAddressBytes();
|
||||
IPAddress withoutScope = new IPAddress(bytes);
|
||||
return withoutScope;
|
||||
}
|
||||
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
|
||||
32
certmgr/Core/Validation/SettingValidatorT.cs
Normal file
32
certmgr/Core/Validation/SettingValidatorT.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
using CertMgr.Core.Utils;
|
||||
|
||||
namespace CertMgr.Core.Validation;
|
||||
|
||||
public abstract class SettingValidator<T> : ISettingValidator<T>
|
||||
{
|
||||
protected SettingValidator(string settingName)
|
||||
{
|
||||
SettingName = settingName;
|
||||
}
|
||||
|
||||
public string SettingName { [DebuggerStepThrough] get; }
|
||||
|
||||
public abstract Task<ValidationResult> ValidateAsync(T? settingValue, CancellationToken cancellationToken);
|
||||
|
||||
public Task<ValidationResult> ValidateAsync(object? settingValue, CancellationToken cancellationToken)
|
||||
{
|
||||
T? typedValue;
|
||||
try
|
||||
{
|
||||
typedValue = (T?)settingValue;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new CertMgrException(e, "SettingValidator of type '{0}' failed to convert value of type '{1}' to type '{2}' (setting-name = '{3}')", GetType().ToString(false), settingValue?.GetType().ToString(false) ?? "<null>", typeof(T).ToString(false), SettingName);
|
||||
}
|
||||
|
||||
return ValidateAsync(typedValue, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
using CertMgr.CertGen;
|
||||
using CertMgr.CertGen.Utils;
|
||||
using CertMgr.Core.Attributes;
|
||||
using CertMgr.Core.Converters.Impl;
|
||||
using CertMgr.Core.Jobs;
|
||||
@@ -19,10 +20,15 @@ public sealed class CertificateSettings : JobSettings
|
||||
ValidityPeriod = TimeSpan.FromDays(365);
|
||||
}
|
||||
|
||||
[Setting("subject", IsMandatory = true, Validator = typeof(StringValidator.IsNotNull))]
|
||||
[Setting("subject", IsMandatory = true, Validator = typeof(SubjectValidator))]
|
||||
public string? Subject { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||
|
||||
[Setting("subject-alternate-name", AlternateNames = ["san"])]
|
||||
// value can be:
|
||||
// - plain hostname
|
||||
// - DNS:hostname (i.e. with 'DNS:' prefix)
|
||||
// - IP:ip-address (i.e. with 'IP:' prefix)
|
||||
// other types (URI, UPN, email) are not supported
|
||||
[Setting("subject-alternate-name", AlternateNames = ["san"], Validator = typeof(SubjectAlternateNameValidator))]
|
||||
public IReadOnlyCollection<string>? SubjectAlternateNames { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||
|
||||
[Setting("algorithm", Default = CertificateAlgorithm.ECDsa, Converter = typeof(EnumConverter))]
|
||||
@@ -31,7 +37,7 @@ public sealed class CertificateSettings : JobSettings
|
||||
[Setting("ecdsa-curve")]
|
||||
public EcdsaCurve? Curve { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||
|
||||
[Setting("rsa-key-size")]
|
||||
[Setting("rsa-key-size", Converter = typeof(RsaKeySizeConverter))]
|
||||
public RsaKeySize? RsaKeySize { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
|
||||
|
||||
[Setting("hash-algorithm", AlternateNames = ["ha"])]
|
||||
@@ -70,7 +76,7 @@ public sealed class CertificateSettings : JobSettings
|
||||
}
|
||||
else if (Algorithm == CertificateAlgorithm.RSA)
|
||||
{
|
||||
if (!RsaKeySize.HasValue || !Enum.IsDefined(RsaKeySize.Value))
|
||||
if (!RsaKeySize.HasValue || !Enum.IsDefined<RsaKeySize>(RsaKeySize.Value))
|
||||
{
|
||||
results.AddInvalid(nameof(RsaKeySize), "value value must be specified: '{0}'", RsaKeySize?.ToString() ?? "<null>");
|
||||
}
|
||||
|
||||
@@ -75,10 +75,17 @@ public sealed class CreateCertificateJob : Job<CertificateSettings>
|
||||
cgcs.FriendlyName = "I'm your friend";
|
||||
if (Settings.Issuer != null)
|
||||
{
|
||||
using (MemoryStream ms = new MemoryStream())
|
||||
try
|
||||
{
|
||||
await Settings.Issuer.ReadAsync(ms, cancellationToken).ConfigureAwait(false);
|
||||
cgcs.Issuer = X509CertificateLoader.LoadPkcs12(ms.GetBuffer(), Settings.IssuerPassword, flags);
|
||||
using (MemoryStream ms = new MemoryStream())
|
||||
{
|
||||
await Settings.Issuer.ReadAsync(ms, cancellationToken).ConfigureAwait(false);
|
||||
cgcs.Issuer = X509CertificateLoader.LoadPkcs12(ms.GetBuffer(), Settings.IssuerPassword, flags);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new CertGenException(e, "Failed to load issuer's certificate");
|
||||
}
|
||||
}
|
||||
cgcs.SubjectName = Settings.Subject;
|
||||
|
||||
49
certmgr/Jobs/RsaKeySizeConverter.cs
Normal file
49
certmgr/Jobs/RsaKeySizeConverter.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using CertMgr.CertGen;
|
||||
using CertMgr.Core.Converters;
|
||||
|
||||
namespace CertMgr.Jobs;
|
||||
|
||||
internal sealed class RsaKeySizeConverter : ValueConverter<RsaKeySize?>
|
||||
{
|
||||
protected override Task<RsaKeySize?> DoConvertAsync(string rawValue, Type targetType, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(rawValue))
|
||||
{
|
||||
return Task.FromResult((RsaKeySize?)null);
|
||||
}
|
||||
|
||||
RsaKeySize? keySize;
|
||||
|
||||
Type? realType = Nullable.GetUnderlyingType(targetType);
|
||||
if (realType == null)
|
||||
{
|
||||
realType = targetType;
|
||||
}
|
||||
|
||||
if (Enum.TryParse(rawValue, true, out RsaKeySize tmp) && Enum.IsDefined(realType, tmp))
|
||||
{
|
||||
keySize = tmp;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (string.Equals(rawValue, "2048"))
|
||||
{
|
||||
keySize = RsaKeySize.KeySize2048;
|
||||
}
|
||||
else if (string.Equals(rawValue, "4096"))
|
||||
{
|
||||
keySize = RsaKeySize.KeySize4096;
|
||||
}
|
||||
else if (string.Equals(rawValue, "8192"))
|
||||
{
|
||||
keySize = RsaKeySize.KeySize8192;
|
||||
}
|
||||
else
|
||||
{
|
||||
keySize = null;
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(keySize);
|
||||
}
|
||||
}
|
||||
46
certmgr/Jobs/SubjectAlternateNameValidator.cs
Normal file
46
certmgr/Jobs/SubjectAlternateNameValidator.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using CertMgr.Core.Utils;
|
||||
using CertMgr.Core.Validation;
|
||||
|
||||
namespace CertMgr.Jobs;
|
||||
|
||||
internal sealed class SubjectAlternateNameValidator : SettingValidator<string>
|
||||
{
|
||||
public SubjectAlternateNameValidator(string settingName)
|
||||
: base(settingName)
|
||||
{
|
||||
}
|
||||
|
||||
public override Task<ValidationResult> ValidateAsync(string? settingValue, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(settingValue))
|
||||
{
|
||||
return Task.FromResult(new ValidationResult(SettingName, false, "value must not be empty"));
|
||||
}
|
||||
|
||||
ReadOnlySpan<char> span = settingValue.AsSpan();
|
||||
if (span.StartsWith("DNS:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!NetUtils.IsValidDns(span.Slice(4)))
|
||||
{
|
||||
return Task.FromResult(new ValidationResult(SettingName, false, "value '{0}' is not valid DNS name", span.Slice(4).ToString()));
|
||||
}
|
||||
}
|
||||
else if (span.StartsWith("IP:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!NetUtils.IsValidIPAny(span.Slice(3)))
|
||||
{
|
||||
return Task.FromResult(new ValidationResult(SettingName, false, "value '{0}' is not valid IP address", span.Slice(3).ToString()));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// fallback to dns name as other alt-names (UPN, email, URI..) are not supported
|
||||
if (!NetUtils.IsValidDns(span))
|
||||
{
|
||||
return Task.FromResult(new ValidationResult(SettingName, false, "value '{0}' is not valid DNS name (no prefix)", span.Slice(4).ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(new ValidationResult(SettingName, true, "valid"));
|
||||
}
|
||||
}
|
||||
@@ -6,18 +6,31 @@ internal static class Program
|
||||
{
|
||||
private static async Task<int> Main(string[] args)
|
||||
{
|
||||
// args = [
|
||||
// "--job=create-certificate",
|
||||
// "--issuer-certificate=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",
|
||||
"--issuer-certificate=file|o|c:\\friend2.pfx",
|
||||
"--issuer-password=aaa",
|
||||
"--subject=hello",
|
||||
"--subject=CN=hello",
|
||||
"--san=world",
|
||||
"--algorithm=ecdsa",
|
||||
"--ecdsa-curve=p384",
|
||||
"--storage=file|w|c:\\mycert.pfx",
|
||||
"--san=DNS:zdrastvujte",
|
||||
"--san=IP:192.168.131.1",
|
||||
"--algorithm=rsa",
|
||||
"--rsa-key-size=2048",
|
||||
"--storage=file|w|c:\\friend-rsa.pfx",
|
||||
"--validity-period=2d" ];
|
||||
|
||||
using CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));
|
||||
using CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromMinutes(3));
|
||||
|
||||
JobExecutor executor = new JobExecutor();
|
||||
int errorLevel = await executor.ExecuteAsync(args, cts.Token).ConfigureAwait(false);
|
||||
|
||||
86
certmgrTest/NetUtilsTest.cs
Normal file
86
certmgrTest/NetUtilsTest.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using CertMgr.Core.Utils;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace certmgrTest
|
||||
{
|
||||
public class NetUtilsTest
|
||||
{
|
||||
[Test]
|
||||
public void IPv6Test()
|
||||
{
|
||||
string[] validIPv6Addresses = new[]
|
||||
{
|
||||
"2001:0db8:0000:0000:0000:ff00:0042:8329", // full form
|
||||
"2001:db8:0:0:0:ff00:42:8329", // shortened zeros
|
||||
"2001:db8::ff00:42:8329", // compressed zeros
|
||||
"::1", // loopback
|
||||
"::", // unspecified address
|
||||
"fe80::1ff:fe23:4567:890a", // link-local address
|
||||
"2001:db8:1234:ffff:ffff:ffff:ffff:ffff", // maximum range
|
||||
"fd12:3456:789a:1::1", // unique local address (ULA)
|
||||
"2001:0:3238:DFE1:63::FEFB", // mixed compression
|
||||
"0:0:0:0:0:0:0:1", // loopback without compression
|
||||
"2001:db8::123", // short address
|
||||
"ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", // all bits set to 1
|
||||
"2001:db8:85a3::8a2e:370:7334", // example from RFC 4291
|
||||
"::ffff:192.0.2.128" // IPv4-mapped IPv6 address
|
||||
};
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (string ip in validIPv6Addresses)
|
||||
{
|
||||
try
|
||||
{
|
||||
bool valid = NetUtils.IsValidIPv6(ip);
|
||||
Assert.That(valid, Is.True, string.Format("expected that IPv6 address '{0}' is valid", ip));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Assert.Fail(string.Format("address: '{0}' failed with: {1}: {2}", ip, e.GetType().Name, e.Message));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IPv6_InvalidTest()
|
||||
{
|
||||
string[] invalidIPv6Addresses = new[]
|
||||
{
|
||||
"2001:::85a3::8a2e:370:7334", // multiple "::"
|
||||
"2001:db8:85a3:0:0:8a2e:370g:7334", // invalid hex character (g)
|
||||
"1200::AB00:1234::2552:7777:1313", // two "::" sections
|
||||
"2001:db8:85a3:z:0:8a2e:370:7334", // invalid hex segment
|
||||
"2001:db8:85a3:0:0:8a2e:370:7334:1234", // too many segments (9)
|
||||
"2001:db8:85a3", // too few segments (3)
|
||||
":2001:db8::1", // leading colon without pair
|
||||
"2001:db8::1:", // trailing colon without pair
|
||||
"2001:db8::1::", // multiple compression points
|
||||
"2001:db8:85a3:0:0:8a2e:370:7334/64", // contains CIDR mask
|
||||
// "2001:db8:85a3:0:0:8a2e:370:7334%", // missing interface ID after % <= this is valid (at least on windows)
|
||||
"::ffff:999.0.2.128", // invalid IPv4-mapped part
|
||||
"::ffff:192.0.2.256", // IPv4 part out of range
|
||||
"2001:db8:85a3::8a2e:370:7334:xyz", // trailing junk
|
||||
"2001:db8:::", // triple colon
|
||||
};
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (string ip in invalidIPv6Addresses)
|
||||
{
|
||||
try
|
||||
{
|
||||
bool valid = NetUtils.IsValidIPv6(ip);
|
||||
Assert.That(valid, Is.False, string.Format("expected that IPv6 address '{0}' is NOT valid", ip));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Assert.Fail(string.Format("address: '{0}' failed with: {1}: {2}", ip, e.GetType().Name, e.Message));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user