diff --git a/BTCPayServer.Abstractions/BTCPayServer.Abstractions.csproj b/BTCPayServer.Abstractions/BTCPayServer.Abstractions.csproj new file mode 100644 index 000000000..aa1b118f6 --- /dev/null +++ b/BTCPayServer.Abstractions/BTCPayServer.Abstractions.csproj @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/BTCPayServer.Abstractions/Contracts/IBTCPayServerExtension.cs b/BTCPayServer.Abstractions/Contracts/IBTCPayServerExtension.cs new file mode 100644 index 000000000..525f60976 --- /dev/null +++ b/BTCPayServer.Abstractions/Contracts/IBTCPayServerExtension.cs @@ -0,0 +1,17 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace BTCPayServer.Contracts +{ + public interface IBTCPayServerExtension + { + public string Identifier { get;} + string Name { get; } + Version Version { get; } + string Description { get; } + + void Execute(IApplicationBuilder applicationBuilder, IServiceProvider applicationBuilderApplicationServices); + void Execute(IServiceCollection applicationBuilder); + } +} diff --git a/BTCPayServer.Abstractions/Contracts/INotificationHandler.cs b/BTCPayServer.Abstractions/Contracts/INotificationHandler.cs new file mode 100644 index 000000000..c144e9bd8 --- /dev/null +++ b/BTCPayServer.Abstractions/Contracts/INotificationHandler.cs @@ -0,0 +1,20 @@ +using System; + +namespace BTCPayServer.Contracts +{ + public interface INotificationHandler + { + string NotificationType { get; } + Type NotificationBlobType { get; } + void FillViewModel(object notification, NotificationViewModel vm); + } + + public class NotificationViewModel + { + public string Id { get; set; } + public DateTimeOffset Created { get; set; } + public string Body { get; set; } + public string ActionLink { get; set; } + public bool Seen { get; set; } + } +} diff --git a/BTCPayServer.Abstractions/Contracts/IStoreNavExtension.cs b/BTCPayServer.Abstractions/Contracts/IStoreNavExtension.cs new file mode 100644 index 000000000..59b6d1958 --- /dev/null +++ b/BTCPayServer.Abstractions/Contracts/IStoreNavExtension.cs @@ -0,0 +1,21 @@ +namespace BTCPayServer.Contracts +{ + public interface INavExtension + { + string Partial { get; } + + string Location { get; } + } + + public class NavExtension: INavExtension + { + public NavExtension(string partial, string location) + { + Partial = partial; + Location = location; + } + + public string Partial { get; } + public string Location { get; } + } +} diff --git a/BTCPayServer/Contracts/ISyncSummaryProvider.cs b/BTCPayServer.Abstractions/Contracts/ISyncSummaryProvider.cs similarity index 96% rename from BTCPayServer/Contracts/ISyncSummaryProvider.cs rename to BTCPayServer.Abstractions/Contracts/ISyncSummaryProvider.cs index 44cdc918c..9843da32e 100644 --- a/BTCPayServer/Contracts/ISyncSummaryProvider.cs +++ b/BTCPayServer.Abstractions/Contracts/ISyncSummaryProvider.cs @@ -6,5 +6,4 @@ namespace BTCPayServer.Contracts string Partial { get; } } - } diff --git a/BTCPayServer.Common/BTCPayNetwork.cs b/BTCPayServer.Common/BTCPayNetwork.cs index 155649991..a1b7ac1c0 100644 --- a/BTCPayServer.Common/BTCPayNetwork.cs +++ b/BTCPayServer.Common/BTCPayNetwork.cs @@ -24,6 +24,8 @@ namespace BTCPayServer var settings = new BTCPayDefaultSettings(); _Settings.Add(chainType, settings); settings.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", NBXplorerDefaultSettings.GetFolderName(chainType)); + settings.DefaultExtensionDirectory = + StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", "Extensions"); settings.DefaultConfigurationFile = Path.Combine(settings.DefaultDataDirectory, "settings.config"); settings.DefaultPort = (chainType == NetworkType.Mainnet ? 23000 : chainType == NetworkType.Regtest ? 23002 : @@ -39,6 +41,7 @@ namespace BTCPayServer } public string DefaultDataDirectory { get; set; } + public string DefaultExtensionDirectory { get; set; } public string DefaultConfigurationFile { get; set; } public int DefaultPort { get; set; } } diff --git a/BTCPayServer.Test/BTCPayServer.Test.csproj b/BTCPayServer.Test/BTCPayServer.Test.csproj new file mode 100644 index 000000000..3a9c797c9 --- /dev/null +++ b/BTCPayServer.Test/BTCPayServer.Test.csproj @@ -0,0 +1,14 @@ + + + netcoreapp3.1 + true + false + true + + + + + + + + diff --git a/BTCPayServer.Test/Resources/img/screengrab.png b/BTCPayServer.Test/Resources/img/screengrab.png new file mode 100644 index 000000000..bb571a435 Binary files /dev/null and b/BTCPayServer.Test/Resources/img/screengrab.png differ diff --git a/BTCPayServer.Test/TestExtension.cs b/BTCPayServer.Test/TestExtension.cs new file mode 100644 index 000000000..e2c6dec8a --- /dev/null +++ b/BTCPayServer.Test/TestExtension.cs @@ -0,0 +1,25 @@ +using System; +using BTCPayServer.Contracts; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace BTCPayServer.Test +{ + public class TestExtension: IBTCPayServerExtension + { + public string Identifier { get; } = "BTCPayServer.Test"; + public string Name { get; } = "Test Plugin!"; + public Version Version { get; } = new Version(1,0,0,0); + public string Description { get; } = "This is a description of the loaded test extension!"; + public void Execute(IApplicationBuilder applicationBuilder, IServiceProvider applicationBuilderApplicationServices) + { + + } + + public void Execute(IServiceCollection services) + { + services.AddSingleton(new NavExtension("TestExtensionNavExtension", "header-nav")); + services.AddHostedService(); + } + } +} diff --git a/BTCPayServer.Test/TestExtensionController.cs b/BTCPayServer.Test/TestExtensionController.cs new file mode 100644 index 000000000..348c0e241 --- /dev/null +++ b/BTCPayServer.Test/TestExtensionController.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; + +namespace BTCPayServer.Test +{ + [Route("extensions/test")] + public class TestExtensionController : Controller + { + // GET + public IActionResult Index() + { + return View(); + } + + + } +} diff --git a/BTCPayServer.Test/Views/Shared/TestExtensionNavExtension.cshtml b/BTCPayServer.Test/Views/Shared/TestExtensionNavExtension.cshtml new file mode 100644 index 000000000..37530f946 --- /dev/null +++ b/BTCPayServer.Test/Views/Shared/TestExtensionNavExtension.cshtml @@ -0,0 +1,2 @@ + + diff --git a/BTCPayServer.Test/Views/TestExtension/Index.cshtml b/BTCPayServer.Test/Views/TestExtension/Index.cshtml new file mode 100644 index 000000000..f87d76718 --- /dev/null +++ b/BTCPayServer.Test/Views/TestExtension/Index.cshtml @@ -0,0 +1,9 @@ +
+
+

Challenge Completed!!

+ Here is also an image loaded from the plugin
+ + + +
+
diff --git a/BTCPayServer.Test/Views/_ViewImports.cshtml b/BTCPayServer.Test/Views/_ViewImports.cshtml new file mode 100644 index 000000000..afa82bbf2 --- /dev/null +++ b/BTCPayServer.Test/Views/_ViewImports.cshtml @@ -0,0 +1 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/BTCPayServer.Test/ss.cs b/BTCPayServer.Test/ss.cs new file mode 100644 index 000000000..adfaa8ca5 --- /dev/null +++ b/BTCPayServer.Test/ss.cs @@ -0,0 +1,44 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace BTCPayServer.Test +{ + public class ApplicationPartsLogger : IHostedService + { + private readonly ILogger _logger; + private readonly ApplicationPartManager _partManager; + + public ApplicationPartsLogger(ILogger logger, ApplicationPartManager partManager) + { + _logger = logger; + _partManager = partManager; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + // Get the names of all the application parts. This is the short assembly name for AssemblyParts + var applicationParts = _partManager.ApplicationParts.Select(x => x.Name); + + // Create a controller feature, and populate it from the application parts + var controllerFeature = new ControllerFeature(); + _partManager.PopulateFeature(controllerFeature); + + // Get the names of all of the controllers + var controllers = controllerFeature.Controllers.Select(x => x.Name); + + // Log the application parts and controllers + _logger.LogInformation("Found the following application parts: '{ApplicationParts}' with the following controllers: '{Controllers}'", + string.Join(", ", applicationParts), string.Join(", ", controllers)); + + return Task.CompletedTask; + } + + // Required by the interface + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } +} diff --git a/BTCPayServer/BTCPayExtensions/ExtensionManager.cs b/BTCPayServer/BTCPayExtensions/ExtensionManager.cs new file mode 100644 index 000000000..93e7289a6 --- /dev/null +++ b/BTCPayServer/BTCPayExtensions/ExtensionManager.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Reflection; +using BTCPayServer.Configuration; +using BTCPayServer.Contracts; +using McMaster.NETCore.Plugins; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; + +namespace BTCPayServer +{ + public static class ExtensionManager + { + public const string BTCPayExtensionSuffix =".btcpay"; + private static readonly List _pluginAssemblies = new List(); + private static ILogger _logger; + + public static IMvcBuilder AddExtensions(this IMvcBuilder mvcBuilder, IServiceCollection serviceCollection, + IConfiguration config, ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(typeof(ExtensionManager)); + var extensionsFolder = config.GetExtensionDir(DefaultConfiguration.GetNetworkType(config)); + var extensions = new List(); + + _logger.LogInformation($"Loading extensions from {extensionsFolder}"); + Directory.CreateDirectory(extensionsFolder); + ExecuteCommands(extensionsFolder); + List<(PluginLoader, Assembly, IFileProvider)> plugins = new List<(PluginLoader, Assembly, IFileProvider)>(); + foreach (var dir in Directory.GetDirectories(extensionsFolder)) + { + var pluginName = Path.GetFileName(dir); + + var plugin = PluginLoader.CreateFromAssemblyFile( + Path.Combine(dir, pluginName + ".dll"), // create a plugin from for the .dll file + config => + // this ensures that the version of MVC is shared between this app and the plugin + config.PreferSharedTypes = true); + + mvcBuilder.AddPluginLoader(plugin); + var pluginAssembly = plugin.LoadDefaultAssembly(); + _pluginAssemblies.Add(pluginAssembly); + var fileProvider = CreateEmbeddedFileProviderForAssembly(pluginAssembly); + plugins.Add((plugin, pluginAssembly, fileProvider)); + extensions.AddRange(GetAllExtensionTypesFromAssembly(pluginAssembly) + .Select(GetExtensionInstanceFromType)); + } + + foreach (var extension in extensions) + { + _logger.LogInformation($"Adding and executing extension {extension.Identifier} - {extension.Version}"); + serviceCollection.AddSingleton(extension); + extension.Execute(serviceCollection); + } + + return mvcBuilder; + } + + public static void UseExtensions(this IApplicationBuilder applicationBuilder) + { + foreach (var extension in applicationBuilder.ApplicationServices + .GetServices()) + { + extension.Execute(applicationBuilder, + applicationBuilder.ApplicationServices); + } + + var webHostEnvironment = applicationBuilder.ApplicationServices.GetService(); + List providers = new List() {webHostEnvironment.WebRootFileProvider}; + providers.AddRange( + _pluginAssemblies + .Select(CreateEmbeddedFileProviderForAssembly)); + webHostEnvironment.WebRootFileProvider = new CompositeFileProvider(providers); + } + + private static Type[] GetAllExtensionTypesFromAssembly(Assembly assembly) + { + return assembly.GetTypes().Where(type => + typeof(IBTCPayServerExtension).IsAssignableFrom(type) && + !type.IsAbstract).ToArray(); + } + + private static IBTCPayServerExtension GetExtensionInstanceFromType(Type type) + { + return (IBTCPayServerExtension)Activator.CreateInstance(type, Array.Empty()); + } + + private static IFileProvider CreateEmbeddedFileProviderForAssembly(Assembly assembly) + { + return new EmbeddedFileProvider(assembly); + } + + private static void ExecuteCommands(string extensionsFolder) + { + var pendingCommands = GetPendingCommands(extensionsFolder); + foreach (var command in pendingCommands) + { + ExecuteCommand(command, extensionsFolder); + } + File.Delete(Path.Combine(extensionsFolder, "commands")); + } + + private static void ExecuteCommand((string command, string extension) command, string extensionsFolder) + { + var dirName = Path.Combine(extensionsFolder, command.extension); + switch (command.command) + { + case "delete": + if (Directory.Exists(dirName)) + { + Directory.Delete(dirName, true); + } + break; + case "install": + var fileName = dirName + BTCPayExtensionSuffix; + if (File.Exists(fileName)) + { + ZipFile.ExtractToDirectory(fileName, dirName, true); + File.Delete(fileName); + } + break; + } + } + + public static (string command, string extension)[] GetPendingCommands(string extensionsFolder) + { + if (!File.Exists(Path.Combine(extensionsFolder, "commands"))) + return Array.Empty<(string command, string extension)>(); + var commands = File.ReadAllLines(Path.Combine(extensionsFolder, "commands")); + return commands.Select(s => + { + var split = s.Split(':'); + return (split[0].ToLower(CultureInfo.InvariantCulture), split[1]); + }).ToArray(); + } + + public static void QueueCommands(string extensionsFolder, params ( string action, string extension)[] commands) + { + File.AppendAllLines(Path.Combine(extensionsFolder, "commands"), + commands.Select((tuple) => $"{tuple.action}:{tuple.extension}")); + } + + public static void CancelCommands(string extensionDir, string extension) + { + var cmds = GetPendingCommands(extensionDir).Where(tuple => + !tuple.extension.Equals(extension, StringComparison.InvariantCultureIgnoreCase)).ToArray(); + + File.Delete(Path.Combine(extensionDir, "commands")); + QueueCommands(extensionDir, cmds); + + } + } +} diff --git a/BTCPayServer/BTCPayExtensions/ExtensionService.cs b/BTCPayServer/BTCPayExtensions/ExtensionService.cs new file mode 100644 index 000000000..1757bcab8 --- /dev/null +++ b/BTCPayServer/BTCPayExtensions/ExtensionService.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using BTCPayServer.Configuration; +using BTCPayServer.Contracts; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; + +namespace BTCPayServer +{ + public class ExtensionService + { + private readonly BTCPayServerOptions _btcPayServerOptions; + private readonly HttpClient _githubClient; + + public ExtensionService(IEnumerable btcPayServerExtensions, + IHttpClientFactory httpClientFactory, BTCPayServerOptions btcPayServerOptions) + { + LoadedExtensions = btcPayServerExtensions; + _githubClient = httpClientFactory.CreateClient(); + _githubClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("btcpayserver", "1")); + _btcPayServerOptions = btcPayServerOptions; + } + + public IEnumerable LoadedExtensions { get; } + + public async Task> GetRemoteExtensions(string remote) + { + var resp = await _githubClient + .GetStringAsync(new Uri($"https://api.github.com/repos/{remote}/contents")); + var files = JsonConvert.DeserializeObject(resp); + return await Task.WhenAll(files.Where(file => file.Name.EndsWith($"{ExtensionManager.BTCPayExtensionSuffix}.json", StringComparison.InvariantCulture)).Select(async file => + { + return await _githubClient.GetStringAsync(file.DownloadUrl).ContinueWith( + task => JsonConvert.DeserializeObject(task.Result), TaskScheduler.Current); + })); + } + + public async Task DownloadRemoteExtension(string remote, string extension) + { + var dest = _btcPayServerOptions.ExtensionDir; + var resp = await _githubClient + .GetStringAsync(new Uri($"https://api.github.com/repos/{remote}/contents")); + var files = JsonConvert.DeserializeObject(resp); + var ext = files.SingleOrDefault(file => file.Name == $"{extension}{ExtensionManager.BTCPayExtensionSuffix}"); + if (ext is null) + { + throw new Exception("Extension not found on remote"); + } + + var filedest = Path.Combine(dest, ext.Name); + Directory.CreateDirectory(Path.GetDirectoryName(filedest)); + new WebClient().DownloadFile(new Uri(ext.DownloadUrl), filedest); + } + + public void InstallExtension(string extension) + { + var dest = _btcPayServerOptions.ExtensionDir; + UninstallExtension(extension); + ExtensionManager.QueueCommands(dest, ("install", extension)); + } + + public async Task UploadExtension(IFormFile extension) + { + var dest = _btcPayServerOptions.ExtensionDir; + var filedest = Path.Combine(dest, extension.FileName); + Directory.CreateDirectory(Path.GetDirectoryName(filedest)); + if (Path.GetExtension(filedest) == ExtensionManager.BTCPayExtensionSuffix) + { + await using var stream = new FileStream(filedest, FileMode.Create); + await extension.CopyToAsync(stream); + } + } + + public void UninstallExtension(string extension) + { + var dest = _btcPayServerOptions.ExtensionDir; + ExtensionManager.QueueCommands(dest, ("delete", extension)); + } + + public class AvailableExtension : IBTCPayServerExtension + { + public string Identifier { get; set; } + public string Name { get; set; } + public Version Version { get; set; } + public string Description { get; set; } + + public void Execute(IApplicationBuilder applicationBuilder, + IServiceProvider applicationBuilderApplicationServices) + { + } + + public void Execute(IServiceCollection applicationBuilder) + { + } + } + + class GithubFile + { + [JsonProperty("name")] public string Name { get; set; } + + [JsonProperty("sha")] public string Sha { get; set; } + + [JsonProperty("download_url")] public string DownloadUrl { get; set; } + } + + public (string command, string extension)[] GetPendingCommands() + { + return ExtensionManager.GetPendingCommands(_btcPayServerOptions.ExtensionDir); + } + + public void CancelCommands(string extension) + { + ExtensionManager.CancelCommands(_btcPayServerOptions.ExtensionDir, extension); + } + } +} diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 49a852827..213bc917d 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -4,8 +4,11 @@ Exe + + true + @@ -52,6 +55,7 @@ + all @@ -141,6 +145,7 @@ + diff --git a/BTCPayServer/Components/NotificationsDropdown/NotificationSummaryViewModel.cs b/BTCPayServer/Components/NotificationsDropdown/NotificationSummaryViewModel.cs index 41eaf556c..8f135903d 100644 --- a/BTCPayServer/Components/NotificationsDropdown/NotificationSummaryViewModel.cs +++ b/BTCPayServer/Components/NotificationsDropdown/NotificationSummaryViewModel.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using BTCPayServer.Contracts; using BTCPayServer.Models.NotificationViewModels; namespace BTCPayServer.Components.NotificationsDropdown diff --git a/BTCPayServer/Configuration/BTCPayServerOptions.cs b/BTCPayServer/Configuration/BTCPayServerOptions.cs index 5d546f292..1adc7f0e8 100644 --- a/BTCPayServer/Configuration/BTCPayServerOptions.cs +++ b/BTCPayServer/Configuration/BTCPayServerOptions.cs @@ -80,6 +80,7 @@ namespace BTCPayServer.Configuration { NetworkType = DefaultConfiguration.GetNetworkType(conf); DataDir = conf.GetDataDir(NetworkType); + ExtensionDir = conf.GetExtensionDir(NetworkType); Logs.Configuration.LogInformation("Network: " + NetworkType.ToString()); if (conf.GetOrDefault("launchsettings", false) && NetworkType != NetworkType.Regtest) @@ -166,6 +167,7 @@ namespace BTCPayServer.Configuration PostgresConnectionString = conf.GetOrDefault("postgres", null); MySQLConnectionString = conf.GetOrDefault("mysql", null); BundleJsCss = conf.GetOrDefault("bundlejscss", true); + DockerDeployment = conf.GetOrDefault("dockerdeployment", true); AllowAdminRegistration = conf.GetOrDefault("allow-admin-registration", false); TorrcFile = conf.GetOrDefault("torrcfile", null); @@ -239,6 +241,8 @@ namespace BTCPayServer.Configuration DisableRegistration = conf.GetOrDefault("disable-registration", true); } + public string ExtensionDir { get; set; } + private SSHSettings ParseSSHConfiguration(IConfiguration conf) { var settings = new SSHSettings(); @@ -281,6 +285,7 @@ namespace BTCPayServer.Configuration public ExternalServices ExternalServices { get; set; } = new ExternalServices(); public BTCPayNetworkProvider NetworkProvider { get; set; } + public bool DockerDeployment { get; set; } public string PostgresConnectionString { get; diff --git a/BTCPayServer/Configuration/ConfigurationExtensions.cs b/BTCPayServer/Configuration/ConfigurationExtensions.cs index ec91e7e4f..a82f8f783 100644 --- a/BTCPayServer/Configuration/ConfigurationExtensions.cs +++ b/BTCPayServer/Configuration/ConfigurationExtensions.cs @@ -67,5 +67,11 @@ namespace BTCPayServer.Configuration var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(networkType); return configuration.GetOrDefault("datadir", defaultSettings.DefaultDataDirectory); } + + public static string GetExtensionDir(this IConfiguration configuration, NetworkType networkType) + { + var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(networkType); + return configuration.GetOrDefault("extensiondir", defaultSettings.DefaultExtensionDirectory); + } } } diff --git a/BTCPayServer/Contracts/IStoreNavExtension.cs b/BTCPayServer/Contracts/IStoreNavExtension.cs deleted file mode 100644 index 2a642a440..000000000 --- a/BTCPayServer/Contracts/IStoreNavExtension.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace BTCPayServer.Contracts -{ - public interface IStoreNavExtension - { - string Partial { get; } - } -} diff --git a/BTCPayServer/Controllers/ServerController.Extensions.cs b/BTCPayServer/Controllers/ServerController.Extensions.cs new file mode 100644 index 000000000..63d35074c --- /dev/null +++ b/BTCPayServer/Controllers/ServerController.Extensions.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Configuration; +using BTCPayServer.Contracts; +using BTCPayServer.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace BTCPayServer.Controllers +{ + public partial class ServerController + { + [HttpGet("server/extensions")] + public async Task ListExtensions( + [FromServices] ExtensionService extensionService, + [FromServices] BTCPayServerOptions btcPayServerOptions, + string remote = "kukks/btcpayserver-extensions") + { + var res = new ListExtensionsViewModel() + { + Installed = extensionService.LoadedExtensions, + Available = await extensionService.GetRemoteExtensions(remote), + Remote = remote, + Commands = extensionService.GetPendingCommands(), + CanShowRestart = btcPayServerOptions.DockerDeployment + }; + return View(res); + } + + public class ListExtensionsViewModel + { + public string Remote { get; set; } + public IEnumerable Installed { get; set; } + public IEnumerable Available { get; set; } + public (string command, string extension)[] Commands { get; set; } + public bool CanShowRestart { get; set; } + } + + [HttpPost("server/extensions/uninstall")] + public IActionResult UnInstallExtension( + [FromServices] ExtensionService extensionService, string extension) + { + extensionService.UninstallExtension(extension); + TempData.SetStatusMessageModel(new StatusMessageModel() + { + Message = "Extension scheduled to be uninstalled", + Severity = StatusMessageModel.StatusSeverity.Success + }); + + return RedirectToAction("ListExtensions"); + } + [HttpPost("server/extensions/cancel")] + public IActionResult CancelExtensionCommands( + [FromServices] ExtensionService extensionService, string extension) + { + extensionService.CancelCommands(extension); + TempData.SetStatusMessageModel(new StatusMessageModel() + { + Message = "Updated", + Severity = StatusMessageModel.StatusSeverity.Success + }); + + return RedirectToAction("ListExtensions"); + } + + [HttpPost("server/extensions/install")] + public async Task InstallExtension( + [FromServices] ExtensionService extensionService, string remote, string extension) + { + try + { + await extensionService.DownloadRemoteExtension(remote, extension); + extensionService.InstallExtension(extension); + TempData.SetStatusMessageModel(new StatusMessageModel() + { + Message = "Extension scheduled to be installed.", + Severity = StatusMessageModel.StatusSeverity.Success + }); + } + catch (Exception e) + { + TempData.SetStatusMessageModel(new StatusMessageModel() + { + Message = e.Message, Severity = StatusMessageModel.StatusSeverity.Error + }); + } + + return RedirectToAction("ListExtensions"); + } + + + [HttpPost("server/extensions/upload")] + public async Task UploadExtension([FromServices] ExtensionService extensionService, + List files) + { + foreach (var formFile in files.Where(file => file.Length > 0)) + { + await extensionService.UploadExtension(formFile); + extensionService.InstallExtension(formFile.FileName.TrimEnd(ExtensionManager.BTCPayExtensionSuffix, + StringComparison.InvariantCultureIgnoreCase)); + } + + return RedirectToAction("ListExtensions", + new {StatusMessage = "Files uploaded, restart server to load extensions"}); + } + } +} diff --git a/BTCPayServer/Controllers/ServerController.cs b/BTCPayServer/Controllers/ServerController.cs index 872b88ed6..ade6bd002 100644 --- a/BTCPayServer/Controllers/ServerController.cs +++ b/BTCPayServer/Controllers/ServerController.cs @@ -105,6 +105,12 @@ namespace BTCPayServer.Controllers public async Task Maintenance(MaintenanceViewModel vm, string command) { vm.CanUseSSH = _sshState.CanUseSSH; + + if (!vm.CanUseSSH) + { + TempData[WellKnownTempData.ErrorMessage] = "Maintenance feature requires access to SSH properly configured in BTCPayServer configuration."; + return View(vm); + } if (!ModelState.IsValid) return View(vm); if (command == "changedomain") @@ -182,6 +188,13 @@ namespace BTCPayServer.Controllers return error; TempData[WellKnownTempData.SuccessMessage] = $"The old docker images will be cleaned soon..."; } + else if (command == "restart") + { + var error = await RunSSH(vm, $"btcpay-restart.sh"); + if (error != null) + return error; + TempData[WellKnownTempData.SuccessMessage] = $"BTCPay will restart momentarily."; + } else { return NotFound(); diff --git a/BTCPayServer/Extensions/StringExtensions.cs b/BTCPayServer/Extensions/StringExtensions.cs new file mode 100644 index 000000000..9ff3ac485 --- /dev/null +++ b/BTCPayServer/Extensions/StringExtensions.cs @@ -0,0 +1,18 @@ +using System; + +namespace BTCPayServer +{ + public static class StringExtensions + { + public static string TrimEnd(this string input, string suffixToRemove, + StringComparison comparisonType) + { + if (input != null && suffixToRemove != null + && input.EndsWith(suffixToRemove, comparisonType)) + { + return input.Substring(0, input.Length - suffixToRemove.Length); + } + else return input; + } + } +} diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index e6efea426..2420447cd 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -139,6 +139,7 @@ namespace BTCPayServer.Hosting }); services.TryAddSingleton(); + services.AddSingleton(); services.TryAddTransient(); services.TryAddSingleton(o => { diff --git a/BTCPayServer/Hosting/Startup.cs b/BTCPayServer/Hosting/Startup.cs index dfe61389c..45e48de00 100644 --- a/BTCPayServer/Hosting/Startup.cs +++ b/BTCPayServer/Hosting/Startup.cs @@ -56,7 +56,7 @@ namespace BTCPayServer.Hosting services.AddProviderStorage(); services.AddSession(); services.AddSignalR(); - services.AddMvc(o => + var mvcBuilder= services.AddMvc(o => { o.Filters.Add(new XFrameOptionsAttribute("DENY")); o.Filters.Add(new XContentTypeOptionsAttribute("nosniff")); @@ -91,7 +91,11 @@ namespace BTCPayServer.Hosting #if RAZOR_RUNTIME_COMPILE .AddRazorRuntimeCompilation() #endif + .AddExtensions(services, Configuration, LoggerFactory) .AddControllersAsServices(); + + + services.TryAddScoped(); services.Configure(options => { @@ -174,6 +178,7 @@ namespace BTCPayServer.Hosting private static void ConfigureCore(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider prov, ILoggerFactory loggerFactory, BTCPayServerOptions options) { Logs.Configure(loggerFactory); + app.UseExtensions(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); diff --git a/BTCPayServer/Models/NotificationViewModels/IndexViewModel.cs b/BTCPayServer/Models/NotificationViewModels/IndexViewModel.cs index b3cb8671a..4183c9ed7 100644 --- a/BTCPayServer/Models/NotificationViewModels/IndexViewModel.cs +++ b/BTCPayServer/Models/NotificationViewModels/IndexViewModel.cs @@ -1,5 +1,5 @@ -using System; using System.Collections.Generic; +using BTCPayServer.Contracts; namespace BTCPayServer.Models.NotificationViewModels { @@ -11,12 +11,5 @@ namespace BTCPayServer.Models.NotificationViewModels public List Items { get; set; } } - public class NotificationViewModel - { - public string Id { get; set; } - public DateTimeOffset Created { get; set; } - public string Body { get; set; } - public string ActionLink { get; set; } - public bool Seen { get; set; } - } + } diff --git a/BTCPayServer/Properties/launchSettings.json b/BTCPayServer/Properties/launchSettings.json index e5c90e382..04700235b 100644 --- a/BTCPayServer/Properties/launchSettings.json +++ b/BTCPayServer/Properties/launchSettings.json @@ -20,7 +20,8 @@ "BTCPAY_DEBUGLOG": "debug.log", "BTCPAY_TORRCFILE": "../BTCPayServer.Tests/TestData/Tor/torrc", "BTCPAY_SOCKSENDPOINT": "localhost:9050", - "BTCPAY_UPDATEURL": "" + "BTCPAY_UPDATEURL": "", + "BTCPAY_DOCKERDEPLOYMENT": "true" }, "applicationUrl": "http://127.0.0.1:14142/" }, @@ -51,7 +52,8 @@ "BTCPAY_SSHPASSWORD": "opD3i2282D", "BTCPAY_DEBUGLOG": "debug.log", "BTCPAY_TORRCFILE": "../BTCPayServer.Tests/TestData/Tor/torrc", - "BTCPAY_SOCKSENDPOINT": "localhost:9050" + "BTCPAY_SOCKSENDPOINT": "localhost:9050", + "BTCPAY_DOCKERDEPLOYMENT": "true" }, "applicationUrl": "https://localhost:14142/" }, @@ -84,7 +86,8 @@ "BTCPAY_SSHPASSWORD": "opD3i2282D", "BTCPAY_DEBUGLOG": "debug.log", "BTCPAY_TORRCFILE": "../BTCPayServer.Tests/TestData/Tor/torrc", - "BTCPAY_SOCKSENDPOINT": "localhost:9050" + "BTCPAY_SOCKSENDPOINT": "localhost:9050", + "BTCPAY_DOCKERDEPLOYMENT": "true" }, "applicationUrl": "https://localhost:14142/" } diff --git a/BTCPayServer/Services/Altcoins/Ethereum/EthereumLikeExtensions.cs b/BTCPayServer/Services/Altcoins/Ethereum/EthereumLikeExtensions.cs index f0890da42..781747a40 100644 --- a/BTCPayServer/Services/Altcoins/Ethereum/EthereumLikeExtensions.cs +++ b/BTCPayServer/Services/Altcoins/Ethereum/EthereumLikeExtensions.cs @@ -21,7 +21,7 @@ namespace BTCPayServer.Services.Altcoins.Ethereum serviceCollection.AddSingleton(provider => provider.GetService()); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(provider => provider.GetService()); - serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddTransient(); serviceCollection.AddSingleton(); serviceCollection.AddHttpClient(EthereumInvoiceCreateHttpClient) diff --git a/BTCPayServer/Services/Altcoins/Ethereum/EthereumStoreNavExtension.cs b/BTCPayServer/Services/Altcoins/Ethereum/EthereumStoreNavExtension.cs index 8a2ec25be..e148d4d11 100644 --- a/BTCPayServer/Services/Altcoins/Ethereum/EthereumStoreNavExtension.cs +++ b/BTCPayServer/Services/Altcoins/Ethereum/EthereumStoreNavExtension.cs @@ -3,9 +3,10 @@ using BTCPayServer.Contracts; namespace BTCPayServer.Services.Altcoins.Ethereum { - public class EthereumStoreNavExtension: IStoreNavExtension + public class EthereumNavExtension: INavExtension { public string Partial { get; } = "Ethereum/StoreNavEthereumExtension"; + public string Location { get; } = "store"; } } #endif diff --git a/BTCPayServer/Services/Altcoins/Monero/MoneroLikeExtensions.cs b/BTCPayServer/Services/Altcoins/Monero/MoneroLikeExtensions.cs index 527e2829d..4be1c4a08 100644 --- a/BTCPayServer/Services/Altcoins/Monero/MoneroLikeExtensions.cs +++ b/BTCPayServer/Services/Altcoins/Monero/MoneroLikeExtensions.cs @@ -23,7 +23,7 @@ namespace BTCPayServer.Services.Altcoins.Monero serviceCollection.AddHostedService(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(provider => provider.GetService()); - serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); return serviceCollection; diff --git a/BTCPayServer/Services/Altcoins/Monero/MoneroStoreNavExtension.cs b/BTCPayServer/Services/Altcoins/Monero/MoneroStoreNavExtension.cs index 375951f14..8b5028191 100644 --- a/BTCPayServer/Services/Altcoins/Monero/MoneroStoreNavExtension.cs +++ b/BTCPayServer/Services/Altcoins/Monero/MoneroStoreNavExtension.cs @@ -3,9 +3,10 @@ using BTCPayServer.Contracts; namespace BTCPayServer.Services.Altcoins.Monero { - public class MoneroStoreNavExtension : IStoreNavExtension + public class MoneroNavExtension : INavExtension { public string Partial { get; } = "Monero/StoreNavMoneroExtension"; + public string Location { get; } = "store"; } } #endif diff --git a/BTCPayServer/Services/Notifications/Blobs/InvoiceEventNotification.cs b/BTCPayServer/Services/Notifications/Blobs/InvoiceEventNotification.cs index 2374f166a..2edbd2b46 100644 --- a/BTCPayServer/Services/Notifications/Blobs/InvoiceEventNotification.cs +++ b/BTCPayServer/Services/Notifications/Blobs/InvoiceEventNotification.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using BTCPayServer.Configuration; +using BTCPayServer.Contracts; using BTCPayServer.Controllers; using BTCPayServer.Events; using BTCPayServer.Models.NotificationViewModels; diff --git a/BTCPayServer/Services/Notifications/Blobs/JunkNotification.cs b/BTCPayServer/Services/Notifications/Blobs/JunkNotification.cs index eeecb3df4..c5149ef68 100644 --- a/BTCPayServer/Services/Notifications/Blobs/JunkNotification.cs +++ b/BTCPayServer/Services/Notifications/Blobs/JunkNotification.cs @@ -1,5 +1,5 @@ #if DEBUG -using BTCPayServer.Models.NotificationViewModels; +using BTCPayServer.Contracts; namespace BTCPayServer.Services.Notifications.Blobs { diff --git a/BTCPayServer/Services/Notifications/Blobs/NewVersionNotification.cs b/BTCPayServer/Services/Notifications/Blobs/NewVersionNotification.cs index 3dcfbb7a5..ad6029e44 100644 --- a/BTCPayServer/Services/Notifications/Blobs/NewVersionNotification.cs +++ b/BTCPayServer/Services/Notifications/Blobs/NewVersionNotification.cs @@ -1,3 +1,4 @@ +using BTCPayServer.Contracts; using BTCPayServer.Models.NotificationViewModels; namespace BTCPayServer.Services.Notifications.Blobs diff --git a/BTCPayServer/Services/Notifications/Blobs/PayoutNotification.cs b/BTCPayServer/Services/Notifications/Blobs/PayoutNotification.cs index df70ac3fe..59719e953 100644 --- a/BTCPayServer/Services/Notifications/Blobs/PayoutNotification.cs +++ b/BTCPayServer/Services/Notifications/Blobs/PayoutNotification.cs @@ -1,4 +1,5 @@ using BTCPayServer.Configuration; +using BTCPayServer.Contracts; using BTCPayServer.Controllers; using BTCPayServer.Models.NotificationViewModels; using Microsoft.AspNetCore.Routing; diff --git a/BTCPayServer/Services/Notifications/INotificationHandler.cs b/BTCPayServer/Services/Notifications/INotificationHandler.cs index e22911ddf..204870365 100644 --- a/BTCPayServer/Services/Notifications/INotificationHandler.cs +++ b/BTCPayServer/Services/Notifications/INotificationHandler.cs @@ -1,14 +1,10 @@ using System; +using BTCPayServer.Contracts; using BTCPayServer.Models.NotificationViewModels; namespace BTCPayServer.Services.Notifications { - public interface INotificationHandler - { - string NotificationType { get; } - Type NotificationBlobType { get; } - void FillViewModel(object notification, NotificationViewModel vm); - } + public abstract class NotificationHandler : INotificationHandler { public abstract string NotificationType { get; } diff --git a/BTCPayServer/Services/Notifications/NotificationManager.cs b/BTCPayServer/Services/Notifications/NotificationManager.cs index 36379732a..e5d350582 100644 --- a/BTCPayServer/Services/Notifications/NotificationManager.cs +++ b/BTCPayServer/Services/Notifications/NotificationManager.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using BTCPayServer.Components.NotificationsDropdown; +using BTCPayServer.Contracts; using BTCPayServer.Data; using BTCPayServer.Models.NotificationViewModels; using Microsoft.AspNetCore.Identity; diff --git a/BTCPayServer/Views/Server/ListExtensions.cshtml b/BTCPayServer/Views/Server/ListExtensions.cshtml new file mode 100644 index 000000000..394c4bd27 --- /dev/null +++ b/BTCPayServer/Views/Server/ListExtensions.cshtml @@ -0,0 +1,153 @@ +@model BTCPayServer.Controllers.ServerController.ListExtensionsViewModel +@{ + ViewData.SetActivePageAndTitle(ServerNavPages.Extensions); + var installed = Model.Installed.Select(extension => extension.Identifier); + var availableAndNotInstalled = Model.Available.Where(extension => !installed.Contains(extension.Identifier)); +} + +@if (Model.Commands.Any()) +{ +
+ You need to restart BTCPay Server in order to update your active extensions. + @if (Model.CanShowRestart) + { +
+ +
+ } +
+} + + + +@if (Model.Installed.Any()) +{ +

Installed Extensions

+
+ + @foreach (var extension in Model.Installed) + { + var matchedAvailable = Model.Available.SingleOrDefault(availableExtension => availableExtension.Identifier == extension.Identifier); +
+
+

+ @extension.Name @extension.Version +

+

@extension.Description

+ +
+ @if (matchedAvailable != null) + { +
    +
  • + Current version + @extension.Version +
  • +
  • + Remote version + @matchedAvailable.Version +
  • +
+ + } + +
+ } +
+} +@if (availableAndNotInstalled.Any()) +{ +

Available Extensions

+ +
+ @foreach (var extension in availableAndNotInstalled) + { +
+
+

+ @extension.Name @extension.Version +

+

@extension.Description

+
+ +
+ } +
+} + + +
+
+
+

Add extension manually

+
This is an extremely dangerous operation. Do not upload extensions from someone that you do not trust.
+
+
+ + +
+
+
+
+
+ +@section Scripts { + +} diff --git a/BTCPayServer/Views/Server/Maintenance.cshtml b/BTCPayServer/Views/Server/Maintenance.cshtml index 535c05a5c..c94c86d33 100644 --- a/BTCPayServer/Views/Server/Maintenance.cshtml +++ b/BTCPayServer/Views/Server/Maintenance.cshtml @@ -33,6 +33,16 @@ + +
Restart
+

Restart BTCPay server and related services.

+
+
+ +
+
+ +
Clean

Delete unused docker images present on your system.

diff --git a/BTCPayServer/Views/Server/ServerNavPages.cs b/BTCPayServer/Views/Server/ServerNavPages.cs index 043864e01..e1d4d66fb 100644 --- a/BTCPayServer/Views/Server/ServerNavPages.cs +++ b/BTCPayServer/Views/Server/ServerNavPages.cs @@ -2,6 +2,6 @@ namespace BTCPayServer.Views.Server { public enum ServerNavPages { - Index, Users, Emails, Policies, Theme, Services, Maintenance, Logs, Files + Index, Users, Emails, Policies, Theme, Services, Maintenance, Logs, Files, Extensions } } diff --git a/BTCPayServer/Views/Server/_Nav.cshtml b/BTCPayServer/Views/Server/_Nav.cshtml index 5553d6c86..6b18d4db1 100644 --- a/BTCPayServer/Views/Server/_Nav.cshtml +++ b/BTCPayServer/Views/Server/_Nav.cshtml @@ -1,10 +1,16 @@ -