Merge branch 'dev' of github.com:bunkerity/bunkerweb into dev

This commit is contained in:
fl0ppy-d1sk 2024-02-21 17:52:32 +01:00
commit 4e02e0a467
No known key found for this signature in database
GPG key ID: 93EE47CC3D061500
49 changed files with 380 additions and 581 deletions

View file

@ -75,9 +75,9 @@ BunkerWeb has a pro version to further improve the security of your applications
We have centralised version management and support within the panel for greater ease of use.
More detailed information about this version can be found [by visiting our website](https://www.bunkerweb.io/#services?utm_campaign=self&utm_source=doc).
More detailed information about this version can be found [by visiting our website.](https://www.bunkerweb.io/#services?utm_campaign=self&utm_source=doc)
If you're interested, upgrade to the pro version in just a few clicks, [following the steps here.](https://panel.bunkerweb.io/?utm_campaign=self&utm_source=doc).
If you're interested, upgrade to the pro version in just a few clicks, [following the steps here.](https://panel.bunkerweb.io/?utm_campaign=self&utm_source=doc)
Note that in the documentation, it will be mentioned when a feature is for the pro version.

View file

@ -581,7 +581,7 @@ For example, you can get the request arguments in your template like this :
```
You can power-up your plugin page with additionnal scripting with the **actions.py** file when sending a **POST /plugins/<*plugin_id*>**.
You can power-up your plugin page with additional scripting with the **actions.py** file when sending a **POST /plugins/<*plugin_id*>**.
Here is what is send to the function :

View file

@ -2431,6 +2431,12 @@ By default, BunkerWeb will only listen on IPv4 addresses and won't use IPv6 for
This quickstart is just a tiny fraction of the functionality that BunkerWeb has to offer. Be curious and see what else BunkerWeb has to offer.
The documentation shares free and professional features. The latter will be marked accordingly to avoid confusion.
You can take a detailed look at the many [plugins and settings available](settings.md) in this solution.
If you want to make the most of the solution, don't hesitate to [take a detailed look at the features from the panel.](https://panel.bunkerweb.io/?utm_campaign=self&utm_source=doc)
Details on the use of certain plugins can also be found on the [security tuning page.](security-tuning.md)
For simplified use of the solution, don't hesitate to [try out the web UI.](web-ui.md)
Please note that **BunkerWeb comes in both free and pro versions**, with pro features indicated to avoid confusion.
If you'd like to find out more about the pro version, and use the solution to the full, [visit the panel.](https://panel.bunkerweb.io/?utm_campaign=self&utm_source=doc)

View file

@ -1729,7 +1729,7 @@ After a successful login/password combination, you will be prompted to enter you
systemctl restart bunkerweb
```
## Upgrade pro version
## Upgrade to PRO
In case you have buy a pro version and you already have a BunkerWeb setup with an UI, you can update **pro settings** inside **global config page**.

View file

@ -67,8 +67,8 @@ RUN apk add --no-cache pcre bash python3 yajl && \
mkdir -p /var/www/html && \
mkdir -p /etc/bunkerweb && \
mkdir -p /data/cache && ln -s /data/cache /var/cache/bunkerweb && \
for dir in $(echo "configs plugins") ; do mkdir -p "/data/${dir}" && ln -s "/data/${dir}" "/etc/bunkerweb/${dir}" ; done && \
for dir in $(echo "configs/http configs/stream configs/server-http configs/server-stream configs/default-server-http configs/default-server-stream configs/modsec configs/modsec-crs") ; do mkdir "/data/${dir}" ; done && \
for dir in $(echo "pro configs plugins") ; do mkdir -p "/data/${dir}" && ln -s "/data/${dir}" "/etc/bunkerweb/${dir}" ; done && \
for dir in $(echo "pro/plugins configs/http configs/stream configs/server-http configs/server-stream configs/default-server-http configs/default-server-stream configs/modsec configs/modsec-crs") ; do mkdir "/data/${dir}" ; done && \
chown -R root:nginx /data /etc/nginx /var/cache/bunkerweb /etc/bunkerweb /var/tmp/bunkerweb /var/run/bunkerweb /var/log/bunkerweb /usr/bin/bwcli && \
chmod -R 770 /data /etc/nginx /var/cache/bunkerweb /var/tmp/bunkerweb /var/log/bunkerweb /var/run/bunkerweb && \
chmod 750 cli/main.py gen/main.py helpers/*.sh entrypoint.sh /usr/bin/bwcli deps/python/bin/* && \

View file

@ -128,6 +128,8 @@ api.global.POST["^/confs$"] = function(self)
destination = "/etc/bunkerweb/configs"
elseif self.ctx.bw.uri == "/plugins" then
destination = "/etc/bunkerweb/plugins"
elseif self.ctx.bw.uri == "/pro_plugins" then
destination = "/etc/bunkerweb/pro/plugins"
end
local form, err = upload:new(4096)
if not form then
@ -175,6 +177,8 @@ api.global.POST["^/custom_configs$"] = api.global.POST["^/confs$"]
api.global.POST["^/plugins$"] = api.global.POST["^/confs$"]
api.global.POST["^/pro_plugins$"] = api.global.POST["^/confs$"]
api.global.POST["^/unban$"] = function(self)
read_body()
local data = get_body_data()

View file

@ -40,7 +40,7 @@ resolver {{ DNS_RESOLVERS }} {% if USE_IPV6 == "no" %}ipv6=off{% endif %};
port_in_redirect off;
# lua configs
lua_package_path "/usr/share/bunkerweb/lua/?.lua;/usr/share/bunkerweb/core/?.lua;/etc/bunkerweb/plugins/?.lua;/usr/share/bunkerweb/deps/lib/lua/?.lua;;";
lua_package_path "/usr/share/bunkerweb/lua/?.lua;/usr/share/bunkerweb/core/?.lua;/etc/bunkerweb/plugins/?.lua;/etc/bunkerweb/pro/plugins/?.lua;/usr/share/bunkerweb/deps/lib/lua/?.lua;;";
lua_package_cpath "/usr/share/bunkerweb/deps/lib/?.so;/usr/share/bunkerweb/deps/lib/lua/?.so;;";
lua_ssl_trusted_certificate "/usr/share/bunkerweb/misc/root-ca.pem";
lua_ssl_verify_depth 2;

View file

@ -41,7 +41,7 @@ init_by_lua_block {
-- Load plugins into the datastore
logger:log(NOTICE, "saving plugins into datastore ...")
local plugins = {}
local plugin_paths = { "/usr/share/bunkerweb/core", "/etc/bunkerweb/plugins" }
local plugin_paths = { "/usr/share/bunkerweb/core", "/etc/bunkerweb/plugins", "/etc/bunkerweb/pro/plugins" }
for i, plugin_path in ipairs(plugin_paths) do
local paths = popen("find -L " .. plugin_path .. " -maxdepth 1 -type d ! -path " .. plugin_path)
for path in paths:lines() do

View file

@ -41,7 +41,7 @@ init_by_lua_block {
-- Load plugins into the datastore
logger:log(NOTICE, "saving plugins into datastore ...")
local plugins = {}
local plugin_paths = { "/usr/share/bunkerweb/core", "/etc/bunkerweb/plugins" }
local plugin_paths = { "/usr/share/bunkerweb/core", "/etc/bunkerweb/plugins", "/etc/bunkerweb/pro/plugins" }
for i, plugin_path in ipairs(plugin_paths) do
local paths = popen("find -L " .. plugin_path .. " -maxdepth 1 -type d ! -path " .. plugin_path)
for path in paths:lines() do

View file

@ -23,7 +23,7 @@ resolver_timeout 30s;
tcp_nodelay on;
# lua path and dicts
lua_package_path "/usr/share/bunkerweb/lua/?.lua;/usr/share/bunkerweb/core/?.lua;/etc/bunkerweb/plugins/?.lua;/usr/share/bunkerweb/deps/lib/lua/?.lua;;";
lua_package_path "/usr/share/bunkerweb/lua/?.lua;/usr/share/bunkerweb/core/?.lua;/etc/bunkerweb/plugins/?.lua;/etc/bunkerweb/pro/plugins/?.lua;/usr/share/bunkerweb/deps/lib/lua/?.lua;;";
lua_package_cpath "/usr/share/bunkerweb/deps/lib/?.so;/usr/share/bunkerweb/deps/lib/lua/?.so;;";
lua_ssl_trusted_certificate "/usr/share/bunkerweb/misc/root-ca.pem";
lua_ssl_verify_depth 2;

View file

@ -68,7 +68,7 @@
<!-- end icon -->
</div>
<script>
<script nonce="{{script_nonce}}">
// Use SetupPlugin class that is on static/js/plugins/setup.js
const setPlugin = new SetupPlugin({
info: {

View file

@ -71,7 +71,7 @@
</div>
</div>
<script>
<script nonce="{{script_nonce}}">
// Use SetupPlugin class that is on static/js/plugins/setup.js
const setPlugin = new SetupPlugin({
info: {

View file

@ -228,7 +228,7 @@
<!-- end icon -->
</div>
<script>
<script nonce="{{script_nonce}}">
// Use SetupPlugin class that is on static/js/plugins/setup.js
const setPlugin = new SetupPlugin({
info: {

View file

@ -47,7 +47,7 @@
<!-- end status -->
<script>
<script nonce="{{script_nonce}}">
// Use SetupPlugin class that is on static/js/plugins/setup.js
const setPlugin = new SetupPlugin({
info: {

View file

@ -67,7 +67,7 @@
<!-- end icon -->
</div>
<script>
<script nonce="{{script_nonce}}">
// Use SetupPlugin class that is on static/js/plugins/setup.js
const setPlugin = new SetupPlugin({
info: {

View file

@ -66,7 +66,7 @@
<!-- end icon -->
</div>
<script>
<script nonce="{{script_nonce}}">
// Use SetupPlugin class that is on static/js/plugins/setup.js
const setPlugin = new SetupPlugin({
info: {

View file

@ -66,7 +66,7 @@
<!-- end icon -->
</div>
<script>
<script nonce="{{script_nonce}}">
// Use SetupPlugin class that is on static/js/plugins/setup.js
const setPlugin = new SetupPlugin({
info: {

View file

@ -67,7 +67,7 @@
</div>
</div>
<script>
<script nonce="{{script_nonce}}">
// Use SetupPlugin class that is on static/js/plugins/setup.js
const setPlugin = new SetupPlugin({
info: {

View file

@ -66,7 +66,7 @@
<!-- end icon -->
</div>
<script>
<script nonce="{{script_nonce}}">
// Use SetupPlugin class that is on static/js/plugins/setup.js
const setPlugin = new SetupPlugin({
info: {

View file

@ -66,7 +66,7 @@ try:
elif db_config.get("USE_UI", {"value": "no"})["value"] == "yes":
data["use_ui"] = "yes"
data["external_plugins"] = [f"{plugin['id']}/{plugin['version']}" for plugin in db.get_plugins(external=True)]
data["external_plugins"] = [f"{plugin['id']}/{plugin['version']}" for plugin in db.get_plugins(_type="external")]
data["os"] = {
"name": "Linux",
"version": "Unknown",

View file

@ -48,7 +48,7 @@ def install_plugin(plugin_dir, db) -> bool:
if EXTERNAL_PLUGINS_DIR.joinpath(metadata["id"], "plugin.json").is_file():
old_version = None
for plugin in db.get_plugins(external=True):
for plugin in db.get_plugins(_type="external"):
if plugin["id"] == metadata["id"]:
old_version = plugin["version"]
break
@ -179,7 +179,7 @@ try:
plugin_file.update(
{
"external": True,
"type": "external",
"page": False,
"method": "scheduler",
"data": value,
@ -195,7 +195,7 @@ try:
lock = Lock()
for plugin in db.get_plugins(external=True, with_data=True):
for plugin in db.get_plugins(_type="external", with_data=True):
if plugin["method"] != "scheduler" and plugin["id"] not in external_plugins_ids:
external_plugins.append(plugin)

View file

@ -71,7 +71,7 @@
</div>
</div>
<script>
<script nonce="{{script_nonce}}">
// Use SetupPlugin class that is on static/js/plugins/setup.js
const setPlugin = new SetupPlugin({
info: {

View file

@ -114,7 +114,7 @@
<!-- end icon -->
</div>
<script>
<script nonce="{{script_nonce}}">
// Use SetupPlugin class that is on static/js/plugins/setup.js
const setPlugin = new SetupPlugin({
info: {

View file

@ -87,7 +87,7 @@
</div>
<!-- end status -->
<script>
<script nonce="{{script_nonce}}">
// Use SetupPlugin class that is on static/js/plugins/setup.js
const setPlugin = new SetupPlugin({
info: {

View file

@ -72,7 +72,7 @@
</div>
</div>
<script>
<script nonce="{{script_nonce}}">
// Use SetupPlugin class that is on static/js/plugins/setup.js
const setPlugin = new SetupPlugin({
info: {

View file

@ -65,7 +65,7 @@
<!-- end icon -->
</div>
<script>
<script nonce="{{script_nonce}}">
// Use SetupPlugin class that is on static/js/plugins/setup.js
const setPlugin = new SetupPlugin({
info: {

View file

@ -11,7 +11,7 @@ from os.path import basename, normpath, join
from pathlib import Path
from re import compile as re_compile
from sys import _getframe, path as sys_path
from typing import Any, Dict, List, Optional, Tuple, Union
from typing import Any, Dict, List, Literal, Optional, Tuple, Union
from time import sleep
from traceback import format_exc
@ -294,6 +294,7 @@ class Database:
.with_entities(
Metadata.custom_configs_changed,
Metadata.external_plugins_changed,
Metadata.pro_plugins_changed,
Metadata.config_changed,
Metadata.instances_changed,
)
@ -304,6 +305,7 @@ class Database:
return dict(
custom_configs_changed=metadata is not None and metadata.custom_configs_changed,
external_plugins_changed=metadata is not None and metadata.external_plugins_changed,
pro_plugins_changed=metadata is not None and metadata.pro_plugins_changed,
config_changed=metadata is not None and metadata.config_changed,
instances_changed=metadata is not None and metadata.instances_changed,
)
@ -316,6 +318,7 @@ class Database:
"config",
"custom_configs",
"external_plugins",
"pro_plugins",
"instances",
]
with self.__db_session() as session:
@ -333,6 +336,8 @@ class Database:
metadata.custom_configs_changed = value
if "external_plugins" in changes:
metadata.external_plugins_changed = value
if "pro_plugins" in changes:
metadata.pro_plugins_changed = value
if "instances" in changes:
metadata.instances_changed = value
session.commit()
@ -356,7 +361,9 @@ class Database:
has_all_tables = False
continue
missing_columns = []
extra_columns = []
# Check if any columns are missing
db_columns = inspector.get_columns(table)
self.__logger.debug(f'Checking table "{table}" for missing columns')
for column in Base.metadata.tables[table].columns:
@ -365,12 +372,24 @@ class Database:
self.__logger.warning(f'Column "{column.name}" is missing in table "{table}"')
missing_columns.append(column)
# Check if any columns are extra
self.__logger.debug(f'Checking table "{table}" for extra columns')
for db_column in db_columns:
self.__logger.debug(f'Checking column "{db_column["name"]}" in table "{table}"')
if not any(column.name == db_column["name"] for column in Base.metadata.tables[table].columns):
self.__logger.warning(f'Column "{db_column["name"]}" is extra in table "{table}"')
extra_columns.append(db_column)
try:
with self.__db_session() as session:
if missing_columns:
for column in missing_columns:
self.__logger.warning(f'Adding column "{column.name}" to table "{table}"')
session.execute(text(f"ALTER TABLE {table} ADD COLUMN {column.name} {column.type}"))
if extra_columns:
for column in extra_columns:
self.__logger.warning(f'Removing column "{column["name"]}" from table "{table}"')
session.execute(text(f"ALTER TABLE {table} DROP COLUMN {column['name']}"))
session.commit()
except BaseException:
return False, format_exc()
@ -400,7 +419,6 @@ class Database:
"description": "The general settings for the server",
"version": "0.1",
"stream": "partial",
"external": False,
}
else:
settings = plugin.pop("settings", {})
@ -423,8 +441,8 @@ class Database:
if plugin["stream"] != db_plugin.stream:
updates[Plugins.stream] = plugin["stream"]
if plugin.get("external", False) != db_plugin.external:
updates[Plugins.external] = plugin.get("external", False)
if plugin.get("type", "core") != db_plugin.type:
updates[Plugins.type] = plugin.get("type", "core")
if plugin.get("method", "manual") != db_plugin.method:
updates[Plugins.method] = plugin.get("method", "manual")
@ -446,7 +464,7 @@ class Database:
description=plugin["description"],
version=plugin["version"],
stream=plugin["stream"],
external=plugin.get("external", False),
type=plugin.get("type", "core"),
method=plugin.get("method"),
data=plugin.get("data"),
checksum=plugin.get("checksum"),
@ -1108,11 +1126,11 @@ class Database:
return ""
def update_external_plugins(self, plugins: List[Dict[str, Any]], *, delete_missing: bool = True) -> str:
def update_external_plugins(self, plugins: List[Dict[str, Any]], *, _type: Literal["external", "pro"] = "external", delete_missing: bool = True) -> str:
"""Update external plugins from the database"""
to_put = []
with self.__db_session() as session:
db_plugins = session.query(Plugins).with_entities(Plugins.id).filter_by(external=True).all()
db_plugins = session.query(Plugins).with_entities(Plugins.id).filter_by(type=_type).all()
db_ids = []
if delete_missing and db_plugins:
@ -1128,7 +1146,7 @@ class Database:
settings = plugin.pop("settings", {})
jobs = plugin.pop("jobs", [])
page = plugin.pop("page", False)
plugin["external"] = True
plugin["type"] = _type
db_plugin = (
session.query(Plugins)
.with_entities(
@ -1139,16 +1157,16 @@ class Database:
Plugins.method,
Plugins.data,
Plugins.checksum,
Plugins.external,
Plugins.type,
)
.filter_by(id=plugin["id"])
.first()
)
if db_plugin is not None:
if db_plugin.external is False:
if db_plugin.type not in ("external", "pro"):
self.__logger.warning(
f"Plugin \"{plugin['id']}\" is not external, skipping update (updating a non-external plugin is forbidden for security reasons)",
f"Plugin \"{plugin['id']}\" is not {_type}, skipping update (updating a non-external or non-pro plugin is forbidden for security reasons)",
)
continue
@ -1175,6 +1193,9 @@ class Database:
if plugin.get("checksum") != db_plugin.checksum:
updates[Plugins.checksum] = plugin.get("checksum")
if plugin.get("type") != db_plugin.type:
updates[Plugins.type] = plugin.get("type")
if updates:
session.query(Plugins).filter(Plugins.id == plugin["id"]).update(updates)
@ -1364,7 +1385,7 @@ class Database:
description=plugin["description"],
version=plugin["version"],
stream=plugin["stream"],
external=True,
type=_type,
method=plugin["method"],
data=plugin.get("data"),
checksum=plugin.get("checksum"),
@ -1464,7 +1485,10 @@ class Database:
with suppress(ProgrammingError, OperationalError):
metadata = session.query(Metadata).get(1)
if metadata is not None:
metadata.external_plugins_changed = True
if _type == "external":
metadata.external_plugins_changed = True
elif _type == "pro":
metadata.pro_plugins_changed = True
try:
session.add_all(to_put)
@ -1474,17 +1498,19 @@ class Database:
return ""
def get_plugins(self, *, external: bool = False, with_data: bool = False) -> List[Dict[str, Any]]:
def get_plugins(self, *, _type: Literal["all", "external", "pro"] = "all", with_data: bool = False) -> List[Dict[str, Any]]:
"""Get all plugins from the database."""
plugins = []
with self.__db_session() as session:
entities = [Plugins.id, Plugins.stream, Plugins.name, Plugins.description, Plugins.version, Plugins.external, Plugins.method, Plugins.checksum]
entities = [Plugins.id, Plugins.stream, Plugins.name, Plugins.description, Plugins.version, Plugins.type, Plugins.method, Plugins.checksum]
if with_data:
entities.append(Plugins.data)
for plugin in session.query(Plugins).with_entities(*entities).all():
if external and not plugin.external:
continue
db_plugins = session.query(Plugins).with_entities(*entities)
if _type != "all":
db_plugins = db_plugins.filter_by(type=_type)
for plugin in db_plugins.all():
page = session.query(Plugin_pages).with_entities(Plugin_pages.id).filter_by(plugin_id=plugin.id).first()
data = {
"id": plugin.id,
@ -1492,7 +1518,7 @@ class Database:
"name": plugin.name,
"description": plugin.description,
"version": plugin.version,
"external": plugin.external,
"type": plugin.type,
"method": plugin.method,
"page": page is not None,
"settings": {},

View file

@ -39,6 +39,7 @@ INTEGRATIONS_ENUM = Enum(
"Unknown",
name="integrations_enum",
)
PLUGIN_TYPES_ENUM = Enum("core", "external", "pro", name="plugin_types_enum")
Base = declarative_base()
@ -50,7 +51,7 @@ class Plugins(Base):
description = Column(String(256), nullable=False)
version = Column(String(32), nullable=False)
stream = Column(String(16), nullable=False)
external = Column(Boolean, default=False, nullable=False)
type = Column(PLUGIN_TYPES_ENUM, default="core", nullable=False)
method = Column(METHODS_ENUM, default="manual", nullable=False)
data = Column(LargeBinary(length=(2**32) - 1), nullable=True)
checksum = Column(String(128), nullable=True)
@ -65,7 +66,6 @@ class Settings(Base):
__table_args__ = (
PrimaryKeyConstraint("id", "name"),
UniqueConstraint("id"),
UniqueConstraint("name"),
)
id = Column(String(256), primary_key=True)
@ -273,6 +273,7 @@ class Metadata(Base):
scheduler_first_start = Column(Boolean, nullable=True)
custom_configs_changed = Column(Boolean, default=False, nullable=True)
external_plugins_changed = Column(Boolean, default=False, nullable=True)
pro_plugins_changed = Column(Boolean, default=False, nullable=True)
config_changed = Column(Boolean, default=False, nullable=True)
instances_changed = Column(Boolean, default=False, nullable=True)
integration = Column(INTEGRATIONS_ENUM, default="Unknown", nullable=False)

View file

@ -25,6 +25,7 @@ class Configurator:
settings: str,
core: str,
external_plugins: Union[str, List[Dict[str, Any]]],
pro_plugins: Union[str, List[Dict[str, Any]]],
variables: Union[str, Dict[str, Any]],
logger: Logger,
):
@ -46,6 +47,12 @@ class Configurator:
else:
self.__external_plugins = external_plugins
if isinstance(pro_plugins, str):
self.__pro_plugins = []
self.__load_plugins(pro_plugins, "pro")
else:
self.__pro_plugins = pro_plugins
if isinstance(variables, str):
self.__variables = self.__load_variables(variables)
else:
@ -57,12 +64,14 @@ class Configurator:
def get_settings(self) -> Dict[str, Any]:
return self.__settings
def get_plugins(self, _type: Union[Literal["core"], Literal["external"]]) -> List[Dict[str, Any]]:
return self.__core_plugins if _type == "core" else self.__external_plugins
def get_plugins(self, _type: Literal["core", "external", "pro"]) -> List[Dict[str, Any]]:
return {"core": self.__core_plugins, "external": self.__external_plugins, "pro": self.__pro_plugins}[_type]
def get_plugins_settings(self, _type: Union[Literal["core"], Literal["external"]]) -> Dict[str, Any]:
def get_plugins_settings(self, _type: Literal["core", "external", "pro"]) -> Dict[str, Any]:
if _type == "core":
plugins = self.__core_plugins
elif _type == "pro":
plugins = self.__pro_plugins
else:
plugins = self.__external_plugins
plugins_settings = {}
@ -78,9 +87,7 @@ class Configurator:
servers = {}
for server_name in self.__variables["SERVER_NAME"].strip().split(" "):
if not re_search(self.__settings["SERVER_NAME"]["regex"], server_name):
self.__logger.warning(
f"Ignoring server name {server_name} because regex is not valid",
)
self.__logger.warning(f"Ignoring server name {server_name} because regex is not valid")
continue
names = [server_name]
if f"{server_name}_SERVER_NAME" in self.__variables:
@ -88,9 +95,7 @@ class Configurator:
self.__settings["SERVER_NAME"]["regex"],
self.__variables[f"{server_name}_SERVER_NAME"],
):
self.__logger.warning(
f"Ignoring {server_name}_SERVER_NAME because regex is not valid",
)
self.__logger.warning(f"Ignoring {server_name}_SERVER_NAME because regex is not valid")
else:
names = self.__variables[f"{server_name}_SERVER_NAME"].strip().split(" ")
@ -100,7 +105,7 @@ class Configurator:
def __load_settings(self, path: str) -> Dict[str, Any]:
return loads(Path(path).read_text())
def __load_plugins(self, path: str, _type: str = "core"):
def __load_plugins(self, path: str, _type: Literal["core", "external", "pro"] = "core"):
threads = []
for file in glob(join(path, "*", "plugin.json")):
thread = Thread(target=self.__load_plugin, args=(file, _type))
@ -110,32 +115,26 @@ class Configurator:
for thread in threads:
thread.join()
def __load_plugin(self, file: str, _type: str = "core"):
def __load_plugin(self, file: str, _type: Literal["core", "external", "pro"] = "core"):
self.__semaphore.acquire(timeout=60)
try:
data = self.__load_settings(file)
resp, msg = self.__validate_plugin(data)
if not resp:
self.__logger.warning(
f"Ignoring plugin {file} : {msg}",
)
self.__logger.warning(f"Ignoring {_type} plugin {file} : {msg}")
return
if _type == "external":
if _type != "core":
plugin_content = BytesIO()
with tar_open(fileobj=plugin_content, mode="w:gz", compresslevel=9) as tar:
tar.add(
dirname(file),
arcname=basename(dirname(file)),
recursive=True,
)
tar.add(dirname(file), arcname=basename(dirname(file)), recursive=True)
plugin_content.seek(0, 0)
value = plugin_content.getvalue()
data.update(
{
"external": True,
"type": _type,
"page": "ui" in listdir(dirname(file)),
"method": "manual",
"data": value,
@ -144,14 +143,15 @@ class Configurator:
)
with self.__thread_lock:
self.__external_plugins.append(data)
if _type == "pro":
self.__pro_plugins.append(data)
else:
self.__external_plugins.append(data)
else:
with self.__thread_lock:
self.__core_plugins.append(data)
except:
self.__logger.error(
f"Exception while loading JSON from {file} : {format_exc()}",
)
self.__logger.error(f"Exception while loading JSON from {file} : {format_exc()}")
self.__semaphore.release()
def __load_variables(self, path: str) -> Dict[str, Any]:
@ -173,6 +173,7 @@ class Configurator:
self.__settings,
self.get_plugins_settings("core"),
self.get_plugins_settings("external"),
self.get_plugins_settings("pro"),
]
for settings in default_settings:
for setting, data in settings.items():
@ -226,10 +227,7 @@ class Configurator:
if not where:
return False, f"variable name {variable} doesn't exist"
elif not re_search(where[real_var]["regex"], value):
return (
False,
f"value {value} doesn't match regex {where[real_var]['regex']}",
)
return (False, f"value {value} doesn't match regex {where[real_var]['regex']}")
return True, "ok"
# MULTISITE=yes
prefixed, real_var = self.__var_is_prefixed(variable)
@ -239,10 +237,7 @@ class Configurator:
elif prefixed and where[real_var]["context"] != "multisite":
return False, f"context of {variable} isn't multisite"
elif not re_search(where[real_var]["regex"], value):
return (
False,
f"value {value} doesn't match regex {where[real_var]['regex']}",
)
return (False, f"value {value} doesn't match regex {where[real_var]['regex']}")
return True, "ok"
def __find_var(self, variable: str) -> Tuple[Optional[Dict[str, Any]], str]:
@ -250,6 +245,7 @@ class Configurator:
self.__settings,
self.get_plugins_settings("core"),
self.get_plugins_settings("external"),
self.get_plugins_settings("pro"),
]
for target in targets:
if variable in target:
@ -267,120 +263,57 @@ class Configurator:
def __validate_plugin(self, plugin: dict) -> Tuple[bool, str]:
if not all(key in plugin for key in ("id", "name", "description", "version", "stream", "settings")):
return (
False,
f"Missing mandatory keys for plugin {plugin.get('id', 'unknown')} (id, name, description, version, stream, settings)",
)
return (False, f"Missing mandatory keys for plugin {plugin.get('id', 'unknown')} (id, name, description, version, stream, settings)")
if not self.__plugin_id_rx.match(plugin["id"]):
return (
False,
f"Invalid id for plugin {plugin['id']} (Can only contain numbers, letters, underscores and hyphens (min 1 characters and max 64))",
)
return (False, f"Invalid id for plugin {plugin['id']} (Can only contain numbers, letters, underscores and hyphens (min 1 characters and max 64))")
elif len(plugin["name"]) > 128:
return (
False,
f"Invalid name for plugin {plugin['id']} (Max 128 characters)",
)
return (False, f"Invalid name for plugin {plugin['id']} (Max 128 characters)")
elif len(plugin["description"]) > 256:
return (
False,
f"Invalid description for plugin {plugin['id']} (Max 256 characters)",
)
return (False, f"Invalid description for plugin {plugin['id']} (Max 256 characters)")
elif not self.__plugin_version_rx.match(plugin["version"]):
return (
False,
f"Invalid version for plugin {plugin['id']} (Must be in format \\d+\\.\\d+(\\.\\d+)?)",
)
return (False, f"Invalid version for plugin {plugin['id']} (Must be in format \\d+\\.\\d+(\\.\\d+)?)")
elif plugin["stream"] not in ("yes", "no", "partial"):
return (
False,
f"Invalid stream for plugin {plugin['id']} (Must be yes, no or partial)",
)
return (False, f"Invalid stream for plugin {plugin['id']} (Must be yes, no or partial)")
for setting, data in plugin["settings"].items():
if not all(key in data.keys() for key in ("context", "default", "help", "id", "label", "regex", "type")):
return (
False,
f"missing keys for setting {setting} in plugin {plugin['id']}, must have context, default, help, id, label, regex and type",
)
return (False, f"missing keys for setting {setting} in plugin {plugin['id']}, must have context, default, help, id, label, regex and type")
if not self.__setting_id_rx.match(setting):
return (
False,
f"Invalid setting name for setting {setting} in plugin {plugin['id']} (Can only contain capital letters and underscores (min 1 characters and max 256))",
)
return (False, f"Invalid setting name for setting {setting} in plugin {plugin['id']} (Can only contain capital letters and underscores (min 1 characters and max 256))")
elif data["context"] not in ("global", "multisite"):
return (
False,
f"Invalid context for setting {setting} in plugin {plugin['id']} (Must be global or multisite)",
)
return (False, f"Invalid context for setting {setting} in plugin {plugin['id']} (Must be global or multisite)")
elif len(data["default"]) > 4096:
return (
False,
f"Invalid default for setting {setting} in plugin {plugin['id']} (Max 4096 characters)",
)
return (False, f"Invalid default for setting {setting} in plugin {plugin['id']} (Max 4096 characters)")
elif len(data["help"]) > 512:
return (
False,
f"Invalid help for setting {setting} in plugin {plugin['id']} (Max 512 characters)",
)
return (False, f"Invalid help for setting {setting} in plugin {plugin['id']} (Max 512 characters)")
elif len(data["label"]) > 256:
return (
False,
f"Invalid label for setting {setting} in plugin {plugin['id']} (Max 256 characters)",
)
return (False, f"Invalid label for setting {setting} in plugin {plugin['id']} (Max 256 characters)")
elif len(data["regex"]) > 1024:
return (
False,
f"Invalid regex for setting {setting} in plugin {plugin['id']} (Max 1024 characters)",
)
return (False, f"Invalid regex for setting {setting} in plugin {plugin['id']} (Max 1024 characters)")
elif data["type"] not in ("password", "text", "check", "select"):
return (
False,
f"Invalid type for setting {setting} in plugin {plugin['id']} (Must be password, text, check or select)",
)
return (False, f"Invalid type for setting {setting} in plugin {plugin['id']} (Must be password, text, check or select)")
if "multiple" in data:
if not self.__name_rx.match(data["multiple"]):
return (
False,
f"Invalid multiple for setting {setting} in plugin {plugin['id']} (Can only contain numbers, letters, underscores and hyphens (min 1 characters and max 128))",
)
return (False, f"Invalid multiple for setting {setting} in plugin {plugin['id']} (Can only contain numbers, letters, underscores and hyphens (min 1 characters and max 128))")
for select in data.get("select", []):
if len(select) > 256:
return (
False,
f"Invalid select value {select} for setting {setting} in plugin {plugin['id']} (Max 256 characters)",
)
return (False, f"Invalid select value {select} for setting {setting} in plugin {plugin['id']} (Max 256 characters)")
for job in plugin.get("jobs", []):
if not all(key in job.keys() for key in ("name", "file", "every", "reload")):
return (
False,
f"missing keys for job {job['name']} in plugin {plugin['id']}, must have name, file, every and reload",
)
return (False, f"missing keys for job {job['name']} in plugin {plugin['id']}, must have name, file, every and reload")
if not self.__name_rx.match(job["name"]):
return (
False,
f"Invalid name for job {job['name']} in plugin {plugin['id']}",
)
return (False, f"Invalid name for job {job['name']} in plugin {plugin['id']}")
elif not self.__job_file_rx.match(job["file"]):
return (
False,
f"Invalid file for job {job['name']} in plugin {plugin['id']} (Can only contain numbers, letters, underscores, hyphens and slashes (min 1 characters and max 256))",
)
return (False, f"Invalid file for job {job['name']} in plugin {plugin['id']} (Can only contain numbers, letters, underscores, hyphens and slashes (min 1 characters and max 256))")
elif job["every"] not in ("once", "minute", "hour", "day", "week"):
return (
False,
f"Invalid every for job {job['name']} in plugin {plugin['id']} (Must be once, minute, hour, day or week)",
)
return (False, f"Invalid every for job {job['name']} in plugin {plugin['id']} (Must be once, minute, hour, day or week)")
elif job["reload"] is not True and job["reload"] is not False:
return (
False,
f"Invalid reload for job {job['name']} in plugin {plugin['id']} (Must be true or false)",
)
return (False, f"Invalid reload for job {job['name']} in plugin {plugin['id']} (Must be true or false)")
return True, "ok"

View file

@ -16,18 +16,11 @@ from jinja2 import Environment, FileSystemLoader
class Templator:
def __init__(
self,
templates: str,
core: str,
plugins: str,
output: str,
target: str,
config: Dict[str, Any],
):
def __init__(self, templates: str, core: str, plugins: str, pro_plugins: str, output: str, target: str, config: Dict[str, Any]):
self.__templates = templates
self.__core = core
self.__plugins = plugins
self.__pro_plugins = pro_plugins
self.__output = output
self.__target = target
self.__config = config
@ -43,14 +36,10 @@ class Templator:
def __load_jinja_env(self) -> Environment:
searchpath = [self.__templates]
for subpath in glob(join(self.__core, "*")) + glob(join(self.__plugins, "*")):
for subpath in glob(join(self.__core, "*")) + glob(join(self.__plugins, "*")) + glob(join(self.__pro_plugins, "*")):
if Path(subpath).is_dir():
searchpath.append(join(subpath, "confs"))
return Environment(
loader=FileSystemLoader(searchpath=searchpath),
lstrip_blocks=True,
trim_blocks=True,
)
return Environment(loader=FileSystemLoader(searchpath=searchpath), lstrip_blocks=True, trim_blocks=True)
def __find_templates(self, contexts) -> List[str]:
templates = []

View file

@ -28,47 +28,14 @@ if __name__ == "__main__":
try:
# Parse arguments
parser = ArgumentParser(description="BunkerWeb config generator")
parser.add_argument(
"--settings",
default=join(sep, "usr", "share", "bunkerweb", "settings.json"),
type=str,
help="file containing the main settings",
)
parser.add_argument(
"--templates",
default=join(sep, "usr", "share", "bunkerweb", "confs"),
type=str,
help="directory containing the main template files",
)
parser.add_argument(
"--core",
default=join(sep, "usr", "share", "bunkerweb", "core"),
type=str,
help="directory containing the core plugins",
)
parser.add_argument(
"--plugins",
default=join(sep, "etc", "bunkerweb", "plugins"),
type=str,
help="directory containing the external plugins",
)
parser.add_argument(
"--output",
default=join(sep, "etc", "nginx"),
type=str,
help="where to write the rendered files",
)
parser.add_argument(
"--target",
default=join(sep, "etc", "nginx"),
type=str,
help="where nginx will search for configurations files",
)
parser.add_argument(
"--variables",
type=str,
help="path to the file containing environment variables",
)
parser.add_argument("--settings", default=join(sep, "usr", "share", "bunkerweb", "settings.json"), type=str, help="file containing the main settings")
parser.add_argument("--templates", default=join(sep, "usr", "share", "bunkerweb", "confs"), type=str, help="directory containing the main template files")
parser.add_argument("--core", default=join(sep, "usr", "share", "bunkerweb", "core"), type=str, help="directory containing the core plugins")
parser.add_argument("--plugins", default=join(sep, "etc", "bunkerweb", "plugins"), type=str, help="directory containing the external plugins")
parser.add_argument("--pro-plugins", default=join(sep, "etc", "bunkerweb", "pro", "plugins"), type=str, help="directory containing the pro plugins")
parser.add_argument("--output", default=join(sep, "etc", "nginx"), type=str, help="where to write the rendered files")
parser.add_argument("--target", default=join(sep, "etc", "nginx"), type=str, help="where nginx will search for configurations files")
parser.add_argument("--variables", type=str, help="path to the file containing environment variables")
parser.add_argument("--no-linux-reload", action="store_true", help="disable linux reload")
args = parser.parse_args()
@ -76,6 +43,7 @@ if __name__ == "__main__":
templates_path = Path(normpath(args.templates))
core_path = Path(normpath(args.core))
plugins_path = Path(normpath(args.plugins))
pro_plugins_path = Path(normpath(args.pro_plugins))
output_path = Path(normpath(args.output))
target_path = Path(normpath(args.target))
@ -84,6 +52,7 @@ if __name__ == "__main__":
logger.info(f"Templates : {templates_path}")
logger.info(f"Core : {core_path}")
logger.info(f"Plugins : {plugins_path}")
logger.info(f"Pro plugins : {pro_plugins_path}")
logger.info(f"Output : {output_path}")
logger.info(f"Target : {target_path}")
@ -110,7 +79,7 @@ if __name__ == "__main__":
# Check existences and permissions
logger.info("Checking arguments ...")
files = [settings_path, variables_path]
paths_rx = [core_path, plugins_path, templates_path]
paths_rx = [core_path, plugins_path, pro_plugins_path, templates_path]
paths_rwx = [output_path]
for file in files:
if not file.is_file():
@ -137,13 +106,7 @@ if __name__ == "__main__":
# Compute the config
logger.info("Computing config ...")
config: Dict[str, Any] = Configurator(
str(settings_path),
str(core_path),
str(plugins_path),
str(variables_path),
logger,
).get_config()
config: Dict[str, Any] = Configurator(str(settings_path), str(core_path), str(plugins_path), str(pro_plugins_path), str(variables_path), logger).get_config()
else:
if join(sep, "usr", "share", "bunkerweb", "db") not in sys_path:
sys_path.append(join(sep, "usr", "share", "bunkerweb", "db"))
@ -168,14 +131,7 @@ if __name__ == "__main__":
# Render the templates
logger.info("Rendering templates ...")
templator = Templator(
str(templates_path),
str(core_path),
str(plugins_path),
str(output_path),
str(target_path),
config,
)
templator = Templator(str(templates_path), str(core_path), str(plugins_path), str(pro_plugins_path), str(output_path), str(target_path), config)
templator.render()
if integration not in ("Autoconf", "Swarm", "Kubernetes", "Docker") and not args.no_linux_reload:

View file

@ -79,55 +79,26 @@ if __name__ == "__main__":
try:
# Parse arguments
parser = ArgumentParser(description="BunkerWeb config saver")
parser.add_argument(
"--settings",
default=join(sep, "usr", "share", "bunkerweb", "settings.json"),
type=str,
help="file containing the main settings",
)
parser.add_argument(
"--core",
default=join(sep, "usr", "share", "bunkerweb", "core"),
type=str,
help="directory containing the core plugins",
)
parser.add_argument(
"--plugins",
default=join(sep, "etc", "bunkerweb", "plugins"),
type=str,
help="directory containing the external plugins",
)
parser.add_argument(
"--variables",
type=str,
help="path to the file containing environment variables",
)
parser.add_argument(
"--init",
action="store_true",
help="Only initialize the database",
)
parser.add_argument(
"--method",
default="scheduler",
type=str,
help="The method that is used to save the config",
)
parser.add_argument(
"--no-check-changes",
action="store_true",
help="Set the changes to checked in the database",
)
parser.add_argument("--settings", default=join(sep, "usr", "share", "bunkerweb", "settings.json"), type=str, help="file containing the main settings")
parser.add_argument("--core", default=join(sep, "usr", "share", "bunkerweb", "core"), type=str, help="directory containing the core plugins")
parser.add_argument("--plugins", default=join(sep, "etc", "bunkerweb", "plugins"), type=str, help="directory containing the external plugins")
parser.add_argument("--pro-plugins", default=join(sep, "etc", "bunkerweb", "pro", "plugins"), type=str, help="directory containing the pro plugins")
parser.add_argument("--variables", type=str, help="path to the file containing environment variables")
parser.add_argument("--init", action="store_true", help="Only initialize the database")
parser.add_argument("--method", default="scheduler", type=str, help="The method that is used to save the config")
parser.add_argument("--no-check-changes", action="store_true", help="Set the changes to checked in the database")
args = parser.parse_args()
settings_path = Path(normpath(args.settings))
core_path = Path(normpath(args.core))
plugins_path = Path(normpath(args.plugins))
pro_plugins_path = Path(normpath(args.pro_plugins))
logger.info("Save config started ...")
logger.info(f"Settings : {settings_path}")
logger.info(f"Core : {core_path}")
logger.info(f"Plugins : {plugins_path}")
logger.info(f"Pro plugins : {pro_plugins_path}")
logger.info(f"Init : {args.init}")
integration = "Linux"
@ -154,14 +125,16 @@ if __name__ == "__main__":
apis = []
external_plugins = args.plugins
pro_plugins = args.pro_plugins
if not Path(sep, "usr", "sbin", "nginx").exists() and args.method == "ui":
db = Database(logger, pool=False)
external_plugins = db.get_plugins()
external_plugins = db.get_plugins(_type="external")
pro_plugins = db.get_plugins(_type="pro")
# Check existences and permissions
logger.info("Checking arguments ...")
files = [settings_path] + ([Path(normpath(args.variables))] if args.variables else [])
paths_rx = [core_path, plugins_path]
paths_rx = [core_path, plugins_path, pro_plugins_path]
for file in files:
if not file.is_file():
logger.error(f"Missing file : {file}")
@ -174,9 +147,7 @@ if __name__ == "__main__":
logger.error(f"Missing directory : {path}")
sys_exit(1)
if not access(path, R_OK | X_OK):
logger.error(
f"Missing RX rights on directory : {path}",
)
logger.error(f"Missing RX rights on directory : {path}")
sys_exit(1)
if args.variables:
@ -185,13 +156,7 @@ if __name__ == "__main__":
# Compute the config
logger.info("Computing config ...")
config = Configurator(
str(settings_path),
str(core_path),
external_plugins,
str(variables_path),
logger,
)
config = Configurator(str(settings_path), str(core_path), external_plugins, pro_plugins, str(variables_path), logger)
config_files = config.get_config()
custom_confs = []
for k, v in environ.items():
@ -262,13 +227,7 @@ if __name__ == "__main__":
# Compute the config
if not config_files:
logger.info("Computing config ...")
config = Configurator(
args.settings,
args.core,
external_plugins,
tmp_config,
logger,
)
config = Configurator(args.settings, args.core, external_plugins, pro_plugins, tmp_config, logger)
config_files = config.get_config()
bunkerweb_version = Path(sep, "usr", "share", "bunkerweb", "VERSION").read_text().strip()
@ -279,11 +238,7 @@ if __name__ == "__main__":
"Database not initialized, initializing ...",
)
ret, err = db.init_tables(
[
config.get_settings(),
config.get_plugins("core"),
config.get_plugins("external"),
],
[config.get_settings(), config.get_plugins("core"), config.get_plugins("external"), config.get_plugins("pro")],
bunkerweb_version,
)
@ -303,11 +258,7 @@ if __name__ == "__main__":
logger.info("Database is already initialized, checking for changes ...")
ret, err = db.init_tables(
[
config.get_settings(),
config.get_plugins("core"),
config.get_plugins("external"),
],
[config.get_settings(), config.get_plugins("core"), config.get_plugins("external"), config.get_plugins("pro")],
bunkerweb_version,
)

View file

@ -8,7 +8,7 @@ log "$service" "" "Setup and check /data folder ..."
# Create folders if missing and check permissions
rwx_folders=("cache" "cache/letsencrypt" "lib")
rx_folders=("configs" "configs/http" "configs/stream" "configs/server-http" "configs/server-stream" "configs/default-server-http" "configs/default-server-stream" "configs/modsec" "configs/modsec-crs" "plugins" "www")
rx_folders=("pro" "pro/plugins" "configs" "configs/http" "configs/stream" "configs/server-http" "configs/server-stream" "configs/default-server-http" "configs/default-server-stream" "configs/modsec" "configs/modsec-crs" "plugins" "www")
for folder in "${rwx_folders[@]}" ; do
if [ ! -d "/data/${folder}" ] ; then
mkdir -p "/data/${folder}"

View file

@ -94,7 +94,7 @@ RUN cp helpers/bwcli /usr/bin/ && \
chmod 755 /usr/bin/bwcli && \
mkdir -p /etc/bunkerweb/configs /etc/bunkerweb/plugins /var/cache/bunkerweb /var/tmp/bunkerweb /var/run/bunkerweb /var/log/bunkerweb /var/lib/bunkerweb /var/www/html && \
echo "Linux" > INTEGRATION && \
for dir in $(echo "configs/http configs/stream configs/server-http configs/server-stream configs/default-server-http configs/default-server-stream configs/modsec configs/modsec-crs") ; do mkdir -p "/etc/bunkerweb/${dir}" ; done && \
for dir in $(echo "plugins pro/plugins configs/http configs/stream configs/server-http configs/server-stream configs/default-server-http configs/default-server-stream configs/modsec configs/modsec-crs") ; do mkdir -p "/etc/bunkerweb/${dir}" ; done && \
find . -path deps -prune -o -type f -exec chmod 0740 {} \; && \
find . -path deps -prune -o -type d -exec chmod 0750 {} \; && \
chmod 755 /var/log/bunkerweb && \

View file

@ -78,7 +78,7 @@ RUN cp helpers/bwcli /usr/bin/ && \
chmod 755 /usr/bin/bwcli && \
mkdir -p /etc/bunkerweb/configs /etc/bunkerweb/plugins /var/cache/bunkerweb /var/tmp/bunkerweb /var/run/bunkerweb /var/log/bunkerweb /var/lib/bunkerweb /var/www/html && \
echo "Linux" > INTEGRATION && \
for dir in $(echo "configs/http configs/stream configs/server-http configs/server-stream configs/default-server-http configs/default-server-stream configs/modsec configs/modsec-crs") ; do mkdir -p "/etc/bunkerweb/${dir}" ; done && \
for dir in $(echo "plugins pro/plugins configs/http configs/stream configs/server-http configs/server-stream configs/default-server-http configs/default-server-stream configs/modsec configs/modsec-crs") ; do mkdir -p "/etc/bunkerweb/${dir}" ; done && \
find . -path deps -prune -o -type f -exec chmod 0740 {} \; && \
find . -path deps -prune -o -type d -exec chmod 0750 {} \; && \
chmod 755 /var/log/bunkerweb && \

View file

@ -74,7 +74,7 @@ RUN cp helpers/bwcli /usr/bin/ && \
chmod 755 /usr/bin/bwcli && \
mkdir -p /etc/bunkerweb/configs /etc/bunkerweb/plugins /var/cache/bunkerweb /var/tmp/bunkerweb /var/run/bunkerweb /var/log/bunkerweb /var/lib/bunkerweb /var/www/html && \
echo "Linux" > INTEGRATION && \
for dir in $(echo "configs/http configs/stream configs/server-http configs/server-stream configs/default-server-http configs/default-server-stream configs/modsec configs/modsec-crs") ; do mkdir -p "/etc/bunkerweb/${dir}" ; done && \
for dir in $(echo "plugins pro/plugins configs/http configs/stream configs/server-http configs/server-stream configs/default-server-http configs/default-server-stream configs/modsec configs/modsec-crs") ; do mkdir -p "/etc/bunkerweb/${dir}" ; done && \
find . -path deps -prune -o -type f -exec chmod 0740 {} \; && \
find . -path deps -prune -o -type d -exec chmod 0750 {} \; && \
chmod 755 /var/log/bunkerweb && \

View file

@ -90,7 +90,7 @@ RUN cp helpers/bwcli /usr/bin/ && \
chmod 755 /usr/bin/bwcli && \
mkdir -p /etc/bunkerweb/configs /etc/bunkerweb/plugins /var/cache/bunkerweb /var/tmp/bunkerweb /var/run/bunkerweb /var/log/bunkerweb /var/lib/bunkerweb /var/www/html && \
echo "Linux" > INTEGRATION && \
for dir in $(echo "configs/http configs/stream configs/server-http configs/server-stream configs/default-server-http configs/default-server-stream configs/modsec configs/modsec-crs") ; do mkdir -p "/etc/bunkerweb/${dir}" ; done && \
for dir in $(echo "plugins pro/plugins configs/http configs/stream configs/server-http configs/server-stream configs/default-server-http configs/default-server-stream configs/modsec configs/modsec-crs") ; do mkdir -p "/etc/bunkerweb/${dir}" ; done && \
find . -path deps -prune -o -type f -exec chmod 0740 {} \; && \
find . -path deps -prune -o -type d -exec chmod 0750 {} \; && \
chmod 755 /var/log/bunkerweb && \

View file

@ -90,7 +90,7 @@ RUN cp helpers/bwcli /usr/bin/ && \
chmod 755 /usr/bin/bwcli && \
mkdir -p /etc/bunkerweb/configs /etc/bunkerweb/plugins /var/cache/bunkerweb /var/tmp/bunkerweb /var/run/bunkerweb /var/log/bunkerweb /var/lib/bunkerweb /var/www/html && \
echo "Linux" > INTEGRATION && \
for dir in $(echo "configs/http configs/stream configs/server-http configs/server-stream configs/default-server-http configs/default-server-stream configs/modsec configs/modsec-crs") ; do mkdir -p "/etc/bunkerweb/${dir}" ; done && \
for dir in $(echo "plugins pro/plugins configs/http configs/stream configs/server-http configs/server-stream configs/default-server-http configs/default-server-stream configs/modsec configs/modsec-crs") ; do mkdir -p "/etc/bunkerweb/${dir}" ; done && \
find . -path deps -prune -o -type f -exec chmod 0740 {} \; && \
find . -path deps -prune -o -type d -exec chmod 0750 {} \; && \
chmod 755 /var/log/bunkerweb && \

View file

@ -78,7 +78,7 @@ RUN cp helpers/bwcli /usr/bin/ && \
chmod 755 /usr/bin/bwcli && \
mkdir -p /etc/bunkerweb/configs /etc/bunkerweb/plugins /var/cache/bunkerweb /var/tmp/bunkerweb /var/run/bunkerweb /var/log/bunkerweb /var/lib/bunkerweb /var/www/html && \
echo "Linux" > INTEGRATION && \
for dir in $(echo "configs/http configs/stream configs/server-http configs/server-stream configs/default-server-http configs/default-server-stream configs/modsec configs/modsec-crs") ; do mkdir -p "/etc/bunkerweb/${dir}" ; done && \
for dir in $(echo "plugins pro/plugins configs/http configs/stream configs/server-http configs/server-stream configs/default-server-http configs/default-server-stream configs/modsec configs/modsec-crs") ; do mkdir -p "/etc/bunkerweb/${dir}" ; done && \
find . -path deps -prune -o -type f -exec chmod 0740 {} \; && \
find . -path deps -prune -o -type d -exec chmod 0750 {} \; && \
chmod 755 /var/log/bunkerweb && \

View file

@ -62,8 +62,8 @@ RUN apk add --no-cache bash libgcc libstdc++ libpq openssl libmagic && \
mkdir -p /etc/bunkerweb && \
mkdir -p /data/cache && ln -s /data/cache /var/cache/bunkerweb && \
mkdir -p /data/lib && ln -s /data/lib /var/lib/bunkerweb && \
for dir in $(echo "configs plugins") ; do mkdir -p "/data/${dir}" && ln -s "/data/${dir}" "/etc/bunkerweb/${dir}" ; done && \
for dir in $(echo "configs/http configs/stream configs/server-http configs/server-stream configs/default-server-http configs/default-server-stream configs/modsec configs/modsec-crs") ; do mkdir "/data/${dir}" ; done && \
for dir in $(echo "pro configs plugins") ; do mkdir -p "/data/${dir}" && ln -s "/data/${dir}" "/etc/bunkerweb/${dir}" ; done && \
for dir in $(echo "pro/plugins configs/http configs/stream configs/server-http configs/server-stream configs/default-server-http configs/default-server-stream configs/modsec configs/modsec-crs") ; do mkdir "/data/${dir}" ; done && \
chown -R root:scheduler /data /etc/nginx /var/cache/bunkerweb /var/lib/bunkerweb /etc/bunkerweb /var/tmp/bunkerweb /var/run/bunkerweb /var/log/bunkerweb /usr/bin/bwcli && \
chmod -R 770 /data /etc/nginx /var/cache/bunkerweb /var/lib/bunkerweb /etc/bunkerweb /var/tmp/bunkerweb /var/run/bunkerweb /var/log/bunkerweb && \
find core/*/jobs/* -type f -exec chmod 750 {} \; && \

View file

@ -77,15 +77,10 @@ class JobScheduler(ApiCaller):
with self.__thread_lock:
instances = self.__db.get_instances()
for instance in instances:
api = API(
f"http://{instance['hostname']}:{instance['port']}",
host=instance["server_name"],
)
api = API(f"http://{instance['hostname']}:{instance['port']}", host=instance["server_name"])
apis.append(api)
except:
self.__logger.warning(
f"Exception while getting jobs instances : {format_exc()}",
)
self.__logger.warning(f"Exception while getting jobs instances : {format_exc()}")
return apis
def update_jobs(self):
@ -93,7 +88,9 @@ class JobScheduler(ApiCaller):
def __get_jobs(self):
jobs = {}
for plugin_file in glob(join(sep, "usr", "share", "bunkerweb", "core", "*", "plugin.json")) + glob(join(sep, "etc", "bunkerweb", "plugins", "*", "plugin.json")): # core plugins # external plugins
for plugin_file in (
glob(join(sep, "usr", "share", "bunkerweb", "core", "*", "plugin.json")) + glob(join(sep, "etc", "bunkerweb", "plugins", "*", "plugin.json")) + glob(join(sep, "etc", "bunkerweb", "pro", "plugins", "*", "plugin.json"))
): # core plugins # external plugins # pro plugins
plugin_name = basename(dirname(plugin_file))
jobs[plugin_name] = []
try:
@ -104,15 +101,7 @@ class JobScheduler(ApiCaller):
plugin_jobs = plugin_data["jobs"]
for x, job in enumerate(deepcopy(plugin_jobs)):
if not all(
key in job.keys()
for key in (
"name",
"file",
"every",
"reload",
)
):
if not all(key in job.keys() for key in ("name", "file", "every", "reload")):
self.__logger.warning(f"missing keys for job {job['name']} in plugin {plugin_name}, must have name, file, every and reload, ignoring job")
plugin_jobs.pop(x)
continue
@ -140,9 +129,7 @@ class JobScheduler(ApiCaller):
except FileNotFoundError:
pass
except:
self.__logger.warning(
f"Exception while getting jobs for plugin {plugin_name} : {format_exc()}",
)
self.__logger.warning(f"Exception while getting jobs for plugin {plugin_name} : {format_exc()}")
return jobs
def __str_to_schedule(self, every: str) -> Job:
@ -160,20 +147,12 @@ class JobScheduler(ApiCaller):
reload = True
if self.__integration not in ("Autoconf", "Swarm", "Kubernetes", "Docker"):
self.__logger.info("Reloading nginx ...")
proc = run(
[join(sep, "usr", "sbin", "nginx"), "-s", "reload"],
stdin=DEVNULL,
stderr=PIPE,
env=self.__env,
check=False,
)
proc = run([join(sep, "usr", "sbin", "nginx"), "-s", "reload"], stdin=DEVNULL, stderr=PIPE, env=self.__env, check=False)
reload = proc.returncode == 0
if reload:
self.__logger.info("Successfully reloaded nginx")
else:
self.__logger.error(
f"Error while reloading nginx - returncode: {proc.returncode} - error: {proc.stderr.decode() if proc.stderr else 'Missing stderr'}",
)
self.__logger.error(f"Error while reloading nginx - returncode: {proc.returncode} - error: {proc.stderr.decode() if proc.stderr else 'Missing stderr'}")
else:
self.__logger.info("Reloading nginx ...")
reload = self.send_to_apis("POST", "/reload")
@ -184,33 +163,21 @@ class JobScheduler(ApiCaller):
return reload
def __job_wrapper(self, path: str, plugin: str, name: str, file: str) -> int:
self.__logger.info(
f"Executing job {name} from plugin {plugin} ...",
)
self.__logger.info(f"Executing job {name} from plugin {plugin} ...")
success = True
ret = -1
try:
proc = run(
join(path, "jobs", file),
stdin=DEVNULL,
stderr=STDOUT,
env=self.__env,
check=False,
)
proc = run(join(path, "jobs", file), stdin=DEVNULL, stderr=STDOUT, env=self.__env, check=False)
ret = proc.returncode
except BaseException:
success = False
self.__logger.error(
f"Exception while executing job {name} from plugin {plugin} :\n{format_exc()}",
)
self.__logger.error(f"Exception while executing job {name} from plugin {plugin} :\n{format_exc()}")
with self.__thread_lock:
self.__job_success = False
if self.__job_success and ret >= 2:
success = False
self.__logger.error(
f"Error while executing job {name} from plugin {plugin}",
)
self.__logger.error(f"Error while executing job {name} from plugin {plugin}")
with self.__thread_lock:
self.__job_success = False
@ -223,13 +190,9 @@ class JobScheduler(ApiCaller):
err = self.__db.update_job(plugin, name, success)
if not err:
self.__logger.info(
f"Successfully updated database for the job {name} from plugin {plugin}",
)
self.__logger.info(f"Successfully updated database for the job {name} from plugin {plugin}")
else:
self.__logger.warning(
f"Failed to update database for the job {name} from plugin {plugin}: {err}",
)
self.__logger.warning(f"Failed to update database for the job {name} from plugin {plugin}: {err}")
def setup(self):
for plugin, jobs in self.__jobs.items():
@ -242,9 +205,7 @@ class JobScheduler(ApiCaller):
if every != "once":
self.__str_to_schedule(every).do(self.__job_wrapper, path, plugin, name, file)
except:
self.__logger.error(
f"Exception while scheduling jobs for plugin {plugin} : {format_exc()}",
)
self.__logger.error(f"Exception while scheduling jobs for plugin {plugin} : {format_exc()}")
def run_pending(self) -> bool:
if self.__lock:
@ -278,9 +239,7 @@ class JobScheduler(ApiCaller):
success = False
except:
success = False
self.__logger.error(
f"Exception while reloading after job scheduling : {format_exc()}",
)
self.__logger.error(f"Exception while reloading after job scheduling : {format_exc()}")
if self.__lock:
self.__lock.release()
@ -332,8 +291,6 @@ class JobScheduler(ApiCaller):
ret = self.run_once()
self.setup()
except:
self.__logger.error(
f"Exception while reloading scheduler {format_exc()}",
)
self.__logger.error(f"Exception while reloading scheduler {format_exc()}")
return False
return ret

View file

@ -18,7 +18,7 @@ from tarfile import open as tar_open
from threading import Thread
from time import sleep
from traceback import format_exc
from typing import Any, Dict, List, Optional, Union
from typing import Any, Dict, List, Literal, Optional, Union
for deps_path in [join(sep, "usr", "share", "bunkerweb", *paths) for paths in (("deps", "python"), ("utils",), ("api",), ("db",))]:
if deps_path not in sys_path:
@ -34,6 +34,8 @@ RUN = True
SCHEDULER: Optional[JobScheduler] = None
INTEGRATION = "Linux"
CACHE_PATH = join(sep, "var", "cache", "bunkerweb")
EXTERNAL_PLUGINS_PATH = Path(sep, "etc", "bunkerweb", "plugins")
PRO_PLUGINS_PATH = Path(sep, "etc", "bunkerweb", "pro", "plugins")
SCHEDULER_TMP_ENV_PATH = Path(sep, "var", "tmp", "bunkerweb", "scheduler.env")
SCHEDULER_TMP_ENV_PATH.parent.mkdir(parents=True, exist_ok=True)
SCHEDULER_TMP_ENV_PATH.touch()
@ -61,13 +63,9 @@ def handle_reload(signum, frame):
else:
logger.error("Reload failed")
else:
logger.warning(
"Ignored reload operation because scheduler is not running ...",
)
logger.warning("Ignored reload operation because scheduler is not running ...")
except:
logger.error(
f"Exception while reloading scheduler : {format_exc()}",
)
logger.error(f"Exception while reloading scheduler : {format_exc()}")
signal(SIGHUP, handle_reload)
@ -79,11 +77,7 @@ def stop(status):
_exit(status)
def generate_custom_configs(
configs: List[Dict[str, Any]],
*,
original_path: Union[Path, str] = join(sep, "etc", "bunkerweb", "configs"),
):
def generate_custom_configs(configs: List[Dict[str, Any]], *, original_path: Union[Path, str] = join(sep, "etc", "bunkerweb", "configs")):
if not isinstance(original_path, Path):
original_path = Path(original_path)
@ -113,21 +107,16 @@ def generate_custom_configs(
ret = SCHEDULER.send_files(original_path, "/custom_configs")
if not ret:
logger.error(
"Sending custom configs failed, configuration will not work as expected...",
)
logger.error("Sending custom configs failed, configuration will not work as expected...")
def generate_external_plugins(
plugins: List[Dict[str, Any]],
*,
original_path: Union[Path, str] = join(sep, "etc", "bunkerweb", "plugins"),
):
def generate_external_plugins(plugins: List[Dict[str, Any]], *, original_path: Union[Path, str] = EXTERNAL_PLUGINS_PATH):
if not isinstance(original_path, Path):
original_path = Path(original_path)
pro = original_path.as_posix().endswith("/pro/plugins")
# Remove old external plugins files
logger.info("Removing old external plugins files ...")
# Remove old external/pro plugins files
logger.info(f"Removing old {'pro ' if pro else ''}external plugins files ...")
for file in glob(str(original_path.joinpath("*"))):
file = Path(file)
if file.is_symlink() or file.is_file():
@ -136,7 +125,7 @@ def generate_external_plugins(
rmtree(file, ignore_errors=True)
if plugins:
logger.info("Generating new external plugins ...")
logger.info(f"Generating new {'pro ' if pro else ''}external plugins ...")
original_path.mkdir(parents=True, exist_ok=True)
for plugin in plugins:
tmp_path = original_path.joinpath(plugin["id"], f"{plugin['name']}.tar.gz")
@ -151,13 +140,11 @@ def generate_external_plugins(
chmod(job_file, st.st_mode | S_IEXEC)
if SCHEDULER and SCHEDULER.apis:
logger.info("Sending plugins to BunkerWeb")
ret = SCHEDULER.send_files(original_path, "/plugins")
logger.info(f"Sending {'pro ' if pro else ''}external plugins to BunkerWeb")
ret = SCHEDULER.send_files(original_path, "/pro_plugins" if original_path.as_posix().endswith("/pro/plugins") else "/plugins")
if not ret:
logger.error(
"Sending plugins failed, configuration will not work as expected...",
)
logger.error(f"Sending {'pro ' if pro else ''}external plugins failed, configuration will not work as expected...")
def dict_to_frozenset(d):
@ -181,9 +168,7 @@ if __name__ == "__main__":
# Don't execute if pid file exists
pid_path = Path(sep, "var", "run", "bunkerweb", "scheduler.pid")
if pid_path.is_file():
logger.error(
"Scheduler is already running, skipping execution ...",
)
logger.error("Scheduler is already running, skipping execution ...")
_exit(1)
# Write pid to file
@ -193,11 +178,7 @@ if __name__ == "__main__":
# Parse arguments
parser = ArgumentParser(description="Job scheduler for BunkerWeb")
parser.add_argument(
"--variables",
type=str,
help="path to the file containing environment variables",
)
parser.add_argument("--variables", type=str, help="path to the file containing environment variables")
args = parser.parse_args()
integration_path = Path(sep, "usr", "share", "bunkerweb", "INTEGRATION")
@ -226,21 +207,13 @@ if __name__ == "__main__":
)
env = {}
if INTEGRATION in (
"Swarm",
"Kubernetes",
"Autoconf",
):
if INTEGRATION in ("Swarm", "Kubernetes", "Autoconf"):
while not db.is_initialized():
logger.warning(
"Database is not initialized, retrying in 5s ...",
)
logger.warning("Database is not initialized, retrying in 5s ...")
sleep(5)
while not db.is_autoconf_loaded():
logger.warning(
"Autoconf is not loaded yet in the database, retrying in 5s ...",
)
logger.warning("Autoconf is not loaded yet in the database, retrying in 5s ...")
sleep(5)
env = db.get_config()
@ -259,21 +232,15 @@ if __name__ == "__main__":
check=False,
)
if proc.returncode != 0:
logger.error(
"Config saver failed, configuration will not work as expected...",
)
logger.error("Config saver failed, configuration will not work as expected...")
while not db.is_initialized():
logger.warning(
"Database is not initialized, retrying in 5s ...",
)
logger.warning("Database is not initialized, retrying in 5s ...")
sleep(5)
env = db.get_config()
while not db.is_first_config_saved() or not env:
logger.warning(
"Database doesn't have any config saved yet, retrying in 5s ...",
)
logger.warning("Database doesn't have any config saved yet, retrying in 5s ...")
sleep(5)
env = db.get_config()
else:
@ -290,9 +257,7 @@ if __name__ == "__main__":
SCHEDULER.auto_setup()
if not SCHEDULER.apis:
logger.warning(
"No BunkerWeb API found, retrying in 5s ...",
)
logger.warning("No BunkerWeb API found, retrying in 5s ...")
sleep(5)
db.update_instances([api_to_instance(api) for api in SCHEDULER.apis])
@ -337,73 +302,66 @@ if __name__ == "__main__":
if changes:
err = db.save_custom_configs(custom_configs, "manual")
if err:
logger.error(
f"Couldn't save some manually created custom configs to database: {err}",
)
logger.error(f"Couldn't save some manually created custom configs to database: {err}")
if (scheduler_first_start and db_configs) or changes:
Thread(
target=generate_custom_configs,
args=(db.get_custom_configs(),),
kwargs={"original_path": configs_path},
).start()
Thread(target=generate_custom_configs, args=(db.get_custom_configs(),), kwargs={"original_path": configs_path}).start()
del custom_configs, db_configs
# Check if any external plugin has been added by the user
external_plugins = []
db_plugins = db.get_plugins(external=True)
plugins_dir = Path(sep, "etc", "bunkerweb", "plugins")
for filename in glob(str(plugins_dir.joinpath("*", "plugin.json"))):
with open(filename, "r", encoding="utf-8") as f:
_dir = dirname(filename)
plugin_content = BytesIO()
with tar_open(fileobj=plugin_content, mode="w:gz", compresslevel=9) as tar:
tar.add(_dir, arcname=basename(_dir), recursive=True)
plugin_content.seek(0, 0)
value = plugin_content.getvalue()
def check_plugin_changes(_type: Literal["external", "pro"] = "external"):
# Check if any external or pro plugin has been added by the user
external_plugins = []
db_plugins = db.get_plugins(_type=_type)
for filename in glob(str((EXTERNAL_PLUGINS_PATH if _type == "external" else PRO_PLUGINS_PATH).joinpath("*", "plugin.json"))):
with open(filename, "r", encoding="utf-8") as f:
_dir = dirname(filename)
plugin_content = BytesIO()
with tar_open(fileobj=plugin_content, mode="w:gz", compresslevel=9) as tar:
tar.add(_dir, arcname=basename(_dir), recursive=True)
plugin_content.seek(0, 0)
value = plugin_content.getvalue()
external_plugins.append(
json_load(f)
| {
"external": True,
"page": Path(_dir, "ui").exists(),
"method": "manual",
"data": value,
"checksum": sha256(value).hexdigest(),
}
external_plugins.append(
json_load(f)
| {
"type": _type,
"page": Path(_dir, "ui").exists(),
"method": "manual",
"data": value,
"checksum": sha256(value).hexdigest(),
}
)
tmp_external_plugins = []
for external_plugin in deepcopy(external_plugins):
external_plugin.pop("data", None)
external_plugin.pop("checksum", None)
external_plugin.pop("jobs", None)
external_plugin.pop("method", None)
tmp_external_plugins.append(external_plugin)
tmp_db_plugins = []
for db_plugin in db_plugins.copy():
db_plugin.pop("method", None)
tmp_db_plugins.append(db_plugin)
changes = {hash(dict_to_frozenset(d)) for d in tmp_external_plugins} != {hash(dict_to_frozenset(d)) for d in tmp_db_plugins}
if changes:
err = db.update_external_plugins(external_plugins, _type=_type, delete_missing=True)
if err:
logger.error(f"Couldn't save some manually added {_type} plugins to database: {err}")
if (scheduler_first_start and db_plugins) or changes:
generate_external_plugins(
db.get_plugins(_type=_type, with_data=True),
original_path=EXTERNAL_PLUGINS_PATH if _type == "external" else PRO_PLUGINS_PATH,
)
SCHEDULER.update_jobs()
tmp_external_plugins = []
for external_plugin in deepcopy(external_plugins):
external_plugin.pop("data", None)
external_plugin.pop("checksum", None)
external_plugin.pop("jobs", None)
external_plugin.pop("method", None)
tmp_external_plugins.append(external_plugin)
tmp_db_plugins = []
for db_plugin in db_plugins.copy():
db_plugin.pop("method", None)
tmp_db_plugins.append(db_plugin)
changes = {hash(dict_to_frozenset(d)) for d in tmp_external_plugins} != {hash(dict_to_frozenset(d)) for d in tmp_db_plugins}
if changes:
err = db.update_external_plugins(external_plugins, delete_missing=True)
if err:
logger.error(
f"Couldn't save some manually added plugins to database: {err}",
)
if (scheduler_first_start and db_plugins) or changes:
generate_external_plugins(
db.get_plugins(external=True, with_data=True),
original_path=plugins_dir,
)
SCHEDULER.update_jobs()
del tmp_external_plugins, external_plugins, db_plugins
check_plugin_changes("external")
check_plugin_changes("pro")
logger.info("Executing scheduler ...")
@ -426,9 +384,7 @@ if __name__ == "__main__":
logger.info(f"Sending {join(sep, 'etc', 'nginx')} folder ...")
ret = SCHEDULER.send_files(join(sep, "etc", "nginx"), "/confs")
if not ret:
logger.error(
"Sending nginx configs failed, configuration will not work as expected...",
)
logger.error("Sending nginx configs failed, configuration will not work as expected...")
def send_nginx_cache():
logger.info(f"Sending {CACHE_PATH} folder ...")
@ -498,14 +454,9 @@ if __name__ == "__main__":
)
if proc.returncode != 0:
logger.error(
"Config generator failed, configuration will not work as expected...",
)
logger.error("Config generator failed, configuration will not work as expected...")
else:
copy(
str(nginx_variables_path),
join(sep, "var", "tmp", "bunkerweb", "variables.env"),
)
copy(str(nginx_variables_path), join(sep, "var", "tmp", "bunkerweb", "variables.env"))
if SCHEDULER.apis:
# send nginx configs
@ -542,15 +493,14 @@ if __name__ == "__main__":
else:
logger.warning("No BunkerWeb instance found, skipping nginx reload ...")
except:
logger.error(
f"Exception while reloading after running jobs once scheduling : {format_exc()}",
)
logger.error(f"Exception while reloading after running jobs once scheduling : {format_exc()}")
NEED_RELOAD = False
RUN_JOBS_ONCE = False
CONFIG_NEED_GENERATION = False
CONFIGS_NEED_GENERATION = False
PLUGINS_NEED_GENERATION = False
PRO_PLUGINS_NEED_GENERATION = False
INSTANCES_NEED_GENERATION = False
# infinite schedule for the jobs
@ -567,6 +517,11 @@ if __name__ == "__main__":
stop(1)
# check if the plugins have changed since last time
if changes["pro_plugins_changed"]:
logger.info("Pro plugins changed, generating ...")
changes["external_plugins_changed"] = True
PRO_PLUGINS_NEED_GENERATION = True
if changes["external_plugins_changed"]:
logger.info("External plugins changed, generating ...")
@ -650,10 +605,12 @@ if __name__ == "__main__":
if PLUGINS_NEED_GENERATION:
CHANGES.append("external_plugins")
generate_external_plugins(
db.get_plugins(external=True, with_data=True),
original_path=plugins_dir,
)
generate_external_plugins(db.get_plugins(_type="external", with_data=True))
SCHEDULER.update_jobs()
if PRO_PLUGINS_NEED_GENERATION:
CHANGES.append("pro_plugins")
generate_external_plugins(db.get_plugins(_type="pro", with_data=True), original_path=PRO_PLUGINS_PATH)
SCHEDULER.update_jobs()
if CONFIG_NEED_GENERATION:
@ -662,7 +619,5 @@ if __name__ == "__main__":
env["DATABASE_URI"] = db.database_uri
except:
logger.error(
f"Exception while executing scheduler : {format_exc()}",
)
logger.error(f"Exception while executing scheduler : {format_exc()}")
stop(1)

View file

@ -57,8 +57,8 @@ RUN apk add --no-cache bash && \
mkdir -p /data/cache && ln -s /data/cache /var/cache/bunkerweb && \
mkdir -p /data/lib && ln -s /data/lib /var/lib/bunkerweb && \
mkdir -p /var/log/bunkerweb/ && \
for dir in $(echo "configs plugins") ; do mkdir -p "/data/${dir}" && ln -s "/data/${dir}" "/etc/bunkerweb/${dir}" ; done && \
for dir in $(echo "configs/http configs/stream configs/server-http configs/server-stream configs/default-server-http configs/default-server-stream configs/modsec configs/modsec-crs") ; do mkdir "/data/${dir}" ; done && \
for dir in $(echo "pro configs plugins") ; do mkdir -p "/data/${dir}" && ln -s "/data/${dir}" "/etc/bunkerweb/${dir}" ; done && \
for dir in $(echo "pro/plugins configs/http configs/stream configs/server-http configs/server-stream configs/default-server-http configs/default-server-stream configs/modsec configs/modsec-crs") ; do mkdir "/data/${dir}" ; done && \
chown -R root:ui /data && \
chmod -R 770 /data && \
chown -R root:ui INTEGRATION /var/cache/bunkerweb /var/lib/bunkerweb /etc/bunkerweb /var/tmp/bunkerweb /var/run/bunkerweb /var/log/bunkerweb && \

View file

@ -999,21 +999,18 @@ def plugins():
variables = deepcopy(request.form.to_dict())
del variables["csrf_token"]
if variables["external"] != "True":
flash(f"Can't delete internal plugin {variables['name']}", "error")
if variables["type"] in ("core", "pro"):
flash(f"Can't delete {variables['type']} plugin {variables['name']}", "error")
return redirect(url_for("loading", next=url_for("plugins")))
plugins = app.config["CONFIG"].get_plugins()
for plugin in deepcopy(plugins):
if plugin["external"] is False or plugin["id"] == variables["name"]:
del plugins[plugins.index(plugin)]
for x, plugin in enumerate(deepcopy(plugins)):
if plugin["type"] in ("core", "pro") or plugin["id"] == variables["name"]:
del plugins[x]
err = db.update_external_plugins(plugins)
if err:
flash(
f"Couldn't update external plugins to database: {err}",
"error",
)
flash(f"Couldn't update external plugins to database: {err}", "error")
flash(f"Deleted plugin {variables['name']} successfully")
else:
if not tmp_ui_path.exists() or not listdir(str(tmp_ui_path)):
@ -1130,7 +1127,7 @@ def plugins():
new_plugins.append(
plugin_file
| {
"external": True,
"type": "external",
"page": "ui" in listdir(str(temp_folder_path)),
"method": "ui",
"data": value,
@ -1183,7 +1180,7 @@ def plugins():
if errors >= files_count:
return redirect(url_for("loading", next=url_for("plugins")))
plugins = app.config["CONFIG"].get_plugins(external=True, with_data=True)
plugins = app.config["CONFIG"].get_plugins(_type="external", with_data=True)
for plugin in deepcopy(plugins):
if plugin["id"] in new_plugins_ids:
flash(f"Plugin {plugin['id']} already exists", "error")
@ -1191,10 +1188,7 @@ def plugins():
err = db.update_external_plugins(new_plugins, delete_missing=False)
if err:
flash(
f"Couldn't update external plugins to database: {err}",
"error",
)
flash(f"Couldn't update external plugins to database: {err}", "error")
if operation:
flash(operation)
@ -1217,18 +1211,22 @@ def plugins():
plugins = app.config["CONFIG"].get_plugins()
plugins_internal = 0
plugins_external = 0
plugins_pro = 0
for plugin in plugins:
if plugin["external"] is True:
if plugin["type"] == "external":
plugins_external += 1
elif plugin["type"] == "pro":
plugins_pro += 1
else:
plugins_internal += 1
return render_template(
"plugins.html",
plugins=plugins,
plugins_internal=plugins_internal,
plugins_external=plugins_external,
plugins_count_internal=plugins_internal,
plugins_count_external=plugins_external,
plugins_count_pro=plugins_pro,
username=current_user.get_id(),
)
@ -1391,15 +1389,15 @@ def custom_plugin(plugin: str):
is_used = True
break
return render_template(
Environment(loader=FileSystemLoader(join(sep, "usr", "share", "bunkerweb", "ui", "templates") + "/")).from_string(page.decode("utf-8")),
username=current_user.get_id(),
current_endpoint=plugin,
plugin=curr_plugin,
is_used=is_used,
is_metrics=is_metrics_on,
**app.jinja_env.globals,
)
return render_template(
Environment(loader=FileSystemLoader(join(sep, "usr", "share", "bunkerweb", "ui", "templates") + "/")).from_string(page.decode("utf-8")),
username=current_user.get_id(),
current_endpoint=plugin,
plugin=curr_plugin,
is_used=is_used,
is_metrics=is_metrics_on,
**app.jinja_env.globals,
)
module = db.get_plugin_actions(plugin)

View file

@ -9,7 +9,7 @@ from json import loads as json_loads
from pathlib import Path
from re import search as re_search
from subprocess import run, DEVNULL, STDOUT
from typing import List, Tuple
from typing import List, Literal, Tuple
from uuid import uuid4
@ -83,8 +83,8 @@ class Config:
**self.__settings,
}
def get_plugins(self, *, external: bool = False, with_data: bool = False) -> List[dict]:
plugins = self.__db.get_plugins(external=external, with_data=with_data)
def get_plugins(self, *, _type: Literal["all", "external", "pro"] = "all", with_data: bool = False) -> List[dict]:
plugins = self.__db.get_plugins(_type=_type, with_data=with_data)
plugins.sort(key=itemgetter("name"))
general_plugin = None

View file

@ -214,7 +214,7 @@ class Filter {
if (this.lastType === "all") return;
for (let i = 0; i < logs.length; i++) {
const el = logs[i];
const type = el.getAttribute(`data-${this.prefix}-external`).trim();
const type = el.getAttribute(`data-${this.prefix}-type`).trim();
if (type !== this.lastType) el.classList.add("hidden");
}
}
@ -491,8 +491,8 @@ class Modal {
this.modalTxt.textContent = `Are you sure you want to delete ${elName} ?`;
//external
const isExternal = el
.closest("[data-plugins-external]")
.getAttribute("data-plugins-external")
.closest("[data-plugins-type]")
.getAttribute("data-plugins-type")
.trim()
.includes("external")
? "True"

View file

@ -1,7 +1,6 @@
class Filter {
constructor(prefix = "reports") {
this.prefix = prefix;
this.container = document.querySelector(`[data-${this.prefix}-filter]`);
this.keyInp = document.querySelector("input#keyword");
this.methodValue = "all";
this.statusValue = "all";
@ -11,6 +10,11 @@ class Filter {
}
initHandler() {
this.container =
document.querySelector(`[data-${this.prefix}-filter]`) || null;
if (!this.container) return;
//METHOD HANDLER
this.container.addEventListener("click", (e) => {
try {

View file

@ -1,5 +1,4 @@
{% extends "base.html" %} {% block content %}{% set current_endpoint =
url_for(request.endpoint)[1:].split("/")[-1].strip().replace('_', '-') %} {%
{% extends "base.html" %} {% block content %}{%
include "plugins_modal.html" %}
<!-- info -->
@ -28,7 +27,7 @@ include "plugins_modal.html" %}
<p
class="transition duration-300 ease-in-out pl-2 col-span-1 mb-0 font-sans text-sm font-semibold leading-normal uppercase dark:text-white dark:opacity-80"
>
{{plugins_internal}}
{{plugins_count_internal}}
</p>
</div>
<div class="mx-1 flex items-center my-4">
@ -40,7 +39,19 @@ include "plugins_modal.html" %}
<p
class="transition duration-300 ease-in-out pl-2 col-span-1 mb-0 font-sans text-sm font-semibold leading-normal uppercase dark:text-white dark:opacity-80"
>
{{plugins_external}}
{{plugins_count_external}}
</p>
</div>
<div class="mx-1 flex items-center my-4">
<p
class="transition duration-300 ease-in-out font-bold mb-0 font-sans text-sm leading-normal uppercase dark:text-gray-500 dark:opacity-80"
>
PRO PLUGINS
</p>
<p
class="transition duration-300 ease-in-out pl-2 col-span-1 mb-0 font-sans text-sm font-semibold leading-normal uppercase dark:text-white dark:opacity-80"
>
{{plugins_count_pro}}
</p>
</div>
</div>
@ -48,7 +59,7 @@ include "plugins_modal.html" %}
<!-- upload layout -->
<div
data-{{current_endpoint}}-upload
data-plugins-upload
class="p-4 col-span-12 md:col-span-7 2xl:col-span-4 grid grid-cols-12 relative min-w-0 break-words bg-white shadow-xl dark:bg-slate-850 dark:shadow-dark-xl rounded-2xl bg-clip-border"
>
<h5 class="col-span-12 mb-4 font-bold dark:text-white/90">UPLOAD / RELOAD</h5>
@ -100,7 +111,7 @@ include "plugins_modal.html" %}
<!-- filter -->
<div
data-{{current_endpoint}}-filter
data-plugins-filter
class="h-fit p-4 col-span-12 md:col-span-6 2xl:col-span-4 relative min-w-0 break-words bg-white shadow-xl dark:bg-slate-850 dark:shadow-dark-xl rounded-2xl bg-clip-border"
>
<h5 class="mb-2 font-bold dark:text-white/90">FILTER</h5>
@ -132,7 +143,7 @@ include "plugins_modal.html" %}
Select types
</h5>
<button
data-{{current_endpoint}}-setting-select="types"
data-plugins-setting-select="types"
aria-controls="filter-types"
class="disabled:opacity-75 dark:disabled:text-gray-300 disabled:text-gray-700 disabled:bg-gray-400 disabled:border-gray-400 dark:disabled:bg-gray-800 dark:disabled:border-gray-800 duration-300 ease-in-out dark:opacity-90 dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300 focus:border-green-500 flex justify-between align-middle items-center text-left text-sm leading-5.6 ease w-full rounded-lg border border-solid border-gray-300 bg-white bg-clip-padding px-1.5 py-1 md:px-3 font-normal text-gray-700 transition-all placeholder:text-gray-500"
>
@ -140,12 +151,12 @@ include "plugins_modal.html" %}
aria-description="current type"
id="types"
data-name="types"
data-{{current_endpoint}}-setting-select-text="types"
data-plugins-setting-select-text="types"
>all</span
>
<!-- chevron -->
<svg
data-{{current_endpoint}}-setting-select="types"
data-plugins-setting-select="types"
class="transition-transform h-4 w-4 fill-gray-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
@ -160,12 +171,12 @@ include "plugins_modal.html" %}
<div
id="filter-types"
role="listbox"
data-{{current_endpoint}}-setting-select-dropdown="types"
data-plugins-setting-select-dropdown="types"
class="hidden z-100 absolute h-full flex-col w-full translate-y-16"
>
<button
role="option"
data-{{current_endpoint}}-setting-select-dropdown-btn="types"
data-plugins-setting-select-dropdown-btn="types"
value="all"
class="border-t rounded-t border-b border-l border-r border-gray-300 dark:hover:brightness-90 hover:brightness-90 my-0 relative py-2 px-3 text-left align-middle transition-all rounded-none cursor-pointer leading-normal text-sm ease-in tracking-tight-rem dark:border-slate-600 dark:text-gray-300 dark:bg-primary bg-primary text-gray-300"
>
@ -173,20 +184,28 @@ include "plugins_modal.html" %}
</button>
<button
role="option"
data-{{current_endpoint}}-setting-select-dropdown-btn="types"
value="internal"
data-plugins-setting-select-dropdown-btn="types"
value="core"
class="border-b border-l border-r border-gray-300 dark:hover:brightness-90 hover:brightness-90 bg-white text-gray-700 my-0 relative py-2 px-3 text-left align-middle transition-all rounded-none cursor-pointer leading-normal text-sm ease-in tracking-tight-rem dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300"
>
internal
core
</button>
<button
role="option"
data-{{current_endpoint}}-setting-select-dropdown-btn="types"
data-plugins-setting-select-dropdown-btn="types"
value="external"
class="border-b border-l border-r border-gray-300 dark:hover:brightness-90 hover:brightness-90 bg-white text-gray-700 my-0 relative py-2 px-3 text-left align-middle transition-all rounded-none cursor-pointer leading-normal text-sm ease-in tracking-tight-rem dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300"
>
external
</button>
<button
role="option"
data-plugins-setting-select-dropdown-btn="types"
value="pro"
class="border-b border-l border-r border-gray-300 dark:hover:brightness-90 hover:brightness-90 bg-white text-gray-700 my-0 relative py-2 px-3 text-left align-middle transition-all rounded-none cursor-pointer leading-normal text-sm ease-in tracking-tight-rem dark:border-slate-600 dark:bg-slate-700 dark:text-gray-300"
>
pro
</button>
</div>
<!-- end dropdown-->
</div>
@ -200,14 +219,14 @@ include "plugins_modal.html" %}
>
<h5 class="mb-4 mt-2 font-bold dark:text-white/90 mx-2">LIST</h5>
<div data-{{current_endpoint}}-list class="grid grid-cols-12 gap-3">
<div data-plugins-list class="grid grid-cols-12 gap-3">
{% for plugin in plugins %}
<div
data-{{current_endpoint}}-external="{% if plugin['external'] %} external {% else %} internal {% endif %}"
data-plugins-type="{{plugin['type']}}"
class="py-3 min-h-12 relative col-span-12 sm:col-span-6 2xl:col-span-4 3xl:col-span-3 p-1 flex justify-between items-center transition rounded bg-gray-100 hover:bg-gray-300 dark:bg-slate-700 dark:hover:bg-slate-800"
>
<p
data-{{current_endpoint}}-content
data-plugins-content
class="ml-3 mr-2 break-words mb-0 transition duration-300 ease-in-out dark:opacity-90 text-left text-sm md:text-base text-slate-700 dark:text-gray-200"
>
{{plugin['name']}}
@ -230,9 +249,9 @@ include "plugins_modal.html" %}
</svg>
</a>
{%endif%}
{% if plugin['external'] %}
{% if plugin['type'] == "external" %}
<button
data-{{current_endpoint}}-action="delete"
data-plugins-action="delete"
name="{{plugin['id']}}"
aria-label="delete plugin"
class="z-20 mx-2 inline-block font-bold text-left text-white uppercase align-middle transition-all cursor-pointer text-xs ease-in tracking-tight-rem hover:-translate-y-px"
@ -254,11 +273,11 @@ include "plugins_modal.html" %}
{% if plugins_pro %}
{% for plugin in plugins_pro %}
<div
data-{{current_endpoint}}-external="external"
data-plugins-type="{{plugin['type']}}"
class="py-3 min-h-12 relative col-span-12 sm:col-span-6 2xl:col-span-4 3xl:col-span-3 p-1 flex justify-between items-center transition rounded {% if is_plugin_pro %}bg-gray-100 hover:bg-gray-300 dark:bg-slate-700 dark:hover:bg-slate-800{% else %} bg-gray-100 dark:bg-slate-700 {% endif %}"
>
<p
data-{{current_endpoint}}-content
data-plugins-content
class="{% if not is_plugin_pro %} opacity-80 dark:opacity-60{% endif%} ml-3 mr-2 break-words mb-0 transition duration-300 ease-in-out text-left text-sm md:text-base text-slate-700 dark:text-gray-200"
>
{{plugin['name']}}

View file

@ -282,7 +282,7 @@ try:
"description": "The general settings for the server",
"version": "0.1",
"stream": "partial",
"external": False,
"type": "core",
"checked": False,
"page_checked": True,
"settings": global_settings,
@ -316,27 +316,27 @@ try:
Plugins.description,
Plugins.version,
Plugins.stream,
Plugins.external,
Plugins.type,
Plugins.method,
)
.all()
)
for plugin in plugins:
if not plugin.external and plugin.id in core_plugins:
if plugin.type == "core" and plugin.id in core_plugins:
current_plugin = core_plugins
elif plugin.external and plugin.id in external_plugins:
elif plugin.type == "external" and plugin.id in external_plugins:
current_plugin = external_plugins
else:
print(
f"❌ The {'external' if plugin.external else 'core'} plugin {plugin.name} (id: {plugin.id}) is in the database but should not be, exiting ...",
f"❌ The {'external' if plugin.type == 'external' else 'core'} plugin {plugin.name} (id: {plugin.id}) is in the database but should not be, exiting ...: {plugin}",
flush=True,
)
exit(1)
if plugin.name != current_plugin[plugin.id]["name"] or plugin.description != current_plugin[plugin.id]["description"] or plugin.version != current_plugin[plugin.id]["version"] or plugin.stream != current_plugin[plugin.id]["stream"]:
print(
f"❌ The {'external' if plugin.external else 'core'} plugin {plugin.name} (id: {plugin.id}) is in the database but is not correct, exiting ...\n"
f"❌ The {'external' if plugin.type == 'external' else 'core'} plugin {plugin.name} (id: {plugin.id}) is in the database but is not correct, exiting ...\n"
+ f"{dumps({'name': plugin.name, 'description': plugin.description, 'version': plugin.version, 'stream': plugin.stream})}"
+ f" (database) != {dumps({'name': current_plugin[plugin.id]['name'], 'description': current_plugin[plugin.id]['description'], 'version': current_plugin[plugin.id]['version'], 'stream': current_plugin[plugin.id]['stream']})} (file)", # noqa: E501
flush=True,
@ -357,7 +357,7 @@ try:
or setting.multiple != current_plugin[plugin.id]["settings"][setting.id].get("multiple", None)
):
print(
f"❌ The {'external' if plugin.external else 'core'} plugin {plugin.name} (id: {plugin.id}) is in the database but is not correct, exiting ...\n"
f"❌ The {'external' if plugin.type == 'external' else 'core'} plugin {plugin.name} (id: {plugin.id}) is in the database but is not correct, exiting ...\n"
+ f"{dumps({'default': setting.default, 'help': setting.help, 'label': setting.label, 'regex': setting.regex, 'type': setting.type})}"
+ f" (database) != {dumps({'default': current_plugin[plugin.id]['settings'][setting.id]['default'], 'help': current_plugin[plugin.id]['settings'][setting.id]['help'], 'label': current_plugin[plugin.id]['settings'][setting.id]['label'], 'regex': current_plugin[plugin.id]['settings'][setting.id]['regex'], 'type': current_plugin[plugin.id]['settings'][setting.id]['type']})} (file)", # noqa: E501
flush=True,