Migrate existing code
This commit is contained in:
parent
384ff4a6f3
commit
9b49f880a2
30 changed files with 1789 additions and 5 deletions
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