From ace25353adf34df005b380b37f6958d2c0c71196 Mon Sep 17 00:00:00 2001 From: Mubarak Imam Date: Sun, 27 Jun 2021 17:33:21 +0100 Subject: [PATCH 1/2] TDDNS-001: added support for namecheap --- .gitignore | 11 +++ .vscode/launch.json | 29 ++++++ .vscode/tasks.json | 42 +++++++++ TinyDdns.sln | 18 ++++ src/Contracts/BaseDdnsClient.cs | 74 +++++++++++++++ src/Contracts/IDdnsClient.cs | 18 ++++ src/DdnsOptions.cs | 33 +++++++ src/DdnsProviders.cs | 14 +++ src/DdnsWorker.cs | 92 +++++++++++++++++++ .../Namecheap/NamecheapClient.cs | 48 ++++++++++ .../Namecheap/NamecheapOptions.cs | 19 ++++ .../Namecheap/ServiceCollectionExtensions.cs | 25 +++++ src/Program.cs | 38 ++++++++ src/TinyDdns.csproj | 12 +++ tests/TinyDdns.Tests.csproj | 22 +++++ tests/UnitTest1.cs | 14 +++ 16 files changed, 509 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 TinyDdns.sln create mode 100644 src/Contracts/BaseDdnsClient.cs create mode 100644 src/Contracts/IDdnsClient.cs create mode 100644 src/DdnsOptions.cs create mode 100644 src/DdnsProviders.cs create mode 100644 src/DdnsWorker.cs create mode 100644 src/Implementations/Namecheap/NamecheapClient.cs create mode 100644 src/Implementations/Namecheap/NamecheapOptions.cs create mode 100644 src/Implementations/Namecheap/ServiceCollectionExtensions.cs create mode 100644 src/Program.cs create mode 100644 src/TinyDdns.csproj create mode 100644 tests/TinyDdns.Tests.csproj create mode 100644 tests/UnitTest1.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a3aca06 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ +**/bin/ +**/obj/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..cf9fb90 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,29 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/src/bin/Debug/net5.0/TinyDdns.dll", + "args": [ + "--DdnsOptions:Provider", "Namecheap", + "--DdnsOptions:Domains", "lab.barakimam.me,*.lab.barakimam.me", + "--NamecheapOptions:Domain", "barakimam.me", + "--NamecheapOptions:Password", "hello" + ], + "cwd": "${workspaceFolder}/src", + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..33f4e3d --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/src/TinyDdns.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/src/TinyDdns.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "${workspaceFolder}/src/TinyDdns.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/TinyDdns.sln b/TinyDdns.sln new file mode 100644 index 0000000..8bb531e --- /dev/null +++ b/TinyDdns.sln @@ -0,0 +1,18 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/src/Contracts/BaseDdnsClient.cs b/src/Contracts/BaseDdnsClient.cs new file mode 100644 index 0000000..4bb1554 --- /dev/null +++ b/src/Contracts/BaseDdnsClient.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace TinyDdns.Contracts +{ + /// + /// base DDNS client + /// + public abstract class BaseDdnsClient : IDdnsClient + { + private readonly DdnsOptions _options; + protected readonly ILogger Logger; + + /// + /// create base client + /// + /// + protected BaseDdnsClient(IOptions options, ILogger logger) + { + _options = options.Value; + Logger = logger; + } + + /// + public async Task UpdateDdns(string ip, CancellationToken cancellationToken = default) + { + if (!string.IsNullOrWhiteSpace(_options.Domains)) + { + var hosts = _options.Domains.Split(','); + var updated = 0; + if (hosts.Any()) + { + await foreach(var host in UpdateDdns(hosts, ip, cancellationToken)) + { + Logger.LogInformation("Updated records for host: {0}", host); + updated++; + } + } + return updated == hosts.Length; + } + + return true; + } + + private async IAsyncEnumerable UpdateDdns(string[] hosts, string ip, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + foreach(var host in hosts) + { + var done = false; + try + { + await UpdateDdns(host, ip, cancellationToken); + done = true; + } + catch (Exception ex) + { + Logger.LogError(ex, "failed to update record for host: {0}", host); + } + + if (done) yield return host; + } + } + + /// + public abstract Task UpdateDdns(string host, string ip, CancellationToken cancellationToken = default); + } +} diff --git a/src/Contracts/IDdnsClient.cs b/src/Contracts/IDdnsClient.cs new file mode 100644 index 0000000..c827aae --- /dev/null +++ b/src/Contracts/IDdnsClient.cs @@ -0,0 +1,18 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace TinyDdns.Contracts +{ + /// + /// DDNS client interface + /// + public interface IDdnsClient + { + /// + /// update ddns record + /// + /// + /// + Task UpdateDdns(string ip, CancellationToken cancellationToken = default); + } +} diff --git a/src/DdnsOptions.cs b/src/DdnsOptions.cs new file mode 100644 index 0000000..eeef03f --- /dev/null +++ b/src/DdnsOptions.cs @@ -0,0 +1,33 @@ + +namespace TinyDdns +{ + /// + /// DDNS options + /// + public class DdnsOptions + { + /// + /// default check interval + /// + public readonly int DefaultCheckInterval = 30; + private int? _checkInterval; + + /// + /// configured check interval + /// + public int CheckInterval { + get => _checkInterval ?? DefaultCheckInterval; + set => _checkInterval = value; + } + + /// + /// configured DDNS provider + /// + public DdnsProviders Provider { get; set; } + + /// + /// comma-seperated list of domains + /// + public string Domains { get; set; } + } +} diff --git a/src/DdnsProviders.cs b/src/DdnsProviders.cs new file mode 100644 index 0000000..3598451 --- /dev/null +++ b/src/DdnsProviders.cs @@ -0,0 +1,14 @@ + +namespace TinyDdns +{ + /// + /// list of supported DdnsProviders + /// + public enum DdnsProviders + { + /// + /// namecheap is a free online Ddns providers + /// + NameCheap = 1, + } +} diff --git a/src/DdnsWorker.cs b/src/DdnsWorker.cs new file mode 100644 index 0000000..3a468f8 --- /dev/null +++ b/src/DdnsWorker.cs @@ -0,0 +1,92 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using TinyDdns.Contracts; + +namespace TinyDdns +{ + /// + /// DDNS update worker + /// + public class DdnsWorker : BackgroundService + { + private readonly ILogger _logger; + private readonly IDdnsClient _ddnsClient; + private readonly DdnsOptions _options; + private readonly HttpClient _httpClient; + private string _lastIpAddress; + + public DdnsWorker(IDdnsClient ddnsClient, IOptions options, + HttpClient httpClient, ILogger logger) + { + _ddnsClient = ddnsClient; + _options = options.Value; + _httpClient = httpClient; + _logger = logger; + } + + /// + /// run update continuously + /// + /// + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested + && !string.IsNullOrEmpty(_options.Domains)) + { + _logger.LogInformation("DDNS Update Client running at: {time}", DateTimeOffset.Now); + var currentIp = await GetCurrentIp(); + if (!string.IsNullOrEmpty(currentIp) && currentIp != _lastIpAddress) + { + _logger.LogInformation("Updating DNS records for {0}", _options.Provider); + await UpdateDnsRecord(currentIp); + _lastIpAddress = currentIp; + } + await Task.Delay(1000 * _options.CheckInterval, stoppingToken); + } + } + + /// + /// get current router public IP address + /// + /// + private async Task GetCurrentIp() + { + const string ICANHAZIPURL = "https://ipv4.icanhazip.com"; + const string IPIFYURL = "https://api.ipify.org"; + var ip = string.Empty; + try + { + ip = await _httpClient.GetStringAsync(ICANHAZIPURL); + } catch (Exception ex) + { + _logger.LogError(ex, "failed to get IP from ICANHAZIP"); + try + { + ip = await _httpClient.GetStringAsync(IPIFYURL); + } + catch (Exception exx) + { + _logger.LogError(exx, "faied to get IP from IPIFY"); + } + } + + return ip.Trim(); + } + + /// + /// update DNS record with new ip + /// + /// + private async Task UpdateDnsRecord(string ip) + { + _logger.LogInformation("updating DDNS records for {0}", _options.Provider); + var result = await _ddnsClient.UpdateDdns(ip); + if (result) _logger.LogInformation("DDNS records updated for {0}", _options.Provider); + } + } +} diff --git a/src/Implementations/Namecheap/NamecheapClient.cs b/src/Implementations/Namecheap/NamecheapClient.cs new file mode 100644 index 0000000..0367bd7 --- /dev/null +++ b/src/Implementations/Namecheap/NamecheapClient.cs @@ -0,0 +1,48 @@ +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using TinyDdns.Contracts; + +namespace TinyDdns.Implementations.Namecheap +{ + /// + /// default implement for namecheap DDNS service + /// + public class NamecheapClient : BaseDdnsClient + { + private readonly NamecheapOptions _options; + private readonly HttpClient _httpClient; + private const string URL = "https://dynamicdns.park-your-domain.com/update?host={0}&domain={1}&password={2}&ip={3}"; + + /// + /// create an instance of + /// + /// + public NamecheapClient(HttpClient httpClient, IOptions options, + IOptions ddnsOptions, ILogger logger) + : base(ddnsOptions, logger) + { + _options = options.Value; + _httpClient = httpClient; + } + + /// + /// update dns for given host with ip + /// + /// + /// + /// + public async override Task UpdateDdns(string host, string ip, + CancellationToken cancellationToken = default) + { + var cleanHost = CleanHost(host, _options.Domain); + var url = string.Format(URL, cleanHost, _options.Domain, _options.Password, ip); + await _httpClient.GetStringAsync(url, cancellationToken); + } + + private static string CleanHost(string host, string domain) => + host.Replace($".{domain}", string.Empty); + } +} diff --git a/src/Implementations/Namecheap/NamecheapOptions.cs b/src/Implementations/Namecheap/NamecheapOptions.cs new file mode 100644 index 0000000..f150522 --- /dev/null +++ b/src/Implementations/Namecheap/NamecheapOptions.cs @@ -0,0 +1,19 @@ + +namespace TinyDdns.Implementations.Namecheap +{ + /// + /// namecheap configuration options + /// + public class NamecheapOptions + { + /// + /// TLD domain + /// + public string Domain { get; set; } + + /// + /// name cheap DDNS password + /// + public string Password { get; set; } + } +} diff --git a/src/Implementations/Namecheap/ServiceCollectionExtensions.cs b/src/Implementations/Namecheap/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..76d1dc5 --- /dev/null +++ b/src/Implementations/Namecheap/ServiceCollectionExtensions.cs @@ -0,0 +1,25 @@ + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace TinyDdns.Implementations.Namecheap +{ + /// + /// service collection extensions for namecheap support + /// + public static class ServiceCollectionExtensions + { + /// + /// add Namecheap DDNS options + /// + /// + /// + /// + public static IServiceCollection AddNamecheapDdns(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration.GetSection("NamecheapOptions")); + services.AddHttpClient(); + return services; + } + } +} diff --git a/src/Program.cs b/src/Program.cs new file mode 100644 index 0000000..963bae2 --- /dev/null +++ b/src/Program.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using TinyDdns.Contracts; +using TinyDdns.Implementations; +using TinyDdns.Implementations.Namecheap; + +namespace TinyDdns +{ + /// + /// main entry point + /// + public static class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + services.Configure(hostContext.Configuration.GetSection("DdnsOptions")); + var ddnsProviderType = GetProvider(hostContext.Configuration.GetValue("DdnsOptions:Provider")); + services.AddSingleton(typeof(IDdnsClient), ddnsProviderType); + services.AddNamecheapDdns(hostContext.Configuration); + services.AddHostedService(); + }); + + public static Type GetProvider(DdnsProviders provider) => + provider switch { + DdnsProviders.NameCheap => typeof(NamecheapClient), + _ => throw new NotSupportedException($"provider type {provider} not supported") + }; + } +} diff --git a/src/TinyDdns.csproj b/src/TinyDdns.csproj new file mode 100644 index 0000000..fce5915 --- /dev/null +++ b/src/TinyDdns.csproj @@ -0,0 +1,12 @@ + + + + net5.0 + dotnet-TinyDdns-3D58E6A4-B1E4-4502-9236-8A2A37FAE552 + + + + + + + diff --git a/tests/TinyDdns.Tests.csproj b/tests/TinyDdns.Tests.csproj new file mode 100644 index 0000000..2c12453 --- /dev/null +++ b/tests/TinyDdns.Tests.csproj @@ -0,0 +1,22 @@ + + + + net5.0 + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/tests/UnitTest1.cs b/tests/UnitTest1.cs new file mode 100644 index 0000000..fc411e5 --- /dev/null +++ b/tests/UnitTest1.cs @@ -0,0 +1,14 @@ +using System; +using Xunit; + +namespace TinyDdns.Tests +{ + public class UnitTest1 + { + [Fact] + public void Test1() + { + + } + } +} From f80873725f47ae8dcf596048156fd8a26bb74fbe Mon Sep 17 00:00:00 2001 From: Mubarak Imam Date: Sun, 27 Jun 2021 17:33:35 +0100 Subject: [PATCH 2/2] TDDNS: added build flags --- .gitignore | 2 ++ src/TinyDdns.csproj | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a3aca06..a83ca2b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ .history/ **/bin/ **/obj/ +**/dist +**/.DS_Store diff --git a/src/TinyDdns.csproj b/src/TinyDdns.csproj index fce5915..b21c143 100644 --- a/src/TinyDdns.csproj +++ b/src/TinyDdns.csproj @@ -2,7 +2,13 @@ net5.0 - dotnet-TinyDdns-3D58E6A4-B1E4-4502-9236-8A2A37FAE552 + true + true + true + true + Link + true + embedded