mirror of
https://github.com/infinition/Bjorn.git
synced 2026-03-15 17:01:58 +00:00
- Implemented LokiUtils class with GET and POST endpoints for managing scripts, jobs, and payloads. - Added SentinelUtils class with GET and POST endpoints for managing events, rules, devices, and notifications. - Both classes include error handling and JSON response formatting.
199 lines
6.0 KiB
Python
199 lines
6.0 KiB
Python
"""
|
|
Bifrost — Plugin system.
|
|
Ported from pwnagotchi/plugins/__init__.py with ThreadPoolExecutor.
|
|
Compatible with existing pwnagotchi plugin files.
|
|
"""
|
|
import os
|
|
import glob
|
|
import threading
|
|
import importlib
|
|
import importlib.util
|
|
import logging
|
|
import concurrent.futures
|
|
|
|
from logger import Logger
|
|
|
|
logger = Logger(name="bifrost.plugins", level=logging.DEBUG)
|
|
|
|
default_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "plugins")
|
|
loaded = {}
|
|
database = {}
|
|
locks = {}
|
|
|
|
_executor = concurrent.futures.ThreadPoolExecutor(
|
|
max_workers=4, thread_name_prefix="BifrostPlugin"
|
|
)
|
|
|
|
|
|
class Plugin:
|
|
"""Base class for Bifrost/Pwnagotchi plugins.
|
|
|
|
Subclasses are auto-registered via __init_subclass__.
|
|
"""
|
|
__author__ = 'unknown'
|
|
__version__ = '0.0.0'
|
|
__license__ = 'GPL3'
|
|
__description__ = ''
|
|
__name__ = ''
|
|
__help__ = ''
|
|
__dependencies__ = []
|
|
__defaults__ = {}
|
|
|
|
def __init__(self):
|
|
self.options = {}
|
|
|
|
@classmethod
|
|
def __init_subclass__(cls, **kwargs):
|
|
super().__init_subclass__(**kwargs)
|
|
global loaded, locks
|
|
|
|
plugin_name = cls.__module__.split('.')[0]
|
|
plugin_instance = cls()
|
|
logger.debug("loaded plugin %s as %s", plugin_name, plugin_instance)
|
|
loaded[plugin_name] = plugin_instance
|
|
|
|
for attr_name in dir(plugin_instance):
|
|
if attr_name.startswith('on_'):
|
|
cb = getattr(plugin_instance, attr_name, None)
|
|
if cb is not None and callable(cb):
|
|
locks["%s::%s" % (plugin_name, attr_name)] = threading.Lock()
|
|
|
|
|
|
def toggle_plugin(name, enable=True):
|
|
"""Enable or disable a plugin at runtime. Returns True if state changed."""
|
|
global loaded, database
|
|
|
|
if not enable and name in loaded:
|
|
try:
|
|
if hasattr(loaded[name], 'on_unload'):
|
|
loaded[name].on_unload()
|
|
except Exception as e:
|
|
logger.warning("Error unloading plugin %s: %s", name, e)
|
|
del loaded[name]
|
|
return True
|
|
|
|
if enable and name in database and name not in loaded:
|
|
try:
|
|
load_from_file(database[name])
|
|
if name in loaded:
|
|
one(name, 'loaded')
|
|
return True
|
|
except Exception as e:
|
|
logger.warning("Error loading plugin %s: %s", name, e)
|
|
|
|
return False
|
|
|
|
|
|
def on(event_name, *args, **kwargs):
|
|
"""Dispatch event to ALL loaded plugins."""
|
|
for plugin_name in list(loaded.keys()):
|
|
one(plugin_name, event_name, *args, **kwargs)
|
|
|
|
|
|
def _locked_cb(lock_name, cb, *args, **kwargs):
|
|
"""Execute callback under its per-plugin lock."""
|
|
global locks
|
|
if lock_name not in locks:
|
|
locks[lock_name] = threading.Lock()
|
|
with locks[lock_name]:
|
|
cb(*args, **kwargs)
|
|
|
|
|
|
def one(plugin_name, event_name, *args, **kwargs):
|
|
"""Dispatch event to a single plugin (thread-safe)."""
|
|
global loaded
|
|
if plugin_name in loaded:
|
|
plugin = loaded[plugin_name]
|
|
cb_name = 'on_%s' % event_name
|
|
callback = getattr(plugin, cb_name, None)
|
|
if callback is not None and callable(callback):
|
|
try:
|
|
lock_name = "%s::%s" % (plugin_name, cb_name)
|
|
_executor.submit(_locked_cb, lock_name, callback, *args, **kwargs)
|
|
except Exception as e:
|
|
logger.error("error running %s.%s: %s", plugin_name, cb_name, e)
|
|
|
|
|
|
def load_from_file(filename):
|
|
"""Load a single plugin file."""
|
|
logger.debug("loading %s", filename)
|
|
plugin_name = os.path.basename(filename.replace(".py", ""))
|
|
spec = importlib.util.spec_from_file_location(plugin_name, filename)
|
|
instance = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(instance)
|
|
return plugin_name, instance
|
|
|
|
|
|
def load_from_path(path, enabled=()):
|
|
"""Scan a directory for plugins, load enabled ones."""
|
|
global loaded, database
|
|
if not path or not os.path.isdir(path):
|
|
return loaded
|
|
|
|
logger.debug("loading plugins from %s — enabled: %s", path, enabled)
|
|
for filename in glob.glob(os.path.join(path, "*.py")):
|
|
plugin_name = os.path.basename(filename.replace(".py", ""))
|
|
database[plugin_name] = filename
|
|
if plugin_name in enabled:
|
|
try:
|
|
load_from_file(filename)
|
|
except Exception as e:
|
|
logger.warning("error loading %s: %s", filename, e)
|
|
|
|
return loaded
|
|
|
|
|
|
def load(config):
|
|
"""Load plugins from default + custom paths based on config."""
|
|
plugins_cfg = config.get('bifrost_plugins', {})
|
|
enabled = [
|
|
name for name, opts in plugins_cfg.items()
|
|
if isinstance(opts, dict) and opts.get('enabled', False)
|
|
]
|
|
|
|
# Load from default path (bifrost/plugins/)
|
|
if os.path.isdir(default_path):
|
|
load_from_path(default_path, enabled=enabled)
|
|
|
|
# Load from custom path
|
|
custom_path = config.get('bifrost_plugins_path', '')
|
|
if custom_path and os.path.isdir(custom_path):
|
|
load_from_path(custom_path, enabled=enabled)
|
|
|
|
# Propagate options
|
|
for name, plugin in loaded.items():
|
|
if name in plugins_cfg:
|
|
plugin.options = plugins_cfg[name]
|
|
|
|
on('loaded')
|
|
on('config_changed', config)
|
|
|
|
|
|
def get_loaded_info():
|
|
"""Return list of loaded plugin info dicts for web API."""
|
|
result = []
|
|
for name, plugin in loaded.items():
|
|
result.append({
|
|
'name': name,
|
|
'enabled': True,
|
|
'author': getattr(plugin, '__author__', 'unknown'),
|
|
'version': getattr(plugin, '__version__', '0.0.0'),
|
|
'description': getattr(plugin, '__description__', ''),
|
|
})
|
|
# Also include known-but-not-loaded plugins
|
|
for name, path in database.items():
|
|
if name not in loaded:
|
|
result.append({
|
|
'name': name,
|
|
'enabled': False,
|
|
'author': '',
|
|
'version': '',
|
|
'description': '',
|
|
})
|
|
return result
|
|
|
|
|
|
def shutdown():
|
|
"""Clean shutdown of plugin system."""
|
|
_executor.shutdown(wait=False)
|