GoDaddy Dynamic DNS service

master
Tudor Stanciu 2022-12-27 03:31:03 +02:00
parent fd2b13ea89
commit 1a509c8012
14 changed files with 350 additions and 11 deletions

4
.gitignore vendored
View File

@ -360,4 +360,6 @@ MigrationBackup/
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
FodyWeavers.xsd
*.development.json

View File

@ -0,0 +1,10 @@
using System.Threading.Tasks;
namespace GoDaddyDDNS.Abstractions
{
public interface IDynamicDNSService
{
Task Initialize();
Task Execute();
}
}

View File

@ -0,0 +1,11 @@
using GoDaddyDDNS.Models;
using System.Threading.Tasks;
namespace GoDaddyDDNS.Abstractions
{
internal interface IGoDaddyConnector
{
Task<DNSRecord[]> GetRecords();
Task UpdateRecords(DNSRecord[] records);
}
}

View File

@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace GoDaddyDDNS.Abstractions
{
internal interface IPublicIPTracker
{
Task<string> Get();
}
}

View File

@ -0,0 +1,16 @@
using GoDaddyDDNS.Abstractions;
using GoDaddyDDNS.Services;
using Microsoft.Extensions.DependencyInjection;
namespace GoDaddyDDNS.Extensions
{
internal static class DependencyInjectionExtensions
{
public static void AddApplicationServices(this IServiceCollection services)
{
services.AddHttpClient<IPublicIPTracker, PublicIPTracker>();
services.AddHttpClient<IGoDaddyConnector, GoDaddyConnector>();
services.AddSingleton<IDynamicDNSService, DynamicDNSService>();
}
}
}

View File

@ -6,6 +6,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,10 @@
namespace GoDaddyDDNS.Models
{
internal class DNSRecord
{
public string Name { get; set; }
public string Data { get; set; }
public int Ttl { get; set; }
public string Type { get; set; }
}
}

View File

@ -0,0 +1,8 @@
namespace GoDaddyDDNS.Models
{
internal class ErrorBody
{
public string Code { get; set; }
public string Message { get; set; }
}
}

View File

@ -1,9 +1,6 @@
using GoDaddyDDNS.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace GoDaddyDDNS
{
@ -18,6 +15,7 @@ namespace GoDaddyDDNS
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddApplicationServices();
services.AddHostedService<Worker>();
});
}

View File

@ -0,0 +1,59 @@
using GoDaddyDDNS.Abstractions;
using GoDaddyDDNS.Models;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace GoDaddyDDNS.Services
{
internal class DynamicDNSService : IDynamicDNSService
{
private DNSRecord[] DNSRecords;
private string PublicIP;
private readonly ILogger<DynamicDNSService> _logger;
private readonly IPublicIPTracker _publicIPTracker;
private readonly IGoDaddyConnector _goDaddyConnector;
public DynamicDNSService(ILogger<DynamicDNSService> logger, IPublicIPTracker publicIPTracker, IGoDaddyConnector goDaddyConnector)
{
_logger=logger;
_publicIPTracker=publicIPTracker;
_goDaddyConnector=goDaddyConnector;
}
public async Task Initialize()
{
// check records
var records = await _goDaddyConnector.GetRecords();
if (records == null || records.Length == 0)
throw new Exception("No valid record found.");
DNSRecords = records;
PublicIP = records.First().Data;
}
public async Task Execute()
{
var publicIP = await _publicIPTracker.Get();
if (PublicIP != publicIP)
{
_logger.LogInformation($"[{DateTime.Now}]: Public IP has changed from '{PublicIP}' to '{publicIP}'");
var newRecords = DNSRecords.Select(record =>
{
record.Data = publicIP;
return record;
}).ToArray();
await _goDaddyConnector.UpdateRecords(newRecords);
PublicIP = publicIP;
DNSRecords = newRecords;
}
else
{
_logger.LogInformation($"[{DateTime.Now}]: Public IP has not changed.");
}
}
}
}

View File

