From 1a509c80129513eab4784318cc53f2124f431a44 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Tue, 27 Dec 2022 03:31:03 +0200 Subject: [PATCH] GoDaddy Dynamic DNS service --- .gitignore | 4 +- .../Abstractions/IDynamicDNSService.cs | 10 ++ .../Abstractions/IGoDaddyConnector.cs | 11 ++ .../Abstractions/IPublicIPTracker.cs | 9 ++ .../DependencyInjectionExtensions.cs | 16 +++ src/GoDaddyDDNS/GoDaddyDDNS.csproj | 3 + src/GoDaddyDDNS/Models/DNSRecord.cs | 10 ++ src/GoDaddyDDNS/Models/ErrorBody.cs | 8 ++ src/GoDaddyDDNS/Program.cs | 6 +- src/GoDaddyDDNS/Services/DynamicDNSService.cs | 59 ++++++++++ src/GoDaddyDDNS/Services/GoDaddyConnector.cs | 109 ++++++++++++++++++ src/GoDaddyDDNS/Services/PublicIPTracker.cs | 82 +++++++++++++ src/GoDaddyDDNS/Worker.cs | 31 ++++- src/GoDaddyDDNS/appsettings.json | 3 +- 14 files changed, 350 insertions(+), 11 deletions(-) create mode 100644 src/GoDaddyDDNS/Abstractions/IDynamicDNSService.cs create mode 100644 src/GoDaddyDDNS/Abstractions/IGoDaddyConnector.cs create mode 100644 src/GoDaddyDDNS/Abstractions/IPublicIPTracker.cs create mode 100644 src/GoDaddyDDNS/Extensions/DependencyInjectionExtensions.cs create mode 100644 src/GoDaddyDDNS/Models/DNSRecord.cs create mode 100644 src/GoDaddyDDNS/Models/ErrorBody.cs create mode 100644 src/GoDaddyDDNS/Services/DynamicDNSService.cs create mode 100644 src/GoDaddyDDNS/Services/GoDaddyConnector.cs create mode 100644 src/GoDaddyDDNS/Services/PublicIPTracker.cs diff --git a/.gitignore b/.gitignore index 9491a2f..1793a90 100644 --- a/.gitignore +++ b/.gitignore @@ -360,4 +360,6 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd + +*.development.json \ No newline at end of file diff --git a/src/GoDaddyDDNS/Abstractions/IDynamicDNSService.cs b/src/GoDaddyDDNS/Abstractions/IDynamicDNSService.cs new file mode 100644 index 0000000..400550d --- /dev/null +++ b/src/GoDaddyDDNS/Abstractions/IDynamicDNSService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace GoDaddyDDNS.Abstractions +{ + public interface IDynamicDNSService + { + Task Initialize(); + Task Execute(); + } +} diff --git a/src/GoDaddyDDNS/Abstractions/IGoDaddyConnector.cs b/src/GoDaddyDDNS/Abstractions/IGoDaddyConnector.cs new file mode 100644 index 0000000..9a99d3e --- /dev/null +++ b/src/GoDaddyDDNS/Abstractions/IGoDaddyConnector.cs @@ -0,0 +1,11 @@ +using GoDaddyDDNS.Models; +using System.Threading.Tasks; + +namespace GoDaddyDDNS.Abstractions +{ + internal interface IGoDaddyConnector + { + Task GetRecords(); + Task UpdateRecords(DNSRecord[] records); + } +} diff --git a/src/GoDaddyDDNS/Abstractions/IPublicIPTracker.cs b/src/GoDaddyDDNS/Abstractions/IPublicIPTracker.cs new file mode 100644 index 0000000..82d477f --- /dev/null +++ b/src/GoDaddyDDNS/Abstractions/IPublicIPTracker.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace GoDaddyDDNS.Abstractions +{ + internal interface IPublicIPTracker + { + Task Get(); + } +} diff --git a/src/GoDaddyDDNS/Extensions/DependencyInjectionExtensions.cs b/src/GoDaddyDDNS/Extensions/DependencyInjectionExtensions.cs new file mode 100644 index 0000000..061cb7e --- /dev/null +++ b/src/GoDaddyDDNS/Extensions/DependencyInjectionExtensions.cs @@ -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(); + services.AddHttpClient(); + services.AddSingleton(); + } + } +} diff --git a/src/GoDaddyDDNS/GoDaddyDDNS.csproj b/src/GoDaddyDDNS/GoDaddyDDNS.csproj index c52d18c..135589e 100644 --- a/src/GoDaddyDDNS/GoDaddyDDNS.csproj +++ b/src/GoDaddyDDNS/GoDaddyDDNS.csproj @@ -6,6 +6,9 @@ + + + diff --git a/src/GoDaddyDDNS/Models/DNSRecord.cs b/src/GoDaddyDDNS/Models/DNSRecord.cs new file mode 100644 index 0000000..f5939ad --- /dev/null +++ b/src/GoDaddyDDNS/Models/DNSRecord.cs @@ -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; } + } +} diff --git a/src/GoDaddyDDNS/Models/ErrorBody.cs b/src/GoDaddyDDNS/Models/ErrorBody.cs new file mode 100644 index 0000000..fc4d6a4 --- /dev/null +++ b/src/GoDaddyDDNS/Models/ErrorBody.cs @@ -0,0 +1,8 @@ +namespace GoDaddyDDNS.Models +{ + internal class ErrorBody + { + public string Code { get; set; } + public string Message { get; set; } + } +} diff --git a/src/GoDaddyDDNS/Program.cs b/src/GoDaddyDDNS/Program.cs index c5c50ec..a3ae4da 100644 --- a/src/GoDaddyDDNS/Program.cs +++ b/src/GoDaddyDDNS/Program.cs @@ -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(); }); } diff --git a/src/GoDaddyDDNS/Services/DynamicDNSService.cs b/src/GoDaddyDDNS/Services/DynamicDNSService.cs new file mode 100644 index 0000000..dbb13fb --- /dev/null +++ b/src/GoDaddyDDNS/Services/DynamicDNSService.cs @@ -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 _logger; + private readonly IPublicIPTracker _publicIPTracker; + private readonly IGoDaddyConnector _goDaddyConnector; + + public DynamicDNSService(ILogger 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."); + } + } + } +} diff --git a/src/GoDaddyDDNS/Services/GoDaddyConnector.cs b/src/GoDaddyDDNS/Services/GoDaddyConnector.cs new file mode 100644 index 0000000..2029989 --- /dev/null +++ b/src/GoDaddyDDNS/Services/GoDaddyConnector.cs @@ -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 _logger; + private readonly string _domain; + private readonly string[] _records; + private readonly string _apiKey; + private readonly HttpClient _httpClient; + + public GoDaddyConnector(ILogger logger, IConfiguration configuration, HttpClient httpClient) + { + _logger = logger; + _domain = configuration.GetValue("Domain"); + _records = configuration.GetSection("Records").Get(); + + var key = configuration.GetValue("Key"); + var secret = configuration.GetValue("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 GetRecords() + { + var list = new List(); + 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 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(resultAsString); + if (result.Length > 0) + return result.First(); + else + return null; + } + else + { + var error = JsonConvert.DeserializeObject(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(resultAsString); + throw new Exception($"{error.Code}: {error.Message}"); + }; + } + } +} diff --git a/src/GoDaddyDDNS/Services/PublicIPTracker.cs b/src/GoDaddyDDNS/Services/PublicIPTracker.cs new file mode 100644 index 0000000..6bc57a6 --- /dev/null +++ b/src/GoDaddyDDNS/Services/PublicIPTracker.cs @@ -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 _logger; + private readonly string[] _publicIPProviders; + + public PublicIPTracker(HttpClient httpClient, ILogger logger, IConfiguration configuration) + { + _httpClient=httpClient; + _logger=logger; + _publicIPProviders = configuration.GetSection("PublicIPProviders").Get(); + } + + public async Task 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 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; + } + + } + } +} diff --git a/src/GoDaddyDDNS/Worker.cs b/src/GoDaddyDDNS/Worker.cs index 916db0e..5c0728d 100644 --- a/src/GoDaddyDDNS/Worker.cs +++ b/src/GoDaddyDDNS/Worker.cs @@ -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 _logger; + private readonly IDynamicDNSService _dynamicDNSService; + private readonly int _delay; - public Worker(ILogger logger) + + public Worker(ILogger logger, IDynamicDNSService dynamicDNSService, IConfiguration configuration) { - _logger = logger; + _logger=logger; + _dynamicDNSService=dynamicDNSService; + + var delayInSeconds = configuration.GetValue("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); + } } } diff --git a/src/GoDaddyDDNS/appsettings.json b/src/GoDaddyDDNS/appsettings.json index 475387e..6881c7f 100644 --- a/src/GoDaddyDDNS/appsettings.json +++ b/src/GoDaddyDDNS/appsettings.json @@ -16,5 +16,6 @@ "Domain": "*********", "Records": [ "@" ], "Key": "*********", - "Secret": "*********" + "Secret": "*********", + "ExecutionTimeInSeconds": 60 }