#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Vanara.Extensions;
using Vanara.InteropServices;
using Vanara.PInvoke;
using static Vanara.PInvoke.DnsApi;
namespace Vanara.Net;
/// Represents a DNS service.
///
public class DnsService : IDisposable
{
private readonly IntPtr ctx;
private readonly AutoResetEvent evt = new(false);
private readonly SafePDNS_SERVICE_INSTANCE pSvcInst;
private bool disposed = false, registering = false;
private Win32Error err = 0;
private DNS_SERVICE_REGISTER_REQUEST req;
/// Initializes a new instance of the class.
/// The name of the service.
/// The name of the host of the service.
/// The port.
/// The service priority.
/// The service weight.
/// The service-associated address and port.
/// A dictionary of property keys and values.
public DnsService(string serviceName, string hostName, ushort port, [Optional] ushort priority,
[Optional] ushort weight, [Optional] IPAddress? address, [Optional] IDictionary? properties)
{
using SafeCoTaskMemHandle v4 = address is null ? SafeCoTaskMemHandle.Null : new(address.MapToIPv4().GetAddressBytes());
using SafeCoTaskMemHandle v6 = address is null ? SafeCoTaskMemHandle.Null : new(address.MapToIPv6().GetAddressBytes());
pSvcInst = DnsServiceConstructInstance(serviceName, hostName, v4, v6, port,
priority, weight, (uint)(properties?.Count ?? 0), properties?.Keys.ToArray(), properties?.Values.ToArray());
ctx = (IntPtr)GCHandle.Alloc(this, GCHandleType.Normal);
}
internal DnsService(IntPtr pInst)
{
pSvcInst = new SafePDNS_SERVICE_INSTANCE(pInst);
ctx = (IntPtr)GCHandle.Alloc(this, GCHandleType.Normal);
}
/// A string that represents the name of the host of the service.
public string HostName => pSvcInst.pszHostName;
///
/// A string that represents the service name. This is a fully qualified domain name that begins with a service name, and ends with
/// ".local". It takes the generalized form "<ServiceName>._<ServiceType>._<TransportProtocol>.local". For example, "MyMusicServer._http._tcp.local".
///
public string InstanceName => pSvcInst.pszInstanceName;
/// A value that contains the interface index on which the service was discovered.
public uint InterfaceIndex => pSvcInst.dwInterfaceIndex;
/// The service-associated IPv4 address, if defined.
public IPAddress? Ip4Address => pSvcInst.ip4Address?.Address.MapToIPv4();
/// The service-associated IPv6 address, if defined.
public IPAddress? Ip6Address => pSvcInst.ip6Address?.Address.MapToIPv6();
/// Gets a value indicating whether this instance is registered.
/// if this instance is registered; otherwise, .
public bool IsRegistered => req.Version != 0;
/// A value that represents the port on which the service is running.
public ushort Port => pSvcInst.wPort;
/// A value that represents the service priority.
public ushort Priority => pSvcInst.wPriority;
/// The DNS service properties.
public IReadOnlyDictionary Properties => pSvcInst.properties;
/// A value that represents the service weight.
public ushort Weight => pSvcInst.wWeight;
/// Used to obtain more information about a service advertised on the local network.
///
/// The service name. This is a fully qualified domain name that begins with a service name, and ends with ".local". It takes the
/// generalized form "<ServiceName>._<ServiceType>._<TransportProtocol>.local". For example, "MyMusicServer._http._tcp.local".
///
/// The interface over which the query is sent. If , then all interfaces will be considered.
/// A cancellation token that can be used to cancel a pending asynchronous resolve operation.
/// If successful, returns a new instance; otherwise, throws the appropriate DNS-specific exception.
/// This function is asynchronous.
public static async Task ResolveAsync(string serviceName, NetworkInterface? adapter = null, CancellationToken cancellationToken = default)
{
using ManualResetEvent evt = new(false);
Win32Error err = 0;
IntPtr result = default;
DNS_SERVICE_RESOLVE_REQUEST res = new()
{
Version = DNS_QUERY_REQUEST_VERSION1,
InterfaceIndex = (uint)(adapter?.GetIPProperties().GetIPv4Properties().Index ?? 0),
QueryName = serviceName,
pResolveCompletionCallback = ResolveCallback,
};
DnsServiceResolve(res, out DNS_SERVICE_CANCEL c).ThrowUnless(Win32Error.DNS_REQUEST_PENDING);
await Task.Run(() =>
{
if (WaitHandle.WaitAny(new[] { cancellationToken.WaitHandle, evt }) == 0)
DnsServiceResolveCancel(c);
});
return result != IntPtr.Zero ? new DnsService(result) : throw err.GetException();
void ResolveCallback(Win32Error Status, IntPtr pQueryContext, IntPtr pInstance)
{
if ((err = Status).Succeeded)
result = pInstance;
evt.Set();
}
}
/// Used to remove a registered service.
/// If not successful, throws an appropriate DNS-specific exception.
/// This function is asynchronous.
public async Task DeRegisterAsync()
{
if (!IsRegistered)
return;
if (registering)
throw new InvalidOperationException("Service is already being deregistered.");
registering = true;
DnsServiceDeRegister(req, IntPtr.Zero);
await Task.Run(() => evt.WaitOne());
registering = false;
req = default;
err.ThrowIfFailed();
}
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
public void Dispose()
{
if (disposed)
return;
disposed = true;
pSvcInst?.Dispose();
if (ctx != IntPtr.Zero)
GCHandle.FromIntPtr(ctx).Free();
}
/// Used to register a discoverable service on this device.
///
/// if the DNS protocol should be used to advertise the service; if the mDNS protocol
/// should be used.
///
///
/// An optional value that contains the network interface over which the service is to be advertised. If , then all
/// interfaces will be considered.
///
/// A cancellation token that can be used to cancel the asynchonous operation.
/// Service is already registered.
///
/// This function is asynchronous. To deregister the service, call DnsServiceDeRegister. The registration is tied to the lifetime of the
/// calling process. If the process goes away, the service will be automatically deregistered.
///
public async Task RegisterAsync(bool unicastEnabled = false, NetworkInterface? adapter = null, CancellationToken cancellationToken = default)
{
if (IsRegistered)
throw new InvalidOperationException("Service is already registered.");
if (registering)
throw new InvalidOperationException("Service is already being registered.");
req = new DNS_SERVICE_REGISTER_REQUEST()
{
Version = DNS_QUERY_REQUEST_VERSION1,
InterfaceIndex = (uint)(adapter?.GetIPProperties().GetIPv4Properties().Index ?? 0),
pServiceInstance = pSvcInst,
pQueryContext = ctx,
pRegisterCompletionCallback = RegCallback,
unicastEnabled = unicastEnabled
};
registering = true;
DnsServiceRegister(req, out DNS_SERVICE_CANCEL svcCancel).ThrowUnless(Win32Error.DNS_REQUEST_PENDING);
await Task.Run(() =>
{
if (WaitHandle.WaitAny(new[] { cancellationToken.WaitHandle, evt }) == 0)
DnsServiceResolveCancel(svcCancel);
});
registering = false;
err.ThrowIfFailed();
}
private static void RegCallback(Win32Error Status, IntPtr pQueryContext, IntPtr pInstance)
{
DnsService svc = (DnsService)GCHandle.FromIntPtr(pQueryContext).Target;
using SafePDNS_SERVICE_INSTANCE i = new(pInstance);
svc.err = Status;
svc.evt.Set();
}
}