@ -0,0 +1,109 @@
using GoDaddyDDNS.Abstractions;
using GoDaddyDDNS.Models;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
namespace GoDaddyDDNS.Services
{
internal class GoDaddyConnector : IGoDaddyConnector
{
private const string _recordType = "A";
private const string _recordRoute = "/v1/domains/{domain}/records/{type}/{name}";
private readonly ILogger<GoDaddyConnector> _logger;
private readonly string _domain;
private readonly string[] _records;
private readonly string _apiKey;
private readonly HttpClient _httpClient;
public GoDaddyConnector(ILogger<GoDaddyConnector> logger, IConfiguration configuration, HttpClient httpClient)
{
_logger = logger;
_domain = configuration.GetValue<string>("Domain");
_records = configuration.GetSection("Records").Get<string[]>();
var key = configuration.GetValue<string>("Key");
var secret = configuration.GetValue<string>("Secret");
_apiKey = $"sso-key {key}:{secret}";
httpClient.BaseAddress = new Uri("https://api.godaddy.com");
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
httpClient.DefaultRequestHeaders.Add("Authorization", _apiKey);
_httpClient = httpClient;
}
private string GetRecordRoute(string name) => _recordRoute.Replace("{domain}", _domain).Replace("{type}", _recordType).Replace("{name}", name);
public async Task<DNSRecord[]> GetRecords()
{
var list = new List<DNSRecord>();
foreach (var name in _records)
{
var dnsRecord = await GetRecord(name);
if (dnsRecord == null)
_logger.LogWarning($"The record '{name}' was not found and will be ignored.");
else
list.Add(dnsRecord);
}
return list.ToArray();
}
private async Task<DNSRecord> GetRecord(string name)
{
var route = GetRecordRoute(name);
using (var response = await _httpClient.GetAsync(route))
{
var resultAsString = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
var result = JsonConvert.DeserializeObject<DNSRecord[]>(resultAsString);
if (result.Length > 0)
return result.First();
else
return null;
}
else
{
var error = JsonConvert.DeserializeObject<ErrorBody>(resultAsString);
throw new Exception($"{error.Code}: {error.Message}");
}
}
}
public async Task UpdateRecords(DNSRecord[] records)
{
foreach (var record in records)
await UpdateRecord(record);
}
private async Task UpdateRecord(DNSRecord record)
{
var route = GetRecordRoute(record.Name);
var data = JsonConvert.SerializeObject(new DNSRecord[] { record }, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
var content = new StringContent(data, Encoding.UTF8, "application/json");
using (var response = await _httpClient.PutAsync(route, content))
{
if (response.IsSuccessStatusCode)
return;
var resultAsString = await response.Content.ReadAsStringAsync();
var error = JsonConvert.DeserializeObject<ErrorBody>(resultAsString);
throw new Exception($"{error.Code}: {error.Message}");
};
}
}
}

View File

@ -0,0 +1,82 @@
using GoDaddyDDNS.Abstractions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
namespace GoDaddyDDNS.Services
{
internal class PublicIPTracker : IPublicIPTracker
{
private readonly HttpClient _httpClient;
private readonly ILogger<PublicIPTracker> _logger;
private readonly string[] _publicIPProviders;
public PublicIPTracker(HttpClient httpClient, ILogger<PublicIPTracker> logger, IConfiguration configuration)
{
_httpClient=httpClient;
_logger=logger;
_publicIPProviders = configuration.GetSection("PublicIPProviders").Get<string[]>();
}
public async Task<string> Get()
{
if (_publicIPProviders == null || _publicIPProviders.Length == 0)
throw new Exception("No public IP provider is configured.");
foreach (var provider in _publicIPProviders)
{
var ip = await GetIP(provider);
if (ip != null)
return ip;
}
throw new Exception("The public IP could not be obtained from any of the configured providers.");
}
private async Task<string> GetIP(string providerUrl)
{
string ip = null;
using (var response = await _httpClient.GetAsync(providerUrl))
{
if (response.IsSuccessStatusCode)
ip = await response.Content.ReadAsStringAsync();
else
return null;
}
var valid = ValidateIP(ip);
if (valid)
return ip;
return null;
}
private bool ValidateIP(string ip)
{
var valid = IPAddress.TryParse(ip, out var address);
if (!valid)
{
_logger.LogWarning($"'{ip}' is not a valid IP adress.");
return false;
}
switch (address.AddressFamily)
{
case System.Net.Sockets.AddressFamily.InterNetwork:
return true;
case System.Net.Sockets.AddressFamily.InterNetworkV6:
_logger.LogError($"IPv6 is not implemented: '{ip}'");
return false;
default:
_logger.LogError($"IP '{ip}' is valid but isn't IPv4 or IPv6.");
return false;
}
}
}
}

View File

@ -1,8 +1,8 @@
using GoDaddyDDNS.Abstractions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -11,10 +11,17 @@ namespace GoDaddyDDNS
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly IDynamicDNSService _dynamicDNSService;
private readonly int _delay;
public Worker(ILogger<Worker> logger)
public Worker(ILogger<Worker> logger, IDynamicDNSService dynamicDNSService, IConfiguration configuration)
{
_logger = logger;
_logger=logger;
_dynamicDNSService=dynamicDNSService;
var delayInSeconds = configuration.GetValue<int>("ExecutionTimeInSeconds");
_delay = Convert.ToInt32(TimeSpan.FromSeconds(delayInSeconds).TotalMilliseconds);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
@ -22,8 +29,22 @@ namespace GoDaddyDDNS
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
await Task.Delay(1000, stoppingToken);
await _dynamicDNSService.Execute();
await Task.Delay(_delay, stoppingToken);
}
}
public override async Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Starting worker...");
await _dynamicDNSService.Initialize();
await base.StartAsync(cancellationToken);
}
public override Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Stopping worker...");
return base.StopAsync(cancellationToken);
}
}
}

View File

@ -16,5 +16,6 @@
"Domain": "*********",
"Records": [ "@" ],
"Key": "*********",
"Secret": "*********"
"Secret": "*********",
"ExecutionTimeInSeconds": 60
}