GoDaddy Dynamic DNS service
parent
fd2b13ea89
commit
1a509c8012
|
@ -361,3 +361,5 @@ MigrationBackup/
|
||||||
|
|
||||||
# Fody - auto-generated XML schema
|
# 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>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" 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>
|
</ItemGroup>
|
||||||
</Project>
|
</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.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace GoDaddyDDNS
|
namespace GoDaddyDDNS
|
||||||
{
|
{
|
||||||
|
@ -18,6 +15,7 @@ namespace GoDaddyDDNS
|
||||||
Host.CreateDefaultBuilder(args)
|
Host.CreateDefaultBuilder(args)
|
||||||
.ConfigureServices((hostContext, services) =>
|
.ConfigureServices((hostContext, services) =>
|
||||||
{
|
{
|
||||||
|
services.AddApplicationServices();
|
||||||
services.AddHostedService<Worker>();
|
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.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
@ -11,10 +11,17 @@ namespace GoDaddyDDNS
|
||||||
public class Worker : BackgroundService
|
public class Worker : BackgroundService
|
||||||
{
|
{
|
||||||
private readonly ILogger<Worker> _logger;
|
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)
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
@ -22,8 +29,22 @@ namespace GoDaddyDDNS
|
||||||
while (!stoppingToken.IsCancellationRequested)
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
|
_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": "*********",
|
"Domain": "*********",
|
||||||
"Records": [ "@" ],
|
"Records": [ "@" ],
|
||||||
"Key": "*********",
|
"Key": "*********",
|
||||||
"Secret": "*********"
|
"Secret": "*********",
|
||||||
|
"ExecutionTimeInSeconds": 60
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue