Merge pull request #1788 from btcpayserver/feat/new-version-check

Adding HostedService that checks for new BTCPayServer version on GitHub once a day
This commit is contained in:
Nicolas Dorier 2020-08-03 23:00:17 +09:00 committed by GitHub
commit e399815427
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 243 additions and 24 deletions

View File

@ -3103,5 +3103,61 @@ namespace BTCPayServer.Tests
.GetResult())
.Where(i => i.GetAddress() == h).Any();
}
class MockVersionFetcher : IVersionFetcher
{
public const string MOCK_NEW_VERSION = "9.9.9.9";
public Task<string> Fetch(CancellationToken cancellation)
{
return Task.FromResult(MOCK_NEW_VERSION);
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanCheckForNewVersion()
{
using (var tester = ServerTester.Create(newDb: true))
{
await tester.StartAsync();
var acc = tester.NewAccount();
acc.GrantAccess(true);
var settings = tester.PayTester.GetService<SettingsRepository>();
await settings.UpdateSetting<PoliciesSettings>(new PoliciesSettings() { CheckForNewVersions = true });
var mockEnv = tester.PayTester.GetService<BTCPayServerEnvironment>();
var mockSender = tester.PayTester.GetService<Services.Notifications.NotificationSender>();
var svc = new NewVersionCheckerHostedService(settings, mockEnv, mockSender, new MockVersionFetcher());
await svc.ProcessVersionCheck();
// since last version present in database was null, it should've been updated with version mock returned
var lastVersion = await settings.GetSettingAsync<NewVersionCheckerDataHolder>();
Assert.Equal(MockVersionFetcher.MOCK_NEW_VERSION, lastVersion.LastVersion);
// we should also have notification in UI
var ctrl = acc.GetController<NotificationsController>();
var newVersion = MockVersionFetcher.MOCK_NEW_VERSION;
var vm = Assert.IsType<Models.NotificationViewModels.IndexViewModel>(
Assert.IsType<ViewResult>(ctrl.Index()).Model);
Assert.True(vm.Skip == 0);
Assert.True(vm.Count == 50);
Assert.True(vm.Total == 1);
Assert.True(vm.Items.Count == 1);
var fn = vm.Items.First();
var now = DateTimeOffset.UtcNow;
Assert.True(fn.Created >= now.AddSeconds(-3));
Assert.True(fn.Created <= now);
Assert.Equal($"New version {newVersion} released!", fn.Body);
Assert.Equal($"https://github.com/btcpayserver/btcpayserver/releases/tag/v{newVersion}", fn.ActionLink);
Assert.False(fn.Seen);
}
}
}
}

View File

@ -176,6 +176,7 @@ namespace BTCPayServer.Configuration
SocksEndpoint = endpoint;
}
UpdateUrl = conf.GetOrDefault<Uri>("updateurl", null);
var sshSettings = ParseSSHConfiguration(conf);
if ((!string.IsNullOrEmpty(sshSettings.Password) || !string.IsNullOrEmpty(sshSettings.KeyFile)) && !string.IsNullOrEmpty(sshSettings.Server))
@ -301,5 +302,6 @@ namespace BTCPayServer.Configuration
set;
}
public string TorrcFile { get; set; }
public Uri UpdateUrl { get; set; }
}
}

View File

@ -38,6 +38,7 @@ namespace BTCPayServer.Configuration
app.Option("--sshtrustedfingerprints", "SSH Host public key fingerprint or sha256 (default: empty, it will allow untrusted connections)", CommandOptionType.SingleValue);
app.Option("--torrcfile", "Path to torrc file containing hidden services directories (default: empty)", CommandOptionType.SingleValue);
app.Option("--socksendpoint", "Socks endpoint to connect to onion urls (default: empty)", CommandOptionType.SingleValue);
app.Option("--updateurl", $"Url used for once a day new release version check. Check performed only if value is not empty (default: empty)", CommandOptionType.SingleValue);
app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue);
app.Option("--debugloglevel", "The severity you log (default:information)", CommandOptionType.SingleValue);
app.Option("--disable-registration", "Disables new user registrations (default:true)", CommandOptionType.SingleValue);

