Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace

# Local History for Visual Studio Code
.history/
**/bin/
**/obj/
**/dist
**/.DS_Store
29 changes: 29 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
42 changes: 42 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
18 changes: 18 additions & 0 deletions TinyDdns.sln
Original file line number Diff line number Diff line change
@@ -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
74 changes: 74 additions & 0 deletions src/Contracts/BaseDdnsClient.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// base DDNS client
/// </summary>
public abstract class BaseDdnsClient : IDdnsClient
{
private readonly DdnsOptions _options;
protected readonly ILogger<BaseDdnsClient> Logger;

/// <summary>
/// create base client
/// </summary>
/// <param name="logger"></param>
protected BaseDdnsClient(IOptions<DdnsOptions> options, ILogger<BaseDdnsClient> logger)
{
_options = options.Value;
Logger = logger;
}

/// <inheritdoc />
public async Task<bool> 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<string> 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;
}
}

/// <inheritdoc />
public abstract Task UpdateDdns(string host, string ip, CancellationToken cancellationToken = default);
}
}
18 changes: 18 additions & 0 deletions src/Contracts/IDdnsClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Threading;
using System.Threading.Tasks;

namespace TinyDdns.Contracts
{
/// <summary>
/// DDNS client interface
/// </summary>
public interface IDdnsClient
{
/// <summary>
/// update ddns record
/// </summary>
/// <param name="ip"></param>
/// <returns><see cref="Task{Bool}"/></returns>
Task<bool> UpdateDdns(string ip, CancellationToken cancellationToken = default);
}
}
33 changes: 33 additions & 0 deletions src/DdnsOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@

namespace TinyDdns
{
/// <summary>
/// DDNS options
/// </summary>
public class DdnsOptions
{
/// <summary>
/// default check interval
/// </summary>
public readonly int DefaultCheckInterval = 30;
private int? _checkInterval;

/// <summary>
/// configured check interval
/// </summary>
public int CheckInterval {
get => _checkInterval ?? DefaultCheckInterval;
set => _checkInterval = value;
}

/// <summary>
/// configured DDNS provider
/// </summary>
public DdnsProviders Provider { get; set; }

/// <summary>
/// comma-seperated list of domains
/// </summary>
public string Domains { get; set; }
}
}
14 changes: 14 additions & 0 deletions src/DdnsProviders.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

namespace TinyDdns
{
/// <summary>
/// list of supported DdnsProviders
/// </summary>
public enum DdnsProviders
{
/// <summary>
/// namecheap is a free online Ddns providers
/// </summary>
NameCheap = 1,
}
}
92 changes: 92 additions & 0 deletions src/DdnsWorker.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// DDNS update worker
/// </summary>
public class DdnsWorker : BackgroundService
{
private readonly ILogger<DdnsWorker> _logger;
private readonly IDdnsClient _ddnsClient;
private readonly DdnsOptions _options;
private readonly HttpClient _httpClient;
private string _lastIpAddress;

public DdnsWorker(IDdnsClient ddnsClient, IOptions<DdnsOptions> options,
HttpClient httpClient, ILogger<DdnsWorker> logger)
{
_ddnsClient = ddnsClient;
_options = options.Value;
_httpClient = httpClient;
_logger = logger;
}

/// <summary>
/// run update continuously
/// </summary>
/// <param name="stoppingToken"></param>
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);
}
}

/// <summary>
/// get current router public IP address
/// </summary>
/// <returns><see cref="Task{String}"/></returns>
private async Task<string> 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();
}

/// <summary>
/// update DNS record with new ip
/// </summary>
/// <param name="ip"></param>
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);
}
}
}
48 changes: 48 additions & 0 deletions src/Implementations/Namecheap/NamecheapClient.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// default implement for namecheap DDNS service
/// </summary>
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}";

/// <summary>
/// create an instance of <see cref="NamecheapClient"/>
/// </summary>
/// <param name="httpClient"></param>
public NamecheapClient(HttpClient httpClient, IOptions<NamecheapOptions> options,
IOptions<DdnsOptions> ddnsOptions, ILogger<NamecheapClient> logger)
: base(ddnsOptions, logger)
{
_options = options.Value;
_httpClient = httpClient;
}

/// <summary>
/// update dns for given host with ip
/// </summary>
/// <param name="host"></param>
/// <param name="ip"></param>
/// <param name="cancellationToken"></param>
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);
}
}
Loading