2020-10-21 14:02:20 +02:00
using System ;
using System.Collections.Generic ;
2023-01-16 02:37:17 +01:00
using System.Collections.Specialized ;
2023-01-18 06:15:27 +01:00
using System.Diagnostics.CodeAnalysis ;
2020-10-21 14:02:20 +02:00
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 > ( ) ;
2023-01-18 06:15:27 +01:00
public static bool IsExceptionByPlugin ( Exception exception , [ MaybeNullWhen ( false ) ] out string pluginName )
2021-04-01 05:27:22 +02:00
{
2023-01-18 06:15:27 +01:00
foreach ( var assembly in _pluginAssemblies )
{
var assemblyName = assembly . GetName ( ) . Name ;
if ( assemblyName is null )
continue ;
// Comparison is case sensitive as it is theorically possible to have a different plugin
// with same name but different casing.
if ( exception . Source is not null & &
assemblyName . Equals ( exception . Source , StringComparison . Ordinal ) )
{
pluginName = assemblyName ;
return true ;
}
if ( exception . Message . Contains ( assemblyName , StringComparison . Ordinal ) )
{
pluginName = assemblyName ;
return true ;
}
}
pluginName = null ;
return false ;
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 )
{
2023-01-16 02:37:17 +01:00
var 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 > ( ) ;
2023-01-16 02:37:17 +01:00
var loadedPluginIdentifiers = new HashSet < string > ( ) ;
2020-10-21 14:02:20 +02:00
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
} ) ;
2023-01-16 02:37:17 +01:00
logger . LogInformation ( $"Loading plugins from {pluginsFolder}" ) ;
2020-10-21 14:02:20 +02:00
Directory . CreateDirectory ( pluginsFolder ) ;
ExecuteCommands ( pluginsFolder ) ;
2021-07-08 12:53:34 +02:00
2023-01-16 02:37:17 +01:00
var disabledPlugins = GetDisabledPlugins ( pluginsFolder ) ;
var systemAssembly = typeof ( Program ) . Assembly ;
// Load the referenced assembly plugins
// All referenced plugins should have at least one plugin with exact same plugin identifier
// as the assembly. Except for the system assembly (btcpayserver assembly) which are fake plugins
foreach ( var assembly in AppDomain . CurrentDomain . GetAssemblies ( ) )
2020-10-21 14:02:20 +02:00
{
2023-01-18 06:15:27 +01:00
var assemblyName = assembly . GetName ( ) . Name ;
2023-01-16 02:37:17 +01:00
bool isSystemPlugin = assembly = = systemAssembly ;
2023-01-18 06:15:27 +01:00
if ( ! isSystemPlugin & & disabledPlugins . Contains ( assemblyName ) )
2021-07-08 12:53:34 +02:00
continue ;
2021-12-31 08:59:02 +01:00
2023-01-16 02:37:17 +01:00
foreach ( var plugin in GetPluginInstancesFromAssembly ( assembly ) )
2021-07-08 12:53:34 +02:00
{
2023-01-18 06:15:27 +01:00
if ( ! isSystemPlugin & & plugin . Identifier ! = assemblyName )
2023-01-16 02:37:17 +01:00
continue ;
if ( ! loadedPluginIdentifiers . Add ( plugin . Identifier ) )
continue ;
plugins . Add ( plugin ) ;
plugin . SystemPlugin = isSystemPlugin ;
}
2020-10-21 14:02:20 +02:00
}
2021-12-31 08:59:02 +01:00
2023-01-16 02:37:17 +01:00
var pluginsToLoad = new List < ( string PluginIdentifier , string PluginFilePath ) > ( ) ;
2020-11-05 15:43:14 +01:00
2023-01-16 02:37:17 +01:00
#if DEBUG
// Load from DEBUG_PLUGINS, in an optional appsettings.dev.json
var debugPlugins = config [ "DEBUG_PLUGINS" ] ? ? "" ;
foreach ( var plugin in debugPlugins . Split ( ';' , StringSplitOptions . RemoveEmptyEntries ) )
2020-11-05 15:43:14 +01:00
{
2023-01-16 02:37:17 +01:00
// Formatted either as "<PLUGIN_IDENTIFIER>::<PathToDll>" or "<PathToDll>"
var idx = plugin . IndexOf ( "::" ) ;
if ( idx ! = - 1 )
pluginsToLoad . Add ( ( plugin [ 0. . idx ] , plugin [ ( idx + 1 ) . . ] ) ) ;
else
pluginsToLoad . Add ( ( Path . GetFileNameWithoutExtension ( plugin ) , plugin ) ) ;
2020-11-05 15:43:14 +01:00
}
2023-01-16 02:37:17 +01:00
#endif
2020-11-05 15:43:14 +01:00
2023-01-16 02:37:17 +01:00
// Load from the plugins folder
foreach ( var directory in Directory . GetDirectories ( pluginsFolder ) )
2020-10-21 14:02:20 +02:00
{
2023-01-16 08:31:26 +01:00
var pluginIdentifier = Path . GetFileName ( directory ) ;
2023-01-16 02:37:17 +01:00
var pluginFilePath = Path . Combine ( directory , pluginIdentifier + ".dll" ) ;
2021-04-20 08:38:37 +02:00
if ( ! File . Exists ( pluginFilePath ) )
continue ;
2023-01-16 02:37:17 +01:00
if ( disabledPlugins . Contains ( pluginIdentifier ) )
continue ;
pluginsToLoad . Add ( ( pluginIdentifier , pluginFilePath ) ) ;
}
2021-04-20 08:38:37 +02:00
2023-01-16 02:37:17 +01:00
ReorderPlugins ( pluginsFolder , pluginsToLoad ) ;
foreach ( var toLoad in pluginsToLoad )
{
2023-02-08 07:47:38 +01:00
// This used to be a standalone plugin but due to popular demand has been made as part of core. If we detect an install, we remove the redundant plugin.
if ( toLoad . PluginIdentifier = = "BTCPayServer.Plugins.NFC" )
{
QueueCommands ( pluginsFolder , ( "delete" , toLoad . PluginIdentifier ) ) ;
continue ;
}
2023-01-16 02:37:17 +01:00
if ( ! loadedPluginIdentifiers . Add ( toLoad . PluginIdentifier ) )
continue ;
2021-05-03 08:35:54 +02:00
try
{
var plugin = PluginLoader . CreateFromAssemblyFile (
2023-01-16 02:37:17 +01:00
toLoad . PluginFilePath , // create a plugin from for the .dll file
2021-05-03 08:35:54 +02:00
config = >
{
// this ensures that the version of MVC is shared between this app and the plugin
config . PreferSharedTypes = true ;
2023-01-18 08:46:08 +01:00
config . IsUnloadable = false ;
2021-05-03 08:35:54 +02:00
} ) ;
var pluginAssembly = plugin . LoadDefaultAssembly ( ) ;
2023-01-16 02:37:17 +01:00
var p = GetPluginInstanceFromAssembly ( toLoad . PluginIdentifier , pluginAssembly ) ;
if ( p = = null )
{
logger . LogError ( $"The plugin assembly doesn't contain the plugin {toLoad.PluginIdentifier}" ) ;
}
else
2022-12-13 10:54:41 +01:00
{
2023-01-16 02:37:17 +01:00
mvcBuilder . AddPluginLoader ( plugin ) ;
_pluginAssemblies . Add ( pluginAssembly ) ;
2022-12-13 10:54:41 +01:00
p . SystemPlugin = false ;
plugins . Add ( p ) ;
}
2021-05-03 08:35:54 +02:00
}
catch ( Exception e )
{
2023-01-16 02:37:17 +01:00
logger . LogError ( e ,
$"Error when loading plugin {toLoad.PluginIdentifier}" ) ;
2021-05-03 08:35:54 +02:00
}
2020-10-21 14:02:20 +02:00
}
foreach ( var plugin in plugins )
{
try
{
2023-01-16 02:37:17 +01:00
logger . LogInformation (
2020-10-21 14:02:20 +02:00
$"Adding and executing plugin {plugin.Identifier} - {plugin.Version}" ) ;
plugin . Execute ( serviceCollection ) ;
serviceCollection . AddSingleton ( plugin ) ;
}
catch ( Exception e )
{
2023-01-16 02:37:17 +01:00
logger . LogError (
2020-11-05 15:43:14 +01:00
$"Error when loading plugin {plugin.Identifier} - {plugin.Version}{Environment.NewLine}{e.Message}" ) ;
2020-10-21 14:02:20 +02:00
}
}
return mvcBuilder ;
}
2023-01-16 02:37:17 +01:00
private static void ReorderPlugins ( string pluginsFolder , List < ( string PluginIdentifier , string PluginFilePath ) > pluginsToLoad )
{
Dictionary < string , int > ordersByPlugin = new Dictionary < string , int > ( ) ;
var orderFilePath = Path . Combine ( pluginsFolder , "order" ) ;
int order = 0 ;
if ( File . Exists ( orderFilePath ) )
{
foreach ( var o in File . ReadLines ( orderFilePath ) )
{
if ( ordersByPlugin . TryAdd ( o , order ) )
order + + ;
}
}
foreach ( var p in pluginsToLoad )
{
if ( ordersByPlugin . TryAdd ( p . PluginIdentifier , order ) )
order + + ;
}
pluginsToLoad . Sort ( ( a , b ) = > ordersByPlugin [ a . PluginIdentifier ] - ordersByPlugin [ b . PluginIdentifier ] ) ;
}
2020-10-21 14:02:20 +02:00
public static void UsePlugins ( this IApplicationBuilder applicationBuilder )
{
2023-01-16 02:37:17 +01:00
HashSet < Assembly > assemblies = new HashSet < Assembly > ( ) ;
2020-10-21 14:02:20 +02:00
foreach ( var extension in applicationBuilder . ApplicationServices
. GetServices < IBTCPayServerPlugin > ( ) )
{
extension . Execute ( applicationBuilder ,
applicationBuilder . ApplicationServices ) ;
2023-01-16 02:37:17 +01:00
assemblies . Add ( extension . GetType ( ) . Assembly ) ;
2020-10-21 14:02:20 +02:00
}
var webHostEnvironment = applicationBuilder . ApplicationServices . GetService < IWebHostEnvironment > ( ) ;
2021-12-31 08:59:02 +01:00
List < IFileProvider > providers = new List < IFileProvider > ( ) { webHostEnvironment . WebRootFileProvider } ;
2023-01-16 02:37:17 +01:00
providers . AddRange ( assemblies . Select ( a = > new EmbeddedFileProvider ( a ) ) ) ;
2020-10-21 14:02:20 +02:00
webHostEnvironment . WebRootFileProvider = new CompositeFileProvider ( providers ) ;
}
2020-11-05 15:43:14 +01:00
2023-01-16 02:37:17 +01:00
private static IEnumerable < IBTCPayServerPlugin > GetPluginInstancesFromAssembly ( Assembly assembly )
2020-10-21 14:02:20 +02:00
{
return assembly . GetTypes ( ) . Where ( type = >
2020-11-05 15:43:14 +01:00
typeof ( IBTCPayServerPlugin ) . IsAssignableFrom ( type ) & & type ! = typeof ( PluginService . AvailablePlugin ) & &
2023-01-16 02:37:17 +01:00
! type . IsAbstract ) .
Select ( type = > ( IBTCPayServerPlugin ) Activator . CreateInstance ( type , Array . Empty < object > ( ) ) ) ;
2020-10-21 14:02:20 +02:00
}
2023-01-16 02:37:17 +01:00
private static IBTCPayServerPlugin GetPluginInstanceFromAssembly ( string pluginIdentifier , Assembly assembly )
2020-10-21 14:02:20 +02:00
{
2023-01-16 02:37:17 +01:00
return GetPluginInstancesFromAssembly ( assembly )
. Where ( plugin = > plugin . Identifier = = pluginIdentifier )
. FirstOrDefault ( ) ;
2020-10-21 14:02:20 +02:00
}
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-12-31 08:59:02 +01:00
2021-04-01 05:27:22 +02:00
ExecuteCommand ( ( "enable" , command . extension ) , pluginsFolder , true ) ;
2022-04-01 13:20:19 +02:00
if ( File . Exists ( dirName ) )
{
File . Delete ( dirName ) ;
}
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-12-31 08:59:02 +01:00
case "install" :
2021-04-01 05:27:22 +02:00
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 )
{
2021-12-31 08:59:02 +01:00
File . AppendAllLines ( Path . Combine ( pluginsFolder , "order" ) , new [ ] { command . extension } ) ;
2020-11-05 15:43:14 +01:00
}
2020-10-21 14:02:20 +02:00
File . Delete ( fileName ) ;
}
2021-04-01 05:27:22 +02:00
break ;
2021-12-31 08:59:02 +01:00
2021-04-01 05:27:22 +02:00
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 ) )
{
2021-12-31 08:59:02 +01:00
File . AppendAllLines ( Path . Combine ( pluginsFolder , "disabled" ) , new [ ] { command . extension } ) ;
2021-04-01 05:27:22 +02:00
}
}
else
{
2021-12-31 08:59:02 +01:00
File . AppendAllLines ( Path . Combine ( pluginsFolder , "disabled" ) , new [ ] { command . extension } ) ;
2021-04-01 05:27:22 +02:00
}
}
break ;
2021-12-31 08:59:02 +01:00
2021-04-01 05:27:22 +02:00
case "enable" :
2021-09-27 09:03:59 +02:00
if ( File . Exists ( Path . Combine ( pluginsFolder , "disabled" ) ) )
2021-04-01 05:27:22 +02:00
{
2021-09-27 09:03:59 +02:00
var disabled = File . ReadAllLines ( Path . Combine ( pluginsFolder , "disabled" ) ) ;
if ( disabled . Contains ( command . extension ) )
2021-04-01 05:27:22 +02:00
{
2021-12-31 08:59:02 +01:00
File . WriteAllLines ( Path . Combine ( pluginsFolder , "disabled" ) , disabled . Where ( s = > s ! = command . extension ) ) ;
2021-04-01 05:27:22 +02:00
}
}
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 ( ) ;
}
2021-12-31 08:59:02 +01:00
public static void QueueCommands ( string pluginsFolder , params ( string action , string plugin ) [ ] commands )
2020-10-21 14:02:20 +02:00
{
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 )
{
2021-12-31 08:59:02 +01:00
QueueCommands ( pluginDir , ( "disable" , plugin ) ) ;
2021-04-01 05:27:22 +02:00
}
2023-01-16 02:37:17 +01:00
public static HashSet < string > GetDisabledPlugins ( string pluginsFolder )
2021-04-01 05:27:22 +02:00
{
var disabledFilePath = Path . Combine ( pluginsFolder , "disabled" ) ;
if ( File . Exists ( disabledFilePath ) )
{
2023-01-16 02:37:17 +01:00
return File . ReadLines ( disabledFilePath ) . ToHashSet ( ) ;
2021-04-01 05:27:22 +02:00
}
2023-01-16 02:37:17 +01:00
return new HashSet < string > ( ) ;
2021-04-01 05:27:22 +02:00
}
2020-10-21 14:02:20 +02:00
}
}