View File

@ -443,13 +443,8 @@ namespace BTCPayServer.Controllers
var settings = await _SettingsRepository.GetSettingAsync<ThemeSettings>();
settings.FirstRun = false;
await _SettingsRepository.UpdateSetting<ThemeSettings>(settings);
if (_Options.DisableRegistration)
{
// Once the admin user has been created lock subsequent user registrations (needs to be disabled for unit tests that require multiple users).
Logs.PayServer.LogInformation("First admin created, disabling subscription (disable-registration is set to true)");
policies.LockSubscription = true;
await _SettingsRepository.UpdateSetting(policies);
}
await _SettingsRepository.FirstAdminRegistered(policies, _Options.UpdateUrl != null, _Options.DisableRegistration);
RegisteredAdmin = true;
}
@ -626,7 +621,7 @@ namespace BTCPayServer.Controllers
private bool CanLoginOrRegister()
{
return _btcPayServerEnvironment.IsDevelopping || _btcPayServerEnvironment.IsSecure;
return _btcPayServerEnvironment.IsDeveloping || _btcPayServerEnvironment.IsSecure;
}
private void SetInsecureFlags()

View File

@ -288,7 +288,7 @@ namespace BTCPayServer.Controllers.GreenField
protected bool CanUseInternalLightning(bool doingAdminThings)
{
return (_btcPayServerEnvironment.IsDevelopping || User.IsInRole(Roles.ServerAdmin) ||
return (_btcPayServerEnvironment.IsDeveloping || User.IsInRole(Roles.ServerAdmin) ||
(_cssThemeManager.AllowLightningInternalNodeForAll && !doingAdminThings));
}

View File

@ -148,13 +148,7 @@ namespace BTCPayServer.Controllers.GreenField
await _userManager.AddToRoleAsync(user, Roles.ServerAdmin);
if (!anyAdmin)
{
if (_options.DisableRegistration)
{
// automatically lock subscriptions now that we have our first admin
Logs.PayServer.LogInformation("First admin created, disabling subscription (disable-registration is set to true)");
policies.LockSubscription = true;
await _settingsRepository.UpdateSetting(policies);
}
await _settingsRepository.FirstAdminRegistered(policies, _options.UpdateUrl != null, _options.DisableRegistration);
}
}
_eventAggregator.Publish(new UserRegisteredEvent() { RequestUri = Request.GetAbsoluteRootUri(), User = user, Admin = request.IsAdministrator is true });

View File

@ -172,7 +172,7 @@ namespace BTCPayServer.Controllers
private bool CanUseInternalLightning()
{
return (_BTCPayEnv.IsDevelopping || User.IsInRole(Roles.ServerAdmin) || _CssThemeManager.AllowLightningInternalNodeForAll);
return (_BTCPayEnv.IsDeveloping || User.IsInRole(Roles.ServerAdmin) || _CssThemeManager.AllowLightningInternalNodeForAll);
}
}
}

View File

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.Logging;
using BTCPayServer.Services;
using Microsoft.Extensions.Logging;
namespace BTCPayServer
{
// All logic that would otherwise be duplicated across solution goes into this utility class
// ~If~ Once this starts growing out of control, begin extracting action logic classes out of here
// Also some of logic in here may be result of parallel development of Greenfield API
// It's much better that we extract those common methods then copy paste and maintain same code across codebase
internal static class ActionLogicExtensions
{
internal static async Task FirstAdminRegistered(this SettingsRepository settingsRepository, PoliciesSettings policies,
bool updateCheck, bool disableRegistrations)
{
if (updateCheck)
{
Logs.PayServer.LogInformation("First admin created, enabling checks for new versions");
policies.CheckForNewVersions = updateCheck;
}
if (disableRegistrations)
{
// Once the admin user has been created lock subsequent user registrations (needs to be disabled for unit tests that require multiple users).
Logs.PayServer.LogInformation("First admin created, disabling subscription (disable-registration is set to true)");
policies.LockSubscription = true;
}
if (updateCheck || disableRegistrations)
await settingsRepository.UpdateSetting(policies);
}
}
}

