GoDaddy Dynamic DNS service
parent
fd2b13ea89
commit
1a509c8012
|
@ -360,4 +360,6 @@ MigrationBackup/
|
|||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
FodyWeavers.xsd
|
||||
|
||||
*.development.json
|
|
@ -0,0 +1,10 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace GoDaddyDDNS.Abstractions
|
||||
{
|
||||
public interface IDynamicDNSService
|
||||
{
|
||||
Task Initialize();
|
||||
Task Execute();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using GoDaddyDDNS.Models;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GoDaddyDDNS.Abstractions
|
||||
{
|
||||
internal interface IGoDaddyConnector
|
||||
{
|
||||
Task<DNSRecord[]> GetRecords();
|
||||
Task UpdateRecords(DNSRecord[] records);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace GoDaddyDDNS.Abstractions
|
||||
{
|
||||
internal interface IPublicIPTracker
|
||||
{
|
||||
Task<string> Get();
|
||||
}
|
||||
}
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
namespace GoDaddyDDNS.Models
|
||||
{
|
||||
internal class ErrorBody
|
||||
{
|
||||
public string Code { get; set; }
|
||||
public string Message { get; set; }
|
||||
}
|
||||
}
|
|
@ -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>();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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}");
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,5 +16,6 @@
|
|||
"Domain": "*********",
|
||||
"Records": [ "@" ],
|
||||
"Key": "*********",
|
||||
"Secret": "*********"
|
||||
"Secret": "*********",
|
||||
"ExecutionTimeInSeconds": 60
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue