mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
Add plugin custom command execution functionality to bwcli
This commit is contained in:
parent
8f253c3f2a
commit
46ab8432d4
5 changed files with 264 additions and 45 deletions
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 907 KiB After Width: | Height: | Size: 910 KiB |
|
|
@ -5,10 +5,11 @@ from json import dumps, loads
|
|||
from operator import itemgetter
|
||||
from time import time
|
||||
from dotenv import dotenv_values
|
||||
from os import getenv, sep
|
||||
from os import environ, getenv, sep
|
||||
from os.path import join
|
||||
from pathlib import Path
|
||||
from redis import StrictRedis, Sentinel
|
||||
from subprocess import STDOUT, run
|
||||
from sys import path as sys_path
|
||||
from typing import Any, Optional, Tuple
|
||||
|
||||
|
|
@ -279,3 +280,44 @@ class CLI(ApiCaller):
|
|||
cli_str += "\n"
|
||||
|
||||
return True, cli_str
|
||||
|
||||
def custom(self, plugin_id: str, command: str, *args: str, debug: bool = False) -> Tuple[bool, str]:
|
||||
if not Path(sep, "usr", "share", "bunkerweb", "db").exists():
|
||||
raise Exception("This command can only be executed on the scheduler")
|
||||
|
||||
from Database import Database # type: ignore
|
||||
|
||||
db = Database(self.__logger, sqlalchemy_string=self.__get_variable("DATABASE_URI", None))
|
||||
found = False
|
||||
plugin_type = "core"
|
||||
file_name = None
|
||||
|
||||
for db_plugin in db.get_plugins():
|
||||
if db_plugin["id"] == plugin_id:
|
||||
found = True
|
||||
plugin_type = db_plugin["type"]
|
||||
file_name = db_plugin["bwcli"].get(command, None)
|
||||
break
|
||||
|
||||
if not found:
|
||||
return False, f"Plugin {plugin_id} not found"
|
||||
elif not file_name:
|
||||
return False, f"Command {command} not found for plugin {plugin_id}"
|
||||
|
||||
command_path = (
|
||||
Path(sep, "usr", "share", "bunkerweb", "core", plugin_id)
|
||||
if plugin_type == "core"
|
||||
else (
|
||||
Path(sep, "etc", "bunkerweb", "plugins", plugin_id) if plugin_type == "external" else Path(sep, "etc", "bunkerweb", "pro", "plugins", plugin_id)
|
||||
)
|
||||
).joinpath("bwcli", file_name)
|
||||
|
||||
if not command_path.is_file():
|
||||
return False, f"Command {command} not found for plugin {plugin_id} (file {command_path} not found)"
|
||||
|
||||
proc = run([command_path, *args], stdout=STDOUT, stderr=STDOUT, check=False, env=db.get_config() | environ | ({"LOG_LEVEL": "DEBUG"} if debug else {}))
|
||||
|
||||
if proc.returncode != 0:
|
||||
return False, f"Command {command} failed"
|
||||
|
||||
return True, ""
|
||||
|
|
|
|||
|
|
@ -50,6 +50,13 @@ if __name__ == "__main__":
|
|||
# Bans subparser
|
||||
parser_bans = subparsers.add_parser("bans", help="list current bans")
|
||||
|
||||
# Plugin subparser
|
||||
parser_plugin = subparsers.add_parser("plugin", help="execute a custom command from a plugin")
|
||||
parser_plugin.add_argument("plugin_id", help="the plugin id that you want to execute the command on")
|
||||
parser_plugin.add_argument("command", type=str, help="the command to execute on the plugin")
|
||||
parser_plugin.add_argument("arg", nargs="*", help="the arguments to pass to the command")
|
||||
parser_plugin.add_argument("-d", "--debug", action="store_true", help="sets the LOG_LEVEL env variable to DEBUG")
|
||||
|
||||
# Parse args
|
||||
args = parser.parse_args()
|
||||
|
||||
|
|
@ -64,12 +71,17 @@ if __name__ == "__main__":
|
|||
ret, err = cli.ban(args.ip, args.exp, args.reason)
|
||||
elif args.command == "bans":
|
||||
ret, err = cli.bans()
|
||||
else:
|
||||
ret, err = cli.custom(args.plugin_id, args.command, *args.arg, debug=args.debug)
|
||||
|
||||
if not ret:
|
||||
logger.error(f"CLI command status : ❌ (fail)\n{err}")
|
||||
_exit(1)
|
||||
else:
|
||||
logger.info(f"CLI command status : ✔️ (success)\n{err}")
|
||||
if err:
|
||||
err = f"\n{err}"
|
||||
|
||||
logger.info(f"CLI command status : ✔️ (success){err}")
|
||||
_exit(0)
|
||||
|
||||
except SystemExit as se:
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ from model import (
|
|||
Custom_configs,
|
||||
Selects,
|
||||
Users,
|
||||
BwcliCommands,
|
||||
Metadata,
|
||||
)
|
||||
|
||||
|
|
@ -489,6 +490,30 @@ class Database:
|
|||
|
||||
to_put = []
|
||||
with self.__db_session() as session:
|
||||
db_plugins = session.query(Plugins).with_entities(Plugins.id).all()
|
||||
|
||||
db_ids = []
|
||||
if db_plugins:
|
||||
db_ids = [plugin.id for plugin in db_plugins]
|
||||
ids = [plugin["id"] for plugin in default_plugins]
|
||||
missing_ids = [plugin for plugin in db_ids if plugin not in ids]
|
||||
|
||||
if missing_ids:
|
||||
# Remove plugins that are no longer in the list
|
||||
session.query(Plugins).filter(Plugins.id.in_(missing_ids)).delete()
|
||||
session.query(Plugin_pages).filter(Plugin_pages.plugin_id.in_(missing_ids)).delete()
|
||||
session.query(BwcliCommands).filter(BwcliCommands.plugin_id.in_(missing_ids)).delete()
|
||||
|
||||
for plugin_job in session.query(Jobs).with_entities(Jobs.name).filter(Jobs.plugin_id.in_(missing_ids)):
|
||||
session.query(Jobs_cache).filter(Jobs_cache.job_name == plugin_job.name).delete()
|
||||
session.query(Jobs).filter(Jobs.name == plugin_job.name).delete()
|
||||
|
||||
for plugin_setting in session.query(Settings).with_entities(Settings.id).filter(Settings.plugin_id.in_(missing_ids)):
|
||||
session.query(Selects).filter(Selects.setting_id == plugin_setting.id).delete()
|
||||
session.query(Services_settings).filter(Services_settings.setting_id == plugin_setting.id).delete()
|
||||
session.query(Global_values).filter(Global_values.setting_id == plugin_setting.id).delete()
|
||||
session.query(Settings).filter(Settings.id == plugin_setting.id).delete()
|
||||
|
||||
for plugins in default_plugins:
|
||||
if not isinstance(plugins, list):
|
||||
plugins = [plugins]
|
||||
|
|
@ -496,6 +521,7 @@ class Database:
|
|||
for plugin in plugins:
|
||||
settings = {}
|
||||
jobs = []
|
||||
commands = {}
|
||||
if "id" not in plugin:
|
||||
settings = plugin
|
||||
plugin = {
|
||||
|
|
@ -509,6 +535,9 @@ class Database:
|
|||
settings = plugin.pop("settings", {})
|
||||
jobs = plugin.pop("jobs", [])
|
||||
plugin.pop("page", False)
|
||||
commands = plugin.pop("bwcli", {})
|
||||
if not isinstance(commands, dict):
|
||||
commands = {}
|
||||
|
||||
db_plugin = session.query(Plugins).filter_by(id=plugin["id"]).first()
|
||||
if db_plugin:
|
||||
|
|
@ -664,21 +693,32 @@ class Database:
|
|||
session.query(Jobs_cache).filter(Jobs_cache.job_name == job["name"]).delete()
|
||||
session.query(Jobs).filter(Jobs.name == job["name"]).update(updates)
|
||||
|
||||
core_ui_path = Path(sep, "usr", "share", "bunkerweb", "core", plugin["id"], "ui")
|
||||
path_ui = core_ui_path if core_ui_path.exists() else Path(sep, "etc", "bunkerweb", "plugins", plugin["id"], "ui")
|
||||
path_ui = path_ui if path_ui.exists() else Path(sep, "etc", "bunkerweb", "pro", "plugins", plugin["id"], "ui")
|
||||
plugin_path = (
|
||||
Path(sep, "usr", "share", "bunkerweb", "core", plugin["id"])
|
||||
if plugin.get("type", "core") == "core"
|
||||
else (
|
||||
Path(sep, "etc", "bunkerweb", "plugins", plugin["id"])
|
||||
if plugin.get("type", "core") == "external"
|
||||
else Path(sep, "etc", "bunkerweb", "pro", "plugins", plugin["id"])
|
||||
)
|
||||
)
|
||||
|
||||
if path_ui.exists():
|
||||
path_ui = plugin_path.joinpath("ui")
|
||||
|
||||
db_plugin_page = (
|
||||
session.query(Plugin_pages)
|
||||
.with_entities(
|
||||
Plugin_pages.template_checksum,
|
||||
Plugin_pages.actions_checksum,
|
||||
)
|
||||
.filter_by(plugin_id=plugin["id"])
|
||||
.first()
|
||||
)
|
||||
remove = not path_ui.is_dir() and db_plugin_page
|
||||
|
||||
if path_ui.is_dir():
|
||||
remove = True
|
||||
if {"template.html", "actions.py"}.issubset(listdir(str(path_ui))):
|
||||
db_plugin_page = (
|
||||
session.query(Plugin_pages)
|
||||
.with_entities(
|
||||
Plugin_pages.template_checksum,
|
||||
Plugin_pages.actions_checksum,
|
||||
)
|
||||
.filter_by(plugin_id=plugin["id"])
|
||||
.first()
|
||||
)
|
||||
template = path_ui.joinpath("template.html").read_bytes()
|
||||
actions = path_ui.joinpath("actions.py").read_bytes()
|
||||
template_checksum = sha256(template).hexdigest()
|
||||
|
|
@ -705,20 +745,60 @@ class Database:
|
|||
if updates:
|
||||
self.logger.warning(f'Page for plugin "{plugin["id"]}" already exists, updating it with the new values')
|
||||
session.query(Plugin_pages).filter(Plugin_pages.plugin_id == plugin["id"]).update(updates)
|
||||
remove = False
|
||||
else:
|
||||
if db_plugin:
|
||||
self.logger.warning(f'Page for plugin "{plugin["id"]}" does not exist, creating it')
|
||||
|
||||
to_put.append(
|
||||
Plugin_pages(
|
||||
plugin_id=plugin["id"],
|
||||
template_file=template,
|
||||
template_checksum=template_checksum,
|
||||
actions_file=actions,
|
||||
actions_checksum=actions_checksum,
|
||||
)
|
||||
)
|
||||
remove = False
|
||||
|
||||
if db_plugin_page and remove:
|
||||
self.logger.warning(f'Removing page for plugin "{plugin["id"]}" as it no longer exists')
|
||||
session.query(Plugin_pages).filter_by(plugin_id=plugin["id"]).delete()
|
||||
|
||||
db_names = [command.name for command in session.query(BwcliCommands).with_entities(BwcliCommands.name).filter_by(plugin_id=plugin["id"])]
|
||||
missing_names = [command for command in db_names if command not in commands]
|
||||
|
||||
if missing_names:
|
||||
# Remove commands that are no longer in the list
|
||||
self.logger.warning(f'Removing {len(missing_names)} commands from plugin "{plugin["id"]}" as they are no longer in the list')
|
||||
session.query(BwcliCommands).filter(BwcliCommands.name.in_(missing_names), BwcliCommands.plugin_id == plugin["id"]).delete()
|
||||
|
||||
for command, file_name in commands.items():
|
||||
db_command = session.query(BwcliCommands).with_entities(BwcliCommands.file_name).filter_by(name=command, plugin_id=plugin["id"]).first()
|
||||
command_path = plugin_path.joinpath("bwcli", file_name)
|
||||
|
||||
if command not in db_names or not db_command:
|
||||
if db_plugin:
|
||||
self.logger.warning(f'Command "{command}" does not exist, creating it')
|
||||
|
||||
if not command_path.is_file():
|
||||
self.logger.warning(f'Command "{command}"\'s file "{file_name}" does not exist in the plugin directory, skipping it')
|
||||
continue
|
||||
|
||||
if db_plugin:
|
||||
self.logger.warning(f'Page for plugin "{plugin["id"]}" does not exist, creating it')
|
||||
to_put.append(BwcliCommands(name=command, plugin_id=plugin["id"], file_name=file_name))
|
||||
else:
|
||||
updates = {}
|
||||
|
||||
to_put.append(
|
||||
Plugin_pages(
|
||||
plugin_id=plugin["id"],
|
||||
template_file=template,
|
||||
template_checksum=template_checksum,
|
||||
actions_file=actions,
|
||||
actions_checksum=actions_checksum,
|
||||
)
|
||||
)
|
||||
if file_name != db_command.file_name:
|
||||
updates[BwcliCommands.file_name] = file_name
|
||||
|
||||
if updates:
|
||||
self.logger.warning(f'Command "{command}" already exists, updating it with the new values')
|
||||
if not command_path.is_file():
|
||||
self.logger.warning(f'Command "{command}"\'s file "{file_name}" does not exist in the plugin directory, removing it')
|
||||
session.query(BwcliCommands).filter_by(name=command, plugin_id=plugin["id"]).delete()
|
||||
continue
|
||||
session.query(BwcliCommands).filter_by(name=command, plugin_id=plugin["id"]).update(updates)
|
||||
|
||||
try:
|
||||
session.add_all(to_put)
|
||||
|
|
@ -1267,6 +1347,7 @@ class Database:
|
|||
# Remove plugins that are no longer in the list
|
||||
session.query(Plugins).filter(Plugins.id.in_(missing_ids)).delete()
|
||||
session.query(Plugin_pages).filter(Plugin_pages.plugin_id.in_(missing_ids)).delete()
|
||||
session.query(BwcliCommands).filter(BwcliCommands.plugin_id.in_(missing_ids)).delete()
|
||||
|
||||
for plugin_job in session.query(Jobs).with_entities(Jobs.name).filter(Jobs.plugin_id.in_(missing_ids)):
|
||||
session.query(Jobs_cache).filter(Jobs_cache.job_name == plugin_job.name).delete()
|
||||
|
|
@ -1282,6 +1363,9 @@ class Database:
|
|||
settings = plugin.pop("settings", {})
|
||||
jobs = plugin.pop("jobs", [])
|
||||
page = plugin.pop("page", False)
|
||||
commands = plugin.pop("bwcli", {})
|
||||
if not isinstance(commands, dict):
|
||||
commands = {}
|
||||
plugin["type"] = _type
|
||||
db_plugin = (
|
||||
session.query(Plugins)
|
||||
|
|
@ -1458,22 +1542,33 @@ class Database:
|
|||
session.query(Jobs_cache).filter(Jobs_cache.job_name == job["name"]).delete()
|
||||
session.query(Jobs).filter(Jobs.name == job["name"]).update(updates)
|
||||
|
||||
tmp_ui_path = Path(sep, "var", "tmp", "bunkerweb", "ui", plugin["id"], "ui")
|
||||
path_ui = tmp_ui_path if tmp_ui_path.exists() else Path(sep, "etc", "bunkerweb", "plugins", plugin["id"], "ui")
|
||||
path_ui = path_ui if path_ui.exists() else Path(sep, "etc", "bunkerweb", "pro", "plugins", plugin["id"], "ui")
|
||||
plugin_path = Path(sep, "var", "tmp", "bunkerweb", "ui", plugin["id"])
|
||||
plugin_path = (
|
||||
plugin_path
|
||||
if plugin_path.is_dir()
|
||||
else (
|
||||
Path(sep, "etc", "bunkerweb", "plugins", plugin["id"])
|
||||
if _type == "external"
|
||||
else Path(sep, "etc", "bunkerweb", "pro", "plugins", plugin["id"])
|
||||
)
|
||||
)
|
||||
|
||||
if path_ui.exists():
|
||||
path_ui = plugin_path.joinpath("ui")
|
||||
|
||||
db_plugin_page = (
|
||||
session.query(Plugin_pages)
|
||||
.with_entities(
|
||||
Plugin_pages.template_checksum,
|
||||
Plugin_pages.actions_checksum,
|
||||
)
|
||||
.filter_by(plugin_id=plugin["id"])
|
||||
.first()
|
||||
)
|
||||
remove = not path_ui.is_dir() and db_plugin_page
|
||||
|
||||
if path_ui.is_dir():
|
||||
remove = True
|
||||
if {"template.html", "actions.py"}.issubset(listdir(str(path_ui))):
|
||||
db_plugin_page = (
|
||||
session.query(Plugin_pages)
|
||||
.with_entities(
|
||||
Plugin_pages.template_checksum,
|
||||
Plugin_pages.actions_checksum,
|
||||
)
|
||||
.filter_by(plugin_id=plugin["id"])
|
||||
.first()
|
||||
)
|
||||
|
||||
if not db_plugin_page:
|
||||
changes = True
|
||||
template = path_ui.joinpath("template.html").read_bytes()
|
||||
|
|
@ -1488,6 +1583,7 @@ class Database:
|
|||
actions_checksum=sha256(actions).hexdigest(),
|
||||
)
|
||||
)
|
||||
remove = False
|
||||
else:
|
||||
updates = {}
|
||||
template_path = path_ui.joinpath("template.html")
|
||||
|
|
@ -1515,6 +1611,43 @@ class Database:
|
|||
changes = True
|
||||
session.query(Plugin_pages).filter(Plugin_pages.plugin_id == plugin["id"]).update(updates)
|
||||
|
||||
remove = False
|
||||
|
||||
if db_plugin_page and remove:
|
||||
changes = True
|
||||
session.query(Plugin_pages).filter(Plugin_pages.plugin_id == plugin["id"]).delete()
|
||||
|
||||
db_names = [command.name for command in session.query(BwcliCommands).with_entities(BwcliCommands.name).filter_by(plugin_id=plugin["id"])]
|
||||
missing_names = [command for command in db_names if command not in commands]
|
||||
|
||||
if missing_names:
|
||||
# Remove commands that are no longer in the list
|
||||
session.query(BwcliCommands).filter(BwcliCommands.name.in_(missing_names), BwcliCommands.plugin_id == plugin["id"]).delete()
|
||||
|
||||
for command, file_name in commands.items():
|
||||
db_command = session.query(BwcliCommands).with_entities(BwcliCommands.file_name).filter_by(name=command, plugin_id=plugin["id"]).first()
|
||||
command_path = plugin_path.joinpath("bwcli", file_name)
|
||||
|
||||
if command not in db_names or not db_command:
|
||||
if not command_path.is_file():
|
||||
self.logger.warning(f'Command "{command}"\'s file "{file_name}" does not exist in the plugin directory, skipping it')
|
||||
continue
|
||||
|
||||
changes = True
|
||||
to_put.append(BwcliCommands(name=command, plugin_id=plugin["id"], file_name=file_name))
|
||||
else:
|
||||
updates = {}
|
||||
|
||||
if file_name != db_command.file_name:
|
||||
updates[BwcliCommands.file_name] = file_name
|
||||
|
||||
if updates:
|
||||
changes = True
|
||||
if not command_path.is_file():
|
||||
session.query(BwcliCommands).filter_by(name=command, plugin_id=plugin["id"]).delete()
|
||||
continue
|
||||
session.query(BwcliCommands).filter_by(name=command, plugin_id=plugin["id"]).update(updates)
|
||||
|
||||
continue
|
||||
|
||||
changes = True
|
||||
|
|
@ -1559,11 +1692,19 @@ class Database:
|
|||
job["reload"] = job.get("reload", False)
|
||||
to_put.append(Jobs(plugin_id=plugin["id"], **job))
|
||||
|
||||
if page:
|
||||
tmp_ui_path = Path(sep, "var", "tmp", "bunkerweb", "ui", plugin["id"], "ui")
|
||||
path_ui = tmp_ui_path if tmp_ui_path.exists() else Path(sep, "etc", "bunkerweb", "plugins", plugin["id"], "ui")
|
||||
path_ui = path_ui if path_ui.exists() else Path(sep, "etc", "bunkerweb", "pro", "plugins", plugin["id"], "ui")
|
||||
plugin_path = Path(sep, "var", "tmp", "bunkerweb", "ui", plugin["id"])
|
||||
plugin_path = (
|
||||
plugin_path
|
||||
if plugin_path.is_dir()
|
||||
else (
|
||||
Path(sep, "etc", "bunkerweb", "plugins", plugin["id"])
|
||||
if _type == "external"
|
||||
else Path(sep, "etc", "bunkerweb", "pro", "plugins", plugin["id"])
|
||||
)
|
||||
)
|
||||
|
||||
if page:
|
||||
path_ui = plugin_path.joinpath("ui")
|
||||
if path_ui.exists():
|
||||
if {"template.html", "actions.py"}.issubset(listdir(str(path_ui))):
|
||||
db_plugin_page = (
|
||||
|
|
@ -1615,6 +1756,13 @@ class Database:
|
|||
if updates:
|
||||
session.query(Plugin_pages).filter(Plugin_pages.plugin_id == plugin["id"]).update(updates)
|
||||
|
||||
for command, file_name in plugin.get("bwcli", {}).items():
|
||||
if not plugin_path.joinpath("bwcli", file_name).is_file():
|
||||
self.logger.warning(f'Command "{command}"\'s file "{file_name}" does not exist in the plugin directory, skipping it')
|
||||
continue
|
||||
|
||||
to_put.append(BwcliCommands(name=command, plugin_id=plugin["id"], file_name=file_name))
|
||||
|
||||
if changes:
|
||||
with suppress(ProgrammingError, OperationalError):
|
||||
metadata = session.query(Metadata).get(1)
|
||||
|
|
@ -1656,6 +1804,7 @@ class Database:
|
|||
"method": plugin.method,
|
||||
"page": page is not None,
|
||||
"settings": {},
|
||||
"bwcli": {},
|
||||
"checksum": plugin.checksum,
|
||||
} | ({"data": plugin.data} if with_data else {})
|
||||
|
||||
|
|
@ -1689,6 +1838,9 @@ class Database:
|
|||
select.value for select in session.query(Selects).with_entities(Selects.value).filter_by(setting_id=setting.id)
|
||||
]
|
||||
|
||||
for command in session.query(BwcliCommands).with_entities(BwcliCommands.name, BwcliCommands.file_name).filter_by(plugin_id=plugin.id):
|
||||
data["bwcli"][command.name] = command.file_name
|
||||
|
||||
plugins.append(data)
|
||||
|
||||
return plugins
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ class Plugins(Base):
|
|||
settings = relationship("Settings", back_populates="plugin", cascade="all, delete-orphan")
|
||||
jobs = relationship("Jobs", back_populates="plugin", cascade="all, delete-orphan")
|
||||
pages = relationship("Plugin_pages", back_populates="plugin", cascade="all")
|
||||
commands = relationship("BwcliCommands", back_populates="plugin", cascade="all")
|
||||
|
||||
|
||||
class Settings(Base):
|
||||
|
|
@ -210,6 +211,18 @@ class Users(Base):
|
|||
method = Column(METHODS_ENUM, nullable=False, default="manual")
|
||||
|
||||
|
||||
class BwcliCommands(Base):
|
||||
__tablename__ = "bw_cli_commands"
|
||||
__table_args__ = (UniqueConstraint("plugin_id", "name"),)
|
||||
|
||||
id = Column(Integer, primary_key=True, default=1)
|
||||
name = Column(String(64), nullable=False)
|
||||
plugin_id = Column(String(64), ForeignKey("bw_plugins.id", onupdate="cascade", ondelete="cascade"), nullable=False)
|
||||
file_name = Column(String(256), nullable=False)
|
||||
|
||||
plugin = relationship("Plugins", back_populates="commands")
|
||||
|
||||
|
||||
class Metadata(Base):
|
||||
__tablename__ = "bw_metadata"
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue