2020-10-21 14:02:20 +02:00
|
|
|
using System;
|
|
|
|
using System.Collections.Generic;
|
|
|
|
using System.Globalization;
|
|
|
|
using System.IO;
|
|
|
|
using System.IO.Compression;
|
|
|
|
using System.Linq;
|
|
|
|
using System.Reflection;
|
2020-11-17 13:46:23 +01:00
|
|
|
using BTCPayServer.Abstractions.Contracts;
|
2020-10-21 14:02:20 +02:00
|
|
|
using BTCPayServer.Configuration;
|
|
|
|
using McMaster.NETCore.Plugins;
|
|
|
|
using Microsoft.AspNetCore.Builder;
|
|
|
|
using Microsoft.AspNetCore.Hosting;
|
2021-02-15 13:42:08 +01:00
|
|
|
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
2020-10-21 14:02:20 +02:00
|
|
|
using Microsoft.Extensions.Configuration;
|
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
using Microsoft.Extensions.FileProviders;
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
|
|
|
namespace BTCPayServer.Plugins
|
|
|
|
{
|
|
|
|
public static class PluginManager
|
|
|
|
{
|
|
|
|
public const string BTCPayPluginSuffix = ".btcpay";
|
|
|
|
private static readonly List<Assembly> _pluginAssemblies = new List<Assembly>();
|
2021-04-01 05:27:22 +02:00
|
|
|
private static readonly List<PluginLoader> _plugins = new List<PluginLoader>();
|
2020-10-21 14:02:20 +02:00
|
|
|
private static ILogger _logger;
|
|
|
|
|
2021-07-08 12:53:34 +02:00
|
|
|
private static List<(PluginLoader, Assembly, IFileProvider)> loadedPlugins;
|
2021-04-01 05:27:22 +02:00
|
|
|
public static bool IsExceptionByPlugin(Exception exception)
|
|
|
|
{
|
2021-04-28 09:49:10 +02:00
|
|
|
return _pluginAssemblies.Any(assembly => assembly?.FullName?.Contains(exception.Source!, StringComparison.OrdinalIgnoreCase) is true);
|
2021-04-01 05:27:22 +02:00
|
|
|
}
|
2020-10-21 14:02:20 +02:00
|
|
|
public static IMvcBuilder AddPlugins(this IMvcBuilder mvcBuilder, IServiceCollection serviceCollection,
|
|
|
|
IConfiguration config, ILoggerFactory loggerFactory)
|
|
|
|
{
|
|
|
|
_logger = loggerFactory.CreateLogger(typeof(PluginManager));
|
2021-01-06 15:51:13 +01:00
|
|
|
var pluginsFolder = new DataDirectories().Configure(config).PluginDir;
|
2020-10-21 14:02:20 +02:00
|
|
|
var plugins = new List<IBTCPayServerPlugin>();
|
|
|
|
|
2021-02-15 13:42:08 +01:00
|
|
|
serviceCollection.Configure<KestrelServerOptions>(options =>
|
|
|
|
{
|
|
|
|
options.Limits.MaxRequestBodySize = int.MaxValue; // if don't set default value is: 30 MB
|
|
|
|
});
|
2020-10-21 14:02:20 +02:00
|
|
|
_logger.LogInformation($"Loading plugins from {pluginsFolder}");
|
|
|
|
Directory.CreateDirectory(pluginsFolder);
|
|
|
|
ExecuteCommands(pluginsFolder);
|
2021-07-08 12:53:34 +02:00
|
|
|
loadedPlugins = new List<(PluginLoader, Assembly, IFileProvider)>();
|
|
|
|
var systemPlugins = GetDefaultLoadedPluginAssemblies();
|
|
|
|
|
|
|
|
foreach (Assembly systemExtension in systemPlugins)
|
2020-10-21 14:02:20 +02:00
|
|
|
{
|
2021-07-08 12:53:34 +02:00
|
|
|
var detectedPlugins = GetAllPluginTypesFromAssembly(systemExtension).Select(GetPluginInstanceFromType);
|
|
|
|
if (!detectedPlugins.Any())
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
|
|
|
|
}
|
2021-08-04 16:50:25 +02:00
|
|
|
|
|
|
|
detectedPlugins = detectedPlugins.Select(plugin =>
|
2021-07-08 12:53:34 +02:00
|
|
|
{
|
2021-08-04 16:50:25 +02:00
|
|
|
plugin.SystemPlugin = true;
|
|
|
|
return plugin;
|
|
|
|
});
|
|
|
|
|
|
|
|
loadedPlugins.Add((null,systemExtension, CreateEmbeddedFileProviderForAssembly(systemExtension)));
|
2021-07-08 12:53:34 +02:00
|
|
|
plugins.AddRange(detectedPlugins);
|
2020-10-21 14:02:20 +02:00
|
|
|
}
|
2020-11-05 15:43:14 +01:00
|
|
|
var orderFilePath = Path.Combine(pluginsFolder, "order");
|
2021-04-01 05:27:22 +02:00
|
|
|
|
2020-11-05 15:43:14 +01:00
|
|
|
var availableDirs = Directory.GetDirectories(pluginsFolder);
|
|
|
|
var orderedDirs = new List<string>();
|
|
|
|
if (File.Exists(orderFilePath))
|
|
|
|
{
|
|
|
|
var order = File.ReadLines(orderFilePath);
|
|
|
|
foreach (var s in order)
|
|
|
|
{
|
|
|
|
if (availableDirs.Contains(s))
|
|
|
|
{
|
|
|
|
orderedDirs.Add(s);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
orderedDirs.AddRange(availableDirs.Where(s => !orderedDirs.Contains(s)));
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
orderedDirs = availableDirs.ToList();
|
|
|
|
}
|
|
|
|
|
2021-04-01 05:27:22 +02:00
|
|
|
var disabledPlugins = GetDisabledPlugins(pluginsFolder);
|
2021-05-03 08:35:54 +02:00
|
|
|
|
2020-11-05 15:43:14 +01:00
|
|
|
foreach (var dir in orderedDirs)
|
2020-10-21 14:02:20 +02:00
|
|
|
{
|
|
|
|
var pluginName = Path.GetFileName(dir);
|
2021-04-20 08:38:37 +02:00
|
|
|
var pluginFilePath = Path.Combine(dir, pluginName + ".dll");
|
2021-04-01 05:27:22 +02:00
|
|
|
if (disabledPlugins.Contains(pluginName))
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
2021-04-20 08:38:37 +02:00
|
|
|
if (!File.Exists(pluginFilePath))
|
|
|
|
{
|
|
|
|
_logger.LogError(
|
|
|
|
$"Error when loading plugin {pluginName} - {pluginFilePath} does not exist");
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2021-05-03 08:35:54 +02:00
|
|
|
try
|
|
|
|
{
|
|
|
|
|
|
|
|
var plugin = PluginLoader.CreateFromAssemblyFile(
|
|
|
|
pluginFilePath, // 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;
|
|
|
|
config.IsUnloadable = true;
|
|
|
|
});
|
|
|
|
|
|
|
|
mvcBuilder.AddPluginLoader(plugin);
|
|
|
|
var pluginAssembly = plugin.LoadDefaultAssembly();
|
|
|
|
_pluginAssemblies.Add(pluginAssembly);
|
|
|
|
_plugins.Add(plugin);
|
|
|
|
var fileProvider = CreateEmbeddedFileProviderForAssembly(pluginAssembly);
|
|
|
|
loadedPlugins.Add((plugin, pluginAssembly, fileProvider));
|
|
|
|
plugins.AddRange(GetAllPluginTypesFromAssembly(pluginAssembly)
|
|
|
|
.Select(GetPluginInstanceFromType));
|
|
|
|
|
|
|
|
}
|
|
|
|
catch (Exception e)
|
|
|
|
{
|
|
|
|
_logger.LogError(e,
|
|
|
|
$"Error when loading plugin {pluginName}");
|
|
|
|
}
|
2020-10-21 14:02:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
foreach (var plugin in plugins)
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
|
|
|
_logger.LogInformation(
|
|
|
|
$"Adding and executing plugin {plugin.Identifier} - {plugin.Version}");
|
|
|
|
plugin.Execute(serviceCollection);
|
|
|
|
serviceCollection.AddSingleton(plugin);
|
|
|
|
}
|
|
|
|
catch (Exception e)
|
|
|
|
{
|
2020-11-05 15:43:14 +01:00
|
|
|
_logger.LogError(
|
|
|
|
$"Error when loading plugin {plugin.Identifier} - {plugin.Version}{Environment.NewLine}{e.Message}");
|
2020-10-21 14:02:20 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return mvcBuilder;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void UsePlugins(this IApplicationBuilder applicationBuilder)
|
|
|
|
{
|
|
|
|
foreach (var extension in applicationBuilder.ApplicationServices
|
|
|
|
.GetServices<IBTCPayServerPlugin>())
|
|
|
|
{
|
|
|
|
extension.Execute(applicationBuilder,
|
|
|
|
applicationBuilder.ApplicationServices);
|
|
|
|
}
|
|
|
|
|
|
|
|
var webHostEnvironment = applicationBuilder.ApplicationServices.GetService<IWebHostEnvironment>();
|
|
|
|
List<IFileProvider> providers = new List<IFileProvider>() {webHostEnvironment.WebRootFileProvider};
|
2021-07-08 12:53:34 +02:00
|
|
|
providers.AddRange(loadedPlugins.Select(tuple => tuple.Item3));
|
2020-10-21 14:02:20 +02:00
|
|
|
webHostEnvironment.WebRootFileProvider = new CompositeFileProvider(providers);
|
|
|
|
}
|
2020-11-05 15:43:14 +01:00
|
|
|
|
2020-10-21 14:02:20 +02:00
|
|
|
private static Assembly[] GetDefaultLoadedPluginAssemblies()
|
|
|
|
{
|
2020-11-05 15:43:14 +01:00
|
|
|
return AppDomain.CurrentDomain.GetAssemblies()
|
2020-10-21 14:02:20 +02:00
|
|
|
.ToArray();
|
|
|
|
}
|
|
|
|
|
|
|
|
private static Type[] GetAllPluginTypesFromAssembly(Assembly assembly)
|
|
|
|
{
|
|
|
|
return assembly.GetTypes().Where(type =>
|
2020-11-05 15:43:14 +01:00
|
|
|
typeof(IBTCPayServerPlugin).IsAssignableFrom(type) && type != typeof(PluginService.AvailablePlugin) &&
|
2020-10-21 14:02:20 +02:00
|
|
|
!type.IsAbstract).ToArray();
|
|
|
|
}
|
|
|
|
|
|
|
|
private static IBTCPayServerPlugin GetPluginInstanceFromType(Type type)
|
|
|
|
{
|
|
|
|
return (IBTCPayServerPlugin)Activator.CreateInstance(type, Array.Empty<object>());
|
|
|
|
}
|
|
|
|
|
|
|
|
private static IFileProvider CreateEmbeddedFileProviderForAssembly(Assembly assembly)
|
|
|
|
{
|
|
|
|
return new EmbeddedFileProvider(assembly);
|
|
|
|
}
|
|
|
|
|
|
|
|
private static void ExecuteCommands(string pluginsFolder)
|
|
|
|
{
|
|
|
|
var pendingCommands = GetPendingCommands(pluginsFolder);
|
|
|
|
foreach (var command in pendingCommands)
|
|
|
|
{
|
|
|
|
ExecuteCommand(command, pluginsFolder);
|
|
|
|
}
|
|
|
|
|
|
|
|
File.Delete(Path.Combine(pluginsFolder, "commands"));
|
|
|
|
}
|
|
|
|
|
2020-11-05 15:43:14 +01:00
|
|
|
private static void ExecuteCommand((string command, string extension) command, string pluginsFolder,
|
|
|
|
bool ignoreOrder = false)
|
2020-10-21 14:02:20 +02:00
|
|
|
{
|
|
|
|
var dirName = Path.Combine(pluginsFolder, command.extension);
|
|
|
|
switch (command.command)
|
|
|
|
{
|
2020-11-05 15:43:14 +01:00
|
|
|
case "update":
|
2021-04-01 05:27:22 +02:00
|
|
|
ExecuteCommand(("enable", command.extension), pluginsFolder, true);
|
2020-11-05 15:43:14 +01:00
|
|
|
ExecuteCommand(("delete", command.extension), pluginsFolder, true);
|
|
|
|
ExecuteCommand(("install", command.extension), pluginsFolder, true);
|
|
|
|
break;
|
2020-10-21 14:02:20 +02:00
|
|
|
case "delete":
|
2021-04-01 05:27:22 +02:00
|
|
|
|
|
|
|
ExecuteCommand(("enable", command.extension), pluginsFolder, true);
|
2020-10-21 14:02:20 +02:00
|
|
|
if (Directory.Exists(dirName))
|
|
|
|
{
|
|
|
|
Directory.Delete(dirName, true);
|
2020-11-05 15:43:14 +01:00
|
|
|
if (!ignoreOrder && File.Exists(Path.Combine(pluginsFolder, "order")))
|
|
|
|
{
|
|
|
|
var orders = File.ReadAllLines(Path.Combine(pluginsFolder, "order"));
|
2021-04-01 05:27:22 +02:00
|
|
|
File.WriteAllLines(Path.Combine(pluginsFolder, "order"),
|
2020-11-05 15:43:14 +01:00
|
|
|
orders.Where(s => s != command.extension));
|
|
|
|
}
|
2020-10-21 14:02:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
2021-04-01 05:27:22 +02:00
|
|
|
case "install":
|
|
|
|
ExecuteCommand(("enable", command.extension), pluginsFolder, true);
|
2020-10-21 14:02:20 +02:00
|
|
|
var fileName = dirName + BTCPayPluginSuffix;
|
|
|
|
if (File.Exists(fileName))
|
|
|
|
{
|
|
|
|
ZipFile.ExtractToDirectory(fileName, dirName, true);
|
2020-11-05 15:43:14 +01:00
|
|
|
if (!ignoreOrder)
|
|
|
|
{
|
|
|
|
File.AppendAllLines(Path.Combine(pluginsFolder, "order"), new[] {command.extension});
|
|
|
|
}
|
|
|
|
|
2020-10-21 14:02:20 +02:00
|
|
|
File.Delete(fileName);
|
|
|
|
}
|
|
|
|
|
2021-04-01 05:27:22 +02:00
|
|
|
break;
|
|
|
|
|
|
|
|
case "disable":
|
|
|
|
if (Directory.Exists(dirName))
|
|
|
|
{
|
|
|
|
if (File.Exists(Path.Combine(pluginsFolder, "disabled")))
|
|
|
|
{
|
|
|
|
var disabled = File.ReadAllLines(Path.Combine(pluginsFolder, "disabled"));
|
|
|
|
if (!disabled.Contains(command.extension))
|
|
|
|
{
|
|
|
|
File.AppendAllLines(Path.Combine(pluginsFolder, "disabled"), new []{ command.extension});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
File.AppendAllLines(Path.Combine(pluginsFolder, "disabled"), new []{ command.extension});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "enable":
|
|
|
|
if (Directory.Exists(dirName))
|
|
|
|
{
|
|
|
|
if (File.Exists(Path.Combine(pluginsFolder, "disabled")))
|
|
|
|
{
|
|
|
|
var disabled = File.ReadAllLines(Path.Combine(pluginsFolder, "disabled"));
|
|
|
|
if (!disabled.Contains(command.extension))
|
|
|
|
{
|
|
|
|
File.WriteAllLines(Path.Combine(pluginsFolder, "disabled"), disabled.Where(s=> s!= command.extension));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-21 14:02:20 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static (string command, string plugin)[] GetPendingCommands(string pluginsFolder)
|
|
|
|
{
|
|
|
|
if (!File.Exists(Path.Combine(pluginsFolder, "commands")))
|
|
|
|
return Array.Empty<(string command, string plugin)>();
|
|
|
|
var commands = File.ReadAllLines(Path.Combine(pluginsFolder, "commands"));
|
|
|
|
return commands.Select(s =>
|
|
|
|
{
|
|
|
|
var split = s.Split(':');
|
|
|
|
return (split[0].ToLower(CultureInfo.InvariantCulture), split[1]);
|
|
|
|
}).ToArray();
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void QueueCommands(string pluginsFolder, params ( string action, string plugin)[] commands)
|
|
|
|
{
|
|
|
|
File.AppendAllLines(Path.Combine(pluginsFolder, "commands"),
|
|
|
|
commands.Select((tuple) => $"{tuple.action}:{tuple.plugin}"));
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void CancelCommands(string pluginDir, string plugin)
|
|
|
|
{
|
|
|
|
var cmds = GetPendingCommands(pluginDir).Where(tuple =>
|
|
|
|
!tuple.plugin.Equals(plugin, StringComparison.InvariantCultureIgnoreCase)).ToArray();
|
|
|
|
|
|
|
|
File.Delete(Path.Combine(pluginDir, "commands"));
|
|
|
|
QueueCommands(pluginDir, cmds);
|
|
|
|
}
|
2021-04-01 05:27:22 +02:00
|
|
|
|
|
|
|
public static void DisablePlugin(string pluginDir, string plugin)
|
|
|
|
{
|
|
|
|
|
|
|
|
QueueCommands(pluginDir, ("disable",plugin));
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void Unload()
|
|
|
|
{
|
|
|
|
_plugins.ForEach(loader => loader.Dispose());
|
|
|
|
}
|
|
|
|
|
|
|
|
public static string[] GetDisabledPlugins(string pluginsFolder)
|
|
|
|
{
|
|
|
|
var disabledFilePath = Path.Combine(pluginsFolder, "disabled");
|
|
|
|
if (File.Exists(disabledFilePath))
|
|
|
|
{
|
|
|
|
return File.ReadLines(disabledFilePath).ToArray();
|
|
|
|
}
|
|
|
|
|
|
|
|
return Array.Empty<string>();
|
|
|
|
}
|
2020-10-21 14:02:20 +02:00
|
|
|
}
|
|
|
|
}
|