Add Dynamic DNS support

This commit is contained in:
nicolas.dorier 2019-07-24 17:59:30 +09:00
parent 8e07bf3ffb
commit 8896d89908
No known key found for this signature in database
GPG Key ID: 6618763EF09186FE
7 changed files with 297 additions and 0 deletions

View File

@ -155,6 +155,9 @@
<Content Update="Views\Server\P2PService.cshtml"> <Content Update="Views\Server\P2PService.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack> <Pack>$(IncludeRazorContentInPack)</Pack>
</Content> </Content>
<Content Update="Views\Server\DynamicDnsService.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\SSHService.cshtml"> <Content Update="Views\Server\SSHService.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack> <Pack>$(IncludeRazorContentInPack)</Pack>
</Content> </Content>

View File

@ -545,6 +545,11 @@ namespace BTCPayServer.Controllers
Link = this.Url.Action(nameof(SSHService)) Link = this.Url.Action(nameof(SSHService))
}); });
} }
result.OtherExternalServices.Add(new ServicesViewModel.OtherExternalService()
{
Name = "Dynamic DNS",
Link = this.Url.Action(nameof(DynamicDnsService))
});
foreach (var torService in _torServices.Services) foreach (var torService in _torServices.Services)
{ {
if (torService.VirtualPort == 80) if (torService.VirtualPort == 80)
@ -801,6 +806,39 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(Service), new { cryptoCode = cryptoCode, serviceName = serviceName, nonce = nonce }); return RedirectToAction(nameof(Service), new { cryptoCode = cryptoCode, serviceName = serviceName, nonce = nonce });
} }
[Route("server/services/dynamic-dns")]
public async Task<IActionResult> DynamicDnsService()
{
var settings = (await _SettingsRepository.GetSettingAsync<DynamicDnsSettings>()) ?? new DynamicDnsSettings();
var vm = new DynamicDnsViewModel();
vm.Settings = settings;
return View(vm);
}
[Route("server/services/dynamic-dns")]
[HttpPost]
public async Task<IActionResult> DynamicDnsService(DynamicDnsViewModel viewModel, string command = null)
{
if (!viewModel.Settings.Enabled)
{
StatusMessage = $"The Dynamic DNS service has been disabled";
viewModel.Settings.LastUpdated = null;
await _SettingsRepository.UpdateSetting(viewModel.Settings);
return RedirectToAction();
}
string errorMessage = await viewModel.Settings.SendUpdateRequest(HttpClientFactory.CreateClient());
if (errorMessage == null)
{
StatusMessage = $"The Dynamic DNS has been successfully queried, your configuration is saved";
viewModel.Settings.LastUpdated = DateTimeOffset.UtcNow;
await _SettingsRepository.UpdateSetting(viewModel.Settings);
}
else
{
StatusMessage = errorMessage;
}
return RedirectToAction();
}
[Route("server/services/ssh")] [Route("server/services/ssh")]
public IActionResult SSHService(bool downloadKeyFile = false) public IActionResult SSHService(bool downloadKeyFile = false)
{ {

View File

@ -0,0 +1,68 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Logging;
using BTCPayServer.Services;
namespace BTCPayServer.HostedServices
{
public class DynamicDnsHostedService : BaseAsyncService
{
public DynamicDnsHostedService(IHttpClientFactory httpClientFactory, SettingsRepository settingsRepository)
{
HttpClientFactory = httpClientFactory;
SettingsRepository = settingsRepository;
}
public IHttpClientFactory HttpClientFactory { get; }
public SettingsRepository SettingsRepository { get; }
internal override Task[] InitializeTasks()
{
return new[]
{
CreateLoopTask(UpdateRecord)
};
}
TimeSpan Period = TimeSpan.FromMinutes(60);
async Task UpdateRecord()
{
using (var timeout = CancellationTokenSource.CreateLinkedTokenSource(Cancellation))
{
var settings = await SettingsRepository.GetSettingAsync<DynamicDnsSettings>();
if (settings?.Enabled is true && (settings.LastUpdated is null ||
(DateTimeOffset.UtcNow - settings.LastUpdated) > Period))
{
timeout.CancelAfter(TimeSpan.FromSeconds(20.0));
try
{
var errorMessage = await settings.SendUpdateRequest(HttpClientFactory.CreateClient());
if (errorMessage == null)
{
Logs.PayServer.LogWarning($"Dynamic DNS service is enabled but the request to the provider failed: {errorMessage}");
}
else
{
Logs.PayServer.LogInformation("Dynamic DNS service successfully refresh the DNS record");
}
}
catch (OperationCanceledException) when (timeout.IsCancellationRequested)
{
}
}
}
using (var delayCancel = CancellationTokenSource.CreateLinkedTokenSource(Cancellation))
{
var delay = Task.Delay(Period, delayCancel.Token);
var changed = SettingsRepository.WaitSettingsChanged<DynamicDnsSettings>(Cancellation);
await Task.WhenAny(delay, changed);
delayCancel.Cancel();
}
}
}
}

View File

@ -206,6 +206,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService, RatesHostedService>(); services.AddSingleton<IHostedService, RatesHostedService>();
services.AddSingleton<IHostedService, BackgroundJobSchedulerHostedService>(); services.AddSingleton<IHostedService, BackgroundJobSchedulerHostedService>();
services.AddSingleton<IHostedService, AppHubStreamer>(); services.AddSingleton<IHostedService, AppHubStreamer>();
services.AddSingleton<IHostedService, DynamicDnsHostedService>();
services.AddSingleton<IHostedService, TorServicesHostedService>(); services.AddSingleton<IHostedService, TorServicesHostedService>();
services.AddSingleton<IHostedService, PaymentRequestStreamer>(); services.AddSingleton<IHostedService, PaymentRequestStreamer>();
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>(); services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();

View File

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services;
namespace BTCPayServer.Models.ServerViewModels
{
public class DynamicDnsViewModel
{
public class WellKnownService
{
public WellKnownService(string name, string url)
{
Name = name;
Url = url;
}
public string Name { get; set; }
public string Url { get; set; }
}
public DynamicDnsSettings Settings { get; set; }
public string LastUpdated
{
get
{
if (Settings?.LastUpdated is DateTimeOffset date)
{
return Views.ViewsRazor.ToTimeAgo(date);
}
return null;
}
}
public WellKnownService[] KnownServices { get; set; } = new []
{
new WellKnownService("noip", "https://dynupdate.no-ip.com/nic/update"),
new WellKnownService("dyndns", "https://members.dyndns.org/v3/update"),
new WellKnownService("duckdns", "https://www.duckdns.org/v3/update"),
new WellKnownService("google", "https://domains.google.com/nic/update"),
};
}
}

View File

@ -0,0 +1,84 @@
using System;
using System.Reflection;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Hosting;
using NBitcoin.DataEncoders;
using Newtonsoft.Json;
namespace BTCPayServer.Services
{
public class DynamicDnsSettings
{
[Display(Name = "Url of the Dynamic DNS service you are using")]
public string ServiceUrl { get; set; }
public string Login { get; set; }
[DataType(DataType.Password)]
public string Password { get; set; }
[Display(Name = "Your dynamic DNS hostname")]
public string Hostname { get; set; }
public bool Enabled { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? LastUpdated { get; set; }
public async Task<string> SendUpdateRequest(HttpClient httpClient)
{
string errorMessage = null;
try
{
var result = await httpClient.SendAsync(CreateUpdateRequest());
if (!result.IsSuccessStatusCode)
{
try
{
errorMessage = await result.Content.ReadAsStringAsync();
}
catch { }
errorMessage = $"Error: Invalid return code {result.StatusCode}, expected 200 ({errorMessage.Trim()})";
}
}
catch (Exception ex)
{
errorMessage = $"Error: While querying the Dynamic DNS service ({ex.Message})";
}
return errorMessage;
}
public HttpRequestMessage CreateUpdateRequest()
{
HttpRequestMessage webRequest = new HttpRequestMessage();
if (!Uri.TryCreate(ServiceUrl, UriKind.Absolute, out var uri) || uri.HostNameType == UriHostNameType.Unknown)
{
throw new FormatException($"Invalid {ServiceUrl}");
}
var builder = new UriBuilder(uri);
if (!string.IsNullOrEmpty(Login))
{
builder.UserName = Login;
}
if (!string.IsNullOrEmpty(Password))
{
builder.Password = Password;
}
builder.UserName = builder.UserName ?? string.Empty;
builder.Password = builder.Password ?? string.Empty;
builder.Query = $"hostname={Hostname}";
webRequest.Headers.Authorization = new AuthenticationHeaderValue("Basic", Encoders.Base64.EncodeData(new UTF8Encoding(false).GetBytes($"{builder.UserName}:{builder.Password}")));
webRequest.Headers.TryAddWithoutValidation("User-Agent", $"BTCPayServer/{GetVersion()} btcpayserver@gmail.com");
webRequest.Method = HttpMethod.Get;
webRequest.RequestUri = builder.Uri;
return webRequest;
}
private string GetVersion()
{
return typeof(BTCPayServerEnvironment).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyFileVersionAttribute>().Version;
}
}
}

View File

@ -0,0 +1,60 @@
@model BTCPayServer.Models.ServerViewModels.DynamicDnsViewModel
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Services);
}
<h4>Dynamic DNS Settings</h4>
<partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]" />
<div class="row">
<div class="col-md-8">
<div class="form-group">
<p>
<span>Dynamic DNS service allows you to have a stable DNS name pointing to your server, even if your IP address change regulary. <br />
This is recommended if you are hosting BTCPayServer at home and wish to have a clearnet HTTPS address to access your server.</span>
</p>
<p>Note that you need to properly configure your NAT and BTCPayServer install to get HTTPS certificate.</p>
</div>
<form method="post">
<div class="form-group">
<div class="form-group">
<label asp-for="Settings.ServiceUrl"></label>
<input id="ServiceUrl" asp-for="Settings.ServiceUrl" class="form-control" placeholder="Url" />
<p class="form-text text-muted">
Well-known Dynamic DNS providers are:
@for (int i = 0; i < Model.KnownServices.Length; i++)
{
<a href="#" onclick="document.getElementById('ServiceUrl').value = '@Model.KnownServices[i].Url'; return false;">@Model.KnownServices[i].Name</a><span>@(i == Model.KnownServices.Length - 1 ? "" : ",")</span>
}
</p>
</div>
<div class="form-group">
<label asp-for="Settings.Hostname"></label>
<input asp-for="Settings.Hostname" class="form-control" placeholder="Hostname" />
<p class="form-text text-muted">
<span>The DNS record has been refreshed: </span>
@if (Model.LastUpdated != null)
{
<span>@Model.LastUpdated</span>
}
</p>
</div>
<div class="form-group">
<label asp-for="Settings.Login"></label>
<input asp-for="Settings.Login" class="form-control" placeholder="Login" />
</div>
<div class="form-group">
<label asp-for="Settings.Password"></label>
<input asp-for="Settings.Password" class="form-control" placeholder="Password" />
</div>
<div class="form-group">
<label asp-for="Settings.Enabled"></label>
<input asp-for="Settings.Enabled" class="form-check-inline" type="checkbox" />
</div>
<button name="command" class="btn btn-primary" type="submit" value="Save">Save</button>
</div>
</form>
</div>
</div>