1
0
Fork 0

Migrate existing code

This commit is contained in:
Jöran Malek 2025-01-29 23:53:57 +01:00
parent 384ff4a6f3
commit 9b49f880a2
30 changed files with 1789 additions and 5 deletions

View 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);
}

View 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);
}

View 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;
}
}
}

View 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;
}
}

View 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;
}
}
}

View 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);
}
}

View 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();
}
}
}

View 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());
}
}

View 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]);
}
}
}

View 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);
}
}

View file

@ -0,0 +1,9 @@
namespace DotNetDDI.Integrations.Kea;
public enum LeaseType : byte
{
NonTempraryIPv6 = 0,
TemporaryIPv6 = 1,
IPv6Prefix = 2,
IPv4 = 3
}

View 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;

View 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;

View 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);
}
}
}

View 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;

View 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;
}