Skip to content

Commit

Permalink
Use Unfucked shared libraries after extracting a lot of logic to them.
Browse files Browse the repository at this point in the history
  • Loading branch information
Aldaviva committed Oct 9, 2024
1 parent 75eb691 commit f584fbc
Show file tree
Hide file tree
Showing 18 changed files with 499 additions and 1,688 deletions.
170 changes: 85 additions & 85 deletions GandiDynamicDns/DynamicDnsService.cs
Original file line number Diff line number Diff line change
@@ -1,86 +1,86 @@
using GandiDynamicDns.Net.Dns;
using GandiDynamicDns.Net.Stun;
using GandiDynamicDns.Unfucked.Tasks;
using Microsoft.Extensions.Options;
using System.Diagnostics;
using System.Net;

namespace GandiDynamicDns;

public interface DynamicDnsService: IDisposable {

IPAddress? selfWanAddress { get; }

}

public class DynamicDnsServiceImpl(DnsManager dns, SelfWanAddressClient stun, IOptions<Configuration> configuration, ILogger<DynamicDnsServiceImpl> logger, IHostApplicationLifetime lifetime)
: BackgroundService, DynamicDnsService {

private const string DNS_A_RECORD = "A";

public IPAddress? selfWanAddress { get; private set; }

private readonly EventLog? eventLog =
#if WINDOWS
new("Application") { Source = "GandiDynamicDns" };
#else
null;
#endif

protected override async Task ExecuteAsync(CancellationToken ct) {
if ((await dns.fetchDnsRecords(configuration.Value.subdomain, configuration.Value.domain, DnsRecordType.A, ct)).FirstOrDefault() is { } existingIpAddress) {
try {
selfWanAddress = IPAddress.Parse(existingIpAddress);
} catch (FormatException) { }
}
logger.LogInformation("On startup, the {fqdn} DNS A record was pointing to {address}", configuration.Value.fqdn, selfWanAddress?.ToString() ?? "(nothing)");

while (!ct.IsCancellationRequested) {
await updateDnsRecordIfNecessary(ct);

if (configuration.Value.updateInterval > TimeSpan.Zero) {
await Task2.Delay(configuration.Value.updateInterval, ct);
} else {
lifetime.StopApplication();
break;
}
}
}

private async Task updateDnsRecordIfNecessary(CancellationToken ct = default) {
SelfWanAddressResponse stunResponse = await stun.getSelfWanAddress(ct);
if (stunResponse.selfWanAddress != null && !stunResponse.selfWanAddress.Equals(selfWanAddress)) {
logger.LogInformation("This computer's public IP address changed from {old} to {new} according to {server} ({serverAddr}), updating {fqdn} A record in DNS server", selfWanAddress,
stunResponse.selfWanAddress, stunResponse.server.Host, stunResponse.serverAddress.ToString(), configuration.Value.fqdn);
#if WINDOWS
eventLog?.WriteEntry(
$"This computer's public IP address changed from {selfWanAddress} to {stunResponse.selfWanAddress}, according to {stunResponse.server.Host} ({stunResponse.serverAddress}), updating {configuration.Value.fqdn} A record in DNS server",
EventLogEntryType.Information, 1);
#endif

selfWanAddress = stunResponse.selfWanAddress;
await updateDnsRecord(stunResponse.selfWanAddress, ct);
} else {
logger.LogDebug("Not updating DNS {type} record for {fqdn} because it is already set to {value}", DNS_A_RECORD, configuration.Value.fqdn, selfWanAddress);
}
}

private async Task updateDnsRecord(IPAddress currentIPAddress, CancellationToken ct = default) {
if (!configuration.Value.dryRun) {
await dns.setDnsRecord(configuration.Value.subdomain, configuration.Value.domain, DnsRecordType.A, configuration.Value.dnsRecordTimeToLive, [currentIPAddress.ToString()], ct);
}
}

protected virtual void Dispose(bool disposing) {
if (disposing) {
eventLog?.Dispose();
}
}

public sealed override void Dispose() {
Dispose(true);
base.Dispose();
GC.SuppressFinalize(this);
}

using GandiDynamicDns.Net.Dns;
using Microsoft.Extensions.Options;
using System.Diagnostics;
using System.Net;
using Unfucked;
using Unfucked.STUN;

namespace GandiDynamicDns;

public interface DynamicDnsService: IDisposable {

IPAddress? selfWanAddress { get; }

}

public class DynamicDnsServiceImpl(DnsManager dns, ISelfWanAddressClient stun, IOptions<Configuration> configuration, ILogger<DynamicDnsServiceImpl> logger, IHostApplicationLifetime lifetime)
: BackgroundService, DynamicDnsService {

private const string DNS_A_RECORD = "A";

public IPAddress? selfWanAddress { get; private set; }

private readonly EventLog? eventLog =
#if WINDOWS
new("Application") { Source = "GandiDynamicDns" };
#else
null;
#endif

protected override async Task ExecuteAsync(CancellationToken ct) {
if ((await dns.fetchDnsRecords(configuration.Value.subdomain, configuration.Value.domain, DnsRecordType.A, ct)).FirstOrDefault() is { } existingIpAddress) {
try {
selfWanAddress = IPAddress.Parse(existingIpAddress);
} catch (FormatException) { }
}
logger.LogInformation("On startup, the {fqdn} DNS A record was pointing to {address}", configuration.Value.fqdn, selfWanAddress?.ToString() ?? "(nothing)");

while (!ct.IsCancellationRequested) {
await updateDnsRecordIfNecessary(ct);

if (configuration.Value.updateInterval > TimeSpan.Zero) {
await Tasks.Delay(configuration.Value.updateInterval, ct);
} else {
lifetime.StopApplication();
break;
}
}
}

private async Task updateDnsRecordIfNecessary(CancellationToken ct = default) {
SelfWanAddressResponse stunResponse = await stun.GetSelfWanAddress(ct);
if (stunResponse.SelfWanAddress != null && !stunResponse.SelfWanAddress.Equals(selfWanAddress)) {
logger.LogInformation("This computer's public IP address changed from {old} to {new} according to {server} ({serverAddr}), updating {fqdn} A record in DNS server", selfWanAddress,
stunResponse.SelfWanAddress, stunResponse.Server.Host, stunResponse.ServerAddress.ToString(), configuration.Value.fqdn);
#if WINDOWS
eventLog?.WriteEntry(
$"This computer's public IP address changed from {selfWanAddress} to {stunResponse.SelfWanAddress}, according to {stunResponse.Server.Host} ({stunResponse.ServerAddress}), updating {configuration.Value.fqdn} A record in DNS server",
EventLogEntryType.Information, 1);
#endif

selfWanAddress = stunResponse.SelfWanAddress;
await updateDnsRecord(stunResponse.SelfWanAddress, ct);
} else {
logger.LogDebug("Not updating DNS {type} record for {fqdn} because it is already set to {value}", DNS_A_RECORD, configuration.Value.fqdn, selfWanAddress);
}
}

private async Task updateDnsRecord(IPAddress currentIPAddress, CancellationToken ct = default) {
if (!configuration.Value.dryRun) {
await dns.setDnsRecord(configuration.Value.subdomain, configuration.Value.domain, DnsRecordType.A, configuration.Value.dnsRecordTimeToLive, [currentIPAddress.ToString()], ct);
}
}

protected virtual void Dispose(bool disposing) {
if (disposing) {
eventLog?.Dispose();
}
}

public sealed override void Dispose() {
Dispose(true);
base.Dispose();
GC.SuppressFinalize(this);
}

}
119 changes: 68 additions & 51 deletions GandiDynamicDns/GandiDynamicDns.csproj
Original file line number Diff line number Diff line change
@@ -1,52 +1,69 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net8.0;net8.0-windows</TargetFrameworks>
<RuntimeIdentifiers>win-x64;win-arm64;linux-x64;linux-arm;linux-arm64</RuntimeIdentifiers>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RollForward>latestMajor</RollForward>
<LangVersion>latest</LangVersion>
<NoWarn>$(NoWarn);8524;VSTHRD200</NoWarn>
<Version>0.2.0</Version>
<Authors>Ben Hutchison</Authors>
<Copyright>© 2024 $(Authors)</Copyright>
<Company>$(Authors)</Company>
<AssemblyTitle>Gandi Dynamic DNS</AssemblyTitle>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<ApplicationIcon>favicon.ico</ApplicationIcon>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>

<ItemGroup>
<AdditionalFiles Include="ExceptionAdjustments.txt" />
<Content Include="favicon.ico" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="G6.GandiLiveDns" Version="1.0.0" />
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="8.0.0" />
<PackageReference Include="Stun.Net" Version="8.0.2" />
<PackageReference Include="System.Runtime.Caching" Version="8.0.0" />
</ItemGroup>

<ItemGroup Condition="$(RuntimeIdentifier.StartsWith('win'))">
<None Update="Install service.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

<ItemGroup Condition="$(RuntimeIdentifier.StartsWith('linux'))">
<None Update="gandidynamicdns.service">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="Tests" />
</ItemGroup>

<Project Sdk="Microsoft.NET.Sdk.Worker">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net8.0;net8.0-windows</TargetFrameworks>
<RuntimeIdentifiers>win-x64;win-arm64;linux-x64;linux-arm;linux-arm64</RuntimeIdentifiers>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RollForward>latestMajor</RollForward>
<LangVersion>latest</LangVersion>
<NoWarn>$(NoWarn);8524;VSTHRD200</NoWarn>
<Version>0.2.1</Version>
<Authors>Ben Hutchison</Authors>
<Copyright>© 2024 $(Authors)</Copyright>
<Company>$(Authors)</Company>
<AssemblyTitle>Gandi Dynamic DNS</AssemblyTitle>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<ApplicationIcon>favicon.ico</ApplicationIcon>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>

<ItemGroup>
<AdditionalFiles Include="ExceptionAdjustments.txt" />
<Content Include="favicon.ico" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="G6.GandiLiveDns" Version="1.0.0" />
<PackageReference Include="JetBrains.Annotations" Version="2024.2.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="8.0.1" />
<PackageReference Include="Stun.Net" Version="8.0.2" />
</ItemGroup>

<ItemGroup Condition="$(RuntimeIdentifier.StartsWith('win'))">
<None Update="Install service.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

<ItemGroup Condition="$(RuntimeIdentifier.StartsWith('linux'))">
<None Update="gandidynamicdns.service">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="Tests" />
</ItemGroup>

<ItemGroup>
<Reference Include="Unfucked">
<HintPath>..\..\Unfucked\Unfucked\bin\Debug\net8.0\Unfucked.dll</HintPath>
</Reference>
<Reference Include="Unfucked.Caching">
<HintPath>..\..\Unfucked\Caching\bin\Debug\netstandard2.0\Unfucked.Caching.dll</HintPath>
</Reference>
<Reference Include="Unfucked.DI">
<HintPath>..\..\Unfucked\DI\bin\Debug\net6.0\Unfucked.DI.dll</HintPath>
</Reference>
<Reference Include="Unfucked.DNS">
<HintPath>..\..\Unfucked\DNS\bin\Debug\net6.0\Unfucked.DNS.dll</HintPath>
</Reference>
<Reference Include="Unfucked.STUN">
<HintPath>..\..\Unfucked\STUN\bin\Debug\net8.0\Unfucked.STUN.dll</HintPath>
</Reference>
</ItemGroup>

</Project>
67 changes: 37 additions & 30 deletions GandiDynamicDns/Net/Dns/GandiDnsManager.cs
Original file line number Diff line number Diff line change
@@ -1,31 +1,38 @@
using GandiDynamicDns.Unfucked.Dns;

namespace GandiDynamicDns.Net.Dns;

public interface DnsManager {

Task<IEnumerable<string>> fetchDnsRecords(string subdomain, string domain, DnsRecordType type = DnsRecordType.A, CancellationToken ct = default);

Task setDnsRecord(string subdomain, string domain, DnsRecordType type, TimeSpan timeToLive, IEnumerable<string> values, CancellationToken ct = default);

}

public class GandiDnsManager(IGandiLiveDns gandi): DnsManager {

public static readonly TimeSpan MINIMUM_TIME_TO_LIVE = TimeSpan.FromSeconds(300); // 5 minutes
private static readonly TimeSpan MAXIMUM_TIME_TO_LIVE = TimeSpan.FromSeconds(2_592_000); // 30 days

public async Task<IEnumerable<string>> fetchDnsRecords(string subdomain, string domain, DnsRecordType type = DnsRecordType.A, CancellationToken ct = default) =>
from record in await gandi.GetDomainRecords(domain, ct)
where record.rrset_type == type.ToString() && record.rrset_name == subdomain && record.rrset_values.LongLength != 0
select record.rrset_values[0];

public async Task setDnsRecord(string subdomain, string domain, DnsRecordType type, TimeSpan timeToLive, IEnumerable<string> values, CancellationToken ct = default) =>
await gandi.PutDomainRecord(domain: domain,
name: subdomain,
type: type.ToString(),
values: values.ToArray(),
ttl: (int) (timeToLive > MINIMUM_TIME_TO_LIVE ? timeToLive < MAXIMUM_TIME_TO_LIVE ? timeToLive : MAXIMUM_TIME_TO_LIVE : MINIMUM_TIME_TO_LIVE).TotalSeconds,
cancellationToken: ct);

using Unfucked.DNS;

namespace GandiDynamicDns.Net.Dns;

public interface DnsManager {

Task<IEnumerable<string>> fetchDnsRecords(string subdomain, string domain, DnsRecordType type = DnsRecordType.A, CancellationToken ct = default);

Task setDnsRecord(string subdomain, string domain, DnsRecordType type, TimeSpan timeToLive, IEnumerable<string> values, CancellationToken ct = default);

}

public class GandiDnsManager(IGandiLiveDns gandi): DnsManager {

/// <summary>
/// 5 minutes
/// </summary>
public static readonly TimeSpan MINIMUM_TIME_TO_LIVE = TimeSpan.FromSeconds(300);

/// <summary>
/// 30 days
/// </summary>
private static readonly TimeSpan MAXIMUM_TIME_TO_LIVE = TimeSpan.FromSeconds(2_592_000);

public async Task<IEnumerable<string>> fetchDnsRecords(string subdomain, string domain, DnsRecordType type = DnsRecordType.A, CancellationToken ct = default) =>
from record in await gandi.GetDomainRecords(domain, ct)
where record.rrset_type == type.ToString() && record.rrset_name == subdomain && record.rrset_values.LongLength != 0
select record.rrset_values[0];

public async Task setDnsRecord(string subdomain, string domain, DnsRecordType type, TimeSpan timeToLive, IEnumerable<string> values, CancellationToken ct = default) =>
await gandi.PutDomainRecord(domain: domain,
name: subdomain,
type: type.ToString(),
values: values.ToArray(),
ttl: (int) (timeToLive > MINIMUM_TIME_TO_LIVE ? timeToLive < MAXIMUM_TIME_TO_LIVE ? timeToLive : MAXIMUM_TIME_TO_LIVE : MINIMUM_TIME_TO_LIVE).TotalSeconds,
cancellationToken: ct);

}
Loading

0 comments on commit f584fbc

Please sign in to comment.