View File

@ -10,12 +10,11 @@ namespace BTCPayServer.HostedServices
{
public abstract class BaseAsyncService : IHostedService
{
private CancellationTokenSource _Cts;
private CancellationTokenSource _Cts = new CancellationTokenSource();
protected Task[] _Tasks;
public virtual Task StartAsync(CancellationToken cancellationToken)
{
_Cts = new CancellationTokenSource();
_Tasks = InitializeTasks();
return Task.CompletedTask;
}

View File

@ -0,0 +1,121 @@
using System;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.Logging;
using BTCPayServer.Services;
using BTCPayServer.Services.Notifications;
using BTCPayServer.Services.Notifications.Blobs;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.HostedServices
{
public class NewVersionCheckerHostedService : BaseAsyncService
{
private readonly SettingsRepository _settingsRepository;
private readonly BTCPayServerEnvironment _env;
private readonly NotificationSender _notificationSender;
private readonly IVersionFetcher _versionFetcher;
public NewVersionCheckerHostedService(SettingsRepository settingsRepository, BTCPayServerEnvironment env,
NotificationSender notificationSender, IVersionFetcher versionFetcher)
{
_settingsRepository = settingsRepository;
_env = env;
_notificationSender = notificationSender;
_versionFetcher = versionFetcher;
}
internal override Task[] InitializeTasks()
{
return new Task[] { CreateLoopTask(LoopVersionCheck) };
}
protected async Task LoopVersionCheck()
{
try
{
await ProcessVersionCheck();
}
catch (Exception ex)
{
Logs.Events.LogError(ex, "Error while performing new version check");
}
await Task.Delay(TimeSpan.FromDays(1), Cancellation);
}
public async Task ProcessVersionCheck()
{
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
if (policies.CheckForNewVersions)
{
var tag = await _versionFetcher.Fetch(Cancellation);
if (tag != null && tag != _env.Version)
{
var dh = await _settingsRepository.GetSettingAsync<NewVersionCheckerDataHolder>() ?? new NewVersionCheckerDataHolder();
if (dh.LastVersion != tag)
{
await _notificationSender.SendNotification(new AdminScope(), new NewVersionNotification(tag));
dh.LastVersion = tag;
await _settingsRepository.UpdateSetting(dh);
}
}
}
}
}
public class NewVersionCheckerDataHolder
{
public string LastVersion { get; set; }
}
public interface IVersionFetcher
{
Task<string> Fetch(CancellationToken cancellation);
}
public class GithubVersionFetcher : IVersionFetcher
{
private readonly HttpClient _httpClient;
private readonly Uri _updateurl;
public GithubVersionFetcher(IHttpClientFactory httpClientFactory, BTCPayServerOptions options)
{
_httpClient = httpClientFactory.CreateClient(nameof(GithubVersionFetcher));
_httpClient.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
_httpClient.DefaultRequestHeaders.Add("User-Agent", "BTCPayServer/NewVersionChecker");
_updateurl = options.UpdateUrl;
}
private static readonly Regex _releaseVersionTag = new Regex("^(v[1-9]+(\\.[0-9]+)*(-[0-9]+)?)$");
public async Task<string> Fetch(CancellationToken cancellation)
{
if (_updateurl == null)
return null;
using (var resp = await _httpClient.GetAsync(_updateurl, cancellation))
{
var strResp = await resp.Content.ReadAsStringAsync();
if (resp.IsSuccessStatusCode)
{
var jobj = JObject.Parse(strResp);
var tag = jobj["tag_name"].ToString();
var isReleaseVersionTag = _releaseVersionTag.IsMatch(tag);
return isReleaseVersionTag ? tag : null;
}
else
{
Logs.Events.LogWarning($"Unsuccessful status code returned during new version check. " +
$"Url: {_updateurl}, HTTP Code: {resp.StatusCode}, Response Body: {strResp}");
}
}
return null;
}
}
}

View File

@ -239,7 +239,11 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
services.AddScoped<IAuthorizationHandler, CookieAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, BitpayAuthorizationHandler>();
services.AddSingleton<IVersionFetcher, GithubVersionFetcher>();
services.AddSingleton<IHostedService, NewVersionCheckerHostedService>();
services.AddSingleton<INotificationHandler, NewVersionNotification.Handler>();
services.AddSingleton<INotificationHandler, InvoiceEventNotification.Handler>();
services.AddSingleton<INotificationHandler, PayoutNotification.Handler>();
@ -284,7 +288,7 @@ namespace BTCPayServer.Hosting
{
var btcPayEnv = provider.GetService<BTCPayServerEnvironment>();
var rateLimits = new RateLimitService();
if (btcPayEnv.IsDevelopping)
if (btcPayEnv.IsDeveloping)
{
rateLimits.SetZone($"zone={ZoneLimits.Login} rate=1000r/min burst=100 nodelay");
rateLimits.SetZone($"zone={ZoneLimits.Register} rate=1000r/min burst=100 nodelay");

View File

@ -502,7 +502,7 @@ namespace BTCPayServer.Payments.PayJoin
{
var o = new JObject();
o.Add(new JProperty("errorCode", PayjoinReceiverHelper.GetErrorCode(error)));
if (string.IsNullOrEmpty(debug) || !_env.IsDevelopping)
if (string.IsNullOrEmpty(debug) || !_env.IsDeveloping)
{
o.Add(new JProperty("message", PayjoinReceiverHelper.GetMessage(error)));
}

View File

@ -19,7 +19,8 @@
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver",
"BTCPAY_DEBUGLOG": "debug.log",
"BTCPAY_TORRCFILE": "../BTCPayServer.Tests/TestData/Tor/torrc",
"BTCPAY_SOCKSENDPOINT": "localhost:9050"
"BTCPAY_SOCKSENDPOINT": "localhost:9050",
"BTCPAY_UPDATEURL": ""
},
"applicationUrl": "http://127.0.0.1:14142/"
},

View File

@ -64,7 +64,7 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
await daemonRpcClient.SendCommandAsync<JsonRpcClient.NoRequestModel, SyncInfoResponse>("sync_info",
JsonRpcClient.NoRequestModel.Instance);
summary.TargetHeight = daemonResult.TargetHeight ?? daemonResult.Height;
summary.Synced = daemonResult.Height >= summary.TargetHeight && (summary.TargetHeight > 0 || _btcPayServerEnvironment.IsDevelopping);
summary.Synced = daemonResult.Height >= summary.TargetHeight && (summary.TargetHeight > 0 || _btcPayServerEnvironment.IsDeveloping);
summary.CurrentHeight = daemonResult.Height;
summary.UpdatedAt = DateTime.Now;
summary.DaemonAvailable = true;

View File

@ -54,7 +54,7 @@ namespace BTCPayServer.Services
}
public bool AltcoinsVersion { get; set; }
public bool IsDevelopping
public bool IsDeveloping
{
get
{

View File

@ -24,6 +24,8 @@ namespace BTCPayServer.Services
public bool AllowHotWalletForAll { get; set; }
[Display(Name = "Allow non-admins to import their hot wallets to the node wallet")]
public bool AllowHotWalletRPCImportForAll { get; set; }
[Display(Name = "Check releases on GitHub and alert when new BTCPayServer version is available")]
public bool CheckForNewVersions { get; set; }
[Display(Name = "Display app on website root")]
public string RootAppId { get; set; }

View File

@ -42,6 +42,11 @@
<label asp-for="AllowHotWalletRPCImportForAll" class="form-check-label"></label>
<span asp-validation-for="AllowHotWalletRPCImportForAll" class="text-danger"></span>
</div>
<div class="form-check">
<input asp-for="CheckForNewVersions" type="checkbox" class="form-check-input" />
<label asp-for="CheckForNewVersions" class="form-check-label"></label>
<span asp-validation-for="CheckForNewVersions" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="RootAppId"></label>