Migrate existing code
This commit is contained in:
parent
384ff4a6f3
commit
9b49f880a2
30 changed files with 1789 additions and 5 deletions
10
Integrations/Kea/IKeaDhcpLeaseHandler.cs
Normal file
10
Integrations/Kea/IKeaDhcpLeaseHandler.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
using DotNetDDI.Services.Dhcp;
|
||||
|
||||
using nietras.SeparatedValues;
|
||||
|
||||
namespace DotNetDDI.Integrations.Kea;
|
||||
|
||||
public interface IKeaDhcpLeaseHandler
|
||||
{
|
||||
DhcpLeaseChange? Handle(in SepReader.Row row);
|
||||
}
|
||||
12
Integrations/Kea/IKeaFactory.cs
Normal file
12
Integrations/Kea/IKeaFactory.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
using DotNetDDI.Options;
|
||||
|
||||
namespace DotNetDDI.Integrations.Kea;
|
||||
|
||||
public interface IKeaFactory
|
||||
{
|
||||
KeaDhcp4LeaseHandler CreateHandler4();
|
||||
|
||||
KeaDhcp6LeaseHandler CreateHandler6();
|
||||
|
||||
KeaDhcpLeaseWatcher CreateWatcher(IKeaDhcpLeaseHandler handler, KeaDhcpServerOptions options);
|
||||
}
|
||||
142
Integrations/Kea/KeaDhcp4Lease.cs
Normal file
142
Integrations/Kea/KeaDhcp4Lease.cs
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
|
||||
using nietras.SeparatedValues;
|
||||
|
||||
using Cell = System.ReadOnlySpan<char>;
|
||||
|
||||
namespace DotNetDDI.Integrations.Kea;
|
||||
|
||||
using Lease = KeaDhcp4Lease;
|
||||
|
||||
// ref: https://github.com/isc-projects/kea/blob/Kea-2.5.3/src/lib/dhcpsrv/csv_lease_file4.h
|
||||
public record struct KeaDhcp4Lease(
|
||||
IPAddress Address,
|
||||
PhysicalAddress HWAddr,
|
||||
string? ClientId,
|
||||
TimeSpan ValidLifetime,
|
||||
DateTimeOffset Expire,
|
||||
uint SubnetId,
|
||||
bool FqdnFwd,
|
||||
bool FqdnRev,
|
||||
string Hostname,
|
||||
uint State,
|
||||
string? UserContext,
|
||||
uint PoolId)
|
||||
{
|
||||
public static Lease? Parse(in SepReader.Row row)
|
||||
{
|
||||
Lease result = new();
|
||||
for (int i = 0; i < row.ColCount; i++)
|
||||
{
|
||||
if (Parse(ref result, i, row[i].Span) == false)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool? Parse(ref Lease lease, int column, in Cell span)
|
||||
{
|
||||
return column switch
|
||||
{
|
||||
0 => ToIPAddress(ref lease, span),
|
||||
1 when !span.IsWhiteSpace() => ToHWAddr(ref lease, span),
|
||||
2 => ToClientId(ref lease, span),
|
||||
3 => ToValidLifetime(ref lease, span),
|
||||
4 => ToExpire(ref lease, span),
|
||||
5 => ToSubnetId(ref lease, span),
|
||||
6 => ToFqdnFwd(ref lease, span),
|
||||
7 => ToFqdnRev(ref lease, span),
|
||||
8 => ToHostname(ref lease, span),
|
||||
9 => ToState(ref lease, span),
|
||||
10 => ToUserContext(ref lease, span),
|
||||
11 => ToPoolId(ref lease, span),
|
||||
|
||||
_ => null
|
||||
};
|
||||
|
||||
static bool ToIPAddress(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = IPAddress.TryParse(span, out var address);
|
||||
lease.Address = address!;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToHWAddr(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = PhysicalAddress.TryParse(span, out var hwaddr);
|
||||
lease.HWAddr = hwaddr!;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToClientId(ref Lease lease, in Cell span)
|
||||
{
|
||||
lease.ClientId = span.ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ToValidLifetime(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = uint.TryParse(span, out var validLifetime);
|
||||
lease.ValidLifetime = TimeSpan.FromSeconds(validLifetime);
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToExpire(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = ulong.TryParse(span, out var expire);
|
||||
lease.Expire = DateTimeOffset.FromUnixTimeSeconds(unchecked((long)expire));
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToSubnetId(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = uint.TryParse(span, out var subnetId);
|
||||
lease.SubnetId = subnetId;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToFqdnFwd(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = byte.TryParse(span, out var fqdnFwd);
|
||||
lease.FqdnFwd = fqdnFwd != 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToFqdnRev(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = byte.TryParse(span, out var fqdnRev);
|
||||
lease.FqdnRev = fqdnRev != 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToHostname(ref Lease lease, in Cell span)
|
||||
{
|
||||
lease.Hostname = KeaDhcpLease.Unescape(span);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ToState(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = uint.TryParse(span, out var state);
|
||||
lease.State = state;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToUserContext(ref Lease lease, in Cell span)
|
||||
{
|
||||
lease.UserContext = KeaDhcpLease.Unescape(span);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ToPoolId(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = uint.TryParse(span, out var poolId);
|
||||
lease.PoolId = poolId;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
32
Integrations/Kea/KeaDhcp4LeaseHandler.cs
Normal file
32
Integrations/Kea/KeaDhcp4LeaseHandler.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
using DotNetDDI.Services.Dhcp;
|
||||
|
||||
using nietras.SeparatedValues;
|
||||
|
||||
namespace DotNetDDI.Integrations.Kea;
|
||||
|
||||
public class KeaDhcp4LeaseHandler : IKeaDhcpLeaseHandler
|
||||
{
|
||||
public DhcpLeaseChange? Handle(in SepReader.Row row)
|
||||
{
|
||||
if (KeaDhcp4Lease.Parse(row) is not { } lease)
|
||||
{
|
||||
goto exitNull;
|
||||
}
|
||||
|
||||
if (lease.State != 0)
|
||||
{
|
||||
goto exitNull;
|
||||
}
|
||||
|
||||
DhcpLeaseIdentifier identifier = lease.ClientId switch
|
||||
{
|
||||
string clientId when !string.IsNullOrWhiteSpace(clientId) => new DhcpLeaseClientIdentifier(clientId),
|
||||
_ => new DhcpLeaseHWAddrIdentifier(lease.HWAddr)
|
||||
};
|
||||
|
||||
return new(lease.Address, lease.Hostname, identifier, lease.ValidLifetime);
|
||||
|
||||
exitNull:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
196
Integrations/Kea/KeaDhcp6Lease.cs
Normal file
196
Integrations/Kea/KeaDhcp6Lease.cs
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
|
||||
using nietras.SeparatedValues;
|
||||
|
||||
using Cell = System.ReadOnlySpan<char>;
|
||||
|
||||
namespace DotNetDDI.Integrations.Kea;
|
||||
|
||||
using Lease = KeaDhcp6Lease;
|
||||
|
||||
// ref: https://github.com/isc-projects/kea/blob/Kea-2.5.3/src/lib/dhcpsrv/csv_lease_file6.h
|
||||
public record struct KeaDhcp6Lease(
|
||||
IPAddress Address,
|
||||
string DUId,
|
||||
TimeSpan ValidLifetime,
|
||||
DateTimeOffset Expire,
|
||||
uint SubnetId,
|
||||
uint PrefLifetime,
|
||||
LeaseType LeaseType,
|
||||
uint IAId,
|
||||
byte PrefixLen,
|
||||
bool FqdnFwd,
|
||||
bool FqdnRev,
|
||||
string Hostname,
|
||||
PhysicalAddress HWAddr,
|
||||
uint State,
|
||||
string? UserContext,
|
||||
ushort? HWType,
|
||||
uint? HWAddrSource,
|
||||
uint PoolId)
|
||||
{
|
||||
public static Lease? Parse(in SepReader.Row row)
|
||||
{
|
||||
Lease result = new();
|
||||
for (int i = 0; i < row.ColCount; i++)
|
||||
{
|
||||
if (Parse(ref result, i, row[i].Span) == false)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool? Parse(ref Lease lease, int column, in Cell span)
|
||||
{
|
||||
return column switch
|
||||
{
|
||||
0 => ToAddress(ref lease, span),
|
||||
1 => ToDUId(ref lease, span),
|
||||
2 => ToValidLifetime(ref lease, span),
|
||||
3 => ToExpire(ref lease, span),
|
||||
4 => ToSubnetId(ref lease, span),
|
||||
5 => ToPrefLifetime(ref lease, span),
|
||||
6 => ToLeaseType(ref lease, span),
|
||||
7 => ToIAId(ref lease, span),
|
||||
8 => ToPrefixLen(ref lease, span),
|
||||
9 => ToFqdnFwd(ref lease, span),
|
||||
10 => ToFqdnRev(ref lease, span),
|
||||
11 => ToHostname(ref lease, span),
|
||||
12 when !span.IsWhiteSpace() => ToHWAddr(ref lease, span),
|
||||
13 => ToState(ref lease, span),
|
||||
14 when !span.IsWhiteSpace() => ToUserContext(ref lease, span),
|
||||
15 => ToHWType(ref lease, span),
|
||||
16 => ToHWAddrSource(ref lease, span),
|
||||
17 => ToPoolId(ref lease, span),
|
||||
|
||||
_ => null
|
||||
};
|
||||
|
||||
static bool ToAddress(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = IPAddress.TryParse(span, out var address);
|
||||
lease.Address = address!;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToDUId(ref Lease lease, in Cell span)
|
||||
{
|
||||
lease.DUId = span.ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ToValidLifetime(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = uint.TryParse(span, out var validLifetime);
|
||||
lease.ValidLifetime = TimeSpan.FromSeconds(validLifetime);
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToExpire(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = ulong.TryParse(span, out var expire);
|
||||
lease.Expire = DateTimeOffset.FromUnixTimeSeconds(unchecked((long)expire));
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToSubnetId(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = uint.TryParse(span, out var subnetId);
|
||||
lease.SubnetId = subnetId;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToPrefLifetime(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = uint.TryParse(span, out var prefLifetime);
|
||||
lease.PrefLifetime = prefLifetime;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToLeaseType(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = byte.TryParse(span, out var leaseType);
|
||||
lease.LeaseType = (LeaseType)leaseType;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToIAId(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = uint.TryParse(span, out var iaId);
|
||||
lease.IAId = iaId;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToPrefixLen(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = byte.TryParse(span, out var prefixLen);
|
||||
lease.PrefixLen = prefixLen;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToFqdnFwd(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = byte.TryParse(span, out var fqdnFwd);
|
||||
lease.FqdnFwd = fqdnFwd != 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToFqdnRev(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = byte.TryParse(span, out var fqdnRev);
|
||||
lease.FqdnRev = fqdnRev != 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToHostname(ref Lease lease, in Cell span)
|
||||
{
|
||||
lease.Hostname = KeaDhcpLease.Unescape(span);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ToHWAddr(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = PhysicalAddress.TryParse(span, out var hwAddr);
|
||||
lease.HWAddr = hwAddr!;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToState(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = uint.TryParse(span, out var state);
|
||||
lease.State = state;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToUserContext(ref Lease lease, in Cell span)
|
||||
{
|
||||
lease.UserContext = KeaDhcpLease.Unescape(span);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ToHWType(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = ushort.TryParse(span, out var hwType);
|
||||
lease.HWType = hwType;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToHWAddrSource(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = uint.TryParse(span, out var hwAddrSource);
|
||||
lease.HWAddrSource = hwAddrSource;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool ToPoolId(ref Lease lease, in Cell span)
|
||||
{
|
||||
bool result = uint.TryParse(span, out var poolId);
|
||||
lease.PoolId = poolId;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
24
Integrations/Kea/KeaDhcp6LeaseHandler.cs
Normal file
24
Integrations/Kea/KeaDhcp6LeaseHandler.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
using DotNetDDI.Services.Dhcp;
|
||||
|
||||
using nietras.SeparatedValues;
|
||||
|
||||
namespace DotNetDDI.Integrations.Kea;
|
||||
|
||||
public class KeaDhcp6LeaseHandler : IKeaDhcpLeaseHandler
|
||||
{
|
||||
public DhcpLeaseChange? Handle(in SepReader.Row row)
|
||||
{
|
||||
if (KeaDhcp6Lease.Parse(row) is not { } lease)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
DhcpLeaseIdentifier identifier = lease.DUId switch
|
||||
{
|
||||
string clientId when !string.IsNullOrWhiteSpace(clientId) => new DhcpLeaseClientIdentifier(clientId),
|
||||
_ => new DhcpLeaseHWAddrIdentifier(lease.HWAddr)
|
||||
};
|
||||
|
||||
return new(lease.Address, lease.Hostname, identifier, lease.ValidLifetime);
|
||||
}
|
||||
}
|
||||
63
Integrations/Kea/KeaDhcpLease.cs
Normal file
63
Integrations/Kea/KeaDhcpLease.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
using System.Buffers;
|
||||
using System.Globalization;
|
||||
|
||||
using DotNext.Buffers;
|
||||
|
||||
using CommunityToolkit.HighPerformance.Buffers;
|
||||
|
||||
namespace DotNetDDI.Integrations.Kea;
|
||||
|
||||
public static class KeaDhcpLease
|
||||
{
|
||||
private static ReadOnlySpan<char> EscapeTag => ['&', '#', 'x'];
|
||||
|
||||
// ref: https://github.com/isc-projects/kea/blob/Kea-2.5.3/src/lib/util/csv_file.cc#L479
|
||||
public static string Unescape(in ReadOnlySpan<char> text)
|
||||
{
|
||||
return text.IndexOf(EscapeTag) switch
|
||||
{
|
||||
-1 => text.ToString(),
|
||||
int i => SlowPath(i, text)
|
||||
};
|
||||
|
||||
static string SlowPath(int esc_pos, in ReadOnlySpan<char> text)
|
||||
{
|
||||
SpanReader<char> reader = new(text);
|
||||
using ArrayPoolBufferWriter<char> writer = new(text.Length);
|
||||
while (reader.RemainingCount > 0)
|
||||
{
|
||||
writer.Write(reader.Read(esc_pos));
|
||||
reader.Advance(EscapeTag.Length);
|
||||
|
||||
bool converted = false;
|
||||
char escapedChar = default;
|
||||
if (reader.RemainingCount >= 2)
|
||||
{
|
||||
if (byte.TryParse(reader.RemainingSpan[..2], NumberStyles.AllowHexSpecifier, null, out var b))
|
||||
{
|
||||
converted = true;
|
||||
escapedChar = (char)b;
|
||||
reader.Advance(2);
|
||||
}
|
||||
}
|
||||
|
||||
if (converted)
|
||||
{
|
||||
writer.Write([escapedChar]);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.Write(EscapeTag);
|
||||
}
|
||||
|
||||
esc_pos = reader.RemainingSpan.IndexOf(EscapeTag);
|
||||
if (esc_pos == -1)
|
||||
{
|
||||
writer.Write(reader.ReadToEnd());
|
||||
}
|
||||
}
|
||||
|
||||
return writer.WrittenSpan.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
267
Integrations/Kea/KeaDhcpLeaseWatcher.cs
Normal file
267
Integrations/Kea/KeaDhcpLeaseWatcher.cs
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
using System.Buffers;
|
||||
using System.IO.Pipelines;
|
||||
using System.Text;
|
||||
using System.Threading.Channels;
|
||||
|
||||
using DotNetDDI.Options;
|
||||
using DotNetDDI.Services.Dhcp;
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
using nietras.SeparatedValues;
|
||||
|
||||
namespace DotNetDDI.Integrations.Kea;
|
||||
|
||||
public sealed class KeaDhcpLeaseWatcher : IHostedService
|
||||
{
|
||||
private static readonly FileStreamOptions LeaseFileStreamOptions = new()
|
||||
{
|
||||
Access = FileAccess.Read,
|
||||
Mode = FileMode.Open,
|
||||
Options = FileOptions.SequentialScan | FileOptions.Asynchronous,
|
||||
Share = (FileShare)7,
|
||||
};
|
||||
private static readonly SepReaderOptions MemfileReader = Sep.New(',').Reader(o => o with
|
||||
{
|
||||
DisableColCountCheck = true,
|
||||
DisableFastFloat = true
|
||||
});
|
||||
|
||||
private readonly Decoder _decoder;
|
||||
private readonly FileSystemWatcher _fsw;
|
||||
private readonly IKeaDhcpLeaseHandler _handler;
|
||||
private readonly string _leaseFile;
|
||||
private readonly Pipe _pipe;
|
||||
private readonly DhcpLeaseQueue _queue;
|
||||
private Channel<FileSystemEventArgs>? _eventChannel;
|
||||
private Task? _executeTask;
|
||||
private CancellationTokenSource? _stoppingCts;
|
||||
|
||||
private KeaDhcpServerOptions Options { get; }
|
||||
|
||||
public KeaDhcpLeaseWatcher(KeaDhcpServerOptions options, IKeaDhcpLeaseHandler handler, DhcpLeaseQueue queue)
|
||||
{
|
||||
Options = options = options with { Leases = PathEx.ExpandPath(options.Leases) };
|
||||
_handler = handler;
|
||||
_queue = queue;
|
||||
|
||||
var leases = options.Leases.AsSpan();
|
||||
if (leases.IsWhiteSpace())
|
||||
{
|
||||
throw new ArgumentException($"{nameof(options.Leases)} must not be empty.", nameof(options));
|
||||
}
|
||||
|
||||
var leaseFile = Path.GetFileName(leases);
|
||||
var leaseDirectory = Path.GetDirectoryName(leases).ToString();
|
||||
if (!Directory.Exists(leaseDirectory))
|
||||
{
|
||||
throw new ArgumentException($"{nameof(options.Leases)} must point to a file in an existing path.", nameof(options));
|
||||
}
|
||||
|
||||
_decoder = Encoding.UTF8.GetDecoder();
|
||||
_leaseFile = leaseFile.ToString();
|
||||
_fsw = new(leaseDirectory, _leaseFile);
|
||||
_fsw.Changed += OnLeaseChanged;
|
||||
_fsw.Error += OnLeaseError;
|
||||
_pipe = new();
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_executeTask = Reading(_stoppingCts.Token);
|
||||
return _executeTask is { IsCompleted: true } completed ? completed : Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_executeTask is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_fsw.EnableRaisingEvents = false;
|
||||
try
|
||||
{
|
||||
_stoppingCts!.Cancel();
|
||||
}
|
||||
finally
|
||||
{
|
||||
TaskCompletionSource taskCompletionSource = new();
|
||||
using (cancellationToken.Register(s => ((TaskCompletionSource)s!).SetCanceled(), taskCompletionSource))
|
||||
{
|
||||
await Task.WhenAny(_executeTask, taskCompletionSource.Task).ConfigureAwait(continueOnCapturedContext: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Reading(CancellationToken stoppingToken)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
using CancellationTokenSource loopCts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
|
||||
var loopToken = loopCts.Token;
|
||||
_eventChannel = Channel.CreateUnbounded<FileSystemEventArgs>();
|
||||
_fsw.EnableRaisingEvents = true;
|
||||
using AutoResetEvent resetEvent = new(false);
|
||||
Task reader = Task.CompletedTask;
|
||||
try
|
||||
{
|
||||
for (
|
||||
EventArgs readArgs = EventArgs.Empty;
|
||||
!loopToken.IsCancellationRequested;
|
||||
readArgs = await _eventChannel.Reader.ReadAsync(loopToken))
|
||||
{
|
||||
// Guard for Deleted and renamed away events,
|
||||
// both have to stop this reader immediately.
|
||||
// Just wait for the file being created/renamed to _leaseFile.
|
||||
// Described in [The LFC Process](https://kea.readthedocs.io/en/latest/arm/lfc.html#kea-lfc)
|
||||
switch (readArgs)
|
||||
{
|
||||
case FileSystemEventArgs { ChangeType: WatcherChangeTypes.Deleted }:
|
||||
case RenamedEventArgs renamed when renamed.OldName == _leaseFile:
|
||||
loopCts.Cancel();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (reader is not { IsCompleted: false })
|
||||
{
|
||||
// In any case that the reader failed (for whatever reason)
|
||||
// restart now.
|
||||
// Incoming event could be Changed/Created/Renamed
|
||||
// This doesn't care, as we already lost the file handle.
|
||||
reader = FileReader(resetEvent, stoppingToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
resetEvent.Set();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
finally
|
||||
{
|
||||
_eventChannel.Writer.TryComplete();
|
||||
if (reader is { IsCompleted: false })
|
||||
{
|
||||
await reader.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
|
||||
}
|
||||
|
||||
_pipe.Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FileReader(AutoResetEvent waitHandle, CancellationToken stoppingToken)
|
||||
{
|
||||
SepReader? reader = null;
|
||||
try
|
||||
{
|
||||
PipeWriter writer = _pipe.Writer;
|
||||
using (stoppingToken.Register(s => ((PipeWriter)s!).Complete(null), writer))
|
||||
{
|
||||
using var file = new FileStream(Options.Leases, LeaseFileStreamOptions);
|
||||
|
||||
bool awaitLineFeed = false;
|
||||
int newLinesEncountered = 0;
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
for (; newLinesEncountered > 0; newLinesEncountered--)
|
||||
{
|
||||
if (reader is null)
|
||||
{
|
||||
// LongRunning, force spawning a thread
|
||||
// As this may block for a long time.
|
||||
reader = await Task.Factory.StartNew(
|
||||
s => MemfileReader.From((Stream)s!),
|
||||
_pipe.Reader.AsStream(),
|
||||
stoppingToken,
|
||||
TaskCreationOptions.DenyChildAttach | TaskCreationOptions.LongRunning,
|
||||
TaskScheduler.Default)
|
||||
.ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!reader.MoveNext())
|
||||
{
|
||||
// TODO Error state.
|
||||
return;
|
||||
}
|
||||
|
||||
if (_handler.Handle(reader.Current) is not { } lease)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
await _queue.Write(lease, stoppingToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var memory = writer.GetMemory();
|
||||
int read = await file.ReadAsync(memory, stoppingToken);
|
||||
if (read > 0)
|
||||
{
|
||||
CountNewLines(_decoder, memory[..read], ref newLinesEncountered, ref awaitLineFeed);
|
||||
writer.Advance(read);
|
||||
await writer.FlushAsync(stoppingToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
await waitHandle.WaitOneAsync(stoppingToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
reader?.Dispose();
|
||||
}
|
||||
|
||||
static void CountNewLines(Decoder decoder, in Memory<byte> memory, ref int newLinesEncountered, ref bool awaitLineFeed)
|
||||
{
|
||||
Span<char> buffer = stackalloc char[128];
|
||||
bool completed = false;
|
||||
ReadOnlySequence<byte> sequence = new(memory);
|
||||
var reader = new SequenceReader<byte>(sequence);
|
||||
while (!reader.End)
|
||||
{
|
||||
decoder.Convert(reader.UnreadSpan, buffer, false, out var bytesUsed, out var charsUsed, out completed);
|
||||
reader.Advance(bytesUsed);
|
||||
foreach (ref readonly char c in buffer[..charsUsed])
|
||||
{
|
||||
if (awaitLineFeed || c == '\n')
|
||||
{
|
||||
newLinesEncountered++;
|
||||
awaitLineFeed = false;
|
||||
}
|
||||
else if (c == '\r')
|
||||
{
|
||||
awaitLineFeed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLeaseChanged(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
if (_eventChannel?.Writer is not { } writer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
#pragma warning disable CA2012
|
||||
var task = writer.WriteAsync(e, CancellationToken.None);
|
||||
#pragma warning restore
|
||||
if (task.IsCompleted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
task.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private void OnLeaseError(object sender, ErrorEventArgs e)
|
||||
{
|
||||
_eventChannel?.Writer.Complete(e.GetException());
|
||||
}
|
||||
}
|
||||
39
Integrations/Kea/KeaFactoryServices.cs
Normal file
39
Integrations/Kea/KeaFactoryServices.cs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
using DotNetDDI.Options;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace DotNetDDI.Integrations.Kea;
|
||||
|
||||
public static class KeaFactoryServices
|
||||
{
|
||||
public static IServiceCollection AddKeaFactory(this IServiceCollection services)
|
||||
{
|
||||
services.AddTransient<IKeaFactory, KeaFactory>();
|
||||
return services;
|
||||
}
|
||||
|
||||
private class KeaFactory(IServiceProvider services) : IKeaFactory
|
||||
{
|
||||
private ObjectFactory<KeaDhcp4LeaseHandler>? _cachedCreateHandler4;
|
||||
private ObjectFactory<KeaDhcp6LeaseHandler>? _cachedCreateHandler6;
|
||||
private ObjectFactory<KeaDhcpLeaseWatcher>? _cachedCreateWatcher;
|
||||
|
||||
KeaDhcp4LeaseHandler IKeaFactory.CreateHandler4()
|
||||
{
|
||||
_cachedCreateHandler4 ??= ActivatorUtilities.CreateFactory<KeaDhcp4LeaseHandler>([]);
|
||||
return _cachedCreateHandler4(services, null);
|
||||
}
|
||||
|
||||
KeaDhcp6LeaseHandler IKeaFactory.CreateHandler6()
|
||||
{
|
||||
_cachedCreateHandler6 ??= ActivatorUtilities.CreateFactory<KeaDhcp6LeaseHandler>([]);
|
||||
return _cachedCreateHandler6(services, null);
|
||||
}
|
||||
|
||||
KeaDhcpLeaseWatcher IKeaFactory.CreateWatcher(IKeaDhcpLeaseHandler handler, KeaDhcpServerOptions options)
|
||||
{
|
||||
_cachedCreateWatcher ??= ActivatorUtilities.CreateFactory<KeaDhcpLeaseWatcher>([typeof(IKeaDhcpLeaseHandler), typeof(KeaDhcpServerOptions)]);
|
||||
return _cachedCreateWatcher(services, [handler, options]);
|
||||
}
|
||||
}
|
||||
}
|
||||
54
Integrations/Kea/KeaService.cs
Normal file
54
Integrations/Kea/KeaService.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
using System.Collections.Immutable;
|
||||
|
||||
using DotNetDDI.Options;
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace DotNetDDI.Integrations.Kea;
|
||||
|
||||
public class KeaService : IHostedService
|
||||
{
|
||||
private readonly ImmutableArray<IHostedService> _services;
|
||||
|
||||
public KeaService(KeaDhcpOptions options, IKeaFactory factory)
|
||||
{
|
||||
var services = ImmutableArray.CreateBuilder<IHostedService>();
|
||||
|
||||
if (options.Dhcp4 is { } dhcp4Options)
|
||||
{
|
||||
services.Add(factory.CreateWatcher(factory.CreateHandler4(), dhcp4Options));
|
||||
}
|
||||
|
||||
if (options.Dhcp6 is { } dhcp6Options)
|
||||
{
|
||||
services.Add(factory.CreateWatcher(factory.CreateHandler6(), dhcp6Options));
|
||||
}
|
||||
|
||||
_services = services.DrainToImmutable();
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Task[] tasks = new Task[_services.Length];
|
||||
for (int i = 0; i < tasks.Length; i++)
|
||||
{
|
||||
tasks[i] = _services[i].StartAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Task[] tasks = new Task[_services.Length];
|
||||
for (int i = 0; i < tasks.Length; i++)
|
||||
{
|
||||
tasks[i] = _services[i].StopAsync(cancellationToken);
|
||||
}
|
||||
|
||||
var waitTask = Task.WhenAll(tasks);
|
||||
TaskCompletionSource taskCompletionSource = new();
|
||||
using var registration = cancellationToken.Register(s => ((TaskCompletionSource)s!).SetCanceled(), taskCompletionSource);
|
||||
await Task.WhenAny(waitTask, taskCompletionSource.Task).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
9
Integrations/Kea/LeaseType.cs
Normal file
9
Integrations/Kea/LeaseType.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
namespace DotNetDDI.Integrations.Kea;
|
||||
|
||||
public enum LeaseType : byte
|
||||
{
|
||||
NonTempraryIPv6 = 0,
|
||||
TemporaryIPv6 = 1,
|
||||
IPv6Prefix = 2,
|
||||
IPv4 = 3
|
||||
}
|
||||
12
Integrations/PowerDns/Methods.cs
Normal file
12
Integrations/PowerDns/Methods.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
namespace DotNetDDI.Integrations.PowerDns;
|
||||
|
||||
public interface IMethod;
|
||||
|
||||
public record MethodBase(string Method);
|
||||
|
||||
public record Method<TParam>(string Method, TParam Parameters) : MethodBase(Method)
|
||||
where TParam : MethodParameters;
|
||||
|
||||
public record InitializeMethod(InitializeParameters Parameters) : Method<InitializeParameters>("initialize", Parameters), IMethod;
|
||||
|
||||
public record LookupMethod(LookupParameters Parameters) : Method<LookupParameters>("lookup", Parameters), IMethod;
|
||||
55
Integrations/PowerDns/Parameters.cs
Normal file
55
Integrations/PowerDns/Parameters.cs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DotNetDDI.Integrations.PowerDns;
|
||||
|
||||
public record class Parameters;
|
||||
|
||||
[JsonDerivedType(typeof(InitializeParameters))]
|
||||
[JsonDerivedType(typeof(LookupParameters))]
|
||||
public record class MethodParameters : Parameters
|
||||
{
|
||||
[JsonExtensionData]
|
||||
public Dictionary<string, JsonElement> AdditionalProperties { get; set; } = [];
|
||||
|
||||
protected override bool PrintMembers(StringBuilder builder)
|
||||
{
|
||||
if (base.PrintMembers(builder))
|
||||
{
|
||||
builder.Append(", ");
|
||||
}
|
||||
|
||||
builder.Append("AdditionalProperties = [");
|
||||
bool append = false;
|
||||
foreach (var kv in AdditionalProperties)
|
||||
{
|
||||
if (append)
|
||||
{
|
||||
builder.Append(", ");
|
||||
}
|
||||
|
||||
append = true;
|
||||
builder.Append(kv.Key);
|
||||
builder.Append(" = ");
|
||||
builder.Append(kv.Value);
|
||||
}
|
||||
|
||||
builder.Append(']');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public record class InitializeParameters(
|
||||
[property: JsonPropertyName("command")] string Command,
|
||||
[property: JsonPropertyName("timeout")] int Timeout
|
||||
) : MethodParameters;
|
||||
|
||||
public record class LookupParameters(
|
||||
[property: JsonPropertyName("qtype")] string Qtype,
|
||||
[property: JsonPropertyName("qname")] string Qname,
|
||||
[property: JsonPropertyName("remote")] string Remote,
|
||||
[property: JsonPropertyName("local")] string Local,
|
||||
[property: JsonPropertyName("real-remote")] string RealRemote,
|
||||
[property: JsonPropertyName("zone-id")] int ZoneId
|
||||
) : MethodParameters;
|
||||
272
Integrations/PowerDns/PowerDnsHandler.cs
Normal file
272
Integrations/PowerDns/PowerDnsHandler.cs
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
using System.Buffers;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO.Pipelines;
|
||||
using System.Net.Sockets;
|
||||
using System.Text.Json;
|
||||
|
||||
using CommunityToolkit.HighPerformance;
|
||||
using CommunityToolkit.HighPerformance.Buffers;
|
||||
|
||||
using DotNetDDI.Services.Dns;
|
||||
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace DotNetDDI.Integrations.PowerDns;
|
||||
|
||||
public class PowerDnsHandler : ConnectionHandler
|
||||
{
|
||||
delegate MethodBase? HandlerConverter(in JsonElement element);
|
||||
|
||||
private static readonly ReadOnlyDictionary<string, HandlerConverter> Converters;
|
||||
|
||||
private readonly ILogger<PowerDnsHandler> _logger;
|
||||
private readonly DnsRepository _repository;
|
||||
|
||||
static PowerDnsHandler()
|
||||
{
|
||||
Dictionary<string, HandlerConverter> converters = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["initialize"] = new(ToInitialize),
|
||||
["lookup"] = new(ToLookup)
|
||||
};
|
||||
|
||||
Converters = converters.AsReadOnly();
|
||||
|
||||
static InitializeMethod ToInitialize(in JsonElement element)
|
||||
{
|
||||
return new(element.Deserialize(PowerDnsSerializerContext.Default.InitializeParameters)!);
|
||||
}
|
||||
|
||||
static LookupMethod ToLookup(in JsonElement element)
|
||||
{
|
||||
return new(element.Deserialize(PowerDnsSerializerContext.Default.LookupParameters)!);
|
||||
}
|
||||
}
|
||||
|
||||
public PowerDnsHandler(DnsRepository repository, ILogger<PowerDnsHandler> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public override async Task OnConnectedAsync(ConnectionContext connection)
|
||||
{
|
||||
var input = connection.Transport.Input;
|
||||
JsonReaderState state = default;
|
||||
using ArrayPoolBufferWriter<byte> json = new();
|
||||
using ArrayPoolBufferWriter<byte> buffer = new();
|
||||
using var writer = connection.Transport.Output.AsStream();
|
||||
while (!connection.ConnectionClosed.IsCancellationRequested)
|
||||
{
|
||||
if (await ReadAsync(input, connection.ConnectionClosed) is not { IsCanceled: false } read)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var memory in read.Buffer)
|
||||
{
|
||||
buffer.Write(memory.Span);
|
||||
if (!ConsumeJson(buffer, json, ref state))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Reply reply = BoolReply.False;
|
||||
try
|
||||
{
|
||||
MethodBase method;
|
||||
try
|
||||
{
|
||||
using var jsonDocument = JsonDocument.Parse(json.WrittenMemory);
|
||||
var root = jsonDocument.RootElement;
|
||||
if (!root.TryGetProperty("method", out var methodElement))
|
||||
{
|
||||
_logger.LogWarning("Json Document missing required property method: {document}", jsonDocument);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Parse(methodElement, root.GetProperty("parameters")) is not { } methodLocal)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
method = methodLocal;
|
||||
}
|
||||
finally
|
||||
{
|
||||
json.Clear();
|
||||
state = default;
|
||||
}
|
||||
|
||||
reply = await Handle(method, connection.ConnectionClosed).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(writer, reply, PowerDnsSerializerContext.Default.Reply, connection.ConnectionClosed)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
input.AdvanceTo(read.Buffer.End);
|
||||
}
|
||||
|
||||
static async ValueTask<ReadResult?> ReadAsync(PipeReader reader, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await reader.ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static bool ConsumeJson(ArrayPoolBufferWriter<byte> inflight, ArrayPoolBufferWriter<byte> json, ref JsonReaderState state)
|
||||
{
|
||||
bool final = false;
|
||||
Utf8JsonReader reader = new(inflight.WrittenSpan, false, state);
|
||||
while (!final && reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.EndObject && reader.CurrentDepth == 0)
|
||||
{
|
||||
final = true;
|
||||
}
|
||||
}
|
||||
|
||||
state = reader.CurrentState;
|
||||
int consumed = (int)reader.BytesConsumed;
|
||||
if (consumed > 0)
|
||||
{
|
||||
json.Write(inflight.WrittenSpan[..consumed]);
|
||||
|
||||
Span<byte> buffer = default;
|
||||
var remaining = inflight.WrittenCount - consumed;
|
||||
if (remaining > 0)
|
||||
{
|
||||
buffer = inflight.GetSpan(remaining)[..remaining];
|
||||
inflight.WrittenSpan[consumed..].CopyTo(buffer);
|
||||
}
|
||||
|
||||
// clear only clears up until WrittenCount
|
||||
// thus data after write-head is safe
|
||||
inflight.Clear();
|
||||
if (!buffer.IsEmpty)
|
||||
{
|
||||
inflight.Write(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
return final;
|
||||
}
|
||||
|
||||
static MethodBase? Parse(in JsonElement element, in JsonElement parameters)
|
||||
{
|
||||
HandlerConverter? converter = default;
|
||||
return element.GetString() switch
|
||||
{
|
||||
null => null,
|
||||
{ } methodName when !Converters.TryGetValue(methodName, out converter) => new Method<MethodParameters>(methodName, parameters.Deserialize(PowerDnsSerializerContext.Default.MethodParameters)!),
|
||||
_ => converter(parameters)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private ValueTask<Reply> Handle(MethodBase method, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return method switch
|
||||
{
|
||||
InitializeMethod { Parameters: { } init } => HandleInitialize(init),
|
||||
LookupMethod { Parameters: { } lookup } => HandleLookup(lookup),
|
||||
|
||||
_ => LogUnhandled(_logger, method)
|
||||
};
|
||||
|
||||
static ValueTask<Reply> LogUnhandled(ILogger logger, MethodBase method)
|
||||
{
|
||||
logger.LogWarning("Unhandled {Method}", method);
|
||||
return ValueTask.FromResult<Reply>(BoolReply.False);
|
||||
}
|
||||
}
|
||||
|
||||
private ValueTask<Reply> HandleInitialize(InitializeParameters parameters)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Information))
|
||||
{
|
||||
_logger.LogInformation("Handling {parameters}", parameters);
|
||||
}
|
||||
|
||||
return ValueTask.FromResult<Reply>(BoolReply.True);
|
||||
}
|
||||
|
||||
private ValueTask<Reply> HandleLookup(LookupParameters parameters)
|
||||
{
|
||||
AddressFamily? qtype = parameters.Qtype.ToUpperInvariant() switch
|
||||
{
|
||||
"ANY" => AddressFamily.Unknown,
|
||||
"A" => AddressFamily.InterNetwork,
|
||||
"AAAA" => AddressFamily.InterNetworkV6,
|
||||
_ => default(AddressFamily?)
|
||||
};
|
||||
|
||||
if (qtype is null)
|
||||
{
|
||||
_logger.LogWarning("Unhandled QType {QType}", parameters.Qtype);
|
||||
return ValueTask.FromResult<Reply>(BoolReply.False);
|
||||
}
|
||||
|
||||
if (_logger.IsEnabled(LogLevel.Information))
|
||||
{
|
||||
_logger.LogInformation("Lookup {key} in {family}", parameters.Qname, parameters.Qtype);
|
||||
}
|
||||
|
||||
return FindByName(((AddressFamily)qtype, parameters.Qname.AsMemory()), _repository, _logger);
|
||||
|
||||
static async ValueTask<Reply> FindByName((AddressFamily Family, ReadOnlyMemory<char> Qname) query, DnsRepository repository, ILogger logger)
|
||||
{
|
||||
QueryResult[] records = [];
|
||||
|
||||
var qname = query.Qname.Trim().TrimEnd(".");
|
||||
if (qname.Span.IsWhiteSpace())
|
||||
{
|
||||
goto exitEmpty;
|
||||
}
|
||||
|
||||
var results = await repository.FindAsync(record =>
|
||||
{
|
||||
if ((record.RecordType & query.Family) != record.RecordType)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return qname.Span.Equals(record.FQDN, StringComparison.OrdinalIgnoreCase);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
if (results.Count == 0)
|
||||
{
|
||||
goto exitEmpty;
|
||||
}
|
||||
|
||||
records = new QueryResult[results.Count];
|
||||
for (int i = 0; i < results.Count; i++)
|
||||
{
|
||||
DnsRecord record = results[i];
|
||||
#pragma warning disable CS8509 // RecordType is by convention InterNetwork or InterNetworkV6
|
||||
records[i] = new(record.RecordType switch
|
||||
#pragma warning restore
|
||||
{
|
||||
AddressFamily.InterNetwork => "A",
|
||||
AddressFamily.InterNetworkV6 => "AAAA"
|
||||
}, record.FQDN, record.Address.ToString(), (int)record.Lifetime.TotalSeconds);
|
||||
}
|
||||
|
||||
exitEmpty:
|
||||
return new LookupReply(records);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
Integrations/PowerDns/PowerDnsSerializerContext.cs
Normal file
13
Integrations/PowerDns/PowerDnsSerializerContext.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DotNetDDI.Integrations.PowerDns;
|
||||
|
||||
[JsonSerializable(typeof(Reply))]
|
||||
[JsonSerializable(typeof(MethodParameters))]
|
||||
[JsonSourceGenerationOptions(
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
||||
UseStringEnumConverter = true,
|
||||
WriteIndented = false
|
||||
)]
|
||||
internal partial class PowerDnsSerializerContext : JsonSerializerContext;
|
||||
39
Integrations/PowerDns/Replies.cs
Normal file
39
Integrations/PowerDns/Replies.cs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DotNetDDI.Integrations.PowerDns;
|
||||
|
||||
[JsonDerivedType(typeof(BoolReply))]
|
||||
[JsonDerivedType(typeof(LookupReply))]
|
||||
public abstract class Reply;
|
||||
|
||||
public abstract class Reply<T>(T result) : Reply
|
||||
{
|
||||
[JsonPropertyName("result")]
|
||||
public T Result => result;
|
||||
}
|
||||
|
||||
public class BoolReply(bool result) : Reply<bool>(result)
|
||||
{
|
||||
public static BoolReply False { get; } = new BoolReply(false);
|
||||
|
||||
public static BoolReply True { get; } = new BoolReply(true);
|
||||
}
|
||||
|
||||
public class LookupReply(QueryResult[] result) : Reply<QueryResult[]>(result);
|
||||
|
||||
public record QueryResult(
|
||||
[property: JsonPropertyName("qtype")] string QType,
|
||||
[property: JsonPropertyName("qname")] string QName,
|
||||
[property: JsonPropertyName("content")] string Content,
|
||||
[property: JsonPropertyName("ttl")] int TTL
|
||||
)
|
||||
{
|
||||
[JsonPropertyName("auth")]
|
||||
public bool? Auth { get; init; } = default;
|
||||
|
||||
[JsonPropertyName("domain_id")]
|
||||
public int? DomainId { get; init; } = default;
|
||||
|
||||
[JsonPropertyName("scopeMask")]
|
||||
public int? ScopeMask { get; init; } = default